diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..bac8eff0e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +Thanks in advance for your contribution to Discord.Net! + +Before opening a pull request, please consider the following: + +Does your changeset adhere to the Contributing Guidelines? + +Does your changeset address a specific issue or idea? If not, please +break your changes up into multiple requests. + +Have your changes been previously discussed with other members +of the community? We prefer new features to be vetted through +an issue or a discussion in our Discord channel first; bug-fixes +and other small changes are generally fine without prior vetting. + +Please remove this section, and include a summary of your changes +below. +--- diff --git a/.gitignore b/.gitignore index 45e5e009d..654370842 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ build/ bld/ [Bb]in/ @@ -129,7 +127,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj @@ -153,7 +151,6 @@ AppPackages/ # Others *.[Cc]ache ClientBin/ -[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl @@ -206,3 +203,6 @@ project.lock.json docs/api/\.manifest \.idea/ + +# Codealike UID +codealike.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..21e37b295 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,441 @@ +# Changelog + +## [2.4.0] - 2021-05-22 +### Added +- #1726 Add stickers (91a9063) +- #1753 Webhook message edit & delete functionality (f67cd8e) +- #1757 Add ability to add/remove roles by id (4c9910c) +- #1781 Add GetEmotesAsync to IGuild (df23d57) +- #1801 Add missing property to MESSAGE_REACTION_ADD event (0715d7d) +- #1828 Add methods to interact with reactions without a message object (5b244f2) +- #1830 Add ModifyMessageAsync to IMessageChannel (365a848) +- #1844 Add Discord Certified Moderator user flag (4b8d444) + +### Fixed +- #1486 Add type reader when entity type reader exists (c46daaa) +- #1835 Cached message emoji cleanup at MESSAGE_REACTION_REMOVE_EMOJI (8afef82) + +### Misc +- #1778 Remove URI check from EmbedBuilder (25b04c4) +- #1800 Fix spelling in SnowflakeUtils.FromSnowflake (6aff419) + +## [2.3.1] - 2021-03-10 +### Fixed +- #1761 Deadlock in DiscordShardedClient when Ready is never received (73e5cc2) +- #1773 Private methods aren't added as commands (0fc713a) +- #1780 NullReferenceException in pin/unpin audit logs (f794163) +- #1786 Add ChannelType property to ChannelInfo audit log (6ac5ea1) +- #1791 Update Webhook ChannelId from model change (d2518db) +- #1794 Audit log UserId can be null (d41aeee) + +### Misc +- #1774 Add remark regarding CustomStatus as the activity (51b7afe) + +## [2.3.0] - 2021-01-28 +### Added +- #1491 Add INVITE_CREATE and INVITE_DELETE events (1ab670b) +- #1520 Support reading multiple activities (421a0c1) +- #1521 Allow for inherited commands in modules (a51cdf6) +- #1526 Add Direction.Around to GetMessagesAsync (f2130f8) +- #1537 Implement gateway ratelimit (ec673e1) +- #1544 Add MESSAGE_REACTION_REMOVE_EMOJI and RemoveAllReactionsForEmoteAsync (a89f076) +- #1549 Add GetUsersAsync to SocketGuild (30b5a83) +- #1566 Support Gateway Intents (d5d10d3) +- #1573 Add missing properties to Guild and deprecate GuildEmbed (ec212b1) +- #1581 Add includeRoleIds to PruneUsersAsync (a80e5ff) +- #1588 Add GetStreams to AudioClient (1e012ac) +- #1596 Add missing channel properties (2d80037) +- #1604 Add missing application properties (including Teams) (10fcde0) +- #1619 Add "View Guild Insights" to GuildPermission (2592264) +- #1637 Added CultureInvariant RegexOption to WebhookUrlRegex (e3925a7) +- #1659 Add inline replies (e3850e1) +- #1688 Send presence on Identify payload (25d5d36) +- #1721 Add role tags (6a62c47) +- #1722 Add user public flags (c683b29) +- #1724 Add MessageFlags and AllowedMentions to message modify (225550d) +- #1731 Add GuildUser IsPending property (8b25c9b) +- #1690 Add max bitrate value to SocketGuild (aacfea0) + +### Fixed +- #1244 Missing AddReactions permission for DM channels. (e40ca4a) +- #1469 unsupported property causes an exception (468f826) +- #1525 AllowedMentions and AllowedMentionTypes (3325031) +- #1531 Add AllowedMentions to SendFileAsync (ab32607) +- #1532 GuildEmbed.ChannelId as nullable per API documentation (971d519) +- #1546 Different ratelimits for the same route (implement discord buckets) (2f6c017) +- #1548 Incomplete Ready, DownloadUsersAsync, and optimize AlwaysDownloadUsers (dc8c959) +- #1555 InvalidOperationException at MESSAGE_CREATE (bd4672a) +- #1557 Sending 2 requests instead of 1 to create a Guild role. (5430cc8) +- #1571 Not using the new domain name. (df8a0f7) +- #1578 Trim token before passing it to the authorization header (42ba372) +- #1580 Stop TaskCanceledException from bubbling up (b8fa464) +- #1599 Invite audit log without inviter (b95b95b) +- #1602 Add AllowedMentions to webhooks (bd4516b) +- #1603 Cancel reconnection when 4014 (f396cd9) +- #1608 Voice overwrites and CategoryId remarks (43c8fc0) +- #1614 Check error 404 and return null for GetBanAsync (ae9fff6) +- #1621 Parse mentions from message payload (366ca9a) +- #1622 Do not update overwrite cache locally (3860da0) +- #1623 Invoke UserUpdated from GuildMemberUpdated if needed (3085e88) +- #1624 Handle null PreferredLocale in rare cases (c1d04b4) +- #1639 Invite and InviteMetadata properties (dd2e524) +- #1642 Add missing permissions (4b389f3) +- #1647 handicap member downloading for verified bots (fa5ef5e) +- #1652 Update README.MD to reflect new discord domain (03b831e) +- #1667 Audio stream dispose (a2af985) +- #1671 Crosspost throwing InvalidOperationException (9134443) +- #1672 Team is nullable, not optional (be60d81) +- #1681 Emoji url encode (04389a4) +- #1683 SocketGuild.HasAllMembers is false if a user left a guild (47f571e) +- #1686 Revert PremiumSubscriptionCount type (97e71cd) +- #1695 Possible NullReferenceException when receiving InvalidSession (5213916) +- #1702 Rollback Activities to Game (9d7cb39) +- #1727 Move and fix internal AllowedMentions object (4a7f8fe) +- limit request members batch size (084db25) +- UserMentions throwing NullRef (5ed01a3) +- Wrong author for SocketUserMessage.ReferencedMessage (1e9b252) +- Discord sends null when there's no team (05a1f0a) +- IMessage.Embeds docs remarks (a4d32d3) +- Missing MessageReference when sending files (2095701) + +### Misc +- #1545 MutualGuilds optimization (323a677) +- #1551 Update webhook regex to support discord.com (7585789) +- #1556 Add SearchUsersAsync (57880de) +- #1561 Minor refactor to switch expression (42826df) +- #1576 Updating comments for privileged intents (c42bfa6) +- #1678 Change ratelimit messages (47ed806) +- #1714 Update summary of SocketVoiceChannel.Users (e385c40) +- #1720 VoiceRegions and related changes (5934c79) +- Add updated libraries for LastModified (d761846) +- Add alternative documentation link (accd351) +- Temporarily disable StyleCops until all the fixes are impl'd (36de7b2) +- Remove redundant CreateGuildRoleParams (3df0539) +- Add minor tweaks to DiscordSocketConfig docs strings (2cd1880) +- Fix MaxWaitBetweenGuildAvailablesBeforeReady docs string (e31cdc7) +- Missing summary tag for GatewayIntents (3a10018) +- Add new method of role ID copy (857ef77) +- Resolve inheritdocs for IAttachment (9ea3291) +- Mark null as a specific langword in summary (13a41f8) +- Cleanup GatewayReconnectException docs (833ee42) +- Update Docfx.Plugins.LastModified to v1.2.4 (28a6f97) +- Update framework version for tests to Core 3.1 to comply with LTS (4988a07) +- Move bulk deletes remarks from to (62539f0) + +## [2.2.0] - 2020-04-16 +### Added +- #1247 Implement Client Status Support (9da11b4) +- #1310 id overload for RemoveReactionAsync (c88b1da) +- #1319 BOOST (faf23de) +- #1326 Added a Rest property to DiscordShardedClient (9fede34) +- #1348 Add Quote Formatting (265da99) +- #1354 Add support for setting X-RateLimit-Precision (9482204) +- #1355 Provide ParameterInfo with error ParseResult (3755a02) +- #1357 add the "Stream" permission. (b00da3d) +- #1358 Add ChannelFollowAdd MessageType (794eba5) +- #1369 Add SelfStream voice state property (9bb08c9) +- #1372 support X-RateLimit-Reset-After (7b9029d) +- #1373 update audit log models (c54867f) +- #1377 Support filtering audit log entries on user, action type, and before entry id (68eb71c) +- #1386 support guild subscription opt-out (0d54207) +- #1387 #1381 Guild PreferredLocale support (a61adb0) +- #1406 CustomStatusGame Activity (79a0ea9) +- #1413 Implemented Message Reference Property (f86c39d) +- #1414 add StartedAt, EndsAt, Elapsed and Remaining to SpotifyGame. (2bba324) +- #1432 Add ability to modify the banner for guilds (d734ce0) +- suppress messages (cd28892) + +### Fixed +- #1318 #1314 Don't parse tags within code blocks (c977f2e) +- #1333 Remove null coalescing on ToEmbedBuilder Color (120c0f7) +- #1337 Fixed attempting to access a non-present optional value (4edda5b) +- #1346 CommandExecuted event will fire when a parameter precondition fails like what happens when standard precondition fails. (e8cb031) +- #1371 Fix keys of guild update audit (b0a595b) +- #1375 Use double precision for X-Reset-After, set CultureInfo when parsing numeric types (606dac3) +- #1392 patch todo in NamedTypeReader (0bda8a4) +- #1405 add .NET Standard 2.1 support for Color (7f0c0c9) +- #1412 GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. (5439cba) +- #1416 false-positive detection of CustomStatusGame based on Id property (a484651) +- #1418 #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner (1c63fd4) +- #1421 (3ff4e3d) +- include MessageFlags and SuppressEmbedParams (d6d4429) + +### Changed +- #1368 Update ISystemMessage interface to allow reactions (07f4d5f) +- #1417 fix #1415 Re-add support for overwrite permissions for news channels (e627f07) +- use millisecond precision by default (bcb3534) + +### Misc +- #1290 Split Unit and Integration tests into separate projects (a797be9) +- #1328 Fix #1327 Color.ToString returns wrong value (1e8aa08) +- #1329 Fix invalid cref values in docs (363d1c6) +- #1330 Fix spelling mistake in ExclusiveBulkDelete warning (c864f48) +- #1331 Change token explanation (0484fe8) +- #1349 Fixed a spelling error. (af79ed5) +- #1353 [ci skip] Removed duplicate "any" from the readme (15b2a36) +- #1359 Fixing GatewayEncoding comment (52565ed) +- #1379 September 2019 Documentation Update (fd3810e) +- #1382 Fix .NET Core 3.0 compatibility + Drop NS1.3 (d199d93) +- #1388 fix coercion error with DateTime/Offset (3d39704) +- #1393 Utilize ValueTuples (99d7135) +- #1400 Fix #1394 Misworded doc for command params args (1c6ee72) +- #1401 Fix package publishing in azure pipelines (a08d529) +- #1402 Fix packaging (65223a6) +- #1403 Cache regex instances in MessageHelper (007b011) +- #1424 Fix the Comparer descriptions not linking the type (911523d) +- #1426 Fix incorrect and missing colour values for Color fields (9ede6b9) +- #1470 Added System.Linq reference (adf823c) +- temporary sanity checking in SocketGuild (c870e67) +- build and deploy docs automatically (2981d6b) +- 2.2.0 (4b602b4) +- target the Process env-var scope (3c6b376) +- fix metapackage build (1794f95) +- copy only _site to docs-static (a8cdadc) +- do not exit on failed robocopy (fd204ee) +- add idn debugger (91aec9f) +- rename IsStream to IsStreaming (dcd9cdd) +- feature (40844b9) + +## [2.1.1] - 2019-06-08 +### Fixed +- #994: Remainder parameters now ignore character escaping, as there is no reason to escape characters here (2e95c49) +- #1316: `Emote.Equals` now pays no respect to the Name property, since Discord's API does not care about an emote's name (abf3e90) +- #1317: `Emote.GetHashCode` now pays no respect to the Name property, see above (1b54883) +- #1323: Optionals will no longer claim to be specified when a reaction message was not cached (1cc5d73) +- Log messages sourcing from REST events will no longer be raised twice (c78a679) +- News embeds will be processed as `EmbedType.Unknown`, rather than throwing an error and dropping the message (d287ed1) + +### Changed +- #1311: Members may now be disconnected from voice channels by passing `null` as `GuildUserProperties.Channel` (fc48c66) +- #1313: `IMessage.Tags` now includes the EveryoneRole on @everyone and @here mentions (1f55f01) +- #1320: The maximum value for setting slow-mode has been updated to 6 hours, per the new API limit (4433ca7) + +### Misc +- This library's compatibility with Semantic Versioning has been clarified. Please see the README (4d7de17) +- The depency on System.Interactive.Async has been bumped to `3.2.0` (3e65e03) + +## [2.1.0] - 2019-05-18 +### Added +- #1236: Bulk deletes (for messages) may now be accessed via the `MessagesBulkDeleted` event (dec353e) +- #1240: OAuth applications utilizing the `guilds.join` scope may now add users to guilds through any client (1356ea9) +- #1255: Message and attachment spoilers may now be set or detected (f3b20b2) +- #1260: DiscordWebhookClient may be created from a Webhook URL (f2113c7) +- #1261: A `GetCategoryChannel` helper may now be used to retrieve category channels directly from socket guilds (e03c527) +- #1263: "user joined the guild" messages are now supported (00d3f5a) +- #1271: AuthorID may now be retrieved from message delete audit log entries (1ae4220) +- #1293: News Channels are now supported 📰 (9084c42) +- `ExclusiveBulkDelete` configuration setting can be used to control bulk delete event behavior (03e6401) + +### Removed +- #1294: The `IGuildUser` overload of `EmbedBuilder.WithAuthor` no longer exists (b52b54d) + +### Fixed +- #1256: Fetching audit logs no longer raises null reference exceptions when a webhook has been deleted (049b014) +- #1268: Null reference exceptions on `MESSAGE_CREATE` concerning partial member objects no longer occur (377622b) +- #1278: The token validator now internally pads tokens to the proper length (48b327b) +- #1292: Messages now properly initialize empty collections (b2ebc03) +- The `DiscordSocketRestClient` is now properly initialized (a44c13a) +- Exceptions in event handlers are now always logged (f6e3200) + +### Changed +- #1305: Token validation will fail when tokens contain whitespace (bb61efa) + +### Misc +- #1241: Added documentation samples for Webhooks (655a006) +- #1243: Happy new year 🎉 (0275f7d) +- #1257: Improved clarity in comments in the command samples (2473619) +- #1276: Documentation uses a relative path for the logo asset (b80f0e8) +- #1303: EmbedBuilder documentation now builds in the correct spot (51618e6) +- #1304: Updated documentation (4309550) +- CI for this project is now powered by Azure DevOps (this is not a sponsored message 🚀) (9b2bc18) +- IDisposableAnalyzers should now be a development dependency (8003ac8) + +## [2.0.1] - 2019-01-04 +### Fixed +- #1226: Only escape the closing quotation mark of non-remainder strings (65b8c09) +- Commands with async RunModes will now propagate exceptions up to CommandExecuted (497918e) + +### Misc +- #1225: Commands sample no longer hooks the log event twice (552f34c) +- #1227: The logo on the docs index page should scale responsively (d39bf6e) +- #1230: Replaced precondition sample on docs (feed4fd) + +## [2.0.0] - 2018-12-28 +### Added + +- #747: `CommandService` now has a `CommandExecuted` event (e991715) +- #765: Parameters may have a name specified via `NameAttribute` (9c81ab9) +- #773: Both socket clients inherit from `BaseSocketClient` (9b7afec) +- #785: Primitives now automatically load a NullableTypeReader (cb0ff78) +- #819: Support for Welcome Message channels (30e867a) +- #835: Emoji may now be managed from a bot (b4bf046) +- #843: Webhooks may now be managed from a bot (7b2ddd0) +- #863: An embed may be converted to an `EmbedBuilder` using the `.ToEmbedBuilder()` method (5218e6b) +- #877: Support for reading rich presences (34b4e5a) +- #888: Users may now opt-in to using a proxy (678a723) +- #906: API Analyzers to assist users when writing their bot (f69ef2a) +- #907: Full support for channel categories (030422f) +- #913: Animated emoji may be read and written (a19ff18) +- #915: Unused parameters may be discarded, rather than failing the command (5f46aef) +- #929: Standard EqualityComparers for use in LINQ operations with the library's entities (b5e7548) +- #934: Modules now contain an `OnModuleBuilding` method, which is invoked when the module is built (bb8ebc1) +- #952: Added 'All' permission set for categories (6d58796) +- #957: Ratelimit related objects now include request information (500f5f4) +- #962: Add `GetRecommendedShardCountAsync` (fc5e70c) +- #970: Add Spotify track support to user Activities (64b9cc7) +- #973: Added `GetDefaultAvatarUrl` to user (109f663) +- #978: Embeds can be attached alongside a file upload (e9f9b48) +- #984, #1089: `VoiceServerUpdate` events are now publically accessible (e775853, 48fed06) +- #996: Added `DeleteMessageAsync` to `IMessageChannel` (bc6009e) +- #1005: Added dedicated `TimeSpan` TypeReader which "doesn't suck" (b52af7a) +- #1009: Users can now specify the replacement behavior or default typereaders (6b7c6e9) +- #1020: Users can now specify parameters when creating channels (bf5275e) +- #1030: Added `IsDeprecated`, `IsCustom` properties to `VoiceRegion` (510f474) +- #1037: Added `SocketUser.MutualGuilds`, various extension methods to commands (637d9fc) +- #1043: `Discord.Color` is now compatible with `System.Drawing.Color` (c275e57) +- #1055: Added audit logs (39dffe8) +- #1056: Added `GetBanAsync` (97c8931) +- #1102: Added `GetJumpUrl()` to messages (afc3a9d) +- #1123: Commands can now accept named parameters (419c0a5) +- #1124: Preconditions can now set custom error messages (5677f23) +- #1126: `Color` now has equality (a2d8800) +- #1159: Guild channels can now by synced with their parent category (5ea1fb3) +- #1165: Bring Guild and Message models up to date with the API (d30d122) +- #1166: Added `GetVoiceRegionsAsync` to `IGuild` (00717cf) +- #1183: Added Add Guild Member endpoint for OAuth clients (8ef5f81) +- #1196: Channel position can now be specified when creating a channel (a64ab60) +- #1198: The Socket client can now access its underlying REST client (65afd37) +- #1213: Added `GuildEmote#CreatorId` (92bf836) +- 'html' variant added to the `EmbedType` enum (42c879c) +- Modules can now be nested in non-module classes (4edbd8d) +- Added `BanAsync` to guild members (1905fde) +- Added the permisison bit for priority speaker (c1d7818) +- All result types can use `FromError` (748e92b) +- Added support for reading/writing slow mode (97d17cf) +- Added markdown format for URLs (f005af3) +- Reactions can now be added to messages in bulk (5421df1) + +### Fixed +- #742: `DiscordShardedClient#GetGuildFor` will now direct null guilds to Shard 0 (d5e9d6f) +- #743: Various issues with permissions and inheritance of permissions (f996338) +- #755: `IRole.Mention` will correctly tag the @everyone role (6b5a6e7) +- #768: `CreateGuildAsync` will include the icon stream (865080a) +- #866: Revised permissions constants and behavior (dec7cb2) +- #872: Bulk message deletion should no longer fail for incomplete batch sizes (804d918) +- #923: A null value should properly reset a user's nickname (227f61a) +- #938: The reconnect handler should no longer deadlock during Discord outages (73ac9d7) +- #941: Fix behavior of OverrideTypeReader (170a2e0) +- #945: Fix properties on SocketCategoryChannel (810f6d6) +- #959: Webhooks now use the correct parameter when assigning to the Avatar URL (8876597) +- #966: Correct the implementation of HasFlag and ResolveChannel in permissions (32ebdd5) +- #968: Add missing parameter in WebSocket4Net constructor (8537924) +- #981: Enforce a maximum value when parsing timestamps from Discord (bfaa6fc) +- #993: Null content will no longer null-ref on message sends/edits (55299ff) +- #1003: Fixed ordering of parameters in permissions classes (a06e212) +- #1010: EmbedBuilder no longer produces mutable embeds (2988b38) +- #1012: `Embed.Length` should now yield the correct results (a3ce80c) +- #1017: GetReactionUsersAsync includes query parameters (9b29c00) +- #1022: GetReactionUsersAsync is now correctly paginated (79811d0) +- #1023: Fix/update invite-related behaviors (7022149) +- #1031: Messages with no guild-specific data should no longer be lost (3631886) +- #1036: Fixed cases where `RetryMode.RetryRatelimit` were ignored (c618cb3) +- #1044: Populate the guild in `SocketWebhookUser` (6a7810b) +- #1048: The REST client will now create a full GuildUser object (033d312) +- #1049: Fixed null-ref in `GetShardIdFor` (7cfed7f) +- #1059: Include 'view channel' in voice channel's All permissions set (e764daf) +- #1083: Default type readers will now be properly replaced (4bc06a0) +- #1093: Fixed race condition in audio client authentication (322d46e) +- #1139: Fixed consistency in exceptions (9e9a11d) +- #1151: `GetReactionUsersAsync` now uses the correct pagination constant (c898325) +- #1163: Reaction ratelimits are now placed in the same bucket, treated correctly (5ea1fb3) +- #1186: Webhooks can now send files with embeds correctly (c1d5152) +- #1192: CommandExecuted no longer fires twice for RuntimeResults (10233f3) +- #1195: Channel Create audit log events properly deserialize (dca6c33) +- #1202: The UDP client should no longer be used after disposed (ccb16e4) +- #1203: The Audio client should no longer lock up on disconnect (2c93363) +- #1209: MessageUpdated should no longer pass a null after object (91e0f03) +- Ignore messages with no ID in bulk delete (676be40) +- No longer attempt to load generic types as modules (b1eaa44) +- No longer complain when a `PRESENCES_REPLACE` update is received (beb3d46) +- CommandExecuted will be raised on async exception failures (6260749) +- ExecuteResult now contains the entire exception, not an abridged message (f549da5) +- CommandExecuted will no longer be raised twice for exceptions (aec7105) +- The default WebSocket will now close correctly (ac389f5) + +### Changed +- #731: `IUserMessage#GetReactionUsersAsync` now takes an `IEmote` instead of a `string` (5d7f2fc) +- #744: IAsyncEnumerable has been redesigned (5bbd9bb) +- #777: `IGuild#DefaultChannel` will now resolve the first accessible channel, per changes to Discord (1ffcd4b) +- #781: Attempting to add or remove a member's EveryoneRole will throw (506a6c9) +- #801: `EmbedBuilder` will no longer implicitly convert to `Embed`, you must build manually (94f7dd2) +- #804: Command-related tasks will have the 'async' suffix (14fbe40) +- #812: The WebSocket4Net provider has been bumped to version 0.15, allowing support for .NET Standard apps (e25054b) +- #829: DeleteMessagesAsync moved from IMessageChannel to ITextChannel (e00f17f) +- #853: WebSocket will now use `zlib-stream` compression (759db34) +- #874: The `ReadMessages` permission is moving to `ViewChannel` (edfbd05) +- #877: Refactored Games into Activities (34b4e5a) +- #943: Multiple types of quotation marks can now be parsed (thanks 🍎) (cee71ef) +- #955: The `GameParty` model will now use long values (178ea8d) +- #986: Expose the internal entity TypeReaders (660fec0) +- #992: Throw an exception when trying to modify someone else's message (d50fc3b) +- #998: Commands can specify their own `IgnoreExtraArgs` behavior (6d30100) +- #1033: The `ReadMessages` permission bit is now named `ViewChannel` (5f084ad) +- #1042: Content parameter of `SendMessageAsync` is now optional (0ba8b06) +- #1057: An audio channel's `ConnectAsync` now allows users to handle the voice connection elsewhere, such as in Lavalink (890904f) +- #1094: Overhauled invites, added vanity invite support (ffe994a) +- #1108: Reactions now use the undocumented 1/.25 ratelimit, making them 4x faster (6b21b11) +- #1128: Bot tokens will now be validated for common mishaps before use (2de6cef) +- #1140: Check the invite `maxAge` parameter before making the request (649a779) +- #1164: All command results will now be raised in `CommandExecuted` (10f67a8) +- #1171: Clients have been changed to properly make use of `IDisposable` (7366cd4) +- #1172: Invite related methods were moved from `IGuildChannel` to `INestedChannel` (a3f5e0b) +- #1200: HasPrefix extensions now check for null values first (46e2674) +- `IGuildChannel#Nsfw` moved to `ITextChannel`, now maps to the API property (608bc35) +- Preemptive ratelimits are now logged under verbose, rather than warning. (3c1e766) +- The default InviteAge when creating Invites is now 24 hours (9979a02) +- All parameters to `ReplyAsync` have been made optional (b38dca7) +- The socket client will now use additional fields to fill in member/guild information on messages (8fb2c71) +- The Audio Client now uses Voice WS v3 (9ba38d7) + + +### Removed +- #790: Redundant overloads for `AddField` removed from EmbedBuilder (479361b) +- #925: RPC is no longer being maintained nor packaged (b30af57) +- #958: Remove support for user tokens (2fd4f56) +- User logins (including selfbots) are no longer supported (fc5adca) + +### Misc +- #786: Unit tests for the Color structure (22b969c) +- #828: We now include a contributing guide (cd82a0f) +- #876: We now include a standard editorconfig (5c8c784) +- #963: Docs now include a release version, build instructions (88e6244) +- #964: Fix documentation spelling of 'echoes' (fda19b5) +- #967: Unit test permissions (63e6704) +- #968: Bumped version of WebSocket4Net to 0.15.2 (8537924) +- #972: Include sample bots in the source repository (217ec34) +- #1046: We now support .NET Standard 2.0 (bbbac85) +- #1114: Various performance optimizations (82cfdff) +- #1149: The CI will now test on Ubuntu as well as Windows (674a0fc) +- #1161: The entire documentation has been rewritten, all core entities were docstringed (ff0fea9) +- #1175: Documentation changes in command samples (fb8dbca) +- #1177: Added documentation for sharded bots (00097d3) +- #1219: The project now has a logo! 🎉 (5750c3e) +- This project is now licensed to the Discord.Net contributors (710e182) +- Added templates for pull requests (f2ddf51) +- Fixed documentation layout for the logo (bafdce4) + +## [1.0.2] - 2017-09-09 +### Fixed + +- Guilds utilizing Channel Categories will no longer crash bots on the `READY` event. + +## [1.0.1] - 2017-07-05 +### Fixed + +- #732: Fixed parameter preconditions not being loaded from class-based modules (b6dcc9e) +- #726: Fixed CalculateScore throwing an ArgumentException for missing parameters (7597cf5) +- EmbedBuilder URI validation should no longer throw NullReferenceExceptions in certain edge cases (d89804d) +- Fixed module auto-detection for nested modules (d2afb06) + +### Changed +- ShardedCommandContext now inherits from SocketCommandContext (8cd99be) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8248291e8..752b40931 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,4 +41,22 @@ We attempt to conform to the .NET Foundation's [Coding Style](https://github.com where possible. As a general rule, follow the coding style already set in the file you -are editing, or look at a similar file if you are adding a new one. \ No newline at end of file +are editing, or look at a similar file if you are adding a new one. + +### Documentation Style for Members + +When creating a new public member, the member must be annotated with sufficient documentation. This should include the +following, but not limited to: + +* `` summarizing the purpose of the method. +* `` or `` explaining the parameter. +* `` explaining the type of the returned member and what it is. +* `` if the method directly throws an exception. + +The length of the documentation should also follow the ruler as suggested by our +[Visual Studio Code workspace](Discord.Net.code-workspace). + +#### Recommended Reads + +* [Official Microsoft Documentation](https://docs.microsoft.com) +* [Sandcastle User Manual](https://ewsoftware.github.io/XMLCommentsGuide/html/4268757F-CE8D-4E6D-8502-4F7F2E22DDA3.htm) \ No newline at end of file diff --git a/Discord.Net.code-workspace b/Discord.Net.code-workspace new file mode 100644 index 000000000..709eb0e95 --- /dev/null +++ b/Discord.Net.code-workspace @@ -0,0 +1,23 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.rulers": [ + 120 + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "docs/": true, + "**/obj": true, + "**/bin": true, + "samples/": true, + } + } +} \ No newline at end of file diff --git a/Discord.Net.sln b/Discord.Net.sln index 9bb940d8c..084d8a834 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28407.52 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -18,18 +18,30 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.WS4Ne EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03_sharded_client", "samples\03_sharded_client\03_sharded_client.csproj", "{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests.Unit", "test\Discord.Net.Tests.Unit\Discord.Net.Tests.Unit.csproj", "{DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests.Integration", "test\Discord.Net.Tests.Integration\Discord.Net.Tests.Integration.csproj", "{E169E15A-E82C-45BF-8C24-C2CADB7093AA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7CF5621-7D36-433B-B337-5A2E3C101A71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers.Tests", "test\Discord.Net.Analyzers.Tests\Discord.Net.Analyzers.Tests.csproj", "{FC67057C-E92F-4E1C-98BE-46F839C8AD71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{47820065-3CFB-401C-ACEA-862BD564A404}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,18 +112,6 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.Build.0 = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -160,6 +160,78 @@ Global {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.Build.0 = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|x64.Build.0 = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Debug|x86.Build.0 = Debug|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|Any CPU.Build.0 = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|x64.ActiveCfg = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|x64.Build.0 = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|x86.ActiveCfg = Release|Any CPU + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4}.Release|x86.Build.0 = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|x64.Build.0 = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Debug|x86.Build.0 = Debug|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|Any CPU.Build.0 = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|x64.ActiveCfg = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|x64.Build.0 = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|x86.ActiveCfg = Release|Any CPU + {E169E15A-E82C-45BF-8C24-C2CADB7093AA}.Release|x86.Build.0 = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|x64.ActiveCfg = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|x64.Build.0 = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Debug|x86.Build.0 = Debug|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|Any CPU.Build.0 = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|x64.ActiveCfg = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|x64.Build.0 = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|x86.ActiveCfg = Release|Any CPU + {FC67057C-E92F-4E1C-98BE-46F839C8AD71}.Release|x86.Build.0 = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|x64.ActiveCfg = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|x64.Build.0 = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|x86.ActiveCfg = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Debug|x86.Build.0 = Debug|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|Any CPU.Build.0 = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x64.ActiveCfg = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x64.Build.0 = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.ActiveCfg = Release|Any CPU + {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -173,6 +245,12 @@ Global {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} + {E169E15A-E82C-45BF-8C24-C2CADB7093AA} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} + {FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} + {47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/Discord.Net.sln.DotSettings b/Discord.Net.sln.DotSettings new file mode 100644 index 000000000..ca75a7f1b --- /dev/null +++ b/Discord.Net.sln.DotSettings @@ -0,0 +1,16 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/Discord.Net.targets b/Discord.Net.targets index 079ec7749..febd921d1 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,13 +1,15 @@ - 2.0.0 - beta2 - RogueException + 3.0.0 + dev + latest + Discord.Net Contributors discord;discordapp - https://github.com/RogueException/Discord.Net + https://github.com/Discord-Net/Discord.Net http://opensource.org/licenses/MIT + https://github.com/Discord-Net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png git - git://github.com/RogueException/Discord.Net + git://github.com/Discord-Net/Discord.Net $(VersionSuffix)-dev @@ -16,10 +18,10 @@ $(VersionSuffix)-$(BuildNumber) build-$(BuildNumber) - + $(NoWarn);CS1573;CS1591 true true - \ No newline at end of file + diff --git a/LICENSE b/LICENSE index 3f78126e5..fb9480169 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2017 Discord.Net Contributors +Copyright (c) 2015-2021 Discord.Net Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7dc8cd788..87b46fb64 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,67 @@ # Discord.Net + [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) -[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) -[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) +[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) +[![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev) +[![Discord](https://discord.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) + +An unofficial .NET API Wrapper for the Discord client (https://discord.com). + +## Documentation -An unofficial .NET API Wrapper for the Discord client (http://discordapp.com). +- [Nightly](https://docs.stillu.cc/) + - [Latest CI repo](https://github.com/discord-net/docs-static) -Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). +## Installation -## Installation ### Stable (NuGet) + Our stable builds available from NuGet through the Discord.Net metapackage: + - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) The individual components may also be installed from NuGet: + - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) - [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) - [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) - [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) ### Unstable (MyGet) + Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). ## Compiling + In order to compile Discord.Net, you require the following: ### Using Visual Studio + - [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017) - [.NET Core SDK](https://www.microsoft.com/net/download/core) The .NET Core workload must be selected during Visual Studio installation. ### Using Command Line + - [.NET Core SDK](https://www.microsoft.com/net/download/core) ## Known Issues ### WebSockets (Win7 and earlier) + .NET Core 1.1 does not support WebSockets on Win7 and earlier. This issue has been fixed since the release of .NET Core 2.1. It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; alternatively, you may choose to install the [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package. + +## Versioning Guarantees + +This library generally abides by [Semantic Versioning](https://semver.org). Packages are published in MAJOR.MINOR.PATCH version format. + +An increment of the PATCH component always indicates that an internal-only change was made, generally a bugfix. These changes will not affect the public-facing API in any way, and are always guaranteed to be forward- and backwards-compatible with your codebase, any pre-compiled dependencies of your codebase. + +An increment of the MINOR component indicates that some addition was made to the library, and this addition is not backwards-compatible with prior versions. However, Discord.Net **does not guarantee forward-compatibility** on minor additions. In other words, we permit a limited set of breaking changes on a minor version bump. + +Due to the nature of the Discord API, we will oftentimes need to add a property to an entity to support the latest API changes. Discord.Net provides interfaces as a method of consuming entities; and as such, introducing a new field to an entity is technically a breaking change. Major version bumps generally indicate some major change to the library, and as such we are hesitant to bump the major version for every minor addition to the library. To compromise, we have decided that interfaces should be treated as **consumable only**, and your applications should typically not be implementing interfaces. (For applications where interfaces are implemented, such as in test mocks, we apologize for this inconsistency with SemVer). + +Furthermore, while we will never break the API (outside of interface changes) on minor builds, we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord. As such, a minor version increment may require you to recompile your code, and dependencies, such as addons, may also need to be recompiled and republished on the newer version. When a binary breaking change is made, the change will be noted in the release notes. + +An increment of the MAJOR component indicates that breaking changes have been made to the library; consumers should check the release notes to determine what changes need to be made. diff --git a/StyleAnalyzer.targets b/StyleAnalyzer.targets new file mode 100644 index 000000000..0794cfacb --- /dev/null +++ b/StyleAnalyzer.targets @@ -0,0 +1,9 @@ + + + + diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 54b9a1251..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: build-{build} -branches: - only: - - dev -image: Visual Studio 2017 - -nuget: - disable_publish_on_pr: true -pull_requests: - do_not_increment_build_number: true -clone_folder: C:\Projects\Discord.Net -cache: test/Discord.Net.Tests/cache.db - -environment: - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DNET_TEST_TOKEN: - secure: l7h5e7UE7yRd70hAB97kjPiQpPOShwqoBbOzEAYQ+XBd/Pre5OA33IXa3uisdUeQJP/nPFhcOsI+yn7WpuFaoQ== - DNET_TEST_GUILDID: 273160668180381696 -init: -- ps: $Env:BUILD = "$($Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0"))" - -build_script: -- ps: appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -after_build: -- ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: >- - if ($Env:APPVEYOR_REPO_TAG -eq "true") { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" - } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" - } -- ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - -test_script: -- ps: >- - if ($APPVEYOR_PULL_REQUEST_NUMBER -eq "") { - dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - } - -deploy: -- provider: NuGet - server: https://www.myget.org/F/discord-net/api/v2/package - api_key: - secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz - symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package - on: - branch: dev -- provider: NuGet - server: https://www.myget.org/F/rogueexception/api/v2/package - api_key: - secure: D+vW2O2LBf/iJb4f+q8fkyIW2VdIYIGxSYLWNrOD4BHlDBZQlJipDbNarWjUr2Kn - symbol_server: https://www.myget.org/F/rogueexception/symbols/api/v2/package - on: - branch: dev \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..9aa0e5788 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,41 @@ +variables: + buildConfiguration: Release + buildTag: $[ startsWith(variables['Build.SourceBranch'], 'refs/tags') ] + buildNumber: $[ variables['Build.BuildNumber'] ] + +trigger: + tags: + include: + - '*' + branches: + include: + - '*' + +jobs: +- job: Linux + pool: + vmImage: 'ubuntu-latest' + steps: + - template: azure/build.yml + +- job: Windows_build + pool: + vmImage: 'windows-latest' + condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev') + steps: + - template: azure/build.yml + +- job: Windows_deploy + pool: + vmImage: 'windows-latest' + condition: | + and ( + succeeded(), + or ( + eq(variables['Build.SourceBranch'], 'refs/heads/dev'), + eq(variables['buildTag'], True) + ) + ) + steps: + - template: azure/build.yml + - template: azure/deploy.yml diff --git a/azure/build.yml b/azure/build.yml new file mode 100644 index 000000000..63ba93964 --- /dev/null +++ b/azure/build.yml @@ -0,0 +1,27 @@ +steps: +- task: DotNetCoreCLI@2 + inputs: + command: 'restore' + projects: 'Discord.Net.sln' + feedsToUse: 'select' + verbosityRestore: 'Minimal' + displayName: Restore packages + +- script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + displayName: Build projects + +- script: dotnet test "test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) --logger trx + displayName: Unit Tests + +- script: dotnet test "test/Discord.Net.Analyzers.Tests/Discord.Net.Analyzers.Tests.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) --logger trx + displayName: Analyzer Tests + +# - script: dotnet test "test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) --logger trx +# condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) + +- task: PublishTestResults@2 + displayName: Publish test results + condition: succeededOrFailed() + inputs: + testRunner: VSTest + testResultsFiles: '**/*.trx' diff --git a/azure/deploy.yml b/azure/deploy.yml new file mode 100644 index 000000000..61994299e --- /dev/null +++ b/azure/deploy.yml @@ -0,0 +1,35 @@ +steps: +- script: | + dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + displayName: Pack projects + +- task: NuGetCommand@2 + displayName: Pack metapackage (release mode) + condition: eq(variables['buildTag'], True) + inputs: + command: 'pack' + packagesToPack: 'src/Discord.Net/Discord.Net.nuspec' + versioningScheme: 'off' + +- task: NuGetCommand@2 + displayName: Pack metapackage + condition: eq(variables['buildTag'], False) + inputs: + command: 'pack' + packagesToPack: 'src/Discord.Net/Discord.Net.nuspec' + versioningScheme: 'off' + buildProperties: 'suffix=-$(buildNumber)' + +- task: NuGetCommand@2 + displayName: Push to NuGet + inputs: + command: push + nuGetFeedType: external + packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg' + publishFeedCredentials: myget-discord diff --git a/azure/docs.bat b/azure/docs.bat new file mode 100644 index 000000000..504e12991 --- /dev/null +++ b/azure/docs.bat @@ -0,0 +1,15 @@ +ECHO clone docs-static +git clone git@github.com:discord-net/docs-static.git || EXIT /B 1 + +ECHO remove old 'latest' +ECHO Y | RMDIR /S docs-static\latest || EXIT /B 1 + +ECHO build docs +docfx.console\tools\docfx.exe docs/docfx.json -o docs-staging || EXIT /B 1 +ROBOCOPY docs-staging\_site docs-static\latest /MIR + +ECHO commit and deploy +git config --global user.name "Discord.Net CI Robot" && git config --global user.email "robot@foxbot.me" +git -C docs-static add -A || EXIT /B 1 +git -C docs-static commit -m "[ci deploy] %date% %time%: %Build.BuildId%" || EXIT /B 1 +git -C docs-static push --force || EXIT /B 1 diff --git a/azure/docs.yml b/azure/docs.yml new file mode 100644 index 000000000..441864ff8 --- /dev/null +++ b/azure/docs.yml @@ -0,0 +1,16 @@ +steps: + - task: InstallSSHKey@0 + displayName: Install deploy key for docs + inputs: + knownHostsEntry: '|1|gFD+Dvd+09xvjlKReWSg3wE7q1A=|WJnD0RZ5H4HX5U0nA4Kt+R5yf+w= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' + sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN1oXiGqKblqfO+vr3cMLSiV6paD5BT+2RXfeCpVkRWSFCB7dfP2m1osJSBqqoCHvJGfbX1brGa+3fnmBgbqQ9vl1NkAmdjHYz4yfTKAt6KShYKXmPpTWBfbAqO2DUzTfTJ18XxNutK931vbWRtOjAMt7Aohw0FYm541QPr2IHIabTvVTPujVExHnMTB9cyKa8xzMD9W3zRLXJbhwOi0LtpgxX6OC/HpwdWod6TfxKdnkPMmVCOo7GTJITyd1GEFg+eNShBIaAZ557nAr8rm2ybEqYvhqFQI0cYMXbfr934yPoNN5yONE1PxDarr1T3GE3ZCWQw2Rc9CAKxrMTez7h + sshKeySecureFile: docs-static_rsa + + - task: NuGetCommand@2 + displayName: Install DocFx + inputs: + command: custom + arguments: install docfx.console -ExcludeVersion + + - script: azure/docs.bat + displayName: Build and deploy docs diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 296b6d1cb..74290e595 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,46 +1,60 @@ # Contributing to Docs -I don't really have any strict conditions for writing documentation, -but just keep these few guidelines in mind: +First of all, thank you for your interest in contributing to our +documentation work. We really appreciate it! That being said, +there are several guidelines you should attempt to follow when adding +to/changing the documentation. -* Keep code samples in the `guides/samples` folder -* When referencing an object in the API, link to it's page in the -API documentation. -* Documentation should be written in clear and proper English* - -\* If anyone is interested in translating documentation into other -languages, please open an issue or contact me on -Discord (`foxbot#0282`). +## General Guidelines -### Layout +* Keep code samples in each section's `samples` folder +* When referencing an object in the API, link to its page in the + API documentation +* Documentation should be written in an FAQ/Wiki-style format +* Documentation should be written in clear and proper English* -Documentation should be written in a FAQ/Wiki style format. +\* If anyone is interested in translating documentation into other +languages, please open an issue or contact `foxbot#0282` on +Discord. -Recommended reads: +## XML Docstrings Guidelines -* http://docs.microsoft.com -* http://flask.pocoo.org/docs/0.12/ +* When using the `` tag, use concise verbs. For example: -Style consistencies: +```cs +/// Gets or sets the guild user in this object. +public IGuildUser GuildUser { get; set; } +``` -* Use a ruler set at 70 characters +* The `` tag should not be more than 3 lines long. Consider +simplifying the terms or using the `` tag instead. +* When using the `` tag, put the code sample within the +`src/Discord.Net.Examples` project under the corresponding path of +the object and surround the code with a `#region` tag. +* If the remarks you are looking to write are too long, consider +writing a shorter version in the XML docs while keeping the longer +version in the `overwrites` folder using the DocFX overwrites syntax. + * You may find an example of this in the samples provided within + the folder. + +## Docs Guide Guidelines + +* Use a ruler set at 70 characters (use the docs workspace provided +if you are using Visual Studio Code) * Links should use long syntax * Pages should be short and concise, not broad and long Example of long link syntax: -``` +```md Please consult the [API Documentation] for more information. [API Documentation]: xref:System.String ``` -### Compiling - -Documentation is compiled into a static site using [DocFx]. -We currently use the most recent build off the dev branch. - -After making changes, compile your changes into the static site with -`docfx`. You can also view your changes live with `docfx --serve`. +## Recommended Reads -[DocFx]: https://dotnet.github.io/docfx/ \ No newline at end of file +* [Microsoft Docs](https://docs.microsoft.com) +* [Flask Docs](https://flask.pocoo.org/docs/1.0/) +* [DocFX Manual](https://dotnet.github.io/docfx/) +* [Sandcastle XML Guide](http://ewsoftware.github.io/XMLCommentsGuide) \ No newline at end of file diff --git a/docs/Discord.Net.Docs.code-workspace b/docs/Discord.Net.Docs.code-workspace new file mode 100644 index 000000000..d9f442869 --- /dev/null +++ b/docs/Discord.Net.Docs.code-workspace @@ -0,0 +1,21 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.rulers": [ + 70 + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "obj/": true, + "_site/": true, + } + } +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index a672330d4..4a06dccab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,15 @@ # Instructions for Building Documentation -The documentation for the Discord.NET library uses [DocFX][docfx-main]. [Instructions for installing this tool can be found here.][docfx-installing] +The documentation for the Discord.Net library uses [DocFX][docfx-main]. +Instructions for installing this tool can be found [here][docfx-installing]. 1. Navigate to the root of the repository. -2. (Optional) If you intend to target a specific version, ensure that you -have the correct version checked out. -3. Build the library. Run `dotnet build` in the root of this repository. - Ensure that the build passes without errors. -4. Build the docs using `docfx .\docs\docfx.json`. Add the `--serve` parameter -to preview the site locally. Some elements of the page may appear incorrect -when not hosted by a server. - - Remarks: According to the docfx website, this tool does work on Linux under mono. +2. Build the docs using `docfx docs/docfx.json`. Add the `--serve` + parameter to preview the site locally. Some elements of the page + may appear incorrectly when hosted offline. + +Please note that if you intend to target a specific version, ensure +that you have the correct version checked out. [docfx-main]: https://dotnet.github.io/docfx/ -[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html +[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html \ No newline at end of file diff --git a/docs/_overwrites/Commands/CommandException.Overwrite.md b/docs/_overwrites/Commands/CommandException.Overwrite.md new file mode 100644 index 000000000..166a011de --- /dev/null +++ b/docs/_overwrites/Commands/CommandException.Overwrite.md @@ -0,0 +1,31 @@ +--- +uid: Discord.Commands.CommandException +remarks: *content +--- + +This @System.Exception class is typically used when diagnosing +an error thrown during the execution of a command. You will find the +thrown exception passed into +[LogMessage.Exception](xref:Discord.LogMessage.Exception), which is +sent to your [CommandService.Log](xref:Discord.Commands.CommandService.Log) +event handler. + +--- +uid: Discord.Commands.CommandException +example: [*content] +--- + +You may use this information to handle runtime exceptions after +execution. Below is an example of how you may use this: + +```cs +public Task LogHandlerAsync(LogMessage logMessage) +{ + // Note that this casting method requires C#7 and up. + if (logMessage?.Exception is CommandException cmdEx) + { + Console.WriteLine($"{cmdEx.GetBaseException().GetType()} was thrown while executing {cmdEx.Command.Aliases.First()} in {cmdEx.Context.Channel} by {cmdEx.Context.User}."); + } + return Task.CompletedTask; +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md new file mode 100644 index 000000000..d47980df7 --- /dev/null +++ b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md @@ -0,0 +1,22 @@ +--- +uid: Discord.Commands.DontAutoLoadAttribute +remarks: *content +--- + +The attribute can be applied to a public class that inherits +@Discord.Commands.ModuleBase. By applying this attribute, +@Discord.Commands.CommandService.AddModulesAsync* will not discover and +add the marked module to the CommandService. + +--- +uid: Discord.Commands.DontAutoLoadAttribute +example: [*content] +--- + +```cs +[DontAutoLoad] +public class MyModule : ModuleBase +{ + // ... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md new file mode 100644 index 000000000..950d2990c --- /dev/null +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.DontInjectAttribute +remarks: *content +--- + +The attribute can be applied to a public settable property inside a +@Discord.Commands.ModuleBase based class. By applying this attribute, +the marked property will not be automatically injected of the +dependency. See @Guides.Commands.DI to learn more. + +--- +uid: Discord.Commands.DontInjectAttribute +example: [*content] +--- + +```cs +public class MyModule : ModuleBase +{ + [DontInject] + public MyService MyService { get; set; } + + public MyModule() + { + MyService = new MyService(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/ICommandContext.Inclusion.md b/docs/_overwrites/Commands/ICommandContext.Inclusion.md new file mode 100644 index 000000000..4c1257b23 --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Inclusion.md @@ -0,0 +1,5 @@ +An example of how this class is used the command system can be seen +below: + +[!code[Sample module](../../guides/commands/samples/intro/empty-module.cs)] +[!code[Command handler](../../guides/commands/samples/intro/command_handler.cs)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/ICommandContext.Overwrite.md b/docs/_overwrites/Commands/ICommandContext.Overwrite.md new file mode 100644 index 000000000..d9e50b46d --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.ICommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.CommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.SocketCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.ShardCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md new file mode 100644 index 000000000..75b9f93a5 --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -0,0 +1,103 @@ +--- +uid: Discord.Commands.PreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on module-level or +method-level for a command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on parameter-level for a +command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.PreconditionAttribute +example: [*content] +--- + +The following example creates a precondition to see if the user has +sufficient role required to access the command. + +```cs +public class RequireRoleAttribute : PreconditionAttribute +{ + private readonly ulong _roleId; + + public RequireRoleAttribute(ulong roleId) + { + _roleId = roleId; + } + + public override async Task CheckPermissionsAsync(ICommandContext context, + CommandInfo command, IServiceProvider services) + { + var guildUser = context.User as IGuildUser; + if (guildUser == null) + return PreconditionResult.FromError("This command cannot be executed outside of a guild."); + + var guild = guildUser.Guild; + if (guild.Roles.All(r => r.Id != _roleId)) + return PreconditionResult.FromError( + $"The guild does not have the role ({_roleId}) required to access this command."); + + return guildUser.RoleIds.Any(rId => rId == _roleId) + ? PreconditionResult.FromSuccess() + : PreconditionResult.FromError("You do not have the sufficient role required to access this command."); + } +} +``` + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +example: [*content] +--- + +The following example creates a precondition on a parameter-level to +see if the targeted user has a lower hierarchy than the user who +executed the command. + +```cs +public class RequireHierarchyAttribute : ParameterPreconditionAttribute +{ + public override async Task CheckPermissionsAsync(ICommandContext context, + ParameterInfo parameter, object value, IServiceProvider services) + { + // Hierarchy is only available under the socket variant of the user. + if (!(context.User is SocketGuildUser guildUser)) + return PreconditionResult.FromError("This command cannot be used outside of a guild."); + + SocketGuildUser targetUser; + switch (value) + { + case SocketGuildUser targetGuildUser: + targetUser = targetGuildUser; + break; + case ulong userId: + targetUser = await context.Guild.GetUserAsync(userId).ConfigureAwait(false) as SocketGuildUser; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (targetUser == null) + return PreconditionResult.FromError("Target user not found."); + + if (guildUser.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("You cannot target anyone else whose roles are higher than yours."); + + var currentUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false) as SocketGuildUser; + if (currentUser?.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("The bot's role is lower than the targeted user."); + + return PreconditionResult.FromSuccess(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md new file mode 100644 index 000000000..daa0c33cb --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md @@ -0,0 +1,6 @@ +A "precondition" in the command system is used to determine if a +condition is met before entering the command task. Using a +precondition may aid in keeping a well-organized command logic. + +The most common use case being whether a user has sufficient +permission to execute the command. \ No newline at end of file diff --git a/docs/_overwrites/Common/DiscordComparers.Overwrites.md b/docs/_overwrites/Common/DiscordComparers.Overwrites.md new file mode 100644 index 000000000..cbff7cf74 --- /dev/null +++ b/docs/_overwrites/Common/DiscordComparers.Overwrites.md @@ -0,0 +1,29 @@ +--- +uid: Discord.DiscordComparers.ChannelComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare channels. + +--- +uid: Discord.DiscordComparers.GuildComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare guilds. + +--- +uid: Discord.DiscordComparers.MessageComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare messages. + +--- +uid: Discord.DiscordComparers.RoleComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare roles. + +--- +uid: Discord.DiscordComparers.UserComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare users. diff --git a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md new file mode 100644 index 000000000..85c292dd2 --- /dev/null +++ b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md @@ -0,0 +1,69 @@ +--- +uid: Discord.EmbedBuilder +seealso: + - linkId: Discord.EmbedFooterBuilder + - linkId: Discord.EmbedAuthorBuilder + - linkId: Discord.EmbedFieldBuilder +remarks: *content +--- + +This builder class is used to build an @Discord.Embed (rich embed) +object that will be ready to be sent via @Discord.IMessageChannel.SendMessageAsync* +after @Discord.EmbedBuilder.Build* is called. + +--- +uid: Discord.EmbedBuilder +example: [*content] +--- + +#### Basic Usage + +The example below builds an embed and sends it to the chat using the +command system. + +```cs +[Command("embed")] +public async Task SendRichEmbedAsync() +{ + var embed = new EmbedBuilder + { + // Embed property can be set within object initializer + Title = "Hello world!", + Description = "I am a description set by initializer." + }; + // Or with methods + embed.AddField("Field title", + "Field value. I also support [hyperlink markdown](https://example.com)!") + .WithAuthor(Context.Client.CurrentUser) + .WithFooter(footer => footer.Text = "I am a footer.") + .WithColor(Color.Blue) + .WithTitle("I overwrote \"Hello world!\"") + .WithDescription("I am a description.") + .WithUrl("https://example.com") + .WithCurrentTimestamp(); + + //Your embed needs to be built before it is able to be sent + await ReplyAsync(embed: embed.Build()); +} +``` + +![Embed Example](images/embed-example.png) + +#### Usage with Local Images + +The example below sends an image and has the image embedded in the rich +embed. See @Discord.IMessageChannel.SendFileAsync* for more information +about uploading a file or image. + +```cs +[Command("embedimage")] +public async Task SendEmbedWithImageAsync() +{ + var fileName = "image.png"; + var embed = new EmbedBuilder() + { + ImageUrl = $"attachment://{fileName}" + }.Build(); + await Context.Channel.SendFileAsync(fileName, embed: embed); +} +``` diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md new file mode 100644 index 000000000..a9d3539ed --- /dev/null +++ b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md @@ -0,0 +1,25 @@ +The example will build a rich embed with an author field, a footer +field, and 2 normal fields using an @Discord.EmbedBuilder: + +```cs +var exampleAuthor = new EmbedAuthorBuilder() + .WithName("I am a bot") + .WithIconUrl("https://discord.com/assets/e05ead6e6ebc08df9291738d0aa6986d.png"); +var exampleFooter = new EmbedFooterBuilder() + .WithText("I am a nice footer") + .WithIconUrl("https://discord.com/assets/28174a34e77bb5e5310ced9f95cb480b.png"); +var exampleField = new EmbedFieldBuilder() + .WithName("Title of Another Field") + .WithValue("I am an [example](https://example.com).") + .WithInline(true); +var otherField = new EmbedFieldBuilder() + .WithName("Title of a Field") + .WithValue("Notice how I'm inline with that other field next to me.") + .WithInline(true); +var embed = new EmbedBuilder() + .AddField(exampleField) + .AddField(otherField) + .WithAuthor(exampleAuthor) + .WithFooter(exampleFooter) + .Build(); +``` diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md new file mode 100644 index 000000000..c633c29b1 --- /dev/null +++ b/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md @@ -0,0 +1,20 @@ +--- +uid: Discord.EmbedAuthorBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFooterBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFieldBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/IEmote.Inclusion.md b/docs/_overwrites/Common/IEmote.Inclusion.md new file mode 100644 index 000000000..cf93c7eb5 --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Inclusion.md @@ -0,0 +1,26 @@ +The sample below sends a message and adds an @Discord.Emoji and a custom +@Discord.Emote to the message. + +```cs +public async Task SendAndReactAsync(ISocketMessageChannel channel) +{ + var message = await channel.SendMessageAsync("I am a message."); + + // Creates a Unicode-based emoji based on the Unicode string. + // This is effectively the same as new Emoji("💕"). + var heartEmoji = new Emoji("\U0001f495"); + // Reacts to the message with the Emoji. + await message.AddReactionAsync(heartEmoji); + + // Parses a custom emote based on the provided Discord emote format. + // Please note that this does not guarantee the existence of + // the emote. + var emote = Emote.Parse("<:thonkang:282745590985523200>"); + // Reacts to the message with the Emote. + await message.AddReactionAsync(emote); +} +``` + +#### Result + +![React Example](images/react-example.png) \ No newline at end of file diff --git a/docs/_overwrites/Common/IEmote.Overwrites.md b/docs/_overwrites/Common/IEmote.Overwrites.md new file mode 100644 index 000000000..034533d1d --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Overwrites.md @@ -0,0 +1,81 @@ +--- +uid: Discord.IEmote +seealso: + - linkId: Discord.Emote + - linkId: Discord.Emoji + - linkId: Discord.GuildEmote + - linkId: Discord.IUserMessage +remarks: *content +--- + +This interface is often used with reactions. It can represent an +unicode-based @Discord.Emoji, or a custom @Discord.Emote. + +--- +uid: Discord.Emote +seealso: + - linkId: Discord.IEmote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emote format is `<:emoteName:emoteId>`. This can be +> obtained by escaping with a `\` in front of the emote using the +> Discord chat client. + +This class represents a custom emoji. This type of emoji can be +created via the @Discord.Emote.Parse* or @Discord.Emote.TryParse* +method. + +--- +uid: Discord.Emoji +seealso: + - linkId: Discord.Emote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emoji format is Unicode-based. This means only +> something like `🙃` or `\U0001f643` would work, instead of +> `:upside_down:`. +> +> A Unicode-based emoji can be obtained by escaping with a `\` in +> front of the emote using the Discord chat client or by looking up on +> [Emojipedia](https://emojipedia.org). + +This class represents a standard Unicode-based emoji. This type of emoji +can be created by passing the Unicode into the constructor. + +--- +uid: Discord.IEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emoji +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.GuildEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/ObjectProperties.Overwrites.md b/docs/_overwrites/Common/ObjectProperties.Overwrites.md new file mode 100644 index 000000000..e9c365d39 --- /dev/null +++ b/docs/_overwrites/Common/ObjectProperties.Overwrites.md @@ -0,0 +1,174 @@ +--- +uid: Discord.GuildChannelProperties +example: [*content] +--- + +The following example uses @Discord.IGuildChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as IGuildChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.Name = "new-name"; + x.Position = channel.Position - 1; +}); +``` + +--- +uid: Discord.TextChannelProperties +example: [*content] +--- + +The following example uses @Discord.ITextChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as ITextChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.Name = "cool-guys-only"; + x.Topic = "This channel is only for cool guys and adults!!!"; + x.Position = channel.Position - 1; + x.IsNsfw = true; +}); +``` + +--- +uid: Discord.VoiceChannelProperties +example: [*content] +--- + +The following example uses @Discord.IVoiceChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as IVoiceChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.UserLimit = 5; +}); +``` + +--- +uid: Discord.EmoteProperties +example: [*content] +--- + +The following example uses @Discord.IGuild.ModifyEmoteAsync* to +apply changes specified in the properties, + +```cs +await guild.ModifyEmoteAsync(x => +{ + x.Name = "blobo"; +}); +``` + +--- +uid: Discord.MessageProperties +example: [*content] +--- + +The following example uses @Discord.IUserMessage.ModifyAsync* to +apply changes specified in the properties, + +```cs +var message = await channel.SendMessageAsync("boo"); +await Task.Delay(TimeSpan.FromSeconds(1)); +await message.ModifyAsync(x => x.Content = "boi"); +``` + +--- +uid: Discord.GuildProperties +example: [*content] +--- + +The following example uses @Discord.IGuild.ModifyAsync* to +apply changes specified in the properties, + +```cs +var guild = _client.GetGuild(id); +if (guild == null) return; + +await guild.ModifyAsync(x => +{ + x.Name = "VERY Fast Discord Running at Incredible Hihg Speed"; +}); +``` + +--- +uid: Discord.RoleProperties +example: [*content] +--- + +The following example uses @Discord.IRole.ModifyAsync* to +apply changes specified in the properties, + +```cs +var role = guild.GetRole(id); +if (role == null) return; + +await role.ModifyAsync(x => +{ + x.Name = "cool boi"; + x.Color = Color.Gold; + x.Hoist = true; + x.Mentionable = true; +}); +``` + +--- +uid: Discord.GuildUserProperties +example: [*content] +--- + +The following example uses @Discord.IGuildUser.ModifyAsync* to +apply changes specified in the properties, + +```cs +var user = guild.GetUser(id); +if (user == null) return; + +await user.ModifyAsync(x => +{ + x.Nickname = "I need healing"; +}); +``` + +--- +uid: Discord.SelfUserProperties +example: [*content] +--- + +The following example uses @Discord.ISelfUser.ModifyAsync* to +apply changes specified in the properties, + +```cs +await selfUser.ModifyAsync(x => +{ + x.Username = "Mercy"; +}); +``` + +--- +uid: Discord.WebhookProperties +example: [*content] +--- + +The following example uses @Discord.IWebhook.ModifyAsync* to +apply changes specified in the properties, + +```cs +await webhook.ModifyAsync(x => +{ + x.Name = "very fast fox"; + x.ChannelId = newChannelId; +}); +``` \ No newline at end of file diff --git a/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md new file mode 100644 index 000000000..29b547e49 --- /dev/null +++ b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md @@ -0,0 +1,24 @@ +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +remarks: *content +--- + +This attribute is used to override a command parameter's type reading +behaviour. This may be useful when you have multiple custom +@Discord.Commands.TypeReader and would like to specify one. + +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +examples: [*content] +--- + +The following example will override the @Discord.Commands.TypeReader +of @Discord.IUser to `MyUserTypeReader`. + +```cs +public async Task PrintUserAsync( + [OverrideTypeReader(typeof(MyUserTypeReader))] IUser user) +{ + //... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Common/images/embed-example.png b/docs/_overwrites/Common/images/embed-example.png new file mode 100644 index 000000000..f23fb4d70 Binary files /dev/null and b/docs/_overwrites/Common/images/embed-example.png differ diff --git a/docs/_overwrites/Common/images/react-example.png b/docs/_overwrites/Common/images/react-example.png new file mode 100644 index 000000000..822857d3d Binary files /dev/null and b/docs/_overwrites/Common/images/react-example.png differ diff --git a/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll b/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll new file mode 100644 index 000000000..095ea46ef Binary files /dev/null and b/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll differ diff --git a/docs/_template/description-generator/plugins/LICENSE b/docs/_template/description-generator/plugins/LICENSE new file mode 100644 index 000000000..eb92c0a03 --- /dev/null +++ b/docs/_template/description-generator/plugins/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Still Hsu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_template/last-modified/plugins/LICENSE b/docs/_template/last-modified/plugins/LICENSE new file mode 100644 index 000000000..d74703f3d --- /dev/null +++ b/docs/_template/last-modified/plugins/LICENSE @@ -0,0 +1,29 @@ +MIT License + +Copyright (c) 2018 Still Hsu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +============================================================================== + +Humanizer (https://github.com/Humanizr/Humanizer) +The MIT License (MIT) +Copyright (c) .NET Foundation and Contributors + +============================================================================== \ No newline at end of file diff --git a/docs/_template/last-modified/plugins/LastModifiedPostProcessor.dll b/docs/_template/last-modified/plugins/LastModifiedPostProcessor.dll new file mode 100644 index 000000000..ccb2b0e37 Binary files /dev/null and b/docs/_template/last-modified/plugins/LastModifiedPostProcessor.dll differ diff --git a/docs/_template/last-modified/plugins/LibGit2Sharp.dll b/docs/_template/last-modified/plugins/LibGit2Sharp.dll new file mode 100644 index 000000000..316515072 Binary files /dev/null and b/docs/_template/last-modified/plugins/LibGit2Sharp.dll differ diff --git a/docs/_template/last-modified/plugins/LibGit2Sharp.dll.config b/docs/_template/last-modified/plugins/LibGit2Sharp.dll.config new file mode 100644 index 000000000..21a2b5601 --- /dev/null +++ b/docs/_template/last-modified/plugins/LibGit2Sharp.dll.config @@ -0,0 +1,4 @@ + + + + diff --git a/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..f1f45e7d0 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..793f4483a Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/alpine.3.9-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/alpine.3.9-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..49186df25 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/alpine.3.9-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/debian-arm64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/debian-arm64/libgit2-ef5a385.so new file mode 100644 index 000000000..11ef799ab Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/debian-arm64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..dd0f7ffcd Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..5cd5e46b0 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..7d1aafbed Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..be1be9322 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..6eb5c8b0c Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..1ec4b01f5 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/linux-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/osx/libgit2-a904fc6.dylib b/docs/_template/last-modified/plugins/lib/osx/libgit2-a904fc6.dylib new file mode 100644 index 000000000..041256cc3 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/osx/libgit2-a904fc6.dylib differ diff --git a/docs/_template/last-modified/plugins/lib/osx/libgit2-ef5a385.dylib b/docs/_template/last-modified/plugins/lib/osx/libgit2-ef5a385.dylib new file mode 100644 index 000000000..81f71d6ea Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/osx/libgit2-ef5a385.dylib differ diff --git a/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..6166cb4c8 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..3d194a97f Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/ubuntu.16.04-arm64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/ubuntu.16.04-arm64/libgit2-ef5a385.so new file mode 100644 index 000000000..a3282b0d2 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/ubuntu.16.04-arm64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-a904fc6.so b/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-a904fc6.so new file mode 100644 index 000000000..b3528eee1 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-a904fc6.so differ diff --git a/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-ef5a385.so b/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-ef5a385.so new file mode 100644 index 000000000..0360ce3e0 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-ef5a385.so differ diff --git a/docs/_template/last-modified/plugins/lib/win32/x64/git2-a904fc6.dll b/docs/_template/last-modified/plugins/lib/win32/x64/git2-a904fc6.dll new file mode 100644 index 000000000..dd0ca9cd3 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/win32/x64/git2-a904fc6.dll differ diff --git a/docs/_template/last-modified/plugins/lib/win32/x64/git2-ef5a385.dll b/docs/_template/last-modified/plugins/lib/win32/x64/git2-ef5a385.dll new file mode 100644 index 000000000..7ffcdf97f Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/win32/x64/git2-ef5a385.dll differ diff --git a/docs/_template/last-modified/plugins/lib/win32/x86/git2-a904fc6.dll b/docs/_template/last-modified/plugins/lib/win32/x86/git2-a904fc6.dll new file mode 100644 index 000000000..627d344cc Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/win32/x86/git2-a904fc6.dll differ diff --git a/docs/_template/last-modified/plugins/lib/win32/x86/git2-ef5a385.dll b/docs/_template/last-modified/plugins/lib/win32/x86/git2-ef5a385.dll new file mode 100644 index 000000000..9fe2e3e01 Binary files /dev/null and b/docs/_template/last-modified/plugins/lib/win32/x86/git2-ef5a385.dll differ diff --git a/docs/_template/light-dark-theme/docfx-material-license.md b/docs/_template/light-dark-theme/docfx-material-license.md new file mode 100644 index 000000000..4576c42b1 --- /dev/null +++ b/docs/_template/light-dark-theme/docfx-material-license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Oscar Vásquez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_template/light-dark-theme/partials/affix.tmpl.partial b/docs/_template/light-dark-theme/partials/affix.tmpl.partial new file mode 100644 index 000000000..b3ce60b5c --- /dev/null +++ b/docs/_template/light-dark-theme/partials/affix.tmpl.partial @@ -0,0 +1,34 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + diff --git a/docs/_template/light-dark-theme/partials/head.tmpl.partial b/docs/_template/light-dark-theme/partials/head.tmpl.partial new file mode 100644 index 000000000..c214e7548 --- /dev/null +++ b/docs/_template/light-dark-theme/partials/head.tmpl.partial @@ -0,0 +1,33 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + {{#_description}}{{/_description}} + + + + + + + + + + + + + + + + + + + + + {{#_noindex}}{{/_noindex}} + {{#_enableSearch}}{{/_enableSearch}} + {{#_enableNewTab}}{{/_enableNewTab}} + diff --git a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial new file mode 100644 index 000000000..3142f8c54 --- /dev/null +++ b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial @@ -0,0 +1,8 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + + + \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/cornerify.js b/docs/_template/light-dark-theme/styles/cornerify.js new file mode 100644 index 000000000..4430f2d01 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/cornerify.js @@ -0,0 +1,3 @@ +window.onload = function (e) { + $('img').corner(); +} diff --git a/docs/_template/light-dark-theme/styles/dark.css b/docs/_template/light-dark-theme/styles/dark.css new file mode 100644 index 000000000..dd55ae949 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/dark.css @@ -0,0 +1,322 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ + +@import url('vs2015.css'); +html, +body { + background: #212121; + color: #C0C0C0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #E0E0E0; +} + +button, +a { + color: #64B5F6; +} + +.sidenav{ + background-color: rgb(30, 30, 30); +} + +button:hover, +button:focus, +a:hover, +a:focus, +.btn:focus, +.btn:hover{ + color: #2196F3; +} + +a.disable, +a.disable:hover { + color: #EEEEEE; +} + +.divider { + color: #37474F; +} + +hr { + border-color: #37474F; +} + +.subnav { + background: #383838 +} + +.inheritance h5, +.inheritedMembers h5 { + border-bottom: 1px solid #37474F; +} + +article h4 { + border-bottom: 1px solid #37474F; +} + +.docs-search { + background: #424242; +} + +.search-results-group-heading { + color: #424242; +} + +.search-close { + color: #424242; +} + +.sidetoc { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.sideaffix { + overflow: visible; +} + +.sideaffix>div.contribution>ul>li>a.contribution-link:hover { + background-color: #333333; +} + +/* toc */ + +.toc .nav>li>a { + color: rgb(218, 218, 218); +} + +.toc .nav>li>a:hover, +.toc .nav>li>a:focus { + color: #E0E0E0; +} + +.toc .nav>li.active>a { + color: #90CAF9; +} + +.toc .nav>li.active>a:hover, +.toc .nav>li.active>a:focus { + background-color: #37474F; + color: #4FC3F7; +} + +.sidefilter { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.affix ul>li>a:hover { + background: none; + color: #EEEEEE; +} + +.affix ul>li.active>a, +.affix ul>li.active>a:before { + color: #B3E5FC; +} + +.affix ul>li>a { + color: #EEEEEE; +} + +.affix>ul>li.active>a, +.affix>ul>li.active>a:before { + color: #B3E5FC; +} + +.tryspan { + border-color: #37474F; +} + +.footer { + border-top: 1px solid #5F5F5F; + background: #616161; +} + +/* alert */ +.alert-info { + color: #d9edf7; + background: #004458; + border-color: #005873; +} + +.alert-warning { + color: #fffaf2; + background: #80551a; + border-color: #99661f; +} + +.alert-danger { + color: #fff2f2; + background: #4d0000; + border-color: #660000; +} + +/* For tabbed content */ + +.tabGroup { + margin-top: 1rem; +} + +.tabGroup ul[role="tablist"] { + margin: 0; + padding: 0; + list-style: none; +} + +.tabGroup ul[role="tablist"]>li { + list-style: none; + display: inline-block; +} + +.tabGroup a[role="tab"] { + color: white; + box-sizing: border-box; + display: inline-block; + padding: 5px 7.5px; + text-decoration: none; + border-bottom: 2px solid #fff; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus, +.tabGroup a[role="tab"][aria-selected="true"] { + border-bottom: 2px solid #607D8B; +} + +.tabGroup a[role="tab"][aria-selected="true"] { + color: #81D4FA; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus { + color: #29B6F6; +} + +.tabGroup a[role="tab"]:focus { + outline: 1px solid #607D8B; + outline-offset: -1px; +} + +@media (min-width: 768px) { + .tabGroup a[role="tab"] { + padding: 5px 15px; + } +} + +.tabGroup section[role="tabpanel"] { + border: 1px solid #607D8B; + padding: 15px; + margin: 0; + overflow: hidden; +} + +.tabGroup section[role="tabpanel"]>.codeHeader, +.tabGroup section[role="tabpanel"]>pre { + margin-left: -16px; + margin-right: -16px; +} + +.tabGroup section[role="tabpanel"]> :first-child { + margin-top: 0; +} + +.tabGroup section[role="tabpanel"]>pre:last-child { + display: block; + margin-bottom: -16px; +} + +.mainContainer[dir='rtl'] main ul[role="tablist"] { + margin: 0; +} + +/* code */ + +code { + color: white; + background-color: #4a4c52; + border-radius: 4px; + padding: 3px 7px; +} + +pre { + background-color: #282a36; +} + +/* table */ + +.table-striped>tbody>tr:nth-of-type(odd) { + background-color: #333333; + color: #d3d3d3 +} + +tbody>tr { + background-color: #424242; + color: #c0c0c0 +} + +.table>tbody+tbody { + border-top: 2px solid rgb(173, 173, 173) +} + +/* top navbar */ +.navbar-inverse[role="navigation"] { + background-color: #2C2F33; +} + +/* select */ + +select { + background-color: #3b3b3b; + border-color: #2e2e2e; +} + +/* + Following code regarding collapse container are fetched + or modified from the Materialize project. + + The MIT License (MIT) + Copyright (c) 2014-2018 Materialize + https://github.com/Dogfalo/materialize +*/ + +/* all collapse container */ +.collapse-container.last-modified { + -webkit-box-shadow: 0 2px 2px 0 rgba(50, 50, 50, 0.64), 0 3px 1px -2px rgba(50, 50, 50, 0.62), 0 1px 5px 0 rgba(50, 50, 50, 0.7); + box-shadow: 0 2px 2px 0 rgba(50, 50, 50, 0.64), 0 3px 1px -2px rgba(50, 50, 50, 0.62), 0 1px 5px 0 rgba(50, 50, 50, 0.7); + border-top: 1px solid rgba(96, 96, 96, 0.7); + border-right: 1px solid rgba(96, 96, 96, 0.7); + border-left: 1px solid rgba(96, 96, 96, 0.7); +} + +/* header */ +.collapse-container.last-modified>:nth-child(odd) { + background-color: #3f3f3f; + border-bottom: 1px solid rgba(96, 96, 96, 0.7); +} + +/* body */ +.collapse-container.last-modified>:nth-child(even) { + border-bottom: 1px solid rgba(96, 96, 96, 0.7); + background-color: inherit; +} + +span.arrow-d{ + border-top: 5px solid white +} + +span.arrow-r{ + border-left: 5px solid white +} + +.logo-switcher { + background: url("../marketing/logo/SVG/Combinationmark White.svg") no-repeat; +} diff --git a/docs/_template/light-dark-theme/styles/docfx.vendor.minify.css b/docs/_template/light-dark-theme/styles/docfx.vendor.minify.css new file mode 100644 index 000000000..771cb0b7e --- /dev/null +++ b/docs/_template/light-dark-theme/styles/docfx.vendor.minify.css @@ -0,0 +1,1021 @@ +@font-face { + font-family: 'Glyphicons Halflings'; + font-display: fallback; + src: url(../fonts/glyphicons-halflings-regular.eot); + src: url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'), url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'), url(../fonts/glyphicons-halflings-regular.woff) format('woff'), url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'), url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg') +} + +body { + margin: 0; +} + +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +article, +footer, +header, +nav { + display: block; +} + +a { + background-color: transparent; +} + +a:active, +a:hover { + outline: 0; +} + +strong { + font-weight: 700; +} + +h1 { + margin: .67em 0; +} + +svg:not(:root) { + overflow: hidden; +} + +pre { + overflow: auto; +} + +code, +pre { + font-size: 1em; +} + +button, +input, +select { + margin: 0; + font: inherit; + color: inherit; +} + +.glyphicon { + font-style: normal; +} + +button { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button { + -webkit-appearance: button; + cursor: pointer; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +table { + border-spacing: 0; + border-collapse: collapse; +} + +td, +th { + padding: 0; +} + +@media print { + + pre, + tr { + page-break-inside: avoid; + } + + *, + :after, + :before { + color: #000 !important; + text-shadow: none !important; + background: 0 0 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " ("attr(href) ")"; + } + + a[href^="#"]:after { + content: ""; + } + + pre { + border: 1px solid #999; + } + + thead { + display: table-header-group; + } + + h3, + p { + orphans: 3; + widows: 3; + } + + h3 { + page-break-after: avoid; + } + + .navbar { + display: none; + } + + .table { + border-collapse: collapse !important; + } + + .table td, + .table th { + background-color: #fff !important; + } + + .table-bordered td, + .table-bordered th { + border: 1px solid #ddd !important; + } +} + +.collapsing, +.dropdown, +.dropup { + position: relative +} + +.collapsing { + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility +} + +.btn, +.btn:active, +.form-control, +.navbar-toggle { + background-image: none; +} + +body { + background-color: #fff; +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.glyphicon-filter:before { + content: "\e138"; +} + +*, +:after, +:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 10px; + -webkit-tap-highlight-color: transparent; +} + +body { + font-size: 14px; + line-height: 1.42857143; + color: #333; +} + +button, +input, +select { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +a { + color: #337ab7; + text-decoration: none; +} + +a:focus, +a:hover { + color: #23527c; + text-decoration: underline; +} + +a:focus { + outline: -webkit-focus-ring-color auto 5px; + outline-offset: -2px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +h1, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +h1, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h1 { + font-size: 36px; +} + +h3 { + font-size: 24px; +} + +h4 { + font-size: 18px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 12px; +} + +p { + margin: 0 0 10px; +} + +pre { + line-height: 1.42857143; +} + +.small { + font-size: 85%; +} + +pre code, +table { + background-color: transparent; +} + +ul { + margin-top: 0; +} + +ul ul { + margin-bottom: 0; +} + +ul { + margin-bottom: 10px; +} + +@media (min-width:768px) { + .container { + width: 750px; + } +} + +code { + padding: 2px 4px; + font-size: 90%; +} + +th { + text-align: left; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #333; + border-radius: 4px; +} + +.container { + margin-right: auto; + margin-left: auto; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + border-radius: 0; +} + +.container { + padding-right: 15px; + padding-left: 15px; +} + +@media (min-width:992px) { + .container { + width: 970px; + } +} + +@media (min-width:1200px) { + .container { + width: 1170px; + } +} + +.row { + margin-right: -15px; + margin-left: -15px; +} + +.col-md-10, +.col-md-2 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +@media (min-width:992px) { + + .col-md-10, + .col-md-2 { + float: left; + } + + .col-md-10 { + width: 83.33333333%; + } + + .col-md-2 { + width: 16.66666667%; + } +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} + +.table>tbody>tr>td, +.table>thead>tr>th { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} + +.table>thead>tr>th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} + +.table>thead:first-child>tr:first-child>th { + border-top: 0; +} + +.table-condensed>tbody>tr>td, +.table-condensed>thead>tr>th { + padding: 5px; +} + +.table-bordered, +.table-bordered>tbody>tr>td, +.table-bordered>thead>tr>th { + border: 1px solid #ddd; +} + +.table-bordered>thead>tr>th { + border-bottom-width: 2px; +} + +.table-striped>tbody>tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +.table-responsive { + min-height: .01%; + overflow-x: auto; +} + +@media screen and (max-width:767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + + .table-responsive>.table { + margin-bottom: 0; + } + + .table-responsive>.table>tbody>tr>td, + .table-responsive>.table>thead>tr>th { + white-space: nowrap; + } + + .table-responsive>.table-bordered { + border: 0; + } + + .table-responsive>.table-bordered>tbody>tr>td:first-child, + .table-responsive>.table-bordered>thead>tr>th:first-child { + border-left: 0; + } + + .table-responsive>.table-bordered>tbody>tr>td:last-child, + .table-responsive>.table-bordered>thead>tr>th:last-child { + border-right: 0; + } + + .table-responsive>.table-bordered>tbody>tr:last-child>td { + border-bottom: 0; + } +} + +.form-control { + font-size: 14px; + line-height: 1.42857143; + color: #555; + display: block; +} + +.form-control { + width: 100%; + height: 34px; + padding: 6px 12px; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); +} + +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #999; +} + +.form-control::-webkit-input-placeholder { + color: #999; +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-group { + margin-bottom: 15px; +} + +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn:active:focus, +.btn:focus { + outline: -webkit-focus-ring-color auto 5px; + outline-offset: -2px; +} + +.btn:focus, +.btn:hover { + color: #333; + text-decoration: none; +} + +.btn:active { + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} + +.collapse { + display: none +} + +.collapse.in { + display: block +} + +.nav>li, +.nav>li>a { + display: block; + position: relative; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav>li>a { + padding: 10px 15px; +} + +.nav>li>a:focus, +.nav>li>a:hover { + text-decoration: none; + background-color: #eee; +} + +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} + +@media (min-width:768px) { + .navbar { + border-radius: 4px; + } + + .navbar-header { + float: left; + } + + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } +} + +.container>.navbar-collapse, +.container>.navbar-header { + margin-right: -15px; + margin-left: -15px; +} + +.navbar-brand { + float: left; + height: 50px; + padding: 15px; + font-size: 18px; + line-height: 20px; +} + +.navbar-brand:focus, +.navbar-brand:hover { + text-decoration: none; +} + +@media (min-width:768px) { + + .container>.navbar-collapse, + .container>.navbar-header { + margin-right: 0; + margin-left: 0; + } + + .navbar>.container .navbar-brand { + margin-left: -15px; + } +} + +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle:focus { + outline: 0; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar+.icon-bar { + margin-top: 4px; +} + +.navbar-nav { + margin: 7.5px -15px; +} + +.navbar-nav>li>a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} + +@media (min-width:768px) { + .navbar-toggle { + display: none; + } + + .navbar-nav { + float: left; + margin: 0; + } + + .navbar-nav>li { + float: left; + } + + .navbar-nav>li>a { + padding-top: 15px; + padding-bottom: 15px; + } +} + +.navbar-form { + padding: 10px 15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + margin: 8px -15px; +} + +@media (min-width:768px) { + .navbar-form .form-group { + display: inline-block; + } + + .navbar-form .form-group { + margin-bottom: 0; + vertical-align: middle; + } + + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +.breadcrumb>li { + display: inline-block; +} + +@media (max-width:767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} + +@media (min-width:768px) { + .navbar-right { + float: right !important; + margin-right: -15px; + } +} + +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-inverse { + background-color: #222; + border-color: #080808; +} + +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} + +.navbar-inverse .navbar-brand:focus, +.navbar-inverse .navbar-brand:hover { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav>li>a { + color: #9d9d9d; +} + +.navbar-inverse .navbar-nav>li>a:focus, +.navbar-inverse .navbar-nav>li>a:hover { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:focus, +.navbar-inverse .navbar-nav>.active>a:hover { + color: #fff; + background-color: #080808; +} + +.navbar-inverse .navbar-toggle { + border-color: #333; +} + +.navbar-inverse .navbar-toggle:focus, +.navbar-inverse .navbar-toggle:hover { + background-color: #333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} + +.breadcrumb>li+li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} + +.alert { + margin-bottom: 20px; +} + +.alert { + padding: 15px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert>p { + margin-bottom: 0; +} + +.container:after, +.container:before, +.nav:after, +.nav:before, +.navbar-collapse:after, +.navbar-collapse:before, +.navbar-header:after, +.navbar-header:before, +.navbar:after, +.navbar:before, +.row:after, +.row:before { + display: table; + content: " "; +} + +.container:after, +.nav:after, +.navbar-collapse:after, +.navbar-header:after, +.navbar:after, +.row:after { + clear: both; +} + +.pull-right { + float: right !important; +} + +.affix { + position: fixed; +} + +@media (max-width:767px) { + .hidden-xs { + display: none !important; + } +} + +@media (min-width:768px) and (max-width:991px) { + .hidden-sm { + display: none !important; + } +} + +@media print { + .hidden-print { + display: none !important; + } +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.pagination { + display: inline-block; +} + +.pagination { + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination>li { + display: inline; +} + +.pagination>li>a { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} + +.pagination>li:first-child>a { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.pagination>li:last-child>a { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination>li>a:focus, +.pagination>li>a:hover { + z-index: 2; + color: #23527c; + background-color: #eee; + border-color: #ddd; +} + +.pagination>.active>a, +.pagination>.active>a:focus, +.pagination>.active>a:hover { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} + +.pagination>.disabled>a, +.pagination>.disabled>a:focus, +.pagination>.disabled>a:hover { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} diff --git a/docs/_template/light-dark-theme/styles/gray.css b/docs/_template/light-dark-theme/styles/gray.css new file mode 100644 index 000000000..463561be5 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/gray.css @@ -0,0 +1,324 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ + +@import url('vs2015.css'); +html, +body { + background: #23272A; + color: #dddddd; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #EEEEEE; +} + +button, +a { + color: #64B5F6; +} + +.sidenav{ + background-color: rgb(30, 30, 30); +} + +button:hover, +button:focus, +a:hover, +a:focus, +.btn:focus, +.btn:hover{ + color: #2196F3; +} + +a.disable, +a.disable:hover { + color: #EEEEEE; +} + +.divider { + color: #37474F; +} + +hr { + border-color: #37474F; +} + +/* top navbar */ +/*.navbar-inverse[role="navigation"] { + background-color: #2C2F33; +}*/ + +/* sub navbar (below top) */ +.subnav { + background: rgb(69, 75, 82) +} + + +.inheritance h5, +.inheritedMembers h5 { + border-bottom: 1px solid #37474F; +} + +article h4 { + border-bottom: 1px solid #37474F; +} + +.docs-search { + background: #424242; +} + +.search-results-group-heading { + color: #424242; +} + +.search-close { + color: #424242; +} + +.sidetoc { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.sideaffix { + overflow: visible; +} + +.sideaffix>div.contribution>ul>li>a.contribution-link:hover { + background-color: #333333; +} + +/* toc */ + +.toc .nav>li>a { + color: rgb(218, 218, 218); +} + +.toc .nav>li>a:hover, +.toc .nav>li>a:focus { + color: #E0E0E0; +} + +.toc .nav>li.active>a { + color: #90CAF9; +} + +.toc .nav>li.active>a:hover, +.toc .nav>li.active>a:focus { + background-color: #37474F; + color: #4FC3F7; +} + +.sidefilter { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.affix ul>li>a:hover { + background: none; + color: #EEEEEE; +} + +.affix ul>li.active>a, +.affix ul>li.active>a:before { + color: #B3E5FC; +} + +.affix ul>li>a { + color: #EEEEEE; +} + +.affix>ul>li.active>a, +.affix>ul>li.active>a:before { + color: #B3E5FC; +} + +.tryspan { + border-color: #37474F; +} + +.footer { + border-top: 1px solid #5F5F5F; + background: #2C2F33; +} + +/* alert */ +.alert-info { + color: #f3fdff; + background: #40788A; + border-color: #2F7A95; +} + +.alert-warning { + color: #fffaf2; + background: #936C36; + border-color: #AE8443; +} + +.alert-danger { + color: #fff4f4; + background: #834040; + border-color: #8C2F2F +} + +/* For tabbed content */ + +.tabGroup { + margin-top: 1rem; +} + +.tabGroup ul[role="tablist"] { + margin: 0; + padding: 0; + list-style: none; +} + +.tabGroup ul[role="tablist"]>li { + list-style: none; + display: inline-block; +} + +.tabGroup a[role="tab"] { + color: white; + box-sizing: border-box; + display: inline-block; + padding: 5px 7.5px; + text-decoration: none; + border-bottom: 2px solid #fff; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus, +.tabGroup a[role="tab"][aria-selected="true"] { + border-bottom: 2px solid #607D8B; +} + +.tabGroup a[role="tab"][aria-selected="true"] { + color: #81D4FA; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus { + color: #29B6F6; +} + +.tabGroup a[role="tab"]:focus { + outline: 1px solid #607D8B; + outline-offset: -1px; +} + +@media (min-width: 768px) { + .tabGroup a[role="tab"] { + padding: 5px 15px; + } +} + +.tabGroup section[role="tabpanel"] { + border: 1px solid #607D8B; + padding: 15px; + margin: 0; + overflow: hidden; +} + +.tabGroup section[role="tabpanel"]>.codeHeader, +.tabGroup section[role="tabpanel"]>pre { + margin-left: -16px; + margin-right: -16px; +} + +.tabGroup section[role="tabpanel"]> :first-child { + margin-top: 0; +} + +.tabGroup section[role="tabpanel"]>pre:last-child { + display: block; + margin-bottom: -16px; +} + +.mainContainer[dir='rtl'] main ul[role="tablist"] { + margin: 0; +} + +/* code */ + +code { + color: white; + background-color: #5B646B; + border-radius: 4px; + padding: 3px 7px; +} + +pre { + background-color: #282a36; +} + +/* table */ + +.table-striped>tbody>tr:nth-of-type(odd) { + background-color: #333333; + color: #d3d3d3 +} + +tbody>tr { + background-color: #424242; + color: #c0c0c0 +} + +.table>tbody+tbody { + border-top: 2px solid rgb(173, 173, 173) +} + +/* select */ + +select { + background-color: #3b3b3b; + border-color: #2e2e2e; +} + +/* + Following code regarding collapse container are fetched + or modified from the Materialize project. + + The MIT License (MIT) + Copyright (c) 2014-2018 Materialize + https://github.com/Dogfalo/materialize +*/ + +/* all collapse container */ +.collapse-container.last-modified { + -webkit-box-shadow: 0 2px 2px 0 rgba(50, 50, 50, 0.64), 0 3px 1px -2px rgba(50, 50, 50, 0.62), 0 1px 5px 0 rgba(50, 50, 50, 0.7); + box-shadow: 0 2px 2px 0 rgba(50, 50, 50, 0.64), 0 3px 1px -2px rgba(50, 50, 50, 0.62), 0 1px 5px 0 rgba(50, 50, 50, 0.7); + border-top: 1px solid rgba(96, 96, 96, 0.7); + border-right: 1px solid rgba(96, 96, 96, 0.7); + border-left: 1px solid rgba(96, 96, 96, 0.7); +} + +/* header */ +.collapse-container.last-modified>:nth-child(odd) { + background-color: #3f3f3f; + border-bottom: 1px solid rgba(96, 96, 96, 0.7); +} + +/* body */ +.collapse-container.last-modified>:nth-child(even) { + border-bottom: 1px solid rgba(96, 96, 96, 0.7); + background-color: inherit; +} + +span.arrow-d{ + border-top: 5px solid white +} + +span.arrow-r{ + border-left: 5px solid white +} + +.logo-switcher { + background: url("../marketing/logo/SVG/Combinationmark White.svg") no-repeat; +} diff --git a/docs/_template/light-dark-theme/styles/light.css b/docs/_template/light-dark-theme/styles/light.css new file mode 100644 index 000000000..a2ba30788 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/light.css @@ -0,0 +1,117 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ + +@import url('tomorrow.css'); +html, +body { + background: #fff; + color: #000; +} + +.sideaffix { + overflow: visible; +} + +/* links */ + +a:active, a:hover, a:visited { + color: #0078d7; +} + +a { + color: #0050c5; + cursor: pointer; + text-decoration: none; + word-wrap: break-word; +} + +/* alert */ +.alert-info { + color: #165e82; + background-color: #c1e0ef; + border-color: #8cbfd8; +} + +.alert-warning { + color: #825e16; + background-color: #efe0c1; + border-color: #d8bf8c; +} + +.alert-danger { + color: #821616; + background-color: #efc1c1; + border-color: #d88c8c; +} + +/* code */ + +code { + color: #9c3a3f; + background-color: #ececec; + border-radius: 4px; + padding: 3px 7px; +} + +/* table */ + +.table-striped>tbody>tr:nth-of-type(odd) { + color: #333333; + background-color: #d3d3d3 +} + +tbody>tr { + color: #424242; + background-color: #c0c0c0 +} + +.table>tbody+tbody { + border-top: 2px solid rgb(173, 173, 173) +} + +/* select */ + +select { + background-color: #fcfcfc; + border-color: #aeb1b5; +} + +/* + Following code regarding collapse container are fetched + or modified from the Materialize project. + + The MIT License (MIT) + Copyright (c) 2014-2018 Materialize + https://github.com/Dogfalo/materialize +*/ + +/* all collapse container */ +.collapse-container.last-modified { + -webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2); + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +/* header */ +.collapse-container.last-modified>:nth-child(odd) { + background-color: #fff; + border-bottom: 1px solid #ddd; +} + +/* body */ +.collapse-container.last-modified>:nth-child(even) { + border-bottom: 1px solid #ddd; +} + +span.arrow-d{ + border-top: 5px solid black +} + +span.arrow-r{ + border-left: 5px solid black +} + +.logo-switcher { + background: url("../marketing/logo/SVG/Combinationmark.svg") no-repeat; +} diff --git a/docs/_template/light-dark-theme/styles/master.css b/docs/_template/light-dark-theme/styles/master.css new file mode 100644 index 000000000..cab54ac24 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/master.css @@ -0,0 +1,234 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto|Muli|Fira+Mono'); + +html, +body { + font-family: 'Roboto', 'Segoe UI', Tahoma, Helvetica, sans-serif; + font-display: optional; + height: 100%; + font-size: 15px; +} + +code{ + font-family: 'Fira Mono', 'Courier New', Courier, monospace +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Muli', Verdana, Geneva, Tahoma, sans-serif; + line-height: 130%; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + font-weight: 600; +} + +#logo +{ + max-width: 100px; + max-height: 100px; + width: 38pt; + height: 38pt; + padding: 8pt; +} + +p, +li, +.toc { + text-rendering: optimizeLegibility; + line-height: 160%; +} + +.toc-filter{ + background: inherit !important; +} + +.affix ul>li.active>ul, .affix ul>li.active>a:before, .affix ul>li>a:hover:before{ + white-space: normal; +} + +img { + box-shadow: 0px 0px 3px 0px rgb(66, 66, 66); + max-width: 95% !important; + margin-top: 15px; + margin-bottom: 15px; +} + +.big-logo { + display: block; + box-shadow: none !important; + /* Width value was taken from the original size of the combomark svg */ + width: 951pt; + /* Height was arbitrarily determined */ + min-height: 100pt; + max-width: 90%; +} + +article.content p{ + -webkit-transition: all .75s ease-in-out; + transition: all .75s ease-in-out; +} + +article.content h1, +article.content h2, +article.content h3, +article.content h4, +article.content h5, +article.content h6{ + -webkit-transition: all .25s ease-in-out; + transition: all .25s ease-in-out; +} + +.sideaffix { + line-height: 140%; +} + +.sideaffix > div.contribution { + margin-bottom: 0; +} + +header .navbar { + border-width: 0 0 0px; + border-radius: 0; +} + +body .toc { + background-color: inherit; + overflow: visible; +} + +select { + display: inline-block; + overflow: auto; + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin: 0; + padding: 0 30px 0 6px; + vertical-align: middle; + height: 28px; + border: 1px solid #e3e3e3; + line-height: 16px; + outline: 0; + text-overflow: ellipsis; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + background-image: linear-gradient(45deg, transparent 50%, #707070 0), linear-gradient(135deg, #707070 50%, transparent 0); + background-position: calc(100% - 13px) 11px, calc(100% - 8px) 11px; + background-size: 5px 5px, 5px 6px; + background-repeat: no-repeat; +} + +/* + Following code are fetched or modified from + the Materialize project. + + The MIT License (MIT) + Copyright (c) 2014-2018 Materialize + https://github.com/Dogfalo/materialize +*/ + +/* all collapse container */ + +.collapse-container.last-modified { + margin: 0.5rem 0 1rem 0; +} + +/* header */ + +.collapse-container.last-modified>:nth-child(odd):focus { + outline: 0; +} + +.collapse-container.last-modified>:nth-child(odd) { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + line-height: 1.5; + padding: 0.75rem; + background-image: none; + border: 0px; +} + +/* body */ + +.collapse-container.last-modified>:nth-child(even) { + display: none; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 1rem; + border: 0px; +} + +/* nav bar */ + +.nav { + margin: 0; +} + +.nav li { + -webkit-transition: background-color .3s, color .3s; + transition: background-color .3s, color .3s; +} + +.nav a { + -webkit-transition: background-color .3s, color .3s; + transition: background-color .3s, color .3s; + cursor: pointer; +} + +/* arrow */ + +span.arrow-d{ + top: 6px; position: relative; +} + +span.arrow-r{ + top: 6px; position: relative; +} + +/* widen viewport */ + +@media (min-width: 1085px) { + .container { + width: calc(100% - 15vw); + max-width: calc(100% - 15vw); + } +} + +/* fix level indentation */ + +.level2 { + padding: 0 5px; +} + +.level3 { + padding: 0 5px; + font-size: 90%; +} + +.level4 { + padding: 0 5px; + font-size: 85%; +} + +.level5 { + padding: 0 5px; + font-size: 80%; +} + +.level6 { + padding: 0 5px; + font-size: 75%; +} \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/material.css b/docs/_template/light-dark-theme/styles/material.css new file mode 100644 index 000000000..06a064337 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/material.css @@ -0,0 +1,199 @@ +body { + color: #34393e; + line-height: 1.5; + /*font-size: 16px;*/ + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + word-wrap: break-word +} + +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h1, +article h2, +article h3, +article h4 { + margin-top: 35px; + margin-bottom: 15px; +} + +article h4 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/* NAVBAR */ + +.navbar-brand>img { + color: #fff; +} + +.navbar { + border: none; + /* Both navbars use box-shadow */ + -webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + -moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); +} + +.subnav { + border-top: 1px solid #ddd; + background-color: #fff; +} + +.navbar-inverse { + background-color: #0d47a1; + z-index: 100; +} + +.navbar-inverse .navbar-nav>li>a, +.navbar-inverse .navbar-text { + color: #fff; + /*background-color: #0d47a1;*/ + border-bottom: 3px solid transparent; + padding-bottom: 12px; +} + +.navbar-inverse .navbar-nav>li>a:focus, +.navbar-inverse .navbar-nav>li>a:hover { + color: #fff; + background-color: #1157c0; + border-bottom: 3px solid white; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:focus, +.navbar-inverse .navbar-nav>.active>a:hover { + color: #fff; + background-color: #1157c0; + border-bottom: 3px solid white; +} + +.navbar-form .form-control { + border: none; + border-radius: 20px; +} + +/* SIDEBAR */ + +/*.toc .level1>li { + font-weight: 400; +}*/ + +.toc .nav>li>a { + color: #34393e; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.toc-filter { + padding: 10px; + margin: 0; +} + +.toc-filter>input { + border: 2px solid #ddd; + border-radius: 20px; +} + +.toc-filter>.filter-icon { + display: none; +} + +.sidetoc>.toc { + overflow-x: hidden; +} + +.sidetoc { + border: none; +} + +/* ALERTS */ + +.alert { + padding: 0px 0px 5px 0px; + color: inherit; + background-color: inherit; + border: none; + box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4); +} + +.alert>p { + margin-bottom: 0; + padding: 5px 10px; +} + +.alert>ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert>h5 { + padding: 10px 15px; + margin-top: 0; + text-transform: uppercase; + font-weight: bold; + border-radius: 4px 4px 0 0; +} + +.alert-info>h5 { + color: #1976d2; + border-bottom: 4px solid #1976d2; + background-color: #e3f2fd; +} + +.alert-warning>h5 { + color: #f57f17; + border-bottom: 4px solid #f57f17; + background-color: #fff3e0; +} + +.alert-danger>h5 { + color: #d32f2f; + border-bottom: 4px solid #d32f2f; + background-color: #ffebee; +} + +/* CODE HIGHLIGHT */ +pre { + padding: 9.5px; + margin: 10px 10px 10px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + /*background-color: #fffaef;*/ + border-radius: 4px; + box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4); +} diff --git a/docs/_template/light-dark-theme/styles/styleswitcher.js b/docs/_template/light-dark-theme/styles/styleswitcher.js new file mode 100644 index 000000000..a87b89525 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/styleswitcher.js @@ -0,0 +1,26 @@ +const baseUrl = document.getElementById("docfx-style:rel").content; + +function onThemeSelect(event) { + const theme = event.target.value; + window.localStorage.setItem("theme", theme); + window.themeElement.href = getUrl(theme); +} + +function getUrl(slug) { + return baseUrl + "styles/" + slug + ".css"; +} + +const themeElement = document.createElement("link"); +themeElement.rel = "stylesheet"; + +const theme = window.localStorage.getItem("theme") || "light"; +themeElement.href = getUrl(theme); + +document.head.appendChild(themeElement); +window.themeElement = themeElement; + +document.addEventListener("DOMContentLoaded", function() { + const themeSwitcher = document.getElementById("theme-switcher"); + themeSwitcher.onchange = onThemeSelect; + themeSwitcher.value = theme; +}, false); diff --git a/docs/_template/light-dark-theme/styles/theme-switcher.css b/docs/_template/light-dark-theme/styles/theme-switcher.css new file mode 100644 index 000000000..c6e27c93a --- /dev/null +++ b/docs/_template/light-dark-theme/styles/theme-switcher.css @@ -0,0 +1,9 @@ +div.theme-switch-field { + padding-left: 10px; + padding-bottom: 15px +} + +div.theme-switch-field > p{ + font-weight: bold; + font-size: 1.2em; +} \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/tomorrow.css b/docs/_template/light-dark-theme/styles/tomorrow.css new file mode 100644 index 000000000..026a62fe3 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/tomorrow.css @@ -0,0 +1,72 @@ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #8e908c; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #c82829; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #f5871f; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #eab700; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #718c00; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #4271ae; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #8959a8; +} + +.hljs { + display: block; + overflow-x: auto; + background: white; + color: #4d4d4c; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/docs/_template/light-dark-theme/styles/vs2015.css b/docs/_template/light-dark-theme/styles/vs2015.css new file mode 100644 index 000000000..d8c14a046 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/vs2015.css @@ -0,0 +1,115 @@ +/* + * Visual Studio 2015 dark style + * Author: Nicolas LLOBERA + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282a36; + color: #DCDCDC; +} + +.hljs-keyword, +.hljs-literal, +.hljs-symbol, +.hljs-name { + color: #569CD6; +} +.hljs-link { + color: #569CD6; + text-decoration: underline; +} + +.hljs-built_in, +.hljs-type { + color: #4EC9B0; +} + +.hljs-number, +.hljs-class { + color: #B8D7A3; +} + +.hljs-string, +.hljs-meta-string { + color: #D69D85; +} + +.hljs-regexp, +.hljs-template-tag { + color: #9A5334; +} + +.hljs-subst, +.hljs-function, +.hljs-title, +.hljs-params, +.hljs-formula { + color: #DCDCDC; +} + +.hljs-comment, +.hljs-quote { + color: #57A64A; + font-style: italic; +} + +.hljs-doctag { + color: #608B4E; +} + +.hljs-meta, +.hljs-meta-keyword, +.hljs-tag { + color: #9B9B9B; +} + +.hljs-variable, +.hljs-template-variable { + color: #BD63C5; +} + +.hljs-attr, +.hljs-attribute, +.hljs-builtin-name { + color: #9CDCFE; +} + +.hljs-section { + color: gold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +/*.hljs-code { + font-family:'Monospace'; +}*/ + +.hljs-bullet, +.hljs-selector-tag, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #D7BA7D; +} + +.hljs-addition { + background-color: #144212; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #600; + display: inline-block; + width: 100%; +} diff --git a/docs/api/index.md b/docs/api/index.md index d9433363f..c16ca1363 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,13 +1,16 @@ +--- +uid: API.Docs +--- # API Documentation -This is where you will find documentation for all members and objects in Discord.Net +This is where you will find documentation for all members and objects in Discord.Net. -__Commonly Used Entities__ +# Commonly Used Entities -* @Discord.WebSocket -* @Discord.WebSocket.DiscordSocketClient -* @Discord.WebSocket.SocketGuildChannel -* @Discord.WebSocket.SocketGuildUser -* @Discord.WebSocket.SocketMessage -* @Discord.WebSocket.SocketRole \ No newline at end of file +* @Discord.WebSocket +* @Discord.WebSocket.DiscordSocketClient +* @Discord.WebSocket.SocketGuildChannel +* @Discord.WebSocket.SocketGuildUser +* @Discord.WebSocket.SocketMessage +* @Discord.WebSocket.SocketRole \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index 50ae39092..759dc174f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -1,74 +1,63 @@ { - "metadata": [ - { - "src": [ - { - "src": "..", - "files": [ - "src/**/*.cs" - ], - "exclude": [ - "**/obj/**", - "**/bin/**", - "_site/**" - ] - } - ], - "dest": "api", - "filter": "filterConfig.yml" + "metadata": [{ + "src": [{ + "src": "../src", + "files": [ + "**.csproj" + ] + }], + "dest": "api", + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "netstandard2.0" } - ], + }], "build": { - "content": [ + "content": [{ + "files": ["api/**.yml", "api/index.md"] + }, { - "files": [ - "api/**.yml", - "api/index.md" - ] + "files": ["toc.yml", "index.md"] }, { - "files": [ - "guides/**.md", - "guides/**/toc.yml", - "toc.yml", - "*.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "resource": [ + "files": ["faq/**.md", "faq/**/toc.yml"] + }, { - "files": [ - "**/images/**", - "**/samples/**" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "overwrite": [ + "files": ["guides/**.md", "guides/**/toc.yml"] + }, { - "files": [ - "apidoc/**.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] + "src": "../", + "files": [ "CHANGELOG.md" ] } ], + "resource": [{ + "files": [ + "**/images/**", + "**/samples/**", + "langwordMapping.yml", + "marketing/logo/**.svg", + "marketing/logo/**.png", + "favicon.ico" + ] + }], "dest": "_site", "template": [ - "default" + "default", + "_template/light-dark-theme", + "_template/last-modified", + "_template/description-generator" ], + "postProcessors": ["ExtractSearchIndex", "LastModifiedPostProcessor", "DescriptionPostProcessor"], + "overwrite": "_overwrites/**/**.md", "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta" + "_appTitle": "Discord.Net Documentation", + "_appFooter": "Discord.Net (c) 2015-2020 2.2.0", + "_enableSearch": true, + "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", + "_appFaviconPath": "favicon.ico" }, - "noLangKeyword": false + "xrefService": [ + "https://xref.docs.microsoft.com/query?uid={uid}" + ] } } diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md new file mode 100644 index 000000000..35c71709f --- /dev/null +++ b/docs/faq/basics/basic-operations.md @@ -0,0 +1,123 @@ +--- +uid: FAQ.Basics.BasicOp +title: Questions about Basic Operations +--- + +# Basic Operations Questions + +In the following section, you will find commonly asked questions and +answers regarding basic usage of the library, as well as +language-specific tips when using this library. + +## How should I safely check a type? + +> [!WARNING] +> Direct casting (e.g., `(Type)type`) is **the least recommended** +> way of casting, as it *can* throw an [InvalidCastException] +> when the object isn't the desired type. +> +> Please refer to [this post] for more details. + +In Discord.Net, the idea of polymorphism is used throughout. You may +need to cast the object as a certain type before you can perform any +action. + +A good and safe casting example: + +[!code-csharp[Casting](samples/cast.cs)] + +[InvalidCastException]: https://docs.microsoft.com/en-us/dotnet/api/system.invalidcastexception +[this post]: https://docs.microsoft.com/en-us/dotnet/csharp/how-to/safely-cast-using-pattern-matching-is-and-as-operators + +## How do I send a message? + +> [!TIP] +> The [GetChannel] method by default returns an [IChannel], allowing +> channel types such as [IVoiceChannel], [ICategoryChannel] +> to be returned; consequently, you cannot send a message +> to channels like those. + +Any implementation of [IMessageChannel] has a [SendMessageAsync] +method. You can get the channel via [GetChannel] under the client. +Remember, when using Discord.Net, polymorphism is a common recurring +theme. This means an object may take in many shapes or form, which +means casting is your friend. You should attempt to cast the channel +as an [IMessageChannel] or any other entity that implements it to be +able to message. + +[SendMessageAsync]: xref:Discord.IMessageChannel.SendMessageAsync* +[GetChannel]: xref:Discord.WebSocket.DiscordSocketClient.GetChannel* + +## How can I tell if a message is from X, Y, Z channel? + +You may check the message channel type. Visit [Glossary] to see the +various types of channels. + +[Glossary]: xref:FAQ.Glossary#message-channels + +## How can I get the guild from a message? + +There are 2 ways to do this. You can do either of the following, + +1. Cast the user as an [IGuildUser] and use its [IGuild] property. +2. Cast the channel as an [IGuildChannel] and use its [IGuild] property. + +## How do I add hyperlink text to an embed? + +Embeds can use standard [markdown] in the description field as well +as in field values. With that in mind, links can be added with +`[text](link)`. + +[markdown]: https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline- + +## How do I add reactions to a message? + +Any entity that implements [IUserMessage] has an [AddReactionAsync] +method. This method expects an [IEmote] as a parameter. +In Discord.Net, an Emote represents a custom-image emote, while an +Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] +implement [IEmote] and are valid options. + +# [Adding a reaction to another message](#tab/emoji-others) + +[!code-csharp[Emoji](samples/emoji-others.cs)] + +# [Adding a reaction to a sent message](#tab/emoji-self) + +[!code-csharp[Emoji](samples/emoji-self.cs)] + +*** + +[AddReactionAsync]: xref:Discord.IMessage.AddReactionAsync* + +## What is a "preemptive rate limit?" + +A preemptive rate limit is Discord.Net's way of telling you to slow +down before you get hit by the real rate limit. Hitting a real rate +limit might prevent your entire client from sending any requests for +a period of time. This is calculated based on the HTTP header +returned by a Discord response. + +## Why am I getting so many preemptive rate limits when I try to add more than one reactions? + +This is due to how HTML header works, mistreating +0.25sec/action to 1sec. This causes the lib to throw preemptive rate +limit more frequently than it should for methods such as adding +reactions. + +## Can I opt-out of preemptive rate limits? + +Unfortunately, not at the moment. See [#401](https://github.com/RogueException/Discord.Net/issues/401). + +[IChannel]: xref:Discord.IChannel +[ICategoryChannel]: xref:Discord.ICategoryChannel +[IGuildChannel]: xref:Discord.IGuildChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGuild]: xref:Discord.IGuild +[IVoiceChannel]: xref:Discord.IVoiceChannel +[IGuildUser]: xref:Discord.IGuildUser +[IMessageChannel]: xref:Discord.IMessageChannel +[IUserMessage]: xref:Discord.IUserMessage +[IEmote]: xref:Discord.IEmote +[Emote]: xref:Discord.Emote +[Emoji]: xref:Discord.Emoji \ No newline at end of file diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md new file mode 100644 index 000000000..1176ee3fd --- /dev/null +++ b/docs/faq/basics/client-basics.md @@ -0,0 +1,94 @@ +--- +uid: FAQ.Basics.ClientBasics +title: Basic Questions about Client +--- + +# Client Basics Questions + +In the following section, you will find commonly asked questions and +answers about common issues that you may face when utilizing the +various clients offered by the library. + +## My client keeps returning 401 upon logging in! + +> [!WARNING] +> Userbot/selfbot (logging in with a user token) is no +> longer supported with this library starting from 2.0, as +> logging in under a user account may result in account termination. +> +> For more information, see issue [827] & [958], as well as the official +> [Discord API Terms of Service]. + +There are few possible reasons why this may occur. + +1. You are not using the appropriate [TokenType]. If you are using a + bot account created from the Discord Developer portal, you should + be using `TokenType.Bot`. +2. You are not using the correct login credentials. Please keep in + mind that a token is **different** from a *client secret*. + +[TokenType]: xref:Discord.TokenType +[827]: https://github.com/RogueException/Discord.Net/issues/827 +[958]: https://github.com/RogueException/Discord.Net/issues/958 +[Discord API Terms of Service]: https://discord.com/developers/docs/legal + +## How do I do X, Y, Z when my bot connects/logs on? Why do I get a `NullReferenceException` upon calling any client methods after connect? + +Your bot should **not** attempt to interact in any way with +guilds/servers until the [Ready] event fires. When the bot +connects, it first has to download guild information from +Discord for you to get access to any server +information; the client is not ready at this point. + +Technically, the [GuildAvailable] event fires once the data for a +particular guild has downloaded; however, it is best to wait for all +guilds to be downloaded. Once all downloads are complete, the [Ready] +event is triggered, then you can proceed to do whatever you like. + +[Ready]: xref:Discord.WebSocket.DiscordSocketClient.Ready +[GuildAvailable]: xref:Discord.WebSocket.BaseSocketClient.GuildAvailable + +## How do I get a message's previous content when that message is edited? + +If you need to do anything with messages (e.g., checking Reactions, +checking the content of edited/deleted messages), you must set the +[MessageCacheSize] in your [DiscordSocketConfig] settings in order to +use the cached message entity. Read more about it [here](xref:Guides.Concepts.Events#cacheable). + +1. Message Cache must be enabled. +2. Hook the MessageUpdated event. This event provides a *before* and + *after* object. +3. Only messages received *after* the bot comes online will be + available in the cache. + +[MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig +[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated + +## What is a shard/sharded client, and how is it different from the `DiscordSocketClient`? +As your bot grows in popularity, it is recommended that you should section your bot off into separate processes. +The [DiscordShardedClient] is essentially a class that allows you to easily create and manage multiple [DiscordSocketClient] +instances, with each one serving a different amount of guilds. + +There are very few differences from the [DiscordSocketClient] class, and it is very straightforward +to modify your existing code to use a [DiscordShardedClient] when necessary. + +1. You need to specify the total amount of shards, or shard ids, via [DiscordShardedClient]'s constructors. +2. The [Connected], [Disconnected], [Ready], and [LatencyUpdated] events + are replaced with [ShardConnected], [ShardDisconnected], [ShardReady], and [ShardLatencyUpdated]. +3. Every event handler you apply/remove to the [DiscordShardedClient] is applied/removed to each shard. + If you wish to control a specific shard's events, you can access an individual shard through the `Shards` property. + +If you do not wish to use the [DiscordShardedClient] and instead reuse the same [DiscordSocketClient] code and manually shard them, +you can do so by specifying the [ShardId] for the [DiscordSocketConfig] and pass that to the [DiscordSocketClient]'s constructor. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordShardedClient]: xref:Discord.WebSocket.DiscordShardedClient +[Connected]: xref:Discord.WebSocket.DiscordSocketClient.Connected +[Disconnected]: xref:Discord.WebSocket.DiscordSocketClient.Disconnected +[LatencyUpdated]: xref:Discord.WebSocket.DiscordSocketClient.LatencyUpdated +[ShardConnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardConnected +[ShardDisconnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardDisconnected +[ShardReady]: xref:Discord.WebSocket.DiscordShardedClient.ShardReady +[ShardLatencyUpdated]: xref:Discord.WebSocket.DiscordShardedClient.ShardLatencyUpdated +[ShardId]: xref:Discord.WebSocket.DiscordSocketConfig.ShardId diff --git a/docs/faq/basics/getting-started.md b/docs/faq/basics/getting-started.md new file mode 100644 index 000000000..e254226d0 --- /dev/null +++ b/docs/faq/basics/getting-started.md @@ -0,0 +1,82 @@ +--- +uid: FAQ.Basics.GetStarted +title: Beginner Questions / How to Get Started +--- + +# Basic Concepts / Getting Started + +In this following section, you will find commonly asked questions and +answers about how to get started with Discord.Net, as well as basic +introduction to the Discord API ecosystem. + +## How do I add my bot to my server/guild? + +You can do so by using the [permission calculator] provided +by [FiniteReality]. +This tool allows you to set permissions that the bot will be assigned +with, and invite the bot into your guild. With this method, bots will +also be assigned a unique role that a regular user cannot use; this +is what we call a `Managed` role. Because you cannot assign this +role to any other users, it is much safer than creating a single +role which, intentionally or not, can be applied to other users +to escalate their privilege. + +[FiniteReality]: https://github.com/FiniteReality/permissions-calculator +[permission calculator]: https://finitereality.github.io/permissions-calculator + +## What is a token? + +A token is a credential used to log into an account. This information +should be kept **private** and for your eyes only. Anyone with your +token can log into your account. This risk applies to both user +and bot accounts. That also means that you should **never** hardcode +your token or add it into source control, as your identity may be +stolen by scrape bots on the internet that scours through +constantly to obtain a token. + +## What is a client/user/object ID? + +Each user and object on Discord has its own snowflake ID generated +based on various conditions. + +![Snowflake Generation](images/snowflake.png) + +Anyone can see the ID; it is public. It is merely used to +identify an object in the Discord ecosystem. Many things in the +Discord ecosystem require an ID to retrieve or identify the said +object. + +There are 2 common ways to obtain the said ID. + +### [Discord Developer Mode](#tab/dev-mode) + +By enabling the developer mode you can right click on most objects +to obtain their snowflake IDs (please note that this may not apply to +all objects, such as role IDs, or DM channel IDs). + +![Developer Mode](images/dev-mode.png) + +### [Escape Character](#tab/escape-char) + +You can escape an object by using `\` in front the object in the +Discord client. For example, when you do `\@Example#1234` in chat, +it will return the user ID of the aforementioned user. + +![Escaping mentions](images/mention-escape.png) + +*** + +## How do I get the role ID? + +> [!WARNING] +> Right-clicking on the role and copying the ID will **not** work. +> This will only copy the message ID. + +Several common ways to do this: + +1. (Easiest) Right click on the role either in the Server Settings + or in the user's role list. + ![Roles](images/role-copy.png) +2. Make the role mentionable and mention the role, and escape it + using the `\` character in front. +3. Inspect the roles collection within the guild via your debugger. \ No newline at end of file diff --git a/docs/faq/basics/images/dev-mode.png b/docs/faq/basics/images/dev-mode.png new file mode 100644 index 000000000..fd20b95d1 Binary files /dev/null and b/docs/faq/basics/images/dev-mode.png differ diff --git a/docs/faq/basics/images/mention-escape.png b/docs/faq/basics/images/mention-escape.png new file mode 100644 index 000000000..927978061 Binary files /dev/null and b/docs/faq/basics/images/mention-escape.png differ diff --git a/docs/faq/basics/images/role-copy.png b/docs/faq/basics/images/role-copy.png new file mode 100644 index 000000000..1dbc2982f Binary files /dev/null and b/docs/faq/basics/images/role-copy.png differ diff --git a/docs/faq/basics/images/snowflake.png b/docs/faq/basics/images/snowflake.png new file mode 100644 index 000000000..816a10eee Binary files /dev/null and b/docs/faq/basics/images/snowflake.png differ diff --git a/docs/faq/basics/samples/cast.cs b/docs/faq/basics/samples/cast.cs new file mode 100644 index 000000000..73ef5237f --- /dev/null +++ b/docs/faq/basics/samples/cast.cs @@ -0,0 +1,15 @@ +public async Task MessageReceivedHandler(SocketMessage msg) +{ + // Option 1: + // Using the `as` keyword, which will return `null` if the object isn't the desired type. + var usermsg = msg as SocketUserMessage; + // We bail when the message isn't the desired type. + if (msg == null) return; + + // Option 2: + // Using the `is` keyword to cast (C#7 or above only) + if (msg is SocketUserMessage usermsg) + { + // Do things + } +} \ No newline at end of file diff --git a/docs/faq/basics/samples/emoji-others.cs b/docs/faq/basics/samples/emoji-others.cs new file mode 100644 index 000000000..dd3e6317f --- /dev/null +++ b/docs/faq/basics/samples/emoji-others.cs @@ -0,0 +1,18 @@ +// bail if the message is not a user one (system messages cannot have reactions) +var usermsg = msg as IUserMessage; +if (usermsg == null) return; + +// standard Unicode emojis +Emoji emoji = new Emoji("👍"); +// or +// Emoji emoji = new Emoji("\uD83D\uDC4D"); + +// custom guild emotes +Emote emote = Emote.Parse("<:dotnet:232902710280716288>"); +// using Emote.TryParse may be safer in regards to errors being thrown; +// please note that the method does not verify if the emote exists, +// it simply creates the Emote object for you. + +// add the reaction to the message +await usermsg.AddReactionAsync(emoji); +await usermsg.AddReactionAsync(emote); \ No newline at end of file diff --git a/docs/faq/basics/samples/emoji-self.cs b/docs/faq/basics/samples/emoji-self.cs new file mode 100644 index 000000000..cd4cff171 --- /dev/null +++ b/docs/faq/basics/samples/emoji-self.cs @@ -0,0 +1,17 @@ +// capture the message you're sending in a variable +var msg = await channel.SendMessageAsync("This will have reactions added."); + +// standard Unicode emojis +Emoji emoji = new Emoji("👍"); +// or +// Emoji emoji = new Emoji("\uD83D\uDC4D"); + +// custom guild emotes +Emote emote = Emote.Parse("<:dotnet:232902710280716288>"); +// using Emote.TryParse may be safer in regards to errors being thrown; +// please note that the method does not verify if the emote exists, +// it simply creates the Emote object for you. + +// add the reaction to the message +await msg.AddReactionAsync(emoji); +await msg.AddReactionAsync(emote); \ No newline at end of file diff --git a/docs/faq/commands/dependency-injection.md b/docs/faq/commands/dependency-injection.md new file mode 100644 index 000000000..0a5de3e32 --- /dev/null +++ b/docs/faq/commands/dependency-injection.md @@ -0,0 +1,54 @@ +--- +uid: FAQ.Commands.DI +title: Questions about Dependency Injection with Commands +--- + +# Dependency-injection-related Questions + +In the following section, you will find common questions and answers +to utilizing dependency injection with @Discord.Commands, as well as +common troubleshooting steps regarding DI. + +## What is a service? Why does my module not hold any data after execution? + +In Discord.Net, modules are created similarly to ASP.NET, meaning +that they have a transient nature; modules are spawned whenever a +request is received, and are killed from memory when the execution +finishes. In other words, you cannot store persistent +data inside a module. Consider using a service if you wish to +workaround this. + +Service is often used to hold data externally so that they persist +throughout execution. Think of it like a chest that holds +whatever you throw at it that won't be affected by anything unless +you want it to. Note that you should also learn Microsoft's +implementation of [Dependency Injection] \([video]) before proceeding, +as well as how it works in [Discord.Net](xref:Guides.Commands.DI#usage-in-modules). + +A brief example of service and dependency injection can be seen below. + +[!code-csharp[DI](samples/DI.cs)] + +[Dependency Injection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection +[video]: https://www.youtube.com/watch?v=QtDTfn8YxXg + +## Why is my `CommandService` complaining about a missing dependency? + +If you encounter an error similar to `Failed to create MyModule, +dependency MyExternalDependency was not found.`, you may have +forgotten to add the external dependency to the dependency container. + +Starting from Discord.Net 2.0, all dependencies required by each +module must be present when the module is loaded into the +[CommandService]. This means when loading the module, you must pass a +valid [IServiceProvider] with the dependency loaded before the module +can be successfully registered. + +For example, if your module, `MyModule`, requests a `DatabaseService` +in its constructor, the `DatabaseService` must be present in the +[IServiceProvider] when registering `MyModule`. + +[!code-csharp[Missing Dependencies](samples/missing-dep.cs)] + +[IServiceProvider]: xref:System.IServiceProvider +[CommandService]: xref:Discord.Commands.CommandService diff --git a/docs/faq/commands/general.md b/docs/faq/commands/general.md new file mode 100644 index 000000000..de6d48dc1 --- /dev/null +++ b/docs/faq/commands/general.md @@ -0,0 +1,147 @@ +--- +uid: FAQ.Commands.General +title: General Questions about Commands +--- + +# Command-related Questions + +In the following section, you will find commonly asked questions and +answered regarding general command usage when using @Discord.Commands. + +## How can I restrict some of my commands so only specific users can execute them? + +Based on how you want to implement the restrictions, you can use the +built-in [RequireUserPermission] precondition, which allows you to +restrict the command based on the user's current permissions in the +guild or channel (*e.g., `GuildPermission.Administrator`, +`ChannelPermission.ManageMessages`*). + +If, however, you wish to restrict the commands based on the user's +role, you can either create your custom precondition or use +Joe4evr's [Preconditions Addons] that provides a few custom +preconditions that aren't provided in the stock library. +Its source can also be used as an example for creating your +custom preconditions. + +[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute +[Preconditions Addons]: https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions + +## Why am I getting an error about `Assembly.GetEntryAssembly`? + +You may be confusing @Discord.Commands.CommandService.AddModulesAsync* +with @Discord.Commands.CommandService.AddModuleAsync*. The former +is used to add modules via the assembly, while the latter is used to +add a single module. + +## What does [Remainder] do in the command signature? + +The [RemainderAttribute] leaves the string unparsed, meaning you +do not have to add quotes around the text for the text to be +recognized as a single object. Please note that if your method has +multiple parameters, the remainder attribute can only be applied to +the last parameter. + +[!code-csharp[Remainder](samples/Remainder.cs)] + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute + +## Discord.Net keeps saying that a `MessageReceived` handler is blocking the gateway, what should I do? + +By default, the library warns the user about any long-running event +handler that persists for **more than 3 seconds**. Any event +handlers that are run on the same thread as the gateway task, the task +in charge of keeping the connection alive, may block the processing of +heartbeat, and thus terminating the connection. + +In this case, the library detects that a `MessageReceived` +event handler is blocking the gateway thread. This warning is +typically associated with the command handler as it listens for that +particular event. If the command handler is blocking the thread, then +this **might** mean that you have a long-running command. + +> [!NOTE] +> In rare cases, runtime errors can also cause blockage, usually +> associated with Mono, which is not supported by this library. + +To prevent a long-running command from blocking the gateway +thread, a flag called [RunMode] is explicitly designed to resolve +this issue. + +There are 2 main `RunMode`s. + +1. `RunMode.Sync` +2. `RunMode.Async` + +`Sync` is the default behavior and makes the command to be run on the +same thread as the gateway one. `Async` will spin the task off to a +different thread from the gateway one. + +> [!IMPORTANT] +> While specifying `RunMode.Async` allows the command to be spun off +> to a different thread, keep in mind that by doing so, there will be +> **potentially unwanted consequences**. Before applying this flag, +> please consider whether it is necessary to do so. +> +> Further details regarding `RunMode.Async` can be found below. + +You can set the `RunMode` either by specifying it individually via +the `CommandAttribute` or by setting the global default with +the [DefaultRunMode] flag under `CommandServiceConfig`. + +# [CommandAttribute](#tab/cmdattrib) + +[!code-csharp[Command Attribute](samples/runmode-cmdattrib.cs)] + +# [CommandServiceConfig](#tab/cmdconfig) + +[!code-csharp[Command Service Config](samples/runmode-cmdconfig.cs)] + +*** + +*** + +[RunMode]: xref:Discord.Commands.RunMode +[CommandAttribute]: xref:Discord.Commands.CommandAttribute +[DefaultRunMode]: xref:Discord.Commands.CommandServiceConfig.DefaultRunMode + +## How does `RunMode.Async` work, and why is Discord.Net *not* using it by default? + +`RunMode.Async` works by spawning a new `Task` with an unawaited +[Task.Run], essentially making the task that is used to invoke the +command task to be finished on a different thread. This design means +that [ExecuteAsync] will be forced to return a successful +[ExecuteResult] regardless of the actual execution result. + +The following are the known caveats with `RunMode.Async`, + +1. You can potentially introduce a race condition. +2. Unnecessary overhead caused by the [async state machine]. +3. [ExecuteAsync] will immediately return [ExecuteResult] instead of + other result types (this is particularly important for those who wish + to utilize [RuntimeResult] in 2.0). +4. Exceptions are swallowed in the `ExecuteAsync` result. + +However, there are ways to remedy some of these. + +For #3, in Discord.Net 2.0, the library introduces a new event called +[CommandService.CommandExecuted], which is raised whenever the command is executed. +This event will be raised regardless of +the `RunMode` type and will return the appropriate execution result +and the associated @Discord.Commands.CommandInfo if applicable. + +For #4, exceptions are caught in [CommandService.Log] event under +[LogMessage.Exception] as [CommandException] and in the +[CommandService.CommandExecuted] event under the [IResult] as +[ExecuteResult.Exception]. + +[Task.Run]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run +[async state machine]: https://www.red-gate.com/simple-talk/dotnet/net-tools/c-async-what-is-it-and-how-does-it-work/ +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[RuntimeResult]: xref:Discord.Commands.RuntimeResult +[CommandService.CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[CommandService.Log]: xref:Discord.Commands.CommandService.Log +[LogMessage.Exception]: xref:Discord.LogMessage.Exception* +[ExecuteResult.Exception]: xref:Discord.Commands.ExecuteResult.Exception* +[CommandException]: xref:Discord.Commands.CommandException +[IResult]: xref:Discord.Commands.IResult \ No newline at end of file diff --git a/docs/faq/commands/samples/DI.cs b/docs/faq/commands/samples/DI.cs new file mode 100644 index 000000000..ce4454bc2 --- /dev/null +++ b/docs/faq/commands/samples/DI.cs @@ -0,0 +1,28 @@ +public class MyService +{ + public string MyCoolString { get; set; } +} +public class Setup +{ + public IServiceProvider BuildProvider() => + new ServiceCollection() + .AddSingleton() + .BuildServiceProvider(); +} +public class MyModule : ModuleBase +{ + // Inject via public settable prop + public MyService MyService { get; set; } + + // ...or via the module's constructor + + // private readonly MyService _myService; + // public MyModule (MyService myService) => _myService = myService; + + [Command("string")] + public Task GetOrSetStringAsync(string input) + { + if (string.IsNullOrEmpty(_myService.MyCoolString)) _myService.MyCoolString = input; + return ReplyAsync(_myService.MyCoolString); + } +} \ No newline at end of file diff --git a/docs/faq/commands/samples/Remainder.cs b/docs/faq/commands/samples/Remainder.cs new file mode 100644 index 000000000..337fb6e45 --- /dev/null +++ b/docs/faq/commands/samples/Remainder.cs @@ -0,0 +1,20 @@ +// Input: +// !echo Coffee Cake + +// Output: +// Coffee Cake +[Command("echo")] +public Task EchoRemainderAsync([Remainder]string text) => ReplyAsync(text); + +// Output: +// CommandError.BadArgCount +[Command("echo-hassle")] +public Task EchoAsync(string text) => ReplyAsync(text); + +// The message would be seen as having multiple parameters, +// while the method only accepts one. +// Wrapping the message in quotes solves this. +// This way, the system knows the entire message is to be parsed as a +// single String. +// e.g., +// !echo "Coffee Cake" \ No newline at end of file diff --git a/docs/faq/commands/samples/missing-dep.cs b/docs/faq/commands/samples/missing-dep.cs new file mode 100644 index 000000000..d3fb9085b --- /dev/null +++ b/docs/faq/commands/samples/missing-dep.cs @@ -0,0 +1,29 @@ +public class MyModule : ModuleBase +{ + private readonly DatabaseService _dbService; + public MyModule(DatabaseService dbService) + => _dbService = dbService; +} +public class CommandHandler +{ + private readonly CommandService _commands; + private readonly IServiceProvider _services; + public CommandHandler(DiscordSocketClient client) + { + _services = new ServiceCollection() + .AddService() + .AddService(client) + // We are missing DatabaseService! + .BuildServiceProvider(); + } + public async Task RegisterCommandsAsync() + { + // ... + // The method fails here because DatabaseService is a required + // dependency and cannot be resolved by the dependency + // injection service at runtime since the service is not + // registered in this instance of _services. + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + // ... + } +} \ No newline at end of file diff --git a/docs/faq/commands/samples/runmode-cmdattrib.cs b/docs/faq/commands/samples/runmode-cmdattrib.cs new file mode 100644 index 000000000..253acc4a9 --- /dev/null +++ b/docs/faq/commands/samples/runmode-cmdattrib.cs @@ -0,0 +1,7 @@ +[Command("process", RunMode = RunMode.Async)] +public async Task ProcessAsync(string input) +{ + // Does heavy calculation here. + await Task.Delay(TimeSpan.FromMinute(1)); + await ReplyAsync(input); +} \ No newline at end of file diff --git a/docs/faq/commands/samples/runmode-cmdconfig.cs b/docs/faq/commands/samples/runmode-cmdconfig.cs new file mode 100644 index 000000000..11d9cc295 --- /dev/null +++ b/docs/faq/commands/samples/runmode-cmdconfig.cs @@ -0,0 +1,10 @@ +public class Setup +{ + private readonly CommandService _command; + + public Setup() + { + var config = new CommandServiceConfig{ DefaultRunMode = RunMode.Async }; + _command = new CommandService(config); + } +} \ No newline at end of file diff --git a/docs/faq/misc/glossary.md b/docs/faq/misc/glossary.md new file mode 100644 index 000000000..4b661f65c --- /dev/null +++ b/docs/faq/misc/glossary.md @@ -0,0 +1,82 @@ +--- +uid: FAQ.Glossary +title: Common Terminologies / Glossary +--- + +# Glossary + +This is an additional chapter for quick references to various common +types that you may see within Discord.Net. To see more information +regarding each type of object, click on the object to navigate +to our API documentation page where you might find more explanation +about it. + +## Common Types + +* A **Guild** ([IGuild]) is an isolated collection of users and +channels, and are often referred to as "servers". + - Example: [Discord API](https://discord.gg/jkrBmQR) +* A **Channel** ([IChannel]) represents a generic channel. + - Example: #dotnet_discord-net + - See [Channel Types](#channel-types) + +[IGuild]: xref:Discord.IGuild +[IChannel]: xref:Discord.IChannel + +## Channel Types + +### Message Channels +* A **Text Channel** ([ITextChannel]) is a message channel from a +Guild. +* A **DM Channel** ([IDMChannel]) is a message channel from a DM. +* A **Group Channel** ([IGroupChannel]) is a message channel from a +Group. + - This is rarely used due to the bot's inability to join groups. +* A **Private Channel** ([IPrivateChannel]) is a DM or a Group. +* A **Message Channel** ([IMessageChannel]) can be any of the above. + +### Misc Channels +* A **Guild Channel** ([IGuildChannel]) is a guild channel in a guild. + - This can be any channels that may exist in a guild. +* A **Voice Channel** ([IVoiceChannel]) is a voice channel in a guild. +* A **Category Channel** ([ICategoryChannel]) (2.0+) is a category that +holds one or more sub-channels. +* A **Nested Channel** ([INestedChannel]) (2.0+) is a channel that can +exist under a category. + +[INestedChannel]: xref:Discord.INestedChannel +[IGuildChannel]: xref:Discord.IGuildChannel +[IMessageChannel]: xref:Discord.IMessageChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGroupChannel]: xref:Discord.IGroupChannel +[IDMChannel]: xref:Discord.IDMChannel +[IPrivateChannel]: xref:Discord.IPrivateChannel +[IVoiceChannel]: xref:Discord.IVoiceChannel +[ICategoryChannel]: xref:Discord.ICategoryChannel + +## Emoji Types + +* An **Emote** ([Emote]) is a custom emote from a guild. + - Example: `<:dotnet:232902710280716288>` +* An **Emoji** ([Emoji]) is a Unicode emoji. + - Example: `👍` + +[Emote]: xref:Discord.Emote +[Emoji]: xref:Discord.Emoji + +## Activity Types + +* A **Game** ([Game]) refers to a user's game activity. +* A **Rich Presence** ([RichGame]) refers to a user's detailed +gameplay status. + - Visit [Rich Presence Intro] on Discord docs for more info. +* A **Streaming Status** ([StreamingGame]) refers to user's activity +for streaming on services such as Twitch. +* A **Spotify Status** ([SpotifyGame]) (2.0+) refers to a user's +activity for listening to a song on Spotify. + +[Game]: xref:Discord.Game +[RichGame]: xref:Discord.RichGame +[StreamingGame]: xref:Discord.StreamingGame +[SpotifyGame]: xref:Discord.SpotifyGame +[Rich Presence Intro]: https://discord.com/developers/docs/rich-presence/best-practices diff --git a/docs/faq/misc/legacy.md b/docs/faq/misc/legacy.md new file mode 100644 index 000000000..5931579d3 --- /dev/null +++ b/docs/faq/misc/legacy.md @@ -0,0 +1,29 @@ +--- +uid: FAQ.Legacy +title: Questions about Legacy Versions +--- + +# Legacy Questions + +This section refers to legacy library-related questions that do not +apply to the latest or recent version of the Discord.Net library. + +## X, Y, Z does not work! It doesn't return a valid value anymore. + +If you are currently using an older version of the stable branch, +please upgrade to the latest pre-release version to ensure maximum +compatibility. Several features may be broken in older +versions and will likely not be fixed in the version branch due to +their breaking nature. + +Visit the repo's [release tag] to see the latest public pre-release. + +[release tag]: https://github.com/RogueException/Discord.Net/releases + +## I came from an earlier version of Discord.Net 1.0, and DependencyMap doesn't seem to exist anymore in the later revision? What happened to it? + +The `DependencyMap` has been replaced with Microsoft's +[DependencyInjection] Abstractions. An example usage can be seen +[here](https://github.com/foxbot/DiscordBotBase/blob/csharp/src/DiscordBot/Program.cs#L36). + +[DependencyInjection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml new file mode 100644 index 000000000..393e948f6 --- /dev/null +++ b/docs/faq/toc.yml @@ -0,0 +1,18 @@ +- name: Basic Concepts + items: + - name: Getting Started + topicUid: FAQ.Basics.GetStarted + - name: Basic Operations + topicUid: FAQ.Basics.BasicOp + - name: Client Basics + topicUid: FAQ.Basics.ClientBasics +- name: Commands + items: + - name: General + topicUid: FAQ.Commands.General + - name: Dependency Injection + topicUid: FAQ.Commands.DI +- name: Glossary + topicUid: FAQ.Glossary +- name: Legacy or Upgrade + topicUid: FAQ.Legacy diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 000000000..34a2cd1ca Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml index 715b39606..598fd3442 100644 --- a/docs/filterConfig.yml +++ b/docs/filterConfig.yml @@ -1,11 +1,10 @@ apiRules: - exclude: - uidRegex: ^Discord\.API$ -- exclude: - uidRegex: ^Discord\.API.*$ + uidRegex: ^Discord\.Net\..*$ + type: Namespace - exclude: - uidRegex: ^Discord\.Net\.Converters$ + uidRegex: ^Discord\.Analyzers$ + type: Namespace - exclude: - uidRegex: ^Discord\.Net.*$ -- exclude: - uidRegex: ^RegexAnalyzer$ \ No newline at end of file + uidRegex: ^Discord\.API$ + type: Namespace \ No newline at end of file diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md deleted file mode 100644 index 2b012af0e..000000000 --- a/docs/guides/commands/commands.md +++ /dev/null @@ -1,343 +0,0 @@ -# The Command Service - -[Discord.Commands](xref:Discord.Commands) provides an Attribute-based -command parser. - -## Setup - -To use Commands, you must create a [Command Service] and a Command -Handler. - -Included below is a very barebone Command Handler. You can extend your -Command Handler as much as you like; however, the below is the bare -minimum. - -The `CommandService` will optionally accept a [CommandServiceConfig], -which _does_ set a few default values for you. It is recommended to -look over the properties in [CommandServiceConfig] and their default -values. - -[!code-csharp[Command Handler](samples/command_handler.cs)] - -[Command Service]: xref:Discord.Commands.CommandService -[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig - -## With Attributes - -In 1.0, Commands can be defined ahead of time with attributes, or at -runtime with builders. - -For most bots, ahead-of-time Commands should be all you need, and this -is the recommended method of defining Commands. - -### Modules - -The first step to creating Commands is to create a _module_. - -A Module is an organizational pattern that allows you to write your -Commands in different classes and have them automatically loaded. - -Discord.Net's implementation of Modules is influenced heavily from -ASP.NET Core's Controller pattern. This means that the lifetime of a -module instance is only as long as the Command is being invoked. - -**Avoid using long-running code** in your modules wherever possible. -You should **not** be implementing very much logic into your modules, -instead, outsource to a service for that. - -If you are unfamiliar with Inversion of Control, it is recommended to -read the MSDN article on [IoC] and [Dependency Injection]. - -To begin, create a new class somewhere in your project and inherit the -class from [ModuleBase]. This class **must** be `public`. - ->[!NOTE] ->[ModuleBase] is an _abstract_ class, meaning that you may extend it ->or override it as you see fit. Your module may inherit from any ->extension of ModuleBase. - -By now, your module should look like this: - -[!code-csharp[Empty Module](samples/empty-module.cs)] - -[IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx -[Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx -[ModuleBase]: xref:Discord.Commands.ModuleBase`1 - -### Adding Commands - -The next step to creating Commands is actually creating the Commands. - -To create a Command, add a method to your module of type `Task`. -Typically, you will want to mark this method as `async`, although it -is not required. - -Adding parameters to a Command is done by adding parameters to the -parent Task. - -For example, to take an integer as an argument from the user, add `int -arg`; to take a user as an argument from the user, add `IUser user`. -In 1.0, a Command can accept nearly any type of argument; a full list -of types that are parsed by default can be found in the below section -on _Type Readers_. - -Parameters, by default, are always required. To make a parameter -optional, give it a default value. To accept a comma-separated list, -set the parameter to `params Type[]`. - -Should a parameter include spaces, it **must** be wrapped in quotes. -For example, for a Command with a parameter `string food`, you would -execute it with `!favoritefood "Key Lime Pie"`. - -If you would like a parameter to parse until the end of a Command, -flag the parameter with the [RemainderAttribute]. This will allow a -user to invoke a Command without wrapping a parameter in quotes. - -Finally, flag your Command with the [CommandAttribute]. (you must -specify a name for this Command, except for when it is part of a -Module Group - see below) - -[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute -[CommandAttribute]: xref:Discord.Commands.CommandAttribute - -### Command Overloads - -You may add overloads to your Commands, and the Command parser will -automatically pick up on it. - -If for whatever reason, you have two Commands which are ambiguous to -each other, you may use the @Discord.Commands.PriorityAttribute to -specify which should be tested before the other. - -The `Priority` attributes are sorted in ascending order; the higher -priority will be called first. - -### Command Context - -Every Command can access the execution context through the [Context] -property on [ModuleBase]. `ICommandContext` allows you to access the -message, channel, guild, and user that the Command was invoked from, -as well as the underlying Discord client that the Command was invoked -from. - -Different types of Contexts may be specified using the generic variant -of [ModuleBase]. When using a [SocketCommandContext], for example, the -properties on this context will already be Socket entities, so you -will not need to cast them. - -To reply to messages, you may also invoke [ReplyAsync], instead of -accessing the channel through the [Context] and sending a message. - -> [!WARNING] ->Contexts should **NOT** be mixed! You cannot have one module that ->uses `CommandContext` and another that uses `SocketCommandContext`. - -[Context]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_Context -[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext -[ReplyAsync]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_ReplyAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ - -### Example Module - -At this point, your module should look comparable to this example: -[!code-csharp[Example Module](samples/module.cs)] - -#### Loading Modules Automatically - -The Command Service can automatically discover all classes in an -Assembly that inherit [ModuleBase] and load them. - -To opt a module out of auto-loading, flag it with -[DontAutoLoadAttribute]. - -Invoke [CommandService.AddModulesAsync] to discover modules and -install them. - -[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute -[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_ - -#### Loading Modules Manually - -To manually load a module, invoke [CommandService.AddModuleAsync] by -passing in the generic type of your module and optionally, a -dependency map. - -[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1 - -### Module Constructors - -Modules are constructed using Dependency Injection. Any parameters -that are placed in the Module's constructor must be injected into an -@System.IServiceProvider first. Alternatively, you may accept an -`IServiceProvider` as an argument and extract services yourself. - -### Module Properties - -Modules with `public` settable properties will have the dependencies -injected after the construction of the Module. - -### Module Groups - -Module Groups allow you to create a module where Commands are -prefixed. To create a group, flag a module with the -@Discord.Commands.GroupAttribute. - -Module groups also allow you to create **nameless Commands**, where -the [CommandAttribute] is configured with no name. In this case, the -Command will inherit the name of the group it belongs to. - -### Submodules - -Submodules are Modules that reside within another one. Typically, -submodules are used to create nested groups (although not required to -create nested groups). - -[!code-csharp[Groups and Submodules](samples/groups.cs)] - -## With Builders - -**TODO** - -## Dependency Injection - -The Command Service is bundled with a very barebone Dependency -Injection service for your convenience. It is recommended that you use -DI when writing your modules. - -### Setup - -First, you need to create an @System.IServiceProvider; you may create -your own one if you wish. - -Next, add the dependencies that your modules will use to the map. - -Finally, pass the map into the `LoadAssembly` method. Your modules -will be automatically loaded with this dependency map. - -[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] - -### Usage in Modules - -In the constructor of your Module, any parameters will be filled in by -the @System.IServiceProvider that you've passed into `LoadAssembly`. - -Any publicly settable properties will also be filled in the same -manner. - ->[!NOTE] -> Annotating a property with a [DontInjectAttribute] attribute will prevent the -property from being injected. - ->[!NOTE] ->If you accept `CommandService` or `IServiceProvider` as a parameter -in your constructor or as an injectable property, these entries will -be filled by the `CommandService` that the Module is loaded from and -the `ServiceProvider` that is passed into it respectively. - -[!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] - -[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute - -# Preconditions - -Precondition serve as a permissions system for your Commands. Keep in -mind, however, that they are not limited to _just_ permissions and can -be as complex as you want them to be. - ->[!NOTE] ->There are two types of Preconditions. -[PreconditionAttribute] can be applied to Modules, Groups, or Commands; -[ParameterPreconditionAttribute] can be applied to Parameters. - -[PreconditionAttribute]: xref:Discord.Commands.PreconditionAttribute -[ParameterPreconditionAttribute]: xref:Discord.Commands.ParameterPreconditionAttribute - -## Bundled Preconditions - -Commands ship with four bundled Preconditions; you may view their -usages on their respective API pages. - -- @Discord.Commands.RequireContextAttribute -- @Discord.Commands.RequireOwnerAttribute -- @Discord.Commands.RequireBotPermissionAttribute -- @Discord.Commands.RequireUserPermissionAttribute - -## Custom Preconditions - -To write your own Precondition, create a new class that inherits from -either [PreconditionAttribute] or [ParameterPreconditionAttribute] -depending on your use. - -In order for your Precondition to function, you will need to override -the [CheckPermissions] method. - -Your IDE should provide an option to fill this in for you. - -If the context meets the required parameters, return -[PreconditionResult.FromSuccess], otherwise return -[PreconditionResult.FromError] and include an error message if -necessary. - -[!code-csharp[Custom Precondition](samples/require_owner.cs)] - -[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_ICommandContext_Discord_Commands_CommandInfo_IServiceProvider_ -[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess -[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ - -# Type Readers - -Type Readers allow you to parse different types of arguments in -your commands. - -By default, the following Types are supported arguments: - -- bool -- char -- sbyte/byte -- ushort/short -- uint/int -- ulong/long -- float, double, decimal -- string -- DateTime/DateTimeOffset/TimeSpan -- IMessage/IUserMessage -- IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel -- IUser/IGuildUser/IGroupUser -- IRole - -### Creating a Type Readers - -To create a `TypeReader`, create a new class that imports @Discord and -@Discord.Commands and ensure the class inherits from -@Discord.Commands.TypeReader. - -Next, satisfy the `TypeReader` class by overriding the [Read] method. - ->[!NOTE] ->In many cases, Visual Studio can fill this in for you, using the ->"Implement Abstract Class" IntelliSense hint. - -Inside this task, add whatever logic you need to parse the input -string. - -If you are able to successfully parse the input, return -[TypeReaderResult.FromSuccess] with the parsed input, otherwise return -[TypeReaderResult.FromError] and include an error message if -necessary. - -[TypeReaderResult]: xref:Discord.Commands.TypeReaderResult -[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromSuccess_Discord_Commands_TypeReaderValue_ -[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromError_Discord_Commands_CommandError_System_String_ -[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_ICommandContext_System_String_IServiceProvider_ - -#### Sample - -[!code-csharp[TypeReaders](samples/typereader.cs)] - -### Installing TypeReaders - -TypeReaders are not automatically discovered by the Command Service -and must be explicitly added. - -To install a TypeReader, invoke [CommandService.AddTypeReader]. - -[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ diff --git a/docs/guides/commands/dependency-injection.md b/docs/guides/commands/dependency-injection.md new file mode 100644 index 000000000..5dc5b02d2 --- /dev/null +++ b/docs/guides/commands/dependency-injection.md @@ -0,0 +1,47 @@ +--- +uid: Guides.Commands.DI +title: Dependency Injection +--- + +# Dependency Injection + +The Command Service is bundled with a very barebone Dependency +Injection service for your convenience. It is recommended that you use +DI when writing your modules. + +## Setup + +1. Create a @Microsoft.Extensions.DependencyInjection.ServiceCollection. +2. Add the dependencies to the service collection that you wish + to use in the modules. +3. Build the service collection into a service provider. +4. Pass the service collection into @Discord.Commands.CommandService.AddModulesAsync* / @Discord.Commands.CommandService.AddModuleAsync* , @Discord.Commands.CommandService.ExecuteAsync* . + +### Example - Setting up Injection + +[!code-csharp[IServiceProvider Setup](samples/dependency-injection/dependency_map_setup.cs)] + +## Usage in Modules + +In the constructor of your module, any parameters will be filled in by +the @System.IServiceProvider that you've passed. + +Any publicly settable properties will also be filled in the same +manner. + +> [!NOTE] +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. + +> [!NOTE] +> If you accept `CommandService` or `IServiceProvider` as a parameter +> in your constructor or as an injectable property, these entries will +> be filled by the `CommandService` that the module is loaded from and +> the `IServiceProvider` that is passed into it respectively. + +### Example - Injection in Modules + +[!code-csharp[Injection Modules](samples/dependency-injection/dependency_module.cs)] +[!code-csharp[Disallow Dependency Injection](samples/dependency-injection/dependency_module_noinject.cs)] + +[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute \ No newline at end of file diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md new file mode 100644 index 000000000..14341a32b --- /dev/null +++ b/docs/guides/commands/intro.md @@ -0,0 +1,221 @@ +--- +uid: Guides.Commands.Intro +title: Introduction to Command Service +--- + +# The Command Service + +[Discord.Commands](xref:Discord.Commands) provides an attribute-based +command parser. + +## Get Started + +To use commands, you must create a [Command Service] and a command +handler. + +Included below is a barebone command handler. You can extend your +command handler as much as you like; however, the below is the bare +minimum. + +> [!NOTE] +> The `CommandService` will optionally accept a [CommandServiceConfig], +> which *does* set a few default values for you. It is recommended to +> look over the properties in [CommandServiceConfig] and their default +> values. + +[!code-csharp[Command Handler](samples/intro/command_handler.cs)] + +[Command Service]: xref:Discord.Commands.CommandService +[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig + +## With Attributes + +Starting from 1.0, commands can be defined ahead of time with +attributes, or at runtime with builders. + +For most bots, ahead-of-time commands should be all you need, and this +is the recommended method of defining commands. + +### Modules + +The first step to creating commands is to create a _module_. + +A module is an organizational pattern that allows you to write your +commands in different classes and have them automatically loaded. + +Discord.Net's implementation of "modules" is influenced heavily by the +ASP.NET Core's Controller pattern. This means that the lifetime of a +module instance is only as long as the command is being invoked. + +Before we create a module, it is **crucial** for you to remember that +in order to create a module and have it automatically discovered, +your module must: + +* Be public +* Inherit [ModuleBase] + +By now, your module should look like this: + +[!code-csharp[Empty Module](samples/intro/empty-module.cs)] + +> [!NOTE] +> [ModuleBase] is an `abstract` class, meaning that you may extend it +> or override it as you see fit. Your module may inherit from any +> extension of ModuleBase. + +[IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx +[Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx +[ModuleBase]: xref:Discord.Commands.ModuleBase`1 + +### Adding/Creating Commands + +> [!WARNING] +> **Avoid using long-running code** in your modules wherever possible. +> Long-running code, by default, within a command module +> can cause gateway thread to be blocked; therefore, interrupting +> the bot's connection to Discord. +> +> You may read more about it in @FAQ.Commands.General . + +The next step to creating commands is actually creating the commands. + +For a command to be valid, it **must** have a return type of `Task` +or `Task`. Typically, you might want to mark this +method as `async`, although it is not required. + +Then, flag your command with the [CommandAttribute]. Note that you must +specify a name for this command, except for when it is part of a +[Module Group](#module-groups). + +### Command Parameters + +Adding parameters to a command is done by adding parameters to the +parent `Task`. + +For example: + +* To take an integer as an argument from the user, add `int num`. +* To take a user as an argument from the user, add `IUser user`. +* ...etc. + +Starting from 1.0, a command can accept nearly any type of argument; +a full list of types that are parsed by default can +be found in @Guides.Commands.TypeReaders. + +[CommandAttribute]: xref:Discord.Commands.CommandAttribute + +#### Optional Parameters + +Parameters, by default, are always required. To make a parameter +optional, give it a default value (i.e., `int num = 0`). + +#### Parameters with Spaces + +To accept a space-separated list, set the parameter to `params Type[]`. + +Should a parameter include spaces, the parameter **must** be +wrapped in quotes. For example, for a command with a parameter +`string food`, you would execute it with +`!favoritefood "Key Lime Pie"`. + +If you would like a parameter to parse until the end of a command, +flag the parameter with the [RemainderAttribute]. This will +allow a user to invoke a command without wrapping a +parameter in quotes. + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute + +### Command Overloads + +You may add overloads to your commands, and the command parser will +automatically pick up on it. + +If, for whatever reason, you have two commands which are ambiguous to +each other, you may use the @Discord.Commands.PriorityAttribute to +specify which should be tested before the other. + +The `Priority` attributes are sorted in descending order; the higher +priority will be called first. + +### Command Context + +Every command can access the execution context through the [Context] +property on [ModuleBase]. `ICommandContext` allows you to access the +message, channel, guild, user, and the underlying Discord client +that the command was invoked from. + +Different types of `Context` may be specified using the generic variant +of [ModuleBase]. When using a [SocketCommandContext], for example, the +properties on this context will already be Socket entities, so you +will not need to cast them. + +To reply to messages, you may also invoke [ReplyAsync], instead of +accessing the channel through the [Context] and sending a message. + +> [!WARNING] +> Contexts should **NOT** be mixed! You cannot have one module that +> uses `CommandContext` and another that uses `SocketCommandContext`. + +[Context]: xref:Discord.Commands.ModuleBase`1.Context +[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext +[ReplyAsync]: xref:Discord.Commands.ModuleBase`1.ReplyAsync* + +> [!TIP] +> At this point, your module should look comparable to this example: +> [!code-csharp[Example Module](samples/intro/module.cs)] + +#### Loading Modules Automatically + +The Command Service can automatically discover all classes in an +`Assembly` that inherit [ModuleBase] and load them. Invoke +[CommandService.AddModulesAsync] to discover modules and +install them. + +To opt a module out of auto-loading, flag it with +[DontAutoLoadAttribute]. + +[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute +[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* + +#### Loading Modules Manually + +To manually load a module, invoke [CommandService.AddModuleAsync] by +passing in the generic type of your module and optionally, a +service provider. + +[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService.AddModuleAsync* + +### Module Constructors + +Modules are constructed using @Guides.Commands.DI. Any parameters +that are placed in the Module's constructor must be injected into an +@System.IServiceProvider first. + +> [!TIP] +> Alternatively, you may accept an +> `IServiceProvider` as an argument and extract services yourself, +> although this is discouraged. + +### Module Properties + +Modules with `public` settable properties will have the dependencies +injected after the construction of the module. See @Guides.Commands.DI +to learn more. + +### Module Groups + +Module Groups allow you to create a module where commands are +prefixed. To create a group, flag a module with the +@Discord.Commands.GroupAttribute. + +Module Groups also allow you to create **nameless Commands**, where +the [CommandAttribute] is configured with no name. In this case, the +command will inherit the name of the group it belongs to. + +### Submodules + +Submodules are "modules" that reside within another one. Typically, +submodules are used to create nested groups (although not required to +create nested groups). + +[!code-csharp[Groups and Submodules](samples/intro/groups.cs)] diff --git a/docs/guides/commands/namedarguments.md b/docs/guides/commands/namedarguments.md new file mode 100644 index 000000000..890a8463f --- /dev/null +++ b/docs/guides/commands/namedarguments.md @@ -0,0 +1,79 @@ +--- +uid: Guides.Commands.NamedArguments +title: Named Arguments +--- + +# Named Arguments + +By default, arguments for commands are parsed positionally, meaning +that the order matters. But sometimes you may want to define a command +with many optional parameters, and it'd be easier for end-users +to only specify what they want to set, instead of needing them +to specify everything by hand. + +## Setting up Named Arguments + +In order to be able to specify different arguments by name, you have +to create a new class that contains all of the optional values that +the command will use, and apply an instance of +[NamedArgumentTypeAttribute] on it. + +### Example - Creating a Named Arguments Type + +```cs +[NamedArgumentType] +public class NamableArguments +{ + public string First { get; set; } + public string Second { get; set; } + public string Third { get; set; } + public string Fourth { get; set; } +} +``` + +## Usage in a Command + +The command where you want to use these values can be declared like so: +```cs +[Command("act")] +public async Task Act(int requiredArg, NamableArguments namedArgs) +``` + +The command can now be invoked as +`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`. + +A TypeReader for the named arguments container type is +automatically registered. +It's important that any other arguments that would be required +are placed before the container type. + +> [!IMPORTANT] +> A single command can have only __one__ parameter of a +> type annotated with [NamedArgumentTypeAttribute], and it +> **MUST** be the last parameter in the list. +> A command parameter of such an annotated type +> is automatically treated as if that parameter +> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute) +> applied. + +## Complex Types + +The TypeReader for Named Argument Types will look for a TypeReader +of every property type, meaning any other command parameter type +will work just the same. + +You can also read multiple values into a single property +by making that property an `IEnumerable`. So for example, if your +Named Argument Type has the following field, +```cs +public IEnumerable Numbers { get; set; } +``` +then the command can be invoked as +`.cmd numbers: "1, 2, 4, 8, 16, 32"` + +## Additional Notes + +The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute) +is also supported on the properties of a Named Argument Type. + +[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute diff --git a/docs/guides/commands/post-execution.md b/docs/guides/commands/post-execution.md new file mode 100644 index 000000000..782d256b2 --- /dev/null +++ b/docs/guides/commands/post-execution.md @@ -0,0 +1,120 @@ +--- +uid: Guides.Commands.PostExecution +title: Post-command Execution Handling +--- + +# Post-execution Handling for Commands + +When developing commands, you may want to consider building a +post-execution handling system so you can have finer control +over commands. Discord.Net offers several post-execution workflows +for you to work with. + +If you recall, in the [Command Guide], we have shown the following +example for executing and handling commands, + +[!code[Command Handler](samples/intro/command_handler.cs)] + +You may notice that after we perform [ExecuteAsync], we store the +result and print it to the chat, essentially creating the most +fundamental form of a post-execution handler. + +With this in mind, we could start doing things like the following, + +[!code[Basic Command Handler](samples/post-execution/post-execution_basic.cs)] + +However, this may not always be preferred, because you are +creating your post-execution logic *with* the essential command +handler. This design could lead to messy code and could potentially +be a violation of the SRP (Single Responsibility Principle). + +Another major issue is if your command is marked with +`RunMode.Async`, [ExecuteAsync] will **always** return a successful +[ExecuteResult] instead of the actual result. You can learn more +about the impact in @FAQ.Commands.General. + +## CommandExecuted Event + +Enter [CommandExecuted], an event that was introduced in +Discord.Net 2.0. This event is raised whenever a command is +executed regardless of its execution status. This means this +event can be used to streamline your post-execution design, +is not prone to `RunMode.Async`'s [ExecuteAsync] drawbacks. + +Thus, we can begin working on code such as: + +[!code[CommandExecuted demo](samples/post-execution/command_executed_demo.cs)] + +So now we have a streamlined post-execution pipeline, great! What's +next? We can take this further by using [RuntimeResult]. + +### RuntimeResult + +`RuntimeResult` was initially introduced in 1.0 to allow +developers to centralize their command result logic. +In other words, it is a result type that is designed to be +returned when the command has finished its execution. + +However, it wasn't widely adopted due to the aforementioned +[ExecuteAsync] drawback. Since we now have access to a proper +result-handler via the [CommandExecuted] event, we can start +making use of this class. + +The best way to make use of it is to create your version of +`RuntimeResult`. You can achieve this by inheriting the `RuntimeResult` +class. + +The following creates a bare-minimum required for a sub-class +of `RuntimeResult`, + +[!code[Base Use](samples/post-execution/customresult_base.cs)] + +The sky is the limit from here. You can add any additional information +you would like regarding the execution result. + +For example, you may want to add your result type or other +helpful information regarding the execution, or something +simple like static methods to help you create return types easily. + +[!code[Extended Use](samples/post-execution/customresult_extended.cs)] + +After you're done creating your [RuntimeResult], you can +implement it in your command by marking the command return type to +`Task`. + +> [!NOTE] +> You must mark the return type as `Task` instead of +> `Task`. Only the former will be picked up when +> building the module. + +Here's an example of a command that utilizes such logic: + +[!code[Usage](samples/post-execution/customresult_usage.cs)] + +And now we can check for it in our [CommandExecuted] handler: + +[!code[Usage](samples/post-execution/command_executed_adv_demo.cs)] + +## CommandService.Log Event + +We have so far covered the handling of various result types, but we +have not talked about what to do if the command enters a catastrophic +failure (i.e., exceptions). To resolve this, we can make use of the +[CommandService.Log] event. + +All exceptions thrown during a command execution are caught and sent +to the Log event under the [LogMessage.Exception] property +as a [CommandException] type. The [CommandException] class allows +us to access the exception thrown, as well as the context +of the command. + +[!code[Logger Sample](samples/post-execution/command_exception_log.cs)] + +[CommandException]: xref:Discord.Commands.CommandException +[LogMessage.Exception]: xref:Discord.LogMessage.Exception +[CommandService.Log]: xref:Discord.Commands.CommandService.Log +[RuntimeResult]: xref:Discord.Commands.RuntimeResult +[CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[Command Guide]: xref:Guides.Commands.Intro \ No newline at end of file diff --git a/docs/guides/commands/preconditions.md b/docs/guides/commands/preconditions.md new file mode 100644 index 000000000..8e8298b86 --- /dev/null +++ b/docs/guides/commands/preconditions.md @@ -0,0 +1,83 @@ +--- +uid: Guides.Commands.Preconditions +title: Preconditions +--- + +# Preconditions + +Preconditions serve as a permissions system for your Commands. Keep in +mind, however, that they are not limited to _just_ permissions and can +be as complex as you want them to be. + +There are two types of Preconditions you can use: + +* [PreconditionAttribute] can be applied to Modules, Groups, or Commands. +* [ParameterPreconditionAttribute] can be applied to Parameters. + +You may visit their respective API documentation to find out more. + +[PreconditionAttribute]: xref:Discord.Commands.PreconditionAttribute +[ParameterPreconditionAttribute]: xref:Discord.Commands.ParameterPreconditionAttribute + +## Bundled Preconditions + +@Discord.Commands ships with several bundled Preconditions for you +to use. + +* @Discord.Commands.RequireContextAttribute +* @Discord.Commands.RequireOwnerAttribute +* @Discord.Commands.RequireBotPermissionAttribute +* @Discord.Commands.RequireUserPermissionAttribute +* @Discord.Commands.RequireNsfwAttribute + +## Using Preconditions + +To use a precondition, simply apply any valid precondition candidate to +a command method signature as an attribute. + +### Example - Using a Precondition + +[!code-csharp[Precondition usage](samples/preconditions/precondition_usage.cs)] + +## ORing Preconditions + +When writing commands, you may want to allow some of them to be +executed when only some of the precondition checks are passed. + +This is where the [Group] property of a precondition attribute comes in +handy. By assigning two or more preconditions to a group, the command +system will allow the command to be executed when one of the +precondition passes. + +### Example - ORing Preconditions + +[!code-csharp[OR Precondition](samples/preconditions/group_precondition.cs)] + +[Group]: xref:Discord.Commands.PreconditionAttribute.Group + +## Custom Preconditions + +To write your own Precondition, create a new class that inherits from +either [PreconditionAttribute] or [ParameterPreconditionAttribute] +depending on your use. + +In order for your Precondition to function, you will need to override +the [CheckPermissionsAsync] method. + +If the context meets the required parameters, return +[PreconditionResult.FromSuccess], otherwise return +[PreconditionResult.FromError] and include an error message if +necessary. + +> [!NOTE] +> Visual Studio can help you implement missing members +> from the abstract class by using the "Implement Abstract Class" +> IntelliSense hint. + +### Example - Creating a Custom Precondition + +[!code-csharp[Custom Precondition](samples/preconditions/require_role.cs)] + +[CheckPermissionsAsync]: xref:Discord.Commands.PreconditionAttribute.CheckPermissionsAsync* +[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult.FromSuccess* +[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult.FromError* diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs deleted file mode 100644 index da2453aa8..000000000 --- a/docs/guides/commands/samples/command_handler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Reflection; -using Discord; -using Discord.WebSocket; -using Discord.Commands; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - private CommandService _commands; - private DiscordSocketClient _client; - private IServiceProvider _services; - - private static void Main(string[] args) => new Program().StartAsync().GetAwaiter().GetResult(); - - public async Task StartAsync() - { - _client = new DiscordSocketClient(); - _commands = new CommandService(); - - // Avoid hard coding your token. Use an external source instead in your code. - string token = "bot token here"; - - _services = new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(_commands) - .BuildServiceProvider(); - - await InstallCommandsAsync(); - - await _client.LoginAsync(TokenType.Bot, token); - await _client.StartAsync(); - - await Task.Delay(-1); - } - - public async Task InstallCommandsAsync() - { - // Hook the MessageReceived Event into our Command Handler - _client.MessageReceived += HandleCommandAsync; - // Discover all of the commands in this assembly and load them. - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); - } - - private async Task HandleCommandAsync(SocketMessage messageParam) - { - // Don't process the command if it was a System Message - var message = messageParam as SocketUserMessage; - if (message == null) return; - // Create a number to track where the prefix ends and the command begins - int argPos = 0; - // Determine if the message is a command, based on if it starts with '!' or a mention prefix - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; - // Create a Command Context - var context = new SocketCommandContext(_client, message); - // Execute the command. (result does not indicate a return value, - // rather an object stating if the command executed successfully) - var result = await _commands.ExecuteAsync(context, argPos, _services); - if (!result.IsSuccess) - await context.Channel.SendMessageAsync(result.ErrorReason); - } -} \ No newline at end of file diff --git a/docs/guides/commands/samples/dependency-injection/dependency_map_setup.cs b/docs/guides/commands/samples/dependency-injection/dependency_map_setup.cs new file mode 100644 index 000000000..16ca479db --- /dev/null +++ b/docs/guides/commands/samples/dependency-injection/dependency_map_setup.cs @@ -0,0 +1,65 @@ +public class Initialize +{ + private readonly CommandService _commands; + private readonly DiscordSocketClient _client; + + // Ask if there are existing CommandService and DiscordSocketClient + // instance. If there are, we retrieve them and add them to the + // DI container; if not, we create our own. + public Initialize(CommandService commands = null, DiscordSocketClient client = null) + { + _commands = commands ?? new CommandService(); + _client = client ?? new DiscordSocketClient(); + } + + public IServiceProvider BuildServiceProvider() => new ServiceCollection() + .AddSingleton(_client) + .AddSingleton(_commands) + // You can pass in an instance of the desired type + .AddSingleton(new NotificationService()) + // ...or by using the generic method. + // + // The benefit of using the generic method is that + // ASP.NET DI will attempt to inject the required + // dependencies that are specified under the constructor + // for us. + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); +} +public class CommandHandler +{ + private readonly DiscordSocketClient _client; + private readonly CommandService _commands; + private readonly IServiceProvider _services; + + public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client) + { + _commands = commands; + _services = services; + _client = client; + } + + public async Task InitializeAsync() + { + // Pass the service provider to the second parameter of + // AddModulesAsync to inject dependencies to all modules + // that may require them. + await _commands.AddModulesAsync( + assembly: Assembly.GetEntryAssembly(), + services: _services); + _client.MessageReceived += HandleCommandAsync; + } + + public async Task HandleCommandAsync(SocketMessage msg) + { + // ... + // Pass the service provider to the ExecuteAsync method for + // precondition checks. + await _commands.ExecuteAsync( + context: context, + argPos: argPos, + services: _services); + // ... + } +} diff --git a/docs/guides/commands/samples/dependency-injection/dependency_module.cs b/docs/guides/commands/samples/dependency-injection/dependency_module.cs new file mode 100644 index 000000000..3e42074ca --- /dev/null +++ b/docs/guides/commands/samples/dependency-injection/dependency_module.cs @@ -0,0 +1,37 @@ +// After setting up dependency injection, modules will need to request +// the dependencies to let the library know to pass +// them along during execution. + +// Dependency can be injected in two ways with Discord.Net. +// You may inject any required dependencies via... +// the module constructor +// -or- +// public settable properties + +// Injection via constructor +public class DatabaseModule : ModuleBase +{ + private readonly DatabaseService _database; + public DatabaseModule(DatabaseService database) + { + _database = database; + } + + [Command("read")] + public async Task ReadFromDbAsync() + { + await ReplyAsync(_database.GetData()); + } +} + +// Injection via public settable properties +public class DatabaseModule : ModuleBase +{ + public DatabaseService DbService { get; set; } + + [Command("read")] + public async Task ReadFromDbAsync() + { + await ReplyAsync(DbService.GetData()); + } +} diff --git a/docs/guides/commands/samples/dependency-injection/dependency_module_noinject.cs b/docs/guides/commands/samples/dependency-injection/dependency_module_noinject.cs new file mode 100644 index 000000000..48cd52308 --- /dev/null +++ b/docs/guides/commands/samples/dependency-injection/dependency_module_noinject.cs @@ -0,0 +1,29 @@ +// Sometimes injecting dependencies automatically with the provided +// methods in the prior example may not be desired. + +// You may explicitly tell Discord.Net to **not** inject the properties +// by either... +// restricting the access modifier +// -or- +// applying DontInjectAttribute to the property + +// Restricting the access modifier of the property +public class ImageModule : ModuleBase +{ + public ImageService ImageService { get; } + public ImageModule() + { + ImageService = new ImageService(); + } +} + +// Applying DontInjectAttribute +public class ImageModule : ModuleBase +{ + [DontInject] + public ImageService ImageService { get; set; } + public ImageModule() + { + ImageService = new ImageService(); + } +} diff --git a/docs/guides/commands/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs deleted file mode 100644 index a36925904..000000000 --- a/docs/guides/commands/samples/dependency_map_setup.cs +++ /dev/null @@ -1,18 +0,0 @@ -private IServiceProvider _services; -private CommandService _commands; - -public async Task InstallAsync(DiscordSocketClient client) -{ - // Here, we will inject the ServiceProvider with - // all of the services our client will use. - _services = new ServiceCollection() - .AddSingleton(client) - .AddSingleton(_commands) - // You can pass in an instance of the desired type - .AddSingleton(new NotificationService()) - // ...or by using the generic method. - .AddSingleton() - .BuildServiceProvider(); - // ... - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); -} \ No newline at end of file diff --git a/docs/guides/commands/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs deleted file mode 100644 index 561b0f6ac..000000000 --- a/docs/guides/commands/samples/dependency_module.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; - -public class ModuleA : ModuleBase -{ - private readonly DatabaseService _database; - - // Dependencies can be injected via the constructor - public ModuleA(DatabaseService database) - { - _database = database; - } - - public async Task ReadFromDb() - { - var x = _database.getX(); - await ReplyAsync(x); - } -} - -public class ModuleB -{ - - // Public settable properties will be injected - public AnnounceService { get; set; } - - // Public properties without setters will not - public CommandService Commands { get; } - - // Public properties annotated with [DontInject] will not - [DontInject] - public NotificationService { get; set; } - - public ModuleB(CommandService commands) - { - Commands = commands; - } - -} diff --git a/docs/guides/commands/samples/empty-module.cs b/docs/guides/commands/samples/empty-module.cs deleted file mode 100644 index 6483c7cd2..000000000 --- a/docs/guides/commands/samples/empty-module.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Discord.Commands; - -public class InfoModule : ModuleBase -{ - -} \ No newline at end of file diff --git a/docs/guides/commands/samples/intro/command_handler.cs b/docs/guides/commands/samples/intro/command_handler.cs new file mode 100644 index 000000000..480e43c7f --- /dev/null +++ b/docs/guides/commands/samples/intro/command_handler.cs @@ -0,0 +1,55 @@ +public class CommandHandler +{ + private readonly DiscordSocketClient _client; + private readonly CommandService _commands; + + // Retrieve client and CommandService instance via ctor + public CommandHandler(DiscordSocketClient client, CommandService commands) + { + _commands = commands; + _client = client; + } + + public async Task InstallCommandsAsync() + { + // Hook the MessageReceived event into our command handler + _client.MessageReceived += HandleCommandAsync; + + // Here we discover all of the command modules in the entry + // assembly and load them. Starting from Discord.NET 2.0, a + // service provider is required to be passed into the + // module registration method to inject the + // required dependencies. + // + // If you do not use Dependency Injection, pass null. + // See Dependency Injection guide for more information. + await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), + services: null); + } + + private async Task HandleCommandAsync(SocketMessage messageParam) + { + // Don't process the command if it was a system message + var message = messageParam as SocketUserMessage; + if (message == null) return; + + // Create a number to track where the prefix ends and the command begins + int argPos = 0; + + // Determine if the message is a command based on the prefix and make sure no bots trigger commands + if (!(message.HasCharPrefix('!', ref argPos) || + message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) + return; + + // Create a WebSocket-based command context based on the message + var context = new SocketCommandContext(_client, message); + + // Execute the command with the command context we just + // created, along with the service provider for precondition checks. + await _commands.ExecuteAsync( + context: context, + argPos: argPos, + services: null); + } +} diff --git a/docs/guides/commands/samples/intro/empty-module.cs b/docs/guides/commands/samples/intro/empty-module.cs new file mode 100644 index 000000000..db62032c2 --- /dev/null +++ b/docs/guides/commands/samples/intro/empty-module.cs @@ -0,0 +1,8 @@ +using Discord.Commands; + +// Keep in mind your module **must** be public and inherit ModuleBase. +// If it isn't, it will not be discovered by AddModulesAsync! +public class InfoModule : ModuleBase +{ + +} \ No newline at end of file diff --git a/docs/guides/commands/samples/groups.cs b/docs/guides/commands/samples/intro/groups.cs similarity index 52% rename from docs/guides/commands/samples/groups.cs rename to docs/guides/commands/samples/intro/groups.cs index 5f96c34e8..e117a52da 100644 --- a/docs/guides/commands/samples/groups.cs +++ b/docs/guides/commands/samples/intro/groups.cs @@ -4,15 +4,22 @@ public class AdminModule : ModuleBase [Group("clean")] public class CleanModule : ModuleBase { - // ~admin clean 15 + // ~admin clean [Command] - public async Task Default(int count = 10) => Messages(count); + public async Task DefaultCleanAsync() + { + // ... + } // ~admin clean messages 15 [Command("messages")] - public async Task Messages(int count = 10) { } + public async Task CleanAsync(int count) + { + // ... + } } // ~admin ban foxbot#0282 [Command("ban")] - public async Task Ban(IGuildUser user) { } + public Task BanAsync(IGuildUser user) => + Context.Guild.AddBanAsync(user); } \ No newline at end of file diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/intro/module.cs similarity index 58% rename from docs/guides/commands/samples/module.cs rename to docs/guides/commands/samples/intro/module.cs index 1e3555501..e5fe70534 100644 --- a/docs/guides/commands/samples/module.cs +++ b/docs/guides/commands/samples/intro/module.cs @@ -1,24 +1,25 @@ // Create a module with no prefix -public class Info : ModuleBase +public class InfoModule : ModuleBase { - // ~say hello -> hello + // ~say hello world -> hello world [Command("say")] [Summary("Echoes a message.")] - public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) - { - // ReplyAsync is a method on ModuleBase - await ReplyAsync(echo); - } + public Task SayAsync([Remainder] [Summary("The text to echo")] string echo) + => ReplyAsync(echo); + + // ReplyAsync is a method on ModuleBase } // Create a module with the 'sample' prefix [Group("sample")] -public class Sample : ModuleBase +public class SampleModule : ModuleBase { // ~sample square 20 -> 400 [Command("square")] [Summary("Squares a number.")] - public async Task SquareAsync([Summary("The number to square.")] int num) + public async Task SquareAsync( + [Summary("The number to square.")] + int num) { // We can also access the channel from the Command Context. await Context.Channel.SendMessageAsync($"{num}^2 = {Math.Pow(num, 2)}"); @@ -31,9 +32,12 @@ public class Sample : ModuleBase // ~sample userinfo 96642168176807936 --> Khionu#8708 // ~sample whois 96642168176807936 --> Khionu#8708 [Command("userinfo")] - [Summary("Returns info about the current user, or the user parameter, if one passed.")] + [Summary + ("Returns info about the current user, or the user parameter, if one passed.")] [Alias("user", "whois")] - public async Task UserInfoAsync([Summary("The (optional) user to get info for")] SocketUser user = null) + public async Task UserInfoAsync( + [Summary("The (optional) user to get info from")] + SocketUser user = null) { var userInfo = user ?? Context.Client.CurrentUser; await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); diff --git a/docs/guides/commands/samples/post-execution/command_exception_log.cs b/docs/guides/commands/samples/post-execution/command_exception_log.cs new file mode 100644 index 000000000..fa3673e82 --- /dev/null +++ b/docs/guides/commands/samples/post-execution/command_exception_log.cs @@ -0,0 +1,12 @@ +public async Task LogAsync(LogMessage logMessage) +{ + if (logMessage.Exception is CommandException cmdException) + { + // We can tell the user that something unexpected has happened + await cmdException.Context.Channel.SendMessageAsync("Something went catastrophically wrong!"); + + // We can also log this incident + Console.WriteLine($"{cmdException.Context.User} failed to execute '{cmdException.Command.Name}' in {cmdException.Context.Channel}."); + Console.WriteLine(cmdException.ToString()); + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/post-execution/command_executed_adv_demo.cs b/docs/guides/commands/samples/post-execution/command_executed_adv_demo.cs new file mode 100644 index 000000000..dbd75f040 --- /dev/null +++ b/docs/guides/commands/samples/post-execution/command_executed_adv_demo.cs @@ -0,0 +1,13 @@ +public async Task OnCommandExecutedAsync(Optional command, ICommandContext context, IResult result) +{ + switch(result) + { + case MyCustomResult customResult: + // do something extra with it + break; + default: + if (!string.IsNullOrEmpty(result.ErrorReason)) + await context.Channel.SendMessageAsync(result.ErrorReason); + break; + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/post-execution/command_executed_demo.cs b/docs/guides/commands/samples/post-execution/command_executed_demo.cs new file mode 100644 index 000000000..a0b26183e --- /dev/null +++ b/docs/guides/commands/samples/post-execution/command_executed_demo.cs @@ -0,0 +1,38 @@ +public async Task SetupAsync() +{ + await _command.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + // Hook the execution event + _command.CommandExecuted += OnCommandExecutedAsync; + // Hook the command handler + _client.MessageReceived += HandleCommandAsync; +} +public async Task OnCommandExecutedAsync(Optional command, ICommandContext context, IResult result) +{ + // We have access to the information of the command executed, + // the context of the command, and the result returned from the + // execution in this event. + + // We can tell the user what went wrong + if (!string.IsNullOrEmpty(result?.ErrorReason)) + { + await context.Channel.SendMessageAsync(result.ErrorReason); + } + + // ...or even log the result (the method used should fit into + // your existing log handler) + var commandName = command.IsSpecified ? command.Value.Name : "A command"; + await _log.LogAsync(new LogMessage(LogSeverity.Info, + "CommandExecution", + $"{commandName} was executed at {DateTime.UtcNow}.")); +} +public async Task HandleCommandAsync(SocketMessage msg) +{ + var message = messageParam as SocketUserMessage; + if (message == null) return; + int argPos = 0; + if (!(message.HasCharPrefix('!', ref argPos) || + message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) return; + var context = new SocketCommandContext(_client, message); + await _commands.ExecuteAsync(context, argPos, _services); +} diff --git a/docs/guides/commands/samples/post-execution/customresult_base.cs b/docs/guides/commands/samples/post-execution/customresult_base.cs new file mode 100644 index 000000000..895a370c7 --- /dev/null +++ b/docs/guides/commands/samples/post-execution/customresult_base.cs @@ -0,0 +1,6 @@ +public class MyCustomResult : RuntimeResult +{ + public MyCustomResult(CommandError? error, string reason) : base(error, reason) + { + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/post-execution/customresult_extended.cs b/docs/guides/commands/samples/post-execution/customresult_extended.cs new file mode 100644 index 000000000..6754c96aa --- /dev/null +++ b/docs/guides/commands/samples/post-execution/customresult_extended.cs @@ -0,0 +1,10 @@ +public class MyCustomResult : RuntimeResult +{ + public MyCustomResult(CommandError? error, string reason) : base(error, reason) + { + } + public static MyCustomResult FromError(string reason) => + new MyCustomResult(CommandError.Unsuccessful, reason); + public static MyCustomResult FromSuccess(string reason = null) => + new MyCustomResult(null, reason); +} \ No newline at end of file diff --git a/docs/guides/commands/samples/post-execution/customresult_usage.cs b/docs/guides/commands/samples/post-execution/customresult_usage.cs new file mode 100644 index 000000000..44266cfb4 --- /dev/null +++ b/docs/guides/commands/samples/post-execution/customresult_usage.cs @@ -0,0 +1,10 @@ +public class MyModule : ModuleBase +{ + [Command("eat")] + public async Task ChooseAsync(string food) + { + if (food == "salad") + return MyCustomResult.FromError("No, I don't want that!"); + return MyCustomResult.FromSuccess($"Give me the {food}!"). + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/post-execution/post-execution_basic.cs b/docs/guides/commands/samples/post-execution/post-execution_basic.cs new file mode 100644 index 000000000..d1361a1f2 --- /dev/null +++ b/docs/guides/commands/samples/post-execution/post-execution_basic.cs @@ -0,0 +1,14 @@ +// Bad code!!! +var result = await _commands.ExecuteAsync(context, argPos, _services); +if (result.CommandError != null) + switch(result.CommandError) + { + case CommandError.BadArgCount: + await context.Channel.SendMessageAsync( + "Parameter count does not match any command's."); + break; + default: + await context.Channel.SendMessageAsync( + $"An error has occurred {result.ErrorReason}"); + break; + } \ No newline at end of file diff --git a/docs/guides/commands/samples/preconditions/group_precondition.cs b/docs/guides/commands/samples/preconditions/group_precondition.cs new file mode 100644 index 000000000..bae102b9a --- /dev/null +++ b/docs/guides/commands/samples/preconditions/group_precondition.cs @@ -0,0 +1,9 @@ +// The following example only requires the user to either have the +// Administrator permission in this guild or own the bot application. +[RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] +[RequireOwner(Group = "Permission")] +public class AdminModule : ModuleBase +{ + [Command("ban")] + public Task BanAsync(IUser user) => Context.Guild.AddBanAsync(user); +} \ No newline at end of file diff --git a/docs/guides/commands/samples/preconditions/precondition_usage.cs b/docs/guides/commands/samples/preconditions/precondition_usage.cs new file mode 100644 index 000000000..eacee932a --- /dev/null +++ b/docs/guides/commands/samples/preconditions/precondition_usage.cs @@ -0,0 +1,3 @@ +[RequireOwner] +[Command("echo")] +public Task EchoAsync(string input) => ReplyAsync(input); \ No newline at end of file diff --git a/docs/guides/commands/samples/preconditions/require_role.cs b/docs/guides/commands/samples/preconditions/require_role.cs new file mode 100644 index 000000000..d9a393ace --- /dev/null +++ b/docs/guides/commands/samples/preconditions/require_role.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord.Commands; +using Discord.WebSocket; + +// Inherit from PreconditionAttribute +public class RequireRoleAttribute : PreconditionAttribute +{ + // Create a field to store the specified name + private readonly string _name; + + // Create a constructor so the name can be specified + public RequireRoleAttribute(string name) => _name = name; + + // Override the CheckPermissions method + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + // Check if this user is a Guild User, which is the only context where roles exist + if (context.User is SocketGuildUser gUser) + { + // If this command was executed by a user with the appropriate role, return a success + if (gUser.Roles.Any(r => r.Name == _name)) + // Since no async work is done, the result has to be wrapped with `Task.FromResult` to avoid compiler errors + return Task.FromResult(PreconditionResult.FromSuccess()); + // Since it wasn't, fail + else + return Task.FromResult(PreconditionResult.FromError($"You must have a role named {_name} to run this command.")); + } + else + return Task.FromResult(PreconditionResult.FromError("You must be in a guild to run this command.")); + } +} diff --git a/docs/guides/commands/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs deleted file mode 100644 index 3611afab8..000000000 --- a/docs/guides/commands/samples/require_owner.cs +++ /dev/null @@ -1,24 +0,0 @@ -// (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands) - -using Discord.Commands; -using Discord.WebSocket; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Threading.Tasks; - -// Inherit from PreconditionAttribute -public class RequireOwnerAttribute : PreconditionAttribute -{ - // Override the CheckPermissions method - public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) - { - // Get the ID of the bot's owner - var ownerId = (await services.GetService().GetApplicationInfoAsync()).Owner.Id; - // If this command was executed by that user, return a success - if (context.User.Id == ownerId) - return PreconditionResult.FromSuccess(); - // Since it wasn't, fail - else - return PreconditionResult.FromError("You must be the owner of the bot to run this command."); - } -} diff --git a/docs/guides/commands/samples/typereaders/typereader-register.cs b/docs/guides/commands/samples/typereaders/typereader-register.cs new file mode 100644 index 000000000..292caea6f --- /dev/null +++ b/docs/guides/commands/samples/typereaders/typereader-register.cs @@ -0,0 +1,29 @@ +public class CommandHandler +{ + private readonly CommandService _commands; + private readonly DiscordSocketClient _client; + private readonly IServiceProvider _services; + + public CommandHandler(CommandService commands, DiscordSocketClient client, IServiceProvider services) + { + _commands = commands; + _client = client; + _services = services; + } + + public async Task SetupAsync() + { + _client.MessageReceived += CommandHandleAsync; + + // Add BooleanTypeReader to type read for the type "bool" + _commands.AddTypeReader(typeof(bool), new BooleanTypeReader()); + + // Then register the modules + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task CommandHandleAsync(SocketMessage msg) + { + // ... + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/typereader.cs b/docs/guides/commands/samples/typereaders/typereader.cs similarity index 55% rename from docs/guides/commands/samples/typereader.cs rename to docs/guides/commands/samples/typereaders/typereader.cs index d2864a4c7..28611872c 100644 --- a/docs/guides/commands/samples/typereader.cs +++ b/docs/guides/commands/samples/typereaders/typereader.cs @@ -1,10 +1,12 @@ -// Note: This example is obsolete, a boolean type reader is bundled with Discord.Commands +// Please note that the library already supports type reading +// primitive types such as bool. This example is merely used +// to demonstrate how one could write a simple TypeReader. using Discord; using Discord.Commands; public class BooleanTypeReader : TypeReader { - public override Task Read(ICommandContext context, string input, IServiceProvider services) + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { bool result; if (bool.TryParse(input, out result)) diff --git a/docs/guides/commands/typereaders.md b/docs/guides/commands/typereaders.md new file mode 100644 index 000000000..f942c9341 --- /dev/null +++ b/docs/guides/commands/typereaders.md @@ -0,0 +1,70 @@ +--- +uid: Guides.Commands.TypeReaders +title: Type Readers +--- + +# Type Readers + +Type Readers allow you to parse different types of arguments in +your commands. + +By default, the following Types are supported arguments: + +* `bool` +* `char` +* `sbyte`/`byte` +* `ushort`/`short` +* `uint`/`int` +* `ulong`/`long` +* `float`, `double`, `decimal` +* `string` +* `enum` +* `DateTime`/`DateTimeOffset`/`TimeSpan` +* Any nullable value-type (e.g. `int?`, `bool?`) +* Any implementation of `IChannel`/`IMessage`/`IUser`/`IRole` + +## Creating a Type Reader + +To create a `TypeReader`, create a new class that imports @Discord and +@Discord.Commands and ensure the class inherits from +@Discord.Commands.TypeReader. Next, satisfy the `TypeReader` class by +overriding the [ReadAsync] method. + +Inside this Task, add whatever logic you need to parse the input +string. + +If you are able to successfully parse the input, return +[TypeReaderResult.FromSuccess] with the parsed input, otherwise return +[TypeReaderResult.FromError] and include an error message if +necessary. + +> [!NOTE] +> Visual Studio can help you implement missing members +> from the abstract class by using the "Implement Abstract Class" +> IntelliSense hint. + +[TypeReaderResult]: xref:Discord.Commands.TypeReaderResult +[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult.FromSuccess* +[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult.FromError* +[ReadAsync]: xref:Discord.Commands.TypeReader.ReadAsync* + +### Example - Creating a Type Reader + +[!code-csharp[TypeReaders](samples/typereaders/typereader.cs)] + +## Registering a Type Reader + +TypeReaders are not automatically discovered by the Command Service +and must be explicitly added. + +To register a TypeReader, invoke [CommandService.AddTypeReader]. + +> [!IMPORTANT] +> TypeReaders must be added prior to module discovery, otherwise your +> TypeReaders may not work! + +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService.AddTypeReader* + +### Example - Adding a Type Reader + +[!code-csharp[Adding TypeReaders](samples/typereaders/typereader-register.cs)] \ No newline at end of file diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md index 30e5e55cd..d9951a8cc 100644 --- a/docs/guides/concepts/connections.md +++ b/docs/guides/concepts/connections.md @@ -1,51 +1,46 @@ --- +uid: Guides.Concepts.ManageConnections title: Managing Connections --- +# Managing Connections with Discord.Net + In Discord.Net, once a client has been started, it will automatically -maintain a connection to Discord's gateway, until it is manually +maintain a connection to Discord's gateway until it is manually stopped. -### Usage +## Usage To start a connection, invoke the `StartAsync` method on a client that -supports a WebSocket connection. - -These clients include the [DiscordSocketClient] and -[DiscordRpcClient], as well as Audio clients. - -To end a connection, invoke the `StopAsync` method. This will -gracefully close any open WebSocket or UdpSocket connections. +supports a WebSocket connection; to end a connection, invoke the +`StopAsync` method, which gracefully closes any open WebSocket or +UdpSocket connections. Since the Start/Stop methods only signal to an underlying connection -manager that a connection needs to be started, **they return before a -connection is actually made.** +manager that a connection needs to be started, **they return before a +connection is made.** -As a result, you will need to hook into one of the connection-state +As a result, you need to hook into one of the connection-state based events to have an accurate representation of when a client is ready for use. All clients provide a `Connected` and `Disconnected` event, which is raised respectively when a connection opens or closes. In the case of -the DiscordSocketClient, this does **not** mean that the client is +the [DiscordSocketClient], this does **not** mean that the client is ready to be used. -A separate event, `Ready`, is provided on DiscordSocketClient, which +A separate event, `Ready`, is provided on [DiscordSocketClient], which is raised only when the client has finished guild stream or guild -sync, and has a complete guild cache. +sync and has a completed guild cache. [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[DiscordRpcClient]: xref:Discord.Rpc.DiscordRpcClient - -### Samples - -[!code-csharp[Connection Sample](samples/events.cs)] -### Tips +## Reconnection -Avoid running long-running code on the gateway! If you deadlock the -gateway (as explained in [events]), the connection manager will be -unable to recover and reconnect. +> [!TIP] +> Avoid running long-running code on the gateway! If you deadlock the +> gateway (as explained in [events]), the connection manager will +> **NOT** be able to recover and reconnect. Assuming the client disconnected because of a fault on Discord's end, and not a deadlock on your end, we will always attempt to reconnect @@ -53,6 +48,6 @@ and resume a connection. Don't worry about trying to maintain your own connections, the connection manager is designed to be bulletproof and never fail - if -your client doesn't manage to reconnect, you've found a bug! +your client does not manage to reconnect, you have found a bug! -[events]: events.md \ No newline at end of file +[events]: xref:Guides.Concepts.Events diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index 3a5d5496b..5ad5b01f2 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -1,23 +1,26 @@ --- +uid: Guides.Concepts.Entities title: Entities --- ->[!NOTE] -This article is written with the Socket variants of entities in mind, -not the general interfaces or Rest/Rpc entities. +# Entities in Discord.Net + +> [!NOTE] +> This article is written with the Socket variants of entities in mind, +> not the general interfaces or Rest entities. Discord.Net provides a versatile entity system for navigating the Discord API. -### Inheritance +## Inheritance Due to the nature of the Discord API, some entities are designed with multiple variants; for example, `SocketUser` and `SocketGuildUser`. All models will contain the most detailed version of an entity -possible, even if the type is less detailed. +possible, even if the type is less detailed. -For example, in the case of the `MessageReceived` event, a +For example, in the case of the `MessageReceived` event, a `SocketMessage` is passed in with a channel property of type `SocketMessageChannel`. All messages come from channels capable of messaging, so this is the only variant of a channel that can cover @@ -28,44 +31,36 @@ But that doesn't mean a message _can't_ come from a retrieve information about a guild from a message entity, you will need to cast its channel object to a `SocketTextChannel`. -### Navigation +You can find out various types of entities in the @FAQ.Misc.Glossary +page. + +## Navigation All socket entities have navigation properties on them, which allow you to easily navigate to an entity's parent or children. As explained above, you will sometimes need to cast to a more detailed version of an entity to navigate to its parent. -### Accessing Entities +## Accessing Entities The most basic forms of entities, `SocketGuild`, `SocketUser`, and `SocketChannel` can be pulled from the DiscordSocketClient's global cache, and can be retrieved using the respective `GetXXX` method on DiscordSocketClient. ->[!TIP] -It is **vital** that you use the proper IDs for an entity when using -a GetXXX method. It is recommended that you enable Discord's -_developer mode_ to allow easy access to entity IDs, found in -Settings > Appearance > Advanced +> [!TIP] +> It is **vital** that you use the proper IDs for an entity when using +> a `GetXXX` method. It is recommended that you enable Discord's +> _developer mode_ to allow easy access to entity IDs, found in +> Settings > Appearance > Advanced. Read more about it in the +> [FAQ](xref:FAQ.Basics.GetStarted) page. More detailed versions of entities can be pulled from the basic -entities, e.g. `SocketGuild.GetUser`, which returns a -`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a +entities, e.g., `SocketGuild.GetUser`, which returns a +`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a `SocketGuildChannel`. Again, you may need to cast these objects to get a variant of the type that you need. -### Samples - -[!code-csharp[Entity Sample](samples/entities.cs)] - -### Tips - -Avoid using boxing-casts to coerce entities into a variant, use the -[`as`] keyword, and a null-conditional operator instead. - -This allows you to write safer code and avoid [InvalidCastExceptions]. - -For example, `(message.Author as SocketGuildUser)?.Nickname`. +## Sample -[`as`]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/as -[InvalidCastExceptions]: https://msdn.microsoft.com/en-us/library/system.invalidcastexception(v=vs.110).aspx \ No newline at end of file +[!code-csharp[Entity Sample](samples/entities.cs)] \ No newline at end of file diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md index 47db49aa8..293b5dc72 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -1,16 +1,19 @@ --- +uid: Guides.Concepts.Events title: Working with Events --- +# Events in Discord.Net + Events in Discord.Net are consumed in a similar manner to the standard convention, with the exception that every event must be of the type -`System.Threading.Tasks.Task` and instead of using `EventArgs`, the -event's parameters are passed directly into the handler. +@System.Threading.Tasks.Task and instead of using @System.EventArgs, +the event's parameters are passed directly into the handler. This allows for events to be handled in an async context directly instead of relying on `async void`. -### Usage +## Usage To receive data from an event, hook into it using C#'s delegate event pattern. @@ -18,7 +21,7 @@ event pattern. You may either opt to hook an event to an anonymous function (lambda) or a named function. -### Safety +## Safety All events are designed to be thread-safe; events are executed synchronously off the gateway task in the same context as the gateway @@ -39,7 +42,7 @@ a deadlock that will be impossible to recover from. Exceptions in commands will be swallowed by the gateway and logged out through the client's log method. -### Common Patterns +## Common Patterns As you may know, events in Discord.Net are only given a signature of `Func`. There is no room for predefined argument names, @@ -49,7 +52,7 @@ directly. That being said, there are a variety of common patterns that allow you to infer what the parameters in an event mean. -#### Entity, Entity +### Entity, Entity An event handler with a signature of `Func` typically means that the first object will be a clone of the entity @@ -58,10 +61,10 @@ model of the entity _after_ the change was made. This pattern is typically only found on `EntityUpdated` events. -#### Cacheable +### Cacheable An event handler with a signature of `Func` -means that the `before` state of the entity was not provided by the +means that the `before` state of the entity was not provided by the API, so it can either be pulled from the client's cache or downloaded from the API. @@ -70,15 +73,12 @@ object. [Cacheable]: xref:Discord.Cacheable`2 -### Samples - -[!code-csharp[Event Sample](samples/events.cs)] +> [!NOTE] +> Many events relating to a Message entity (i.e., `MessageUpdated` and +> `ReactionAdded`) rely on the client's message cache, which is +> **not** enabled by default. Set the `MessageCacheSize` flag in +> @Discord.WebSocket.DiscordSocketConfig to enable it. -### Tips +## Sample -Many events relating to a Message entity (i.e. `MessageUpdated` and -`ReactionAdded`) rely on the client's message cache, which is -**not** enabled by default. Set the `MessageCacheSize` flag in -[DiscordSocketConfig] to enable it. - -[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig \ No newline at end of file +[!code-csharp[Event Sample](samples/events.cs)] diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index 50d2e9546..b92d2bd53 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -1,19 +1,28 @@ --- -title: Logging +uid: Guides.Concepts.Logging +title: Logging Events/Data --- -Discord.Net's clients provide a [Log] event that all messages will be -disbatched over. +# Logging in Discord.Net + +Discord.Net's clients provide a log event that all messages will be +dispatched over. For more information about events in Discord.Net, see the [Events] section. -[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log -[Events]: events.md +[Events]: xref:Guides.Concepts.Events + +> [!WARNING] +> Due to the nature of Discord.Net's event system, all log event +> handlers will be executed synchronously on the gateway thread. If your +> log output will be dumped to a Web API (e.g., Sentry), you are advised +> to wrap your output in a `Task.Run` so the gateway thread does not +> become blocked while waiting for logging data to be written. -### Usage +## Usage in Client(s) -To receive log events, simply hook the discord client's log method +To receive log events, simply hook the Discord client's @Discord.Rest.BaseDiscordClient.Log to a `Task` with a single parameter of type [LogMessage]. It is recommended that you use an established function instead of a @@ -22,10 +31,10 @@ to a logging function to write their own messages. [LogMessage]: xref:Discord.LogMessage -### Usage in Commands +## Usage in Commands -Discord.Net's [CommandService] also provides a log event, identical -in signature to other log events. +Discord.Net's [CommandService] also provides a @Discord.Commands.CommandService.Log +event, identical in signature to other log events. Data logged through this event is typically coupled with a [CommandException], where information about the command's context @@ -34,14 +43,6 @@ and error can be found and handled. [CommandService]: xref:Discord.Commands.CommandService [CommandException]: xref:Discord.Commands.CommandException -#### Samples +## Sample [!code-csharp[Logging Sample](samples/logging.cs)] - -#### Tips - -Due to the nature of Discord.Net's event system, all log event -handlers will be executed synchronously on the gateway thread. If your -log output will be dumped to a Web API (e.g. Sentry), you are advised -to wrap your output in a `Task.Run` so the gateway thread does not -become blocked while waiting for logging data to be written. \ No newline at end of file diff --git a/docs/guides/concepts/ratelimits.md b/docs/guides/concepts/ratelimits.md new file mode 100644 index 000000000..afeb5f795 --- /dev/null +++ b/docs/guides/concepts/ratelimits.md @@ -0,0 +1,49 @@ +# Ratelimits + +Ratelimits are a core concept of any API - Discords API is no exception. each verified library must follow the ratelimit guidelines. + +### Using the ratelimit callback + +There is a new property within `RequestOptions` called RatelimitCallback. This callback is called when a request is made via the rest api. The callback is called with a `IRateLimitInfo` parameter: + +| Name | Type | Description | +| ---------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| IsGlobal | bool | Whether or not this ratelimit info is global. | +| Limit | int? | The number of requests that can be made. | +| Remaining | int? | The number of remaining requests that can be made. | +| RetryAfter | int? | The total time (in seconds) of when the current rate limit bucket will reset. Can have decimals to match previous millisecond ratelimit precision. | +| Reset | DateTimeOffset? | The time at which the rate limit resets. | +| ResetAfter | TimeSpan? | The absolute time when this ratelimit resets. | +| Bucket | string | A unique string denoting the rate limit being encountered (non-inclusive of major parameters in the route path). | +| Lag | TimeSpan? | The amount of lag for the request. This is used to denote the precise time of when the ratelimit expires. | +| Endpoint | string | The endpoint that this ratelimit info came from. | + +Let's set up a ratelimit callback that will print out the ratelimit info to the console. + +```cs +public async Task MyRatelimitCallback(IRateLimitInfo info) +{ + Console.WriteLine($"{info.IsGlobal} {info.Limit} {info.Remaining} {info.RetryAfter} {info.Reset} {info.ResetAfter} {info.Bucket} {info.Lag} {info.Endpoint}"); +} +``` + +Let's use this callback in a send message function + +```cs +[Command("ping")] +public async Task ping() +{ + var options = new RequestOptions() + { + RatelimitCallback = MyRatelimitCallback + }; + + await Context.Channel.SendMessageAsync("Pong!", options: options); +} +``` + +Running this produces the following output: + +``` +False 5 4 2021-09-09 3:48:14 AM +00:00 00:00:05 a06de0de4a08126315431cc0c55ee3dc 00:00:00.9891364 channels/848511736872828929/messages +``` diff --git a/docs/guides/concepts/samples/connections.cs b/docs/guides/concepts/samples/connections.cs deleted file mode 100644 index f96251a39..000000000 --- a/docs/guides/concepts/samples/connections.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Discord; -using Discord.WebSocket; - -public class Program -{ - private DiscordSocketClient _client; - static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - _client = new DiscordSocketClient(); - - await _client.LoginAsync(TokenType.Bot, "bot token"); - await _client.StartAsync(); - - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); - - await _client.StopAsync(); - // Wait a little for the client to finish disconnecting before allowing the program to return - await Task.Delay(500); - } -} \ No newline at end of file diff --git a/docs/guides/concepts/samples/entities.cs b/docs/guides/concepts/samples/entities.cs index 7655c44e9..64383858d 100644 --- a/docs/guides/concepts/samples/entities.cs +++ b/docs/guides/concepts/samples/entities.cs @@ -1,13 +1,11 @@ public string GetChannelTopic(ulong id) { var channel = client.GetChannel(81384956881809408) as SocketTextChannel; - if (channel == null) return ""; - return channel.Topic; + return channel?.Topic; } -public string GuildOwner(SocketChannel channel) +public SocketGuildUser GetGuildOwner(SocketChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; - if (guild == null) return ""; - return Context.Guild.Owner.Username; + return guild?.Owner; } \ No newline at end of file diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index cf0492cb5..dce625b33 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -14,7 +14,7 @@ public class Program var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; _client = new DiscordSocketClient(_config); - await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); _client.MessageUpdated += MessageUpdated; diff --git a/docs/guides/concepts/samples/logging.cs b/docs/guides/concepts/samples/logging.cs index a2ddf7b90..982fb11b6 100644 --- a/docs/guides/concepts/samples/logging.cs +++ b/docs/guides/concepts/samples/logging.cs @@ -1,29 +1,24 @@ using Discord; using Discord.WebSocket; -public class Program +public class LoggingService { - private DiscordSocketClient _client; - static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() + public LoggingService(DiscordSocketClient client, CommandService command) { - _client = new DiscordSocketClient(new DiscordSocketConfig - { - LogLevel = LogSeverity.Info - }); - - _client.Log += Log; - - await _client.LoginAsync(TokenType.Bot, "bot token"); - await _client.StartAsync(); - - await Task.Delay(-1); + client.Log += LogAsync; + command.Log += LogAsync; } - - private Task Log(LogMessage message) + private Task LogAsync(LogMessage message) { - Console.WriteLine(message.ToString()); + if (message.Exception is CommandException cmdException) + { + Console.WriteLine($"[Command/{message.Severity}] {cmdException.Command.Aliases.First()}" + + $" failed to execute in {cmdException.Context.Channel}."); + Console.WriteLine(cmdException); + } + else + Console.WriteLine($"[General/{message.Severity}] {message}"); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/docs/guides/deployment/deployment.md b/docs/guides/deployment/deployment.md new file mode 100644 index 000000000..0491e841d --- /dev/null +++ b/docs/guides/deployment/deployment.md @@ -0,0 +1,103 @@ +--- +uid: Guides.Deployment +title: Deploying the Bot +--- + +# Deploying a Discord.Net Bot + +After finishing your application, you may want to deploy your bot to a +remote location such as a Virtual Private Server (VPS) or another +computer so you can keep the bot up and running 24/7. + +## Recommended VPS + +For small-medium scaled bots, a cheap VPS (~$5) might be sufficient +enough. Here is a list of recommended VPS provider. + +* [DigitalOcean](https://www.digitalocean.com/) + * Description: American cloud infrastructure provider headquartered + in New York City with data centers worldwide. + * Location(s): + * Asia: Singapore, India + * America: Canada, United States + * Europe: Netherlands, Germany, United Kingdom + * Based in: United States +* [Vultr](https://www.vultr.com/) + * Description: DigitalOcean-like + * Location(s): + * Asia: Japan, Australia, Singapore + * America: United States + * Europe: United Kingdom, France, Netherlands, Germany + * Based in: United States +* [OVH](https://www.ovh.com/) + * Description: French cloud computing company that offers VPS, + dedicated servers and other web services. + * Location(s): + * Asia: Australia, Singapore + * America: United States, Canada + * Europe: United Kingdom, Poland, Germany + * Based in: Europe +* [Scaleway](https://www.scaleway.com/) + * Description: Cheap but powerful VPS owned by [Online.net](https://online.net/). + * Location(s): + * Europe: France, Netherlands + * Based in: Europe +* [Time4VPS](https://www.time4vps.eu/) + * Description: Affordable and powerful VPS Hosting in Europe. + * Location(s): + * Europe: Lithuania + * Based in: Europe + +## .NET Core Deployment + +> [!NOTE] +> This section only covers the very basics of .NET Core deployment. +> To learn more about .NET Core deployment, +> visit [.NET Core application deployment] by Microsoft. + +When redistributing the application - whether for deployment on a +remote machine or for sharing with another user - you may want to +publish the application; in other words, to create a +self-contained package without installing the dependencies +and the runtime on the target platform. + +### Framework-dependent Deployment + +To deploy a framework-dependent package (i.e. files to be used on a +remote machine with the `dotnet` command), simply publish +the package with: + +* `dotnet publish -c Release` + +This will create a package with the **least dependencies** +included with the application; however, the remote machine +must have `dotnet` runtime installed before the remote could run the +program. + +> [!TIP] +> Do not know how to run a .NET Core application with +> the `dotnet` runtime? Navigate to the folder of the program +> (typically under `$projFolder/bin/Release`) and +> enter `dotnet program.dll` where `program.dll` is your compiled +> binaries. + +### Self-contained Deployment + +To deploy a self-contained package (i.e. files to be used on a remote +machine without the `dotnet` runtime), publish with a specific +[Runtime ID] with the `-r` switch. + +This will create a package with dependencies compiled for the target +platform, meaning that all the required dependencies will be included +with the program. This will result in **larger package size**; +however, that means the copy of the runtime that can be run +natively on the target platform. + +For example, the following command will create a Windows +executable (`.exe`) that is ready to be executed on any +Windows 10 x64 based machine: + +* `dotnet publish -c Release -r win10-x64` + +[.NET Core application deployment]: https://docs.microsoft.com/en-us/dotnet/core/deploying/ +[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog \ No newline at end of file diff --git a/docs/guides/emoji/emoji.md b/docs/guides/emoji/emoji.md new file mode 100644 index 000000000..dbf654bbf --- /dev/null +++ b/docs/guides/emoji/emoji.md @@ -0,0 +1,102 @@ +--- +uid: Guides.Emoji +title: Emoji +--- + +# Emoji in Discord.Net + +Before we delve into the difference between an @Discord.Emoji and an +@Discord.Emote in Discord.Net, it is **crucial** to understand what +they both look like behind the scene. When the end-users are sending +or receiving an emoji or emote, they are typically in the form of +`:ok_hand:` or `:reeee:`; however, what goes under the hood is that, +depending on the type of emoji, they are sent in an entirely +different format. + +What does this all mean? It means that you should know that by +reacting with a string like `“:ok_hand:”` will **NOT** automatically +translate to `👌`; rather, it will be treated as-is, +like `:ok_hand:`, thus the server will return a `400 Bad Request`. + +## Emoji + +An emoji is a standard emoji that can be found anywhere else outside +of Discord, which means strings like `👌`, `♥`, `👀` are all +considered an emoji in Discord. However, from the +introduction paragraph we have learned that we cannot +simply send `:ok_hand:` and have Discord take +care of it, but what do we need to send exactly? + +To send an emoji correctly, one must send the emoji in its Unicode +form; this can be obtained in several different ways. + +1. (Easiest) Escape the emoji by using the escape character, `\`, in + your Discord chat client; this will reveal the emoji’s pure Unicode + form, which will allow you to copy-paste into your code. +2. Look it up on Emojipedia, from which you can copy the emoji + easily into your code. + ![Emojipedia](images/emojipedia.png) +3. (Recommended) Look it up in the Emoji list from [FileFormat.Info]; + this will give you the .NET-compatible code that + represents the emoji. + * This is the most recommended method because some systems or + IDE sometimes do not render the Unicode emoji correctly. + ![Fileformat Emoji Source Code](images/fileformat-emoji-src.png) + +### Emoji Declaration + +After obtaining the Unicode representation of the emoji, you may +create the @Discord.Emoji object by passing the string with unicode into its +constructor (e.g. `new Emoji("👌");` or `new Emoji("\uD83D\uDC4C");`). + +Your method of declaring an @Discord.Emoji should look similar to +this: +[!code-csharp[Emoji Sample](samples/emoji-sample.cs)] + +Also you can use `Emoji.Parse()` or `Emoji.TryParse()` methods +for parsing emojis from strings like `:heart:`, `<3` or `❤`. + +[FileFormat.Info]: https://www.fileformat.info/info/emoji/list.htm + +## Emote + +The meat of the debate is here; what is an emote and how does it +differ from an emoji? An emote refers to a **custom emoji** +created on Discord. + +The underlying structure of an emote also differs drastically; an +emote looks sort-of like a mention on Discord. It consists of two +main elements as illustrated below: + +![Emote illustration](images/emote-format.png) + +As you can see, emote uses a completely different format. To obtain +the raw string as shown above for your emote, you would need to +escape the emote using the escape character `\` in chat somewhere. + +### Emote Declaration + +After obtaining the raw emote string, you would need to use +@Discord.Emote.Parse* or @Discord.Emote.TryParse* to create a valid +emote object. + +Your method of declaring an @Discord.Emote should look similar to +this: + +[!code[Emote Sample](samples/emote-sample.cs)] + +> [!TIP] +> For WebSocket users, you may also consider fetching the Emote +> via the @Discord.WebSocket.SocketGuild.Emotes collection. +> [!code-csharp[Socket emote sample](samples/socket-emote-sample.cs)] + +> [!TIP] +> On Discord, any user with Discord Nitro subscription may use +> custom emotes from any guilds they are currently in. This is also +> true for _any_ standard bot accounts; this does not require +> the bot owner to have a Nitro subscription. + +## Additional Information + +To learn more about emote and emojis and how they could be used, +see the documentation of @Discord.IEmote. diff --git a/docs/guides/emoji/images/emojipedia.png b/docs/guides/emoji/images/emojipedia.png new file mode 100644 index 000000000..acad16f28 Binary files /dev/null and b/docs/guides/emoji/images/emojipedia.png differ diff --git a/docs/guides/emoji/images/emote-format.png b/docs/guides/emoji/images/emote-format.png new file mode 100644 index 000000000..981e18227 Binary files /dev/null and b/docs/guides/emoji/images/emote-format.png differ diff --git a/docs/guides/emoji/images/fileformat-emoji-src.png b/docs/guides/emoji/images/fileformat-emoji-src.png new file mode 100644 index 000000000..a43eebb62 Binary files /dev/null and b/docs/guides/emoji/images/fileformat-emoji-src.png differ diff --git a/docs/guides/emoji/samples/emoji-sample.cs b/docs/guides/emoji/samples/emoji-sample.cs new file mode 100644 index 000000000..a36e6f70a --- /dev/null +++ b/docs/guides/emoji/samples/emoji-sample.cs @@ -0,0 +1,6 @@ +public async Task ReactAsync(SocketUserMessage userMsg) +{ + // equivalent to "👌" + var emoji = new Emoji("\uD83D\uDC4C"); + await userMsg.AddReactionAsync(emoji); +} \ No newline at end of file diff --git a/docs/guides/emoji/samples/emote-sample.cs b/docs/guides/emoji/samples/emote-sample.cs new file mode 100644 index 000000000..b05ecc269 --- /dev/null +++ b/docs/guides/emoji/samples/emote-sample.cs @@ -0,0 +1,7 @@ +public async Task ReactWithEmoteAsync(SocketUserMessage userMsg, string escapedEmote) +{ + if (Emote.TryParse(escapedEmote, out var emote)) + { + await userMsg.AddReactionAsync(emote); + } +} \ No newline at end of file diff --git a/docs/guides/emoji/samples/socket-emote-sample.cs b/docs/guides/emoji/samples/socket-emote-sample.cs new file mode 100644 index 000000000..397111512 --- /dev/null +++ b/docs/guides/emoji/samples/socket-emote-sample.cs @@ -0,0 +1,11 @@ +private readonly DiscordSocketClient _client; + +public async Task ReactAsync(SocketUserMessage userMsg, string emoteName) +{ + var emote = _client.Guilds + .SelectMany(x => x.Emotes) + .FirstOrDefault(x => x.Name.IndexOf( + emoteName, StringComparison.OrdinalIgnoreCase) != -1); + if (emote == null) return; + await userMsg.AddReactionAsync(emote); +} \ No newline at end of file diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md new file mode 100644 index 000000000..e1af20d30 --- /dev/null +++ b/docs/guides/getting_started/first-bot.md @@ -0,0 +1,220 @@ +--- +uid: Guides.GettingStarted.FirstBot +title: Start making a bot +--- + +# Making Your First Bot with Discord.Net + +One of the ways to get started with the Discord API is to write a +basic ping-pong bot. This bot will respond to a simple command "ping." +We will expand on this to create more diverse commands later, but for +now, it is a good starting point. + +## Creating a Discord Bot + +Before writing your bot, it is necessary to create a bot account via +the Discord Applications Portal first. + +1. Visit the [Discord Applications Portal]. +2. Create a new application. +3. Give the application a name (this will be the bot's initial username). +4. On the left-hand side, under `Settings`, click `Bot`. + + ![Step 4](images/intro-bot-settings.png) + +5. Click on `Add Bot`. + + ![Step 5](images/intro-add-bot.png) + +6. Confirm the popup. +7. (Optional) If this bot will be public, tick `Public Bot`. + + ![Step 7](images/intro-public-bot.png) + +[Discord Applications Portal]: https://discord.com/developers/applications/ + +## Adding your bot to a server + +Bots **cannot** use invite links; they must be explicitly invited +through the OAuth2 flow. + +1. Open your bot's application on the [Discord Applications Portal]. +2. On the left-hand side, under `Settings`, click `OAuth2`. + + ![Step 2](images/intro-oauth-settings.png) + +3. Scroll down to `OAuth2 URL Generator` and under `Scopes` tick `bot`. + + ![Step 3](images/intro-scopes-bot.png) + +4. Scroll down further to `Bot Permissions` and select the + permissions that you wish to assign your bot with. + + > [!NOTE] + > This will assign the bot with a special "managed" role that no + > one else can use. The permissions can be changed later in the + > roles settings if you ever change your mind! + +5. Open the generated authorization URL in your browser. +6. Select a server. +7. Click on Authorize. + + > [!NOTE] + > Only servers where you have the `MANAGE_SERVER` permission will be + > present in this list. + + ![Step 6](images/intro-authorize.png) + +## Connecting to Discord + +If you have not already created a project and installed Discord.Net, +do that now. + +For more information, see @Guides.GettingStarted.Installation. + +### Async + +Discord.Net uses .NET's [Task-based Asynchronous Pattern (TAP)] +extensively - nearly every operation is asynchronous. It is highly +recommended for these operations to be awaited in a +properly established async context whenever possible. + +To establish an async context, we will be creating an async main method +in your console application. + +[!code-csharp[Async Context](samples/first-bot/async-context.cs)] + +As a result of this, your program will now start into an async context. + +> [!WARNING] +> If your application throws any exceptions within an async context, +> they will be thrown all the way back up to the first non-async method; +> since our first non-async method is the program's `Main` method, this +> means that **all** unhandled exceptions will be thrown up there, which +> will crash your application. +> +> Discord.Net will prevent exceptions in event handlers from crashing +> your program, but any exceptions in your async main **will** cause +> the application to crash. + +[Task-based Asynchronous Pattern (TAP)]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async + +### Creating a logging method + +Before we create and configure a Discord client, we will add a method +to handle Discord.Net's log events. + +To allow agnostic support of as many log providers as possible, we +log information through a `Log` event with a proprietary `LogMessage` +parameter. See the [API Documentation] for this event. + +If you are using your own logging framework, this is where you would +invoke it. For the sake of simplicity, we will only be logging to +the console. + +You may learn more about this concept in @Guides.Concepts.Logging. + +[!code-csharp[Async Context](samples/first-bot/logging.cs)] + +[API Documentation]: xref:Discord.Rest.BaseDiscordClient.Log + +### Creating a Discord Client + +Finally, we can create a new connection to Discord. + +Since we are writing a bot, we will be using a [DiscordSocketClient] +along with socket entities. See @Guides.GettingStarted.Terminology +if you are unsure of the differences. To establish a new connection, +we will create an instance of [DiscordSocketClient] in the new async +main. You may pass in an optional @Discord.WebSocket.DiscordSocketConfig +if necessary. For most users, the default will work fine. + +Before connecting, we should hook the client's `Log` event to the +log handler that we had just created. Events in Discord.Net work +similarly to any other events in C#. + +Next, you will need to "log in to Discord" with the [LoginAsync] +method with the application's "token." + +![Token](images/intro-token.png) + +> [!NOTE] +> Pay attention to what you are copying from the developer portal! +> A token is not the same as the application's "client secret." + + +We may now invoke the client's [StartAsync] method, which will +start connection/reconnection logic. It is important to note that +**this method will return as soon as connection logic has been started!** +Any methods that rely on the client's state should go in an event +handler. This means that you should **not** directly be interacting with +the client before it is fully ready. + +Finally, we will want to block the async main method from returning +when running the application. To do this, we can await an infinite delay +or any other blocking method, such as reading from the console. + +> [!IMPORTANT] +> Your bot's token can be used to gain total access to your bot, so +> **do not** share this token with anyone else! You should store this +> token in an external source if you plan on distributing +> the source code for your bot. +> +> In the following example, we retrieve the token from a pre-defined +> variable, which is **NOT** secure, especially if you plan on +> distributing the application in any shape or form. +> +> We recommend alternative storage such as +> [Environment Variables], an external configuration file, or a +> secrets manager for safe-handling of secrets. +> +> [Environment Variables]: https://en.wikipedia.org/wiki/Environment_variable + +The following lines can now be added: + +[!code-csharp[Create client](samples/first-bot/client.cs)] + +At this point, feel free to start your program and see your bot come +online in Discord. + +> [!WARNING] +> Getting a warning about `A supplied token was invalid.` and/or +> having trouble logging in? Double-check whether you have put in +> the correct credentials and make sure that it is _not_ a client +> secret, which is different from a token. + +> [!WARNING] +> Encountering a `PlatformNotSupportedException` when starting your bot? +> This means that you are targeting a platform where .NET's default +> WebSocket client is not supported. Refer to the [installation guide] +> for how to fix this. + +> [!NOTE] +> For your reference, you may view the [completed program]. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[LoginAsync]: xref:Discord.Rest.BaseDiscordClient.LoginAsync* +[StartAsync]: xref:Discord.WebSocket.DiscordSocketClient.StartAsync* +[installation guide]: xref:Guides.GettingStarted.Installation +[completed program]: samples/first-bot/complete.cs + +# Building a bot with commands + +To create commands for your bot, you may choose from a variety of +command processors available. Throughout the guides, we will be using +the one that Discord.Net ships with. @Guides.Commands.Intro will +guide you through how to setup a program that is ready for +[CommandService]. + +For reference, view an [annotated example] of this structure. + +[annotated example]: samples/first-bot/structure.cs + +It is important to know that the recommended design pattern of bots +should be to separate... + +1. the program (initialization and command handler) +2. the modules (handle commands) +3. the services (persistent storage, pure functions, data manipulation) + +[CommandService]: xref:Discord.Commands.CommandService diff --git a/docs/guides/getting_started/images/appveyor-artifacts.png b/docs/guides/getting_started/images/appveyor-artifacts.png new file mode 100644 index 000000000..2f31b77a9 Binary files /dev/null and b/docs/guides/getting_started/images/appveyor-artifacts.png differ diff --git a/docs/guides/getting_started/images/appveyor-nupkg.png b/docs/guides/getting_started/images/appveyor-nupkg.png new file mode 100644 index 000000000..0cf3cf6c4 Binary files /dev/null and b/docs/guides/getting_started/images/appveyor-nupkg.png differ diff --git a/docs/guides/getting_started/images/install-vs-nuget.png b/docs/guides/getting_started/images/install-vs-nuget.png index 64da79a9f..ecf627d11 100644 Binary files a/docs/guides/getting_started/images/install-vs-nuget.png and b/docs/guides/getting_started/images/install-vs-nuget.png differ diff --git a/docs/guides/getting_started/images/intro-add-bot.png b/docs/guides/getting_started/images/intro-add-bot.png index e40997ed3..3b5343ac6 100644 Binary files a/docs/guides/getting_started/images/intro-add-bot.png and b/docs/guides/getting_started/images/intro-add-bot.png differ diff --git a/docs/guides/getting_started/images/intro-authorize.png b/docs/guides/getting_started/images/intro-authorize.png new file mode 100644 index 000000000..66ca4cb04 Binary files /dev/null and b/docs/guides/getting_started/images/intro-authorize.png differ diff --git a/docs/guides/getting_started/images/intro-bot-settings.png b/docs/guides/getting_started/images/intro-bot-settings.png new file mode 100644 index 000000000..6ac40bfe6 Binary files /dev/null and b/docs/guides/getting_started/images/intro-bot-settings.png differ diff --git a/docs/guides/getting_started/images/intro-client-id.png b/docs/guides/getting_started/images/intro-client-id.png deleted file mode 100644 index e370aa2ec..000000000 Binary files a/docs/guides/getting_started/images/intro-client-id.png and /dev/null differ diff --git a/docs/guides/getting_started/images/intro-create-app.png b/docs/guides/getting_started/images/intro-create-app.png deleted file mode 100644 index 7aceb84b4..000000000 Binary files a/docs/guides/getting_started/images/intro-create-app.png and /dev/null differ diff --git a/docs/guides/getting_started/images/intro-create-bot.png b/docs/guides/getting_started/images/intro-create-bot.png deleted file mode 100644 index 0522358cf..000000000 Binary files a/docs/guides/getting_started/images/intro-create-bot.png and /dev/null differ diff --git a/docs/guides/getting_started/images/intro-oauth-settings.png b/docs/guides/getting_started/images/intro-oauth-settings.png new file mode 100644 index 000000000..7d8c2a64a Binary files /dev/null and b/docs/guides/getting_started/images/intro-oauth-settings.png differ diff --git a/docs/guides/getting_started/images/intro-public-bot.png b/docs/guides/getting_started/images/intro-public-bot.png new file mode 100644 index 000000000..da91366a6 Binary files /dev/null and b/docs/guides/getting_started/images/intro-public-bot.png differ diff --git a/docs/guides/getting_started/images/intro-scopes-bot.png b/docs/guides/getting_started/images/intro-scopes-bot.png new file mode 100644 index 000000000..fa17deb16 Binary files /dev/null and b/docs/guides/getting_started/images/intro-scopes-bot.png differ diff --git a/docs/guides/getting_started/images/intro-token.png b/docs/guides/getting_started/images/intro-token.png index 8617cb76f..0fcdac077 100644 Binary files a/docs/guides/getting_started/images/intro-token.png and b/docs/guides/getting_started/images/intro-token.png differ diff --git a/docs/guides/getting_started/images/nightlies-vs-note.png b/docs/guides/getting_started/images/nightlies-vs-note.png new file mode 100644 index 000000000..0dcf2dea3 Binary files /dev/null and b/docs/guides/getting_started/images/nightlies-vs-note.png differ diff --git a/docs/guides/getting_started/images/nightlies-vs-step1.png b/docs/guides/getting_started/images/nightlies-vs-step1.png new file mode 100644 index 000000000..a399ca66c Binary files /dev/null and b/docs/guides/getting_started/images/nightlies-vs-step1.png differ diff --git a/docs/guides/getting_started/images/nightlies-vs-step2.png b/docs/guides/getting_started/images/nightlies-vs-step2.png new file mode 100644 index 000000000..75cecbb8d Binary files /dev/null and b/docs/guides/getting_started/images/nightlies-vs-step2.png differ diff --git a/docs/guides/getting_started/images/nightlies-vs-step4.png b/docs/guides/getting_started/images/nightlies-vs-step4.png new file mode 100644 index 000000000..6462ab994 Binary files /dev/null and b/docs/guides/getting_started/images/nightlies-vs-step4.png differ diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 5d4c85d81..61e3bb6ec 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -1,146 +1,144 @@ --- +uid: Guides.GettingStarted.Installation title: Installing Discord.Net --- -Discord.Net is distributed through the NuGet package manager, and it -is recommended to use NuGet to get started. +# Discord.Net Installation -Optionally, you may compile from source and install yourself. +Discord.Net is distributed through the NuGet package manager; the most +recommended way for you to install this library. Alternatively, you +may also compile this library yourself should you so desire. -# Supported Platforms +## Supported Platforms -Currently, Discord.Net targets [.NET Standard] 1.3 and offers support -for .NET Standard 1.1. If your application will be targeting .NET -Standard 1.1, please see the [additional steps]. +Discord.Net targets [.NET Standard] both 1.3 and 2.0; this also means +that creating applications using the latest version of [.NET Core] is +the most recommended. If you are bound by Windows-specific APIs or +other limitations, you may also consider targeting [.NET Framework] +4.6.1 or higher. -Since Discord.Net is built on the .NET Standard, it is also -recommended to create applications using [.NET Core], though not -required. When using .NET Framework, it is suggested to target -`.NET Framework 4.6.1` or higher. +> [!WARNING] +> Using this library with [Mono] is not supported until further +> notice. It is known to have issues with the library's WebSockets +> implementation and may crash the application upon startup. +[Mono]: https://www.mono-project.com/ [.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library [.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ +[.NET Framework]: https://docs.microsoft.com/en-us/dotnet/framework/get-started/ [additional steps]: #installing-on-net-standard-11 -# Installing with NuGet +## Installing with NuGet -Release builds of Discord.Net 1.0 will be published to the +Release builds of Discord.Net will be published to the [official NuGet feed]. -Development builds of Discord.Net 1.0, as well as addons *(TODO)* are -published to our development [MyGet feed]. - -Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` - -Not sure how to add a direct feed? See how [with Visual Studio] or -[without Visual Studio]. +Development builds of Discord.Net, as well as add-ons, will be +published to our [MyGet feed]. See +@Guides.GettingStarted.Installation.Nightlies to learn more. [official NuGet feed]: https://nuget.org [MyGet feed]: https://www.myget.org/feed/Packages/discord-net -[with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources -[without Visual Studio]: #configuring-nuget-without-visual-studio -## Using Visual Studio +### [Using Visual Studio](#tab/vs-install) + +1. Create a new solution for your bot +2. In the Solution Explorer, find the "Dependencies" element under your + bot's project +3. Right click on "Dependencies", and select "Manage NuGet packages" + + ![Step 3](images/install-vs-deps.png) + +4. In the "Browse" tab, search for `Discord.Net` +5. Install the `Discord.Net` package + + ![Step 5](images/install-vs-nuget.png) + +### [Using JetBrains Rider](#tab/rider-install) -> [!TIP] ->Don't forget to change your package source if you're installing from -the developer feed. ->Also make sure to check "Enable Prereleases" if installing a dev -build! +1. Create a new solution for your bot +2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution) -1. Create a solution for your bot. -2. In Solution Explorer, find the "Dependencies" element under your -bot's project. -3. Right click on "Dependencies", and select "Manage NuGet packages." -![Step 3](images/install-vs-deps.png) -4. In the "Browse" tab, search for `Discord.Net`. -5. Install the `Discord.Net` package. -![Step 5](images/install-vs-nuget.png) + ![Step 2](images/install-rider-nuget-manager.png) -## Using JetBrains Rider +3. In the "Packages" tab, search for `Discord.Net` -> [!TIP] -Make sure to check the "Prerelease" box if installing a dev build! + ![Step 3](images/install-rider-search.png) -1. Create a new solution for your bot. -2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for -Solution). -![Step 2](images/install-rider-nuget-manager.png) -3. In the "Packages" tab, search for `Discord.Net`. -![Step 3](images/install-rider-search.png) -4. Install by adding the package to your project. -![Step 4](images/install-rider-add.png) +4. Install by adding the package to your project -## Using Visual Studio Code + ![Step 4](images/install-rider-add.png) -> [!TIP] -Don't forget to add the package source to a [NuGet.Config file] if -you're installing from the developer feed. +### [Using Visual Studio Code](#tab/vs-code) -1. Create a new project for your bot. -2. Add `Discord.Net` to your .csproj. +1. Create a new project for your bot +2. Add `Discord.Net` to your `*.csproj` -[!code-xml[Sample .csproj](samples/project.csproj)] +[!code[Sample .csproj](samples/project.xml)] -[NuGet.Config file]: #configuring-nuget-without-visual-studio +### [Using dotnet CLI](#tab/dotnet-cli) -# Compiling from Source +1. Launch a terminal of your choice +2. Navigate to where your `*.csproj` is located +3. Enter `dotnet add package Discord.Net` -In order to compile Discord.Net, you require the following: +*** + +## Compiling from Source + +In order to compile Discord.Net, you will need the following: ### Using Visual Studio -- [Visual Studio 2017](https://www.visualstudio.com/) -- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) +* [Visual Studio 2019](https://visualstudio.microsoft.com/) +* [.NET Core SDK] -The .NET Core and Docker (Preview) workload is required during Visual -Studio installation. +The .NET Core and Docker workload is required during Visual Studio +installation. ### Using Command Line -- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) +* [.NET Core SDK] -# Additional Information +## Additional Information -## Installing on .NET Standard 1.1 +### Installing on Unsupported WebSocket Platform -For applications targeting a runtime corresponding with .NET Standard -1.1 or 1.2, the builtin WebSocket and UDP provider will not work. For -applications which utilize a WebSocket connection to Discord -(WebSocket or RPC), third-party provider packages will need to be -installed and configured. +When running any Discord.Net-powered bot on an older operating system +(e.g. Windows 7) that does not natively support WebSocket, +you may encounter a @System.PlatformNotSupportedException upon +connecting. -First, install the following packages through NuGet, or compile -yourself, if you prefer: +You may resolve this by either targeting .NET Core 2.1 or higher, or +by installing one or more custom packages as listed below. -- Discord.Net.Providers.WS4Net -- Discord.Net.Providers.UDPClient +#### [Targeting .NET Core 2.1](#tab/core2-1) -Note that `Discord.Net.Providers.UDPClient` is _only_ required if your -bot will be utilizing voice chat. +1. Download the latest [.NET Core SDK]. +2. Create or move your existing project to use .NET Core. +3. Modify your `` tag to at least `netcoreapp2.1`, or + by adding the `--framework netcoreapp2.1` switch when building. -Next, you will need to configure your [DiscordSocketClient] to use -these custom providers over the default ones. +#### [Custom Packages](#tab/custom-pkg) -To do this, set the `WebSocketProvider` and the optional -`UdpSocketProvider` properties on the [DiscordSocketConfig] that you -are passing into your client. +1. Install or compile the following packages: -[!code-csharp[NET Standard 1.1 Example](samples/netstd11.cs)] + * `Discord.Net.Providers.WS4Net` + * `Discord.Net.Providers.UDPClient` (Optional) + * This is _only_ required if your bot will be utilizing voice chat. -[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig +2. Configure your [DiscordSocketClient] to use these custom providers +over the default ones. -## Configuring NuGet without Visual Studio + * To do this, set the `WebSocketProvider` and the optional + `UdpSocketProvider` properties on the [DiscordSocketConfig] that you + are passing into your client. -If you plan on deploying your bot or developing outside of Visual -Studio, you will need to create a local NuGet configuration file for -your project. +[!code-csharp[Example](samples/netstd11.cs)] -To do this, create a file named `nuget.config` alongside the root of -your application, where the project solution is located. +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -Paste the following snippets into this configuration file, adding any -additional feeds as necessary. +*** -[!code-xml[NuGet Configuration](samples/nuget.config)] +[.NET Core SDK]: https://dotnet.microsoft.com/download \ No newline at end of file diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md deleted file mode 100644 index db086df21..000000000 --- a/docs/guides/getting_started/intro.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: Getting Started ---- - -# Making a Ping-Pong bot - -One of the first steps to getting started with the Discord API is to -write a basic ping-pong bot. We will expand on this to create more -diverse commands later, but for now, it is a good starting point. - -## Creating a Discord Bot - -Before you can begin writing your bot, it is necessary to create a bot -account on Discord. - -1. Visit the [Discord Applications Portal]. -2. Create a New Application. -3. Give the application a name (this will be the bot's initial -username). -4. Create the Application. - - ![Step 4](images/intro-create-app.png) - -5. In the application review page, click **Create a Bot User**. - - ![Step 5](images/intro-create-bot.png) - -6. Confirm the popup. -7. If this bot will be public, check "Public Bot." **Do not tick any -other options!** - -[Discord Applications Portal]: https://discordapp.com/developers/applications/me - -## Adding your bot to a server - -Bots **cannot** use invite links, they must be explicitly invited -through the OAuth2 flow. - -1. Open your bot's application on the [Discord Applications Portal]. -2. Retrieve the app's **Client ID**. - - ![Step 2](images/intro-client-id.png) - -3. Create an OAuth2 authorization URL -`https://discordapp.com/oauth2/authorize?client_id=&scope=bot` -4. Open the authorization URL in your browser. -5. Select a server. -6. Click on authorize. - - >[!NOTE] - Only servers where you have the `MANAGE_SERVER` permission will be - present in this list. - - ![Step 6](images/intro-add-bot.png) - - -## Connecting to Discord - -If you have not already created a project and installed Discord.Net, -do that now. (see the [Installing](installing.md) section) - -### Async - -Discord.Net uses .NET's [Task-based Asynchronous Pattern (TAP)] -extensively - nearly every operation is asynchronous. - -It is highly recommended that these operations are awaited in a -properly established async context whenever possible. Establishing an -async context can be problematic, but not hard. - -To do so, we will be creating an async main in your console -application, and rewriting the static main method to invoke the new -async main. - -[!code-csharp[Async Context](samples/intro/async-context.cs)] - -As a result of this, your program will now start and immediately -jump into an async context. This will allow us to create a connection -to Discord later on without needing to worry about setting up the -correct async implementation. - ->[!TIP] -If your application throws any exceptions within an async context, -they will be thrown all the way back up to the first non-async method; -since our first non-async method is the program's `Main` method, this -means that **all** unhandled exceptions will be thrown up there, which -will crash your application. Discord.Net will prevent exceptions in -event handlers from crashing your program, but any exceptions in your -async main **will** cause the application to crash. - -[Task-based Asynchronous Pattern (TAP)]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async - -### Creating a logging method - -Before we create and configure a Discord client, we will add a method -to handle Discord.Net's log events. - -To allow agnostic support of as many log providers as possible, we -log information through a `Log` event with a proprietary `LogMessage` -parameter. See the [API Documentation] for this event. - -If you are using your own logging framework, this is where you would -invoke it. For the sake of simplicity, we will only be logging to -the Console. - -[!code-csharp[Async Context](samples/intro/logging.cs)] - -[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log - -### Creating a Discord Client - -Finally, we can create a connection to Discord. Since we are writing -a bot, we will be using a [DiscordSocketClient] along with socket -entities. See the [terminology](terminology.md) if you're unsure of -the differences. - -To do so, create an instance of [DiscordSocketClient] in your async -main, passing in a configuration object only if necessary. For most -users, the default will work fine. - -Before connecting, we should hook the client's `Log` event to the -log handler that was just created. Events in Discord.Net work -similarly to other events in C#, so hook this event the way that -you typically would. - -Next, you will need to "login to Discord" with the `LoginAsync` -method. - -You may create a variable to hold your bot's token (this can be found -on your bot's application page on the [Discord Applications Portal]). - -![Token](images/intro-token.png) - ->[!IMPORTANT] -Your bot's token can be used to gain total access to your bot, so -**do __NOT__ share this token with anyone else!** It may behoove you -to store this token in an external file if you plan on distributing -the source code for your bot. - -We may now invoke the client's `StartAsync` method, which will -start connection/reconnection logic. It is important to note that -**this method returns as soon as connection logic has been started!** - -Any methods that rely on the client's state should go in an event -handler. - -Finally, we will want to block the async main method from returning -until after the application is exited. To do this, we can await an -infinite delay or any other blocking method, such as reading from -the console. - -The following lines can now be added: - -[!code-csharp[Create client](samples/intro/client.cs)] - -At this point, feel free to start your program and see your bot come -online in Discord. - ->[!TIP] -Encountering a `PlatformNotSupportedException` when starting your bot? -This means that you are targeting a platform where .NET's default -WebSocket client is not supported. Refer to the [installation guide] -for how to fix this. - -[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[installation guide]: installing.md#installing-on-net-standard-11 - -### Handling a 'ping' - ->[!WARNING] -Please note that this is *not* a proper way to create a command. -Use the `CommandService` provided by the library instead, as explained -in the [Command Guide] section. - -Now that we have learned how to open a connection to Discord, we can -begin handling messages that users are sending. - -To start out, our bot will listen for any message where the content -is equal to `!ping` and respond back with "Pong!". - -Since we want to listen for new messages, the event to hook into -is [MessageReceived]. - -In your program, add a method that matches the signature of the -`MessageReceived` event - it must be a method (`Func`) that returns -the type `Task` and takes a single parameter, a [SocketMessage]. Also, -since we will be sending data to Discord in this method, we will flag -it as `async`. - -In this method, we will add an `if` block to determine if the message -content fits the rules of our scenario - recall that it must be equal -to `!ping`. - -Inside the branch of this condition, we will want to send a message -back to the channel from which the message comes from - "Pong!". To -find the channel, look for the `Channel` property on the message -parameter. - -Next, we will want to send a message to this channel. Since the -channel object is of type [SocketMessageChannel], we can invoke the -`SendMessageAsync` instance method. For the message content, send back -a string containing "Pong!". - -You should have now added the following lines: - -[!code-csharp[Message](samples/intro/message.cs)] - -Now your first bot is complete. You may continue to add on to this -if you desire, but for any bots that will be carrying out multiple -commands, it is strongly recommended to use the command framework as -shown below. - -For your reference, you may view the [completed program]. - -[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived -[SocketMessage]: xref:Discord.WebSocket.SocketMessage -[SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel -[completed program]: samples/intro/complete.cs -[Command Guide]: ../commands/commands.md - -# Building a bot with commands - -This section will show you how to write a program that is ready for -[Commands](../commands/commands.md). Note that we will not be -explaining _how_ to write Commands or Services, it will only be -covering the general structure. - -For reference, view an [annotated example] of this structure. - -[annotated example]: samples/intro/structure.cs - -It is important to know that the recommended design pattern of bots -should be to separate the program (initialization and command handler), -the modules (handle commands), and the services (persistent storage, -pure functions, data manipulation). - -**todo:** diagram of bot structure \ No newline at end of file diff --git a/docs/guides/getting_started/nightlies.md b/docs/guides/getting_started/nightlies.md new file mode 100644 index 000000000..2b9fde87b --- /dev/null +++ b/docs/guides/getting_started/nightlies.md @@ -0,0 +1,97 @@ +--- +uid: Guides.GettingStarted.Installation.Nightlies +title: Installing Nightly Build +--- + +# Installing Discord.Net Nightly Build + +Before Discord.Net pushes a new set of features into the stable +version, we use nightly builds to test the features with the +community for an extensive period of time. Each nightly build is +compiled by AppVeyor whenever a new commit is made and will be pushed +to our MyGet feed. + +> [!IMPORTANT] +> Although nightlies are generally stable and have more features +> and bug fixes than the current stable build on NuGet, there +> will be breaking changes during the development or +> breaking bugs; these bugs are usually fixed as soon as they +> are discovered, but you should still be aware of that. + +## Installing with MyGet (Recommended) + +MyGet is typically used by many development teams to publish their +latest pre-release packages before the features are finalized and +pushed to NuGet. + +The following is the feed link of Discord.Net, + +* `https://www.myget.org/F/discord-net/api/v3/index.json` + +Depending on which IDE you use, there are many different ways of +adding the feed to your package source. + +### [Using Visual Studio](#tab/vs) + +1. Go to `Tools` > `NuGet Package Manager` > `Package Manager Settings` + + ![VS](images/nightlies-vs-step1.png) + +2. Go to `Package Sources` + + ![Package Sources](images/nightlies-vs-step2.png) + +3. Click on the add icon +4. Fill in the desired name and source as shown below and hit `Update` + + ![Add Source](images/nightlies-vs-step4.png) + +> [!NOTE] +> Remember to tick the `Include pre-release` checkbox to see the +> nightly builds! +> ![Checkbox](images/nightlies-vs-note.png) + +### [Using dotnet CLI](#tab/cli) + +1. Launch a terminal of your choice +2. Navigate to where your `*.csproj` is located +3. Type `dotnet add package Discord.Net --source https://www.myget.org/F/discord-net/api/v3/index.json` + +### [Using Local NuGet.Config](#tab/local-nuget-config) + +If you plan on deploying your bot or developing outside of Visual +Studio, you will need to create a local NuGet configuration file for +your project. + +To do this, create a file named `NuGet.Config` alongside the root of +your application, where the project is located. + +Paste the following snippets into this configuration file, adding any +additional feeds if necessary. + +[!code[NuGet Configuration](samples/nuget.config)] + +After which, you may install the packages by directly modifying the +project file and specifying a version, or by using +the [Package Manager Console](https://docs.microsoft.com/en-us/nuget/tools/powershell-reference) +(`Install-Package Discord.Net -IncludePrerelease`). + +*** + +## Installing from AppVeyor Artifacts + +As mentioned in the first paragraph, we utilize AppVeyor to perform +automated tests and publish the new build. During the publishing +process, we also upload the NuGet packages onto +AppVeyor's Artifact collection. + +The latest build status can be found within our [AppVeyor project]. + +[AppVeyor project]: https://ci.appveyor.com/project/rogueexception/discord-net + +1. In the project, you may find our latest build including the + aforementioned artifacts. + ![Artifacts](images/appveyor-artifacts.png) +2. In the artifacts collection, you should see the latest packages + packed in `*.nupkg` form which you could download from and use. + ![NuPkgs](images/appveyor-nupkg.png) diff --git a/docs/guides/getting_started/samples/first-bot/async-context.cs b/docs/guides/getting_started/samples/first-bot/async-context.cs new file mode 100644 index 000000000..98a3cea15 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/async-context.cs @@ -0,0 +1,8 @@ +public class Program +{ + public static Task Main(string[] args) => new Program().MainAsync(); + + public async Task MainAsync() + { + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/client.cs b/docs/guides/getting_started/samples/first-bot/client.cs new file mode 100644 index 000000000..25be9f807 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/client.cs @@ -0,0 +1,23 @@ +private DiscordSocketClient _client; + +public async Task MainAsync() +{ + _client = new DiscordSocketClient(); + + _client.Log += Log; + + // You can assign your bot token to a string, and pass that in to connect. + // This is, however, insecure, particularly if you plan to have your code hosted in a public repository. + var token = "token"; + + // Some alternative options would be to keep your token in an Environment Variable or a standalone file. + // var token = Environment.GetEnvironmentVariable("NameOfYourEnvironmentVariable"); + // var token = File.ReadAllText("token.txt"); + // var token = JsonConvert.DeserializeObject(File.ReadAllText("config.json")).Token; + + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/complete.cs b/docs/guides/getting_started/samples/first-bot/complete.cs new file mode 100644 index 000000000..542056435 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/complete.cs @@ -0,0 +1,23 @@ +public class Program +{ + private DiscordSocketClient _client; + + public static Task Main(string[] args) => new Program().MainAsync(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + _client.Log += Log; + await _client.LoginAsync(TokenType.Bot, + Environment.GetEnvironmentVariable("DiscordToken")); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); + } + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/logging.cs b/docs/guides/getting_started/samples/first-bot/logging.cs new file mode 100644 index 000000000..c6ffc406e --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/logging.cs @@ -0,0 +1,5 @@ +private Task Log(LogMessage msg) +{ + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/message.cs b/docs/guides/getting_started/samples/first-bot/message.cs similarity index 92% rename from docs/guides/getting_started/samples/intro/message.cs rename to docs/guides/getting_started/samples/first-bot/message.cs index d6fd90778..f636d6f35 100644 --- a/docs/guides/getting_started/samples/intro/message.cs +++ b/docs/guides/getting_started/samples/first-bot/message.cs @@ -1,6 +1,6 @@ public async Task MainAsync() { - // client.Log ... + // ... _client.MessageReceived += MessageReceived; // ... } diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/first-bot/structure.cs similarity index 95% rename from docs/guides/getting_started/samples/intro/structure.cs rename to docs/guides/getting_started/samples/first-bot/structure.cs index a9a018c3a..4e64b1732 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/first-bot/structure.cs @@ -10,11 +10,11 @@ using Discord.WebSocket; class Program { // Program entry point - static void Main(string[] args) + static Task Main(string[] args) { // Call the Program constructor, followed by the // MainAsync method and wait until it finishes (which should be never). - new Program().MainAsync().GetAwaiter().GetResult(); + return new Program().MainAsync(); } private readonly DiscordSocketClient _client; @@ -36,7 +36,7 @@ class Program // you must set the MessageCacheSize. You may adjust the number as needed. //MessageCacheSize = 50, - // If your platform doesn't have native websockets, + // If your platform doesn't have native WebSockets, // add Discord.Net.Providers.WS4Net from NuGet, // add the `using` at the top, and uncomment this line: //WebSocketProvider = WS4NetProvider.Instance @@ -57,7 +57,7 @@ class Program _commands.Log += Log; // Setup your DI container. - _services = ConfigureServices(), + _services = ConfigureServices(); } @@ -116,7 +116,9 @@ class Program await InitCommands(); // Login and connect. - await _client.LoginAsync(TokenType.Bot, /* */); + await _client.LoginAsync(TokenType.Bot, + // < DO NOT HARDCODE YOUR TOKEN > + Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); // Wait infinitely so your bot actually stays connected. @@ -160,7 +162,7 @@ class Program // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully). - var result = await _commands.ExecuteAsync(context, pos); + var result = await _commands.ExecuteAsync(context, pos, _services); // Uncomment the following lines if you want the bot // to send a message if it failed. diff --git a/docs/guides/getting_started/samples/intro/async-context.cs b/docs/guides/getting_started/samples/intro/async-context.cs deleted file mode 100644 index c01ddec55..000000000 --- a/docs/guides/getting_started/samples/intro/async-context.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - } - } -} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/client.cs b/docs/guides/getting_started/samples/intro/client.cs deleted file mode 100644 index a73082052..000000000 --- a/docs/guides/getting_started/samples/intro/client.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Program.cs -using Discord.WebSocket; -// ... -private DiscordSocketClient _client; -public async Task MainAsync() -{ - _client = new DiscordSocketClient(); - - _client.Log += Log; - - string token = "abcdefg..."; // Remember to keep this private! - await _client.LoginAsync(TokenType.Bot, token); - await _client.StartAsync(); - - // Block this task until the program is closed. - await Task.Delay(-1); -} diff --git a/docs/guides/getting_started/samples/intro/complete.cs b/docs/guides/getting_started/samples/intro/complete.cs deleted file mode 100644 index 23b59ce6f..000000000 --- a/docs/guides/getting_started/samples/intro/complete.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Discord; -using Discord.WebSocket; -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - private DiscordSocketClient _client; - - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - _client = new DiscordSocketClient(); - - _client.Log += Log; - _client.MessageReceived += MessageReceived; - - string token = "abcdefg..."; // Remember to keep this private! - await _client.LoginAsync(TokenType.Bot, token); - await _client.StartAsync(); - - // Block this task until the program is closed. - await Task.Delay(-1); - } - - private async Task MessageReceived(SocketMessage message) - { - if (message.Content == "!ping") - { - await message.Channel.SendMessageAsync("Pong!"); - } - } - - private Task Log(LogMessage msg) - { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - } - } -} diff --git a/docs/guides/getting_started/samples/intro/logging.cs b/docs/guides/getting_started/samples/intro/logging.cs deleted file mode 100644 index 4fb85a063..000000000 --- a/docs/guides/getting_started/samples/intro/logging.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Discord; -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - } - - private Task Log(LogMessage msg) - { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj deleted file mode 100644 index feb0b0c40..000000000 --- a/docs/guides/getting_started/samples/project.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - netcoreapp1.1 - true - - - - - - - diff --git a/docs/guides/getting_started/samples/project.xml b/docs/guides/getting_started/samples/project.xml new file mode 100644 index 000000000..179d3f97b --- /dev/null +++ b/docs/guides/getting_started/samples/project.xml @@ -0,0 +1,17 @@ + + + + + + Exe + netcoreapp2.1 + + + + + + + diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index 74f7a6259..0f24edd6f 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -1,5 +1,5 @@ --- -uid: Terminology +uid: Guides.GettingStarted.Terminology title: Terminology --- @@ -7,34 +7,30 @@ title: Terminology ## Preface -Most terms for objects remain the same between 0.9 and 1.0. The major -difference is that the ``Server`` is now called ``Guild`` to stay in -line with Discord internally. +Most terms for objects remain the same between 0.9 and 1.0 and above. +The major difference is that the ``Server`` is now called ``Guild`` +to stay in line with Discord internally. ## Implementation Specific Entities -Discord.Net 1.0 is split into a core library and three different -implementations - `Discord.Net.Core`, `Discord.Net.Rest`, -`Discord.Net.Rpc`, and `Discord.Net.WebSockets`. +Discord.Net is split into a core library and two different +implementations - `Discord.Net.Core`, `Discord.Net.Rest`, and +`Discord.Net.WebSockets`. -As a bot developer, you will only need to use `Discord.Net.WebSockets`, +As a bot developer, you will only need to use `Discord.Net.WebSockets`, but you should be aware of the differences between them. -`Discord.Net.Core` provides a set of interfaces that models Discord's -API. These interfaces are consistent throughout all implementations of -Discord.Net, and if you are writing an implementation-agnostic library -or addon, you can rely on the core interfaces to ensure that your +`Discord.Net.Core` provides a set of interfaces that models Discord's +API. These interfaces are consistent throughout all implementations of +Discord.Net, and if you are writing an implementation-agnostic library +or addon, you can rely on the core interfaces to ensure that your addon will run on all platforms. -`Discord.Net.Rest` provides a set of concrete classes to be used -**strictly** with the REST portion of Discord's API. Entities in this -implementation are prefixed with `Rest` (e.g. `RestChannel`). +`Discord.Net.Rest` provides a set of concrete classes to be used +**strictly** with the REST portion of Discord's API. Entities in this +implementation are prefixed with `Rest` (e.g., `RestChannel`). -`Discord.Net.Rpc` provides a set of concrete classes that are used -with Discord's RPC API. Entities in this implementation are prefixed -with `Rpc` (e.g. `RpcChannel`). - -`Discord.Net.WebSocket` provides a set of concrete classes that are +`Discord.Net.WebSocket` provides a set of concrete classes that are used primarily with Discord's WebSocket API or entities that are kept -in cache. When developing bots, you will be using this implementation. -All entities are prefixed with `Socket` (e.g. `SocketChannel`). \ No newline at end of file +in cache. When developing bots, you will be using this implementation. +All entities are prefixed with `Socket` (e.g., `SocketChannel`). \ No newline at end of file diff --git a/docs/guides/guild_events/creating-guild-events.md b/docs/guides/guild_events/creating-guild-events.md new file mode 100644 index 000000000..64ac0de9b --- /dev/null +++ b/docs/guides/guild_events/creating-guild-events.md @@ -0,0 +1,31 @@ +--- +uid: Guides.GuildEvents.Creating +title: Creating Guild Events +--- + +# Creating guild events + +You can create new guild events by using the `CreateEventAsync` function on a guild. + +### Parameters + +| Name | Type | Summary | +| ------------- | --------------------------------- | ---------------------------------------------------------------------------- | +| name | `string` | Sets the name of the event. | +| startTime | `DateTimeOffset` | Sets the start time of the event. | +| type | `GuildScheduledEventType` | Sets the type of the event. | +| privacyLevel? | `GuildScheduledEventPrivacyLevel` | Sets the privacy level of the event | +| description? | `string` | Sets the description of the event. | +| endTime? | `DateTimeOffset?` | Sets the end time of the event. | +| channelId? | `ulong?` | Sets the channel id of the event, only valid on stage or voice channel types | +| location? | `string` | Sets the location of the event, only valid on external types | + +Lets create a basic test event. + +```cs +var guild = client.GetGuild(guildId); + +var guildEvent = await guild.CreateEventAsync("test event", DateTimeOffset.UtcNow.AddDays(1), GuildScheduledEventType.External, endTime: DateTimeOffset.UtcNow.AddDays(2), location: "Space"); +``` + +This code will create an event that lasts a day and starts tomorrow. It will be an external event thats in space. diff --git a/docs/guides/guild_events/getting-event-users.md b/docs/guides/guild_events/getting-event-users.md new file mode 100644 index 000000000..f4b5388a0 --- /dev/null +++ b/docs/guides/guild_events/getting-event-users.md @@ -0,0 +1,16 @@ +--- +uid: Guides.GuildEvents.GettingUsers +title: Getting Guild Event Users +--- + +# Getting Event Users + +You can get a collection of users who are currently interested in the event by calling `GetUsersAsync`. This method works like any other get users method as in it returns an async enumerable. This method also supports pagination by user id. + +```cs +// get all users and flatten the result into one collection. +var users = await event.GetUsersAsync().FlattenAsync(); + +// get users around the 613425648685547541 id. +var aroundUsers = await event.GetUsersAsync(613425648685547541, Direction.Around).FlattenAsync(); +``` diff --git a/docs/guides/guild_events/intro.md b/docs/guides/guild_events/intro.md new file mode 100644 index 000000000..b60a8c70d --- /dev/null +++ b/docs/guides/guild_events/intro.md @@ -0,0 +1,41 @@ +--- +uid: Guides.GuildEvents.Intro +title: Introduction to Guild Events +--- + +# Guild Events + +Guild events are a way to host events within a guild. They offer alot of features and flexibility. + +## Getting started with guild events + +You can access any events within a guild by calling `GetEventsAsync` on a guild. + +```cs +var guildEvents = await guild.GetEventsAsync(); +``` + +If your working with socket guilds you can just use the `Events` property: + +```cs +var guildEvents = guild.Events; +``` + +There are also new gateway events that you can hook to receive guild scheduled events on. + +```cs +// Fired when a guild event is cancelled. +client.GuildScheduledEventCancelled += ... + +// Fired when a guild event is completed. +client.GuildScheduledEventCompleted += ... + +// Fired when a guild event is started. +client.GuildScheduledEventStarted += ... + +// Fired when a guild event is created. +client.GuildScheduledEventCreated += ... + +// Fired when a guild event is updated. +client.GuildScheduledEventUpdated += ... +``` diff --git a/docs/guides/guild_events/modifying-events.md b/docs/guides/guild_events/modifying-events.md new file mode 100644 index 000000000..05e14ec98 --- /dev/null +++ b/docs/guides/guild_events/modifying-events.md @@ -0,0 +1,23 @@ +--- +uid: Guides.GuildEvents.Modifying +title: Modifying Guild Events +--- + +# Modifying Events + +You can modify events using the `ModifyAsync` method to modify the event, heres the properties you can modify: + +| Name | Type | Description | +| ------------ | --------------------------------- | -------------------------------------------- | +| ChannelId | `ulong?` | Gets or sets the channel id of the event. | +| string | `string` | Gets or sets the location of this event. | +| Name | `string` | Gets or sets the name of the event. | +| PrivacyLevel | `GuildScheduledEventPrivacyLevel` | Gets or sets the privacy level of the event. | +| StartTime | `DateTimeOffset` | Gets or sets the start time of the event. | +| EndTime | `DateTimeOffset` | Gets or sets the end time of the event. | +| Description | `string` | Gets or sets the description of the event. | +| Type | `GuildScheduledEventType` | Gets or sets the type of the event. | +| Status | `GuildScheduledEventStatus` | Gets or sets the status of the event. | + +> [!NOTE] +> All of these properties are optional. diff --git a/docs/guides/interactions/application-commands/01-getting-started.md b/docs/guides/interactions/application-commands/01-getting-started.md new file mode 100644 index 000000000..fc8c8fe30 --- /dev/null +++ b/docs/guides/interactions/application-commands/01-getting-started.md @@ -0,0 +1,32 @@ +--- +uid: Guides.SlashCommands.Intro +title: Introduction to slash commands +--- + + +# Getting started with application commands. + +Welcome! This guide will show you how to use application commands. + +## What is an application command? + +Application commands consist of three different types. Slash commands, context menu User commands and context menu Message commands. +Slash commands are made up of a name, description, and a block of options, which you can think of like arguments to a function. The name and description help users find your command among many others, and the options validate user input as they fill out your command. +Message and User commands are only a name, to the user. So try to make the name descriptive. They're accessed by right clicking (or long press, on mobile) a user or a message, respectively. + +All three varieties of application commands have both Global and Guild variants. Your global commands are available in every guild that adds your application. You can also make commands for a specific guild; they're only available in that guild. The User and Message commands are more limited in quantity than the slash commands. For specifics, check out their respective guide pages. + +An Interaction is the message that your application receives when a user uses a command. It includes the values that the user submitted, as well as some metadata about this particular instance of the command being used: the guild_id, channel_id, member and other fields. You can find all the values in our data models. + +## Authorizing your bot for application commands + +There is a new special OAuth2 scope for applications called `applications.commands`. In order to make Application Commands work within a guild, the guild must authorize your application with the `applications.commands` scope. The bot scope is not enough. + +Head over to your discord applications OAuth2 screen and make sure to select the `application.commands` scope. + +![OAuth2 scoping](slash-commands/images/oauth.png) + +From there you can then use the link to add your bot to a server. + +> [!NOTE] +> In order for users in your guild to use your slash commands, they need to have the "Use Slash Command" permission on the guild. diff --git a/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md b/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md new file mode 100644 index 000000000..02a9cde14 --- /dev/null +++ b/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md @@ -0,0 +1,105 @@ +--- +uid: Guides.ContextCommands.Creating +title: Creating Context Commands +--- + +# Creating context menu commands. + +There are two kinds of Context Menu Commands: User Commands and Message Commands. +Each of these have a Global and Guild variant. +Global menu commands are available for every guild that adds your app. An individual app's global commands are also available in DMs if that app has a bot that shares a mutual guild with the user. + +Guild commands are specific to the guild you specify when making them. Guild commands are not available in DMs. Command names are unique per application within each scope (global and guild). That means: + +- Your app cannot have two global commands with the same name +- Your app cannot have two guild commands within the same name on the same guild +- Your app can have a global and guild command with the same name +- Multiple apps can have commands with the same names + +**Note**: Apps can have a maximum of 5 global context menu commands, and an additional 5 guild-specific context menu commands per guild. + +If you don't have the code for a bot ready yet please follow [this guide](https://docs.stillu.cc/guides/getting_started/first-bot.html). + +## UserCommandBuilder + +The context menu user command builder will help you create user commands. The builder has these available fields and methods: + +| Name | Type | Description | +| -------- | -------- | ------------------------------------------------------------------------------------------------ | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `UserCommandProperties` class used to make Menu commands | + +## MessageCommandBuilder + +The context menu message command builder will help you create message commands. The builder has these available fields and methods: + +| Name | Type | Description | +| -------- | -------- | --------------------------------------------------------------------------------------------------- | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `MessageCommandProperties` class used to make Menu commands | + +**Note**: Context Menu command names can be upper and lowercase, and use spaces. + +Let's use the user command builder to make a global and guild command. + +```cs +// Let's hook the ready event for creating our commands in. +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + // Let's build a guild command! We're going to need a guild so lets just put that in a variable. + var guild = client.GetGuild(guildId); + + // Next, lets create our user and message command builder. This is like the embed builder but for context menu commands. + var guildUserCommand = new UserCommandBuilder(); + var guildMessageCommand = new MessageCommandBuilder(); + + // Note: Names have to be all lowercase and match the regular expression ^[\w -]{3,32}$ + guildUserCommand.WithName("Guild User Command"); + guildMessageCommand.WithName("Guild Message Command"); + + // Descriptions are not used with User and Message commands + //guildCommand.WithDescription(""); + + // Let's do our global commands + var globalUserCommand = new UserCommandBuilder(); + globalCommand.WithName("Global User Command"); + var globalMessageCommand = new MessageCommandBuilder(); + globalMessageCommand.WithName("Global Message Command"); + + + try + { + // Now that we have our builder, we can call the BulkOverwriteApplicationCommandAsync to make our context commands. Note: this will overwrite all your previous commands with this array. + await guild.BulkOverwriteApplicationCommandAsync(new ApplicationCommandProperties[] + { + guildUserCommand.Build(), + guildMessageCommand.Build() + }); + + // With global commands we dont need the guild. + await client.BulkOverwriteGlobalApplicationCommandsAsync(new ApplicationCommandProperties[] + { + globalUserCommand.Build(), + globalMessageCommand.Build() + }) + } + catch(ApplicationCommandException exception) + { + // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + + // You can send this error somewhere or just print it to the console, for this example we're just going to print it. + Console.WriteLine(json); + } +} + +``` + +> [!NOTE] +> Application commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register application commands. diff --git a/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md b/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md new file mode 100644 index 000000000..d4e973d04 --- /dev/null +++ b/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md @@ -0,0 +1,33 @@ +--- +uid: Guides.ContextCommands.Reveiving +title: Receiving Context Commands +--- + +# Receiving Context Menu events + +User commands and Message commands have their own unique event just like the other interaction types. For user commands the event is `UserCommandExecuted` and for message commands the event is `MessageCommandExecuted`. + +```cs +// For message commands +client.MessageCommandExecuted += MessageCommandHandler; + +// For user commands +client.UserCommandExecuted += UserCommandHandler; + +... + +public async Task MessageCommandHandler(SocketMessageCommand arg) +{ + Console.Writeline("Message command received!"); +} + +public async Task UserCommandHandler(SocketUserCommand arg) +{ + Console.Writeline("User command received!"); +} +``` + +User commands contain a SocketUser object called `Member` in their data class, showing the user that was clicked to run the command. +Message commands contain a SocketMessage object called `Message` in their data class, showing the message that was clicked to run the command. + +Both return the user who ran the command, the guild (if any), channel, etc. \ No newline at end of file diff --git a/docs/guides/interactions/application-commands/slash-commands/02-creating-slash-commands.md b/docs/guides/interactions/application-commands/slash-commands/02-creating-slash-commands.md new file mode 100644 index 000000000..9e35de285 --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/02-creating-slash-commands.md @@ -0,0 +1,98 @@ +--- +uid: Guides.SlashCommands.Creating +title: Creating Slash Commands +--- + +# Creating your first slash commands. + +There are two kinds of Slash Commands: global commands and guild commands. +Global commands are available for every guild that adds your app. An individual app's global commands are also available in DMs if that app has a bot that shares a mutual guild with the user. + +Guild commands are specific to the guild you specify when making them. Guild commands are not available in DMs. Command names are unique per application within each scope (global and guild). That means: + +- Your app cannot have two global commands with the same name +- Your app cannot have two guild commands within the same name on the same guild +- Your app can have a global and guild command with the same name +- Multiple apps can have commands with the same names + +**Note**: Apps can have a maximum of 100 global commands, and an additional 100 guild-specific commands per guild. + +**Note**: Global commands will take up to 1 hour to create, delete or modify on guilds. If you need to update a command quickly for testing you can create it as a guild command. + +If you don't have the code for a bot ready yet please follow [this guide](https://docs.stillu.cc/guides/getting_started/first-bot.html). + +## SlashCommandBuilder + +The slash command builder will help you create slash commands. The builder has these available fields and methods: + +| Name | Type | Description | +| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | +| MaxNameLength | const int | The maximum length of a name for a slash command allowed by Discord. | +| MaxDescriptionLength | const int | The maximum length of a commands description allowed by Discord. | +| MaxOptionsCount | const int | The maximum count of command options allowed by Discord | +| Name | string | The name of this slash command. | +| Description | string | A 1-100 length description of this slash command | +| Options | List\ | The options for this command. | +| DefaultPermission | bool | Whether the command is enabled by default when the app is added to a guild. | +| WithName | Function | Sets the field name. | +| WithDescription | Function | Sets the description of the current command. | +| WithDefaultPermission | Function | Sets the default permission of the current command. | +| AddOption | Function | Adds an option to the current slash command. | +| Build | Function | Builds the builder into a `SlashCommandCreationProperties` class used to make slash commands | + +> [!NOTE] +> Slash command names must be all lowercase! + +## Creating a Slash Command + +Let's use the slash command builder to make a global and guild command. + +```cs +// Let's hook the ready event for creating our commands in. +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + // Let's build a guild command! We're going to need a guild so lets just put that in a variable. + var guild = client.GetGuild(guildId); + + // Next, lets create our slash command builder. This is like the embed builder but for slash commands. + var guildCommand = new SlashCommandBuilder(); + + // Note: Names have to be all lowercase and match the regular expression ^[\w-]{3,32}$ + guildCommand.WithName("first-command"); + + // Descriptions can have a max length of 100. + guildCommand.WithDescription("This is my first guild slash command!"); + + // Let's do our global command + var globalCommand = new SlashCommandBuilder(); + globalCommand.WithName("first-global-command"); + globalCommand.WithDescription("This is my frist global slash command"); + + try + { + // Now that we have our builder, we can call the CreateApplicationCommandAsync method to make our slash command. + await guild.CreateApplicationCommandAsync(guildCommand.Build()); + + // With global commands we dont need the guild. + await client.CreateGlobalApplicationCommandAsync(globalCommand.Build()); + // Using the ready event is a simple implementation for the sake of the example. Suitable for testing and development. + // For a production bot, it is recommended to only run the CreateGlobalApplicationCommandAsync() once for each command. + } + catch(ApplicationCommandException exception) + { + // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + + // You can send this error somewhere or just print it to the console, for this example we're just going to print it. + Console.WriteLine(json); + } +} + +``` + +> [!NOTE] +> Slash commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register slash commands. The global commands take up to an hour to register every time the CreateGlobalApplicationCommandAsync() is called for a given command. diff --git a/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md b/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md new file mode 100644 index 000000000..3dbc579fe --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md @@ -0,0 +1,40 @@ +--- +uid: Guides.SlashCommands.Receiving +title: Receiving and Responding to Slash Commands +--- + +# Responding to interactions. + +Interactions are the base thing sent over by Discord. Slash commands are one of the interaction types. We can listen to the `SlashCommandExecuted` event to respond to them. Lets add this to our code: + +```cs +client.SlashCommandExecuted += SlashCommandHandler; + +... + +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + +} +``` + +With every type of interaction there is a `Data` field. This is where the relevant information lives about our command that was executed. In our case, `Data` is a `SocketSlashCommandData` instance. In the data class, we can access the name of the command triggered as well as the options if there were any. For this example, we're just going to respond with the name of the command executed. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + await command.RespondAsync($"You executed {command.Data.Name}"); +} +``` + +Let's try this out! + +![slash command picker](images/slashcommand1.png) + +![slash command result](images/slashcommand2.png) + +> [!NOTE] +> After receiving an interaction, you must respond to acknowledge it. You can choose to respond with a message immediately using `RespondAsync()` or you can choose to send a deferred response with `DeferAsync()`. +> If choosing a deferred response, the user will see a loading state for the interaction, and you'll have up to 15 minutes to edit the original deferred response using `ModifyOriginalResponseAsync()`. You can read more about response types [here](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + +This seems to be working! Next, we will look at parameters for slash commands. diff --git a/docs/guides/interactions/application-commands/slash-commands/04-parameters.md b/docs/guides/interactions/application-commands/slash-commands/04-parameters.md new file mode 100644 index 000000000..6afd83729 --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/04-parameters.md @@ -0,0 +1,102 @@ +--- +uid: Guides.SlashCommands.Parameters +title: Slash Command Parameters +--- + +# Slash command parameters + +Slash commands can have a bunch of parameters, each their own type. Let's first go over the types of parameters we can have. + +| Name | Description | +| --------------- | -------------------------------------------------- | +| SubCommand | A subcommand inside of a subcommand group. | +| SubCommandGroup | The parent command group of subcommands. | +| String | A string of text. | +| Integer | A number. | +| Boolean | True or False. | +| User | A user | +| Channel | A channel, this includes voice text and categories | +| Role | A role. | +| Mentionable | A role or a user. | + +Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field: +| Name | C# Type | +| --------------- | ------------------------------------------------ | +| SubCommand | NA | +| SubCommandGroup | NA | +| String | `string` | +| Integer | `int` | +| Boolean | `bool` | +| User | `SocketGuildUser` or `SocketUser` | +| Role | `SocketRole` | +| Channel | `SocketChannel` | +| Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` | + +Let's start by making a command that takes in a user and lists their roles. + +```cs +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new SlashCommandBuilder() + .WithName("list-roles") + .WithDescription("Lists all roles of a user.") + .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} + +``` + +![list roles command](images/listroles1.png) + +That seems to be working, now Let's handle the interaction. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + } +} + +private async Task HandleListRoleCommand(SocketSlashCommand command) +{ + // We need to extract the user parameter from the command. since we only have one option and it's required, we can just use the first option. + var guildUser = (SocketGuildUser)command.Data.Options.First().Value; + + // We remove the everyone role and select the mention of each role. + var roleList = string.Join(",\n", guildUser.Roles.Where(x => !x.IsEveryone).Select(x => x.Mention)); + + var embedBuiler = new EmbedBuilder() + .WithAuthor(guildUser.ToString(), guildUser.GetAvatarUrl() ?? guildUser.GetDefaultAvatarUrl()) + .WithTitle("Roles") + .WithDescription(roleList) + .WithColor(Color.Green) + .WithCurrentTimestamp(); + + // Now, Let's respond with the embed. + await command.RespondAsync(embed: embedBuiler.Build()); +} +``` + +![working list roles](images/listroles2.png) + +That has worked! Next, we will go over responding ephemerally. diff --git a/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md b/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md new file mode 100644 index 000000000..10b04a8d2 --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md @@ -0,0 +1,23 @@ +--- +uid: Guides.SlashCommands.Ephemeral +title: Ephemeral Responses +--- + +# Responding ephemerally + +What is an ephemeral response? Basically, only the user who executed the command can see the result of it, this is pretty simple to implement. + +> [!NOTE] +> You don't have to run arg.DeferAsync() to capture the interaction, you can use arg.RespondAsync() with a message to capture it, this also follows the ephemeral rule. + +When responding with either `FollowupAsync` or `RespondAsync` you can pass in an `ephemeral` property. When setting it to true it will respond ephemerally, false and it will respond non-ephemerally. + +Let's use this in our list role command. + +```cs +await command.RespondAsync(embed: embedBuiler.Build(), ephemeral: true); +``` + +Running the command now only shows the message to us! + +![ephemeral command](images/ephemeral1.png) \ No newline at end of file diff --git a/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md b/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md new file mode 100644 index 000000000..83d7b283c --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md @@ -0,0 +1,219 @@ +--- +uid: Guides.SlashCommands.SubCommand +title: Sub Commands +--- + +# Subcommands + +Subcommands allow you to have multiple commands available in a single command. They can be useful for representing sub options for a command. For example: A settings command. Let's first look at some limitations with subcommands set by discord. + +- An app can have up to 25 subcommand groups on a top-level command +- An app can have up to 25 subcommands within a subcommand group +- commands can have up to 25 `options` +- options can have up to 25 `choices` + +``` +VALID + +command +| +|__ subcommand +| +|__ subcommand + +---- + +command +| +|__ subcommand-group + | + |__ subcommand +| +|__ subcommand-group + | + |__ subcommand + + +------- + +INVALID + + +command +| +|__ subcommand-group + | + |__ subcommand-group +| +|__ subcommand-group + | + |__ subcommand-group + +---- + +INVALID + +command +| +|__ subcommand + | + |__ subcommand-group +| +|__ subcommand + | + |__ subcommand-group +``` + +Let's write a settings command that can change 3 fields in our bot. + +```cs +public string FieldA { get; set; } = "test"; +public int FieldB { get; set; } = 10; +public bool FieldC { get; set; } = true; + +public async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new SlashCommandBuilder() + .WithName("settings") + .WithDescription("Changes some settings within the bot.") + .AddOption(new SlashCommandOptionBuilder() + .WithName("field-a") + .WithDescription("Gets or sets the field A") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field A") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.String, "the value to set the field", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field A.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("field-b") + .WithDescription("Gets or sets the field B") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field B") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.Integer, "the value to set the fie to.", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field B.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("field-c") + .WithDescription("Gets or sets the field C") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field C") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.Boolean, "the value to set the fie to.", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field C.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` + +All that code generates a command that looks like this: +![settings](images/settings1.png) + +Now that we have our command made, we need to handle the multiple options with this command. So lets add this into our handler: + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; + } +} + +private async Task HandleSettingsCommand(SocketSlashCommand command) +{ + // First lets extract our variables + var fieldName = command.Data.Options.First().Name; + var getOrSet = command.Data.Options.First().Options.First().Name; + // Since there is no value on a get command, we use the ? operator because "Options" can be null. + var value = command.Data.Options.First().Options.First().Options?.FirstOrDefault().Value; + + switch (fieldName) + { + case "field-a": + { + if(getOrSet == "get") + { + await command.RespondAsync($"The value of `field-a` is `{FieldA}`"); + } + else if (getOrSet == "set") + { + this.FieldA = (string)value; + await command.RespondAsync($"`field-a` has been set to `{FieldA}`"); + } + } + break; + case "field-b": + { + if (getOrSet == "get") + { + await command.RespondAsync($"The value of `field-b` is `{FieldB}`"); + } + else if (getOrSet == "set") + { + this.FieldB = (int)value; + await command.RespondAsync($"`field-b` has been set to `{FieldB}`"); + } + } + break; + case "field-c": + { + if (getOrSet == "get") + { + await command.RespondAsync($"The value of `field-c` is `{FieldC}`"); + } + else if (getOrSet == "set") + { + this.FieldC = (bool)value; + await command.RespondAsync($"`field-c` has been set to `{FieldC}`"); + } + } + break; + } +} + +``` + +Now, let's try this out! Running the 3 get commands seems to get the default values we set. + +![settings get](images/settings2.png) + +Now let's try changing each to a different value. + +![settings set](images/settings3.png) + +That has worked! Next, let't look at choices in commands. diff --git a/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md b/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md new file mode 100644 index 000000000..3951e1141 --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md @@ -0,0 +1,85 @@ +--- +uid: Guides.SlashCommands.Choices +title: Slash Command Choices +--- + +# Slash Command Choices. + +With slash command options you can add choices, making the user select between some set values. Lets create a command that asks how much they like our bot! + +Let's set up our slash command: + +```cs +private async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new SlashCommandBuilder() + .WithName("feedback") + .WithDescription("Tell us how much you are enjoying this bot!") + .AddOption(new SlashCommandOptionBuilder() + .WithName("rating") + .WithDescription("The rating your willing to give our bot") + .WithRequired(true) + .AddChoice("Terrible", 1) + .AddChoice("Meh", 2) + .AddChoice("Good", 3) + .AddChoice("Lovely", 4) + .AddChoice("Excellent!", 5) + .WithType(ApplicationCommandOptionType.Integer) + ).Build(); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` +> [!NOTE] +> Your `ApplicationCommandOptionType` specifies which type your choices are, you need to use `ApplicationCommandOptionType.Integer` for choices whos values are whole numbers, `ApplicationCommandOptionType.Number` for choices whos values are doubles, and `ApplicationCommandOptionType.String` for string values. + +We have defined 5 choices for the user to pick from, each choice has a value assigned to it. The value can either be a string or an int. In our case we're going to use an int. This is what the command looks like: + +![feedback style](images/feedback1.png) + +Lets add our code for handling the interaction. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; + case "feedback": + await HandleFeedbackCommand(command); + break; + } +} + +private async Task HandleFeedbackCommand(SocketSlashCommand command) +{ + var embedBuilder = new EmbedBuilder() + .WithAuthor(command.User) + .WithTitle("Feedback") + .WithDescription($"Thanks for your feedback! You rated us {command.Data.Options.First().Value}/5") + .WithColor(Color.Green) + .WithCurrentTimestamp(); + + await command.RespondAsync(embed: embedBuilder.Build()); +} +``` + +And this is the result: + +![feedback working](images/feedback2.png) diff --git a/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md b/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md new file mode 100644 index 000000000..095eda14f --- /dev/null +++ b/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md @@ -0,0 +1,40 @@ +--- +uid: Guides.SlashCommands.BulkOverwrite +title: Slash Command Bulk Overwrites +--- + +If you have too many global commands then you might want to consider using the bulk overwrite function. + +```cs +public async Task Client_Ready() +{ + List applicationCommandProperties = new(); + try + { + // Simple help slash command. + SlashCommandBuilder globalCommandHelp = new SlashCommandBuilder(); + globalCommandHelp.WithName("help"); + globalCommandHelp.WithDescription("Shows information about the bot."); + applicationCommandProperties.Add(globalCommandHelp.Build()); + + // Slash command with name as its parameter. + SlashCommandOptionBuilder slashCommandOptionBuilder = new(); + slashCommandOptionBuilder.WithName("name"); + slashCommandOptionBuilder.WithType(ApplicationCommandOptionType.String); + slashCommandOptionBuilder.WithDescription("Add a family"); + slashCommandOptionBuilder.WithRequired(true); // Only add this if you want it to be required + + SlashCommandBuilder globalCommandAddFamily = new SlashCommandBuilder(); + globalCommandAddFamily.WithName("add-family"); + globalCommandAddFamily.WithDescription("Add a family"); + applicationCommandProperties.Add(globalCommandAddFamily.Build()); + + await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); + } + catch (ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` diff --git a/docs/guides/interactions/application-commands/slash-commands/images/ephemeral1.png b/docs/guides/interactions/application-commands/slash-commands/images/ephemeral1.png new file mode 100644 index 000000000..61eab94b6 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/ephemeral1.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/feedback1.png b/docs/guides/interactions/application-commands/slash-commands/images/feedback1.png new file mode 100644 index 000000000..08e5b8c21 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/feedback1.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/feedback2.png b/docs/guides/interactions/application-commands/slash-commands/images/feedback2.png new file mode 100644 index 000000000..3e75c87db Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/feedback2.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/listroles1.png b/docs/guides/interactions/application-commands/slash-commands/images/listroles1.png new file mode 100644 index 000000000..43015e203 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/listroles1.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/listroles2.png b/docs/guides/interactions/application-commands/slash-commands/images/listroles2.png new file mode 100644 index 000000000..d0b954380 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/listroles2.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/oauth.png b/docs/guides/interactions/application-commands/slash-commands/images/oauth.png new file mode 100644 index 000000000..e0f8224a8 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/oauth.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/settings1.png b/docs/guides/interactions/application-commands/slash-commands/images/settings1.png new file mode 100644 index 000000000..0eb4d711a Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/settings1.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/settings2.png b/docs/guides/interactions/application-commands/slash-commands/images/settings2.png new file mode 100644 index 000000000..5ced63134 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/settings2.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/settings3.png b/docs/guides/interactions/application-commands/slash-commands/images/settings3.png new file mode 100644 index 000000000..485110814 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/settings3.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/slashcommand1.png b/docs/guides/interactions/application-commands/slash-commands/images/slashcommand1.png new file mode 100644 index 000000000..0c4e0aec7 Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/slashcommand1.png differ diff --git a/docs/guides/interactions/application-commands/slash-commands/images/slashcommand2.png b/docs/guides/interactions/application-commands/slash-commands/images/slashcommand2.png new file mode 100644 index 000000000..828d8a2ce Binary files /dev/null and b/docs/guides/interactions/application-commands/slash-commands/images/slashcommand2.png differ diff --git a/docs/guides/interactions/intro.md b/docs/guides/interactions/intro.md new file mode 100644 index 000000000..62b2dfdb5 --- /dev/null +++ b/docs/guides/interactions/intro.md @@ -0,0 +1,10 @@ +--- +uid: Guides.Interactions.Intro +title: Introduction to Interactions +--- + +# Interactions + +Placeholder text does the brrr. + +Links to different sections of guides: msg comp / slash commands. diff --git a/docs/guides/interactions/message-components/01-getting-started.md b/docs/guides/interactions/message-components/01-getting-started.md new file mode 100644 index 000000000..cd5eadd0a --- /dev/null +++ b/docs/guides/interactions/message-components/01-getting-started.md @@ -0,0 +1,66 @@ +--- +uid: Guides.MessageComponents.GettingStarted +title: Getting Started with Components +--- + +# Message Components + +Message components are a framework for adding interactive elements to a message your app or bot sends. They're accessible, customizable, and easy to use. + +## What is a Component + +Components are a new parameter you can use when sending messages with your bot. There are currently 2 different types of components you can use: Buttons and Select Menus. + +## Creating components + +Lets create a simple component that has a button. First thing we need is a way to trigger the message, this can be done via commands or simply a ready event. Lets make a command that triggers our button message. + +```cs +[Command("spawner")] +public async Task Spawn() +{ + // Reply with some components +} +``` + +We now have our command, but we need to actually send the buttons with the command. To do that, lets look at the `ComponentBuilder` class: + +| Name | Description | +| ---------------- | --------------------------------------------------------------------------- | +| `FromMessage` | Creates a new builder from a message. | +| `FromComponents` | Creates a new builder from the provided list of components. | +| `WithSelectMenu` | Adds a `SelectMenuBuilder` to the `ComponentBuilder` at the specific row. | +| `WithButton` | Adds a `ButtonBuilder` to the `ComponentBuilder` at the specific row. | +| `Build` | Builds this builder into a `MessageComponent` used to send your components. | + +We see that we can use the `WithButton` function so lets do that. looking at its parameters it takes: + +- `label` - The display text of the button. +- `customId` - The custom id of the button, this is whats sent by discord when your button is clicked. +- `style` - The discord defined style of the button. +- `emote` - An emote to be displayed with the button. +- `url` - The url of the button if its a link button. +- `disabled` - Whether or not the button is disabled. +- `row` - The row the button will occupy. + +Since were just making a busic button, we dont have to specify anything else besides the label and custom id. + +```cs +var builder = new ComponentBuilder() + .WithButton("label", "custom-id"); +``` + +Lets add this to our command: + +```cs +[Command("spawner")] +public async Task Spawn() +{ + var builder = new ComponentBuilder() + .WithButton("label", "custom-id"); + + await ReplyAsync("Here is a button!", components: builder.Build()); +} +``` + +![](images\image1.png) diff --git a/docs/guides/interactions/message-components/02-responding-to-buttons.md b/docs/guides/interactions/message-components/02-responding-to-buttons.md new file mode 100644 index 000000000..00d651f6b --- /dev/null +++ b/docs/guides/interactions/message-components/02-responding-to-buttons.md @@ -0,0 +1,37 @@ +--- +uid: Guides.MessageComponents.Responding +title: Responding to Components +--- + +# Responding to button clicks + +Responding to buttons is pretty simple, there are a couple ways of doing it and we can cover both. + +### Method 1: Hooking the InteractionCreated Event + +We can hook the `ButtonExecuted` event for button type interactions: + +```cs +client.ButtonExecuted += MyButtonHandler; +``` + +Now, lets write our handler. + +```cs +public async Task MyButtonHandler(SocketMessageComponent component) +{ + // We can now check for our custom id + switch(component.Data.CustomId) + { + // Since we set our buttons custom id as 'custom-id', we can check for it like this: + case "custom-id": + // Lets respond by sending a message saying they clicked the button + await component.RespondAsync($"{component.User.Mention} has clicked the button!"); + break; + } +} +``` + +Running it and clicking the button: + +![](Images/image2.png) \ No newline at end of file diff --git a/docs/guides/interactions/message-components/03-buttons-in-depth.md b/docs/guides/interactions/message-components/03-buttons-in-depth.md new file mode 100644 index 000000000..f9fd67515 --- /dev/null +++ b/docs/guides/interactions/message-components/03-buttons-in-depth.md @@ -0,0 +1,45 @@ +--- +uid: Guides.MessageComponents.Buttons +title: Buttons in Depth +--- + +# Buttons in depth + +There are many changes you can make to buttons, lets take a look at the parameters in the `WithButton` function" +| Name | Type | Description | +|----------|---------------|----------------------------------------------------------------| +| label | `string` | The label text for the button. | +| customId | `string` | The custom id of the button. | +| style | `ButtonStyle` | The style of the button. | +| emote | `IEmote` | A IEmote to be used with this button. | +| url | `string` | A URL to be used only if the `ButtonStyle` is a Link. | +| disabled | `bool` | Whether or not the button is disabled. | +| row | `int` | The row to place the button if it has enough room, otherwise 0 | + +### Label + +This is the front facing text that the user sees. The maximum length is 80 characters. + +### CustomId + +This is the property sent to you by discord when a button is clicked. It is not required for link buttons as they do not emit an event. The maximum length is 100 characters. + +### Style + +Styling your buttons are important for indicating different actions: + +![](Images/image3.png) + +You can do this by using the `ButtonStyle` which has all the styles defined. + +### Emote + +You can specify an `IEmote` when creating buttons to add them to your button. They have the same restrictions as putting guild based emotes in messages. + +### Url + +If you use the link style with your button you can specify a url. When this button is clicked the user is taken to that url. + +### Disabled + +You can specify if your button is disabled, meaning users won't be able to click on it. diff --git a/docs/guides/interactions/message-components/04-select-menus.md b/docs/guides/interactions/message-components/04-select-menus.md new file mode 100644 index 000000000..5181ddf34 --- /dev/null +++ b/docs/guides/interactions/message-components/04-select-menus.md @@ -0,0 +1,76 @@ +--- +uid: Guides.MessageComponents.SelectMenus +title: Select Menus +--- + +# Select menus + +Select menus allow users to select from a range of options, this can be quite useful with configuration commands etc. + +## Creating a select menu + +We can use a `SelectMenuBuilder` to create our menu. + +```cs +var menuBuilder = new SelectMenuBuilder() + .WithPlaceholder("Select an option") + .WithCustomId("menu-1") + .WithMinValues(1) + .WithMaxValues(1) + .AddOption("Option A", "opt-a", "Option B is lying!") + .AddOption("Option B", "opt-b", "Option A is telling the truth!"); + +var builder = new ComponentBuilder() + .WithSelectMenu(menuBuilder); +``` + +Lets add this to a command: + +```cs +[Command("spawner")] +public async Task Spawn() +{ + var menuBuilder = new SelectMenuBuilder() + .WithPlaceholder("Select an option") + .WithCustomId("menu-1") + .WithMinValues(1) + .WithMaxValues(1) + .AddOption("Option A", "opt-a", "Option B is lying!") + .AddOption("Option B", "opt-b", "Option A is telling the truth!"); + + var builder = new ComponentBuilder() + .WithSelectMenu(menuBuilder); + + await ReplyAsync("Whos really lying?", components: builder.Build()); +} +``` + +Running this produces this result: + +![](Images/image4.png) + +And opening the menu we see: + +![](Images/image5.png) + +Lets handle the selection of an option, We can hook the `SelectMenuExecuted` event to handle our select menu: + +```cs +client.SelectMenuExecuted += MyMenuHandler; +``` + +The `SelectMenuExecuted` also supplies a `SocketMessageComponent` argument, we can confirm that its a select menu by checking the `ComponentType` inside of the data field if we need, but the library will do that for us and only execute our handler if its a select menu. + +The values that the user has selected will be inside of the `Values` collection in the Data field. we can list all of them back to the user for this example. + +```cs +public async Task MyMenuHandler(SocketMessageComponent arg) +{ + var text = string.Join(", ", arg.Data.Values); + await arg.RespondAsync($"You have selected {text}"); +} +``` + +Running this produces this result: + +![](Images/image6.png) diff --git a/docs/guides/interactions/message-components/05-advanced.md b/docs/guides/interactions/message-components/05-advanced.md new file mode 100644 index 000000000..49b3f31a6 --- /dev/null +++ b/docs/guides/interactions/message-components/05-advanced.md @@ -0,0 +1,87 @@ +--- +uid: Guides.MessageComponents.Advanced +title: Advanced Concepts +--- + +# Advanced + +Lets say you have some components on an ephemeral slash command, and you want to modify the message that the button is on. The issue with this is that ephemeral messages are not stored and can not be get via rest or other means. + +Luckily, Discord thought of this and introduced a way to modify them with interactions. + +### Using the UpdateAsync method + +Components come with an `UpdateAsync` method that can update the message that the component was on. You can use it like a `ModifyAsync` method. + +Lets use it with a command, we first create our command, in this example im just going to use a message command: + +```cs +var command = new MessageCommandBuilder() + .WithName("testing").Build(); + +await client.GetGuild(guildId).BulkOverwriteApplicationCommandAsync(new [] { command, buttonCommand }); +``` + +Next, we listen for this command, and respond with some components when its used: + +```cs +var menu = new SelectMenuBuilder() +{ + CustomId = "select-1", + Placeholder = "Select Somthing!", + MaxValues = 1, + MinValues = 1, +}; + +menu.AddOption("Meh", "1", "Its not gaming.") + .AddOption("Ish", "2", "Some would say that this is gaming.") + .AddOption("Moderate", "3", "It could pass as gaming") + .AddOption("Confirmed", "4", "We are gaming") + .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); + +var components = new ComponentBuilder() + .WithSelectMenu(menu); + + +await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: componBuild(), ephemeral: true); +break; +``` + +Now, let's listen to the select menu executed event and add a case for `select-1` + +```cs +client.SelectMenuExecuted += SelectMenuHandler; + +... + +public async Task SelectMenuHandler(SocketMessageComponent arg) +{ + switch (arg.Data.CustomId) + { + case "select-1": + var value = arg.Data.Values.First(); + var menu = new SelectMenuBuilder() + { + CustomId = "select-1", + Placeholder = $"{(arg.Message.Components.First().Components.First() as SelectMenu).Options.FirstOrDefault(x => x.Value == value).Label}", + MaxValues = 1, + MinValues = 1, + Disabled = true + }; + + menu.AddOption("Meh", "1", "Its not gaming.") + .AddOption("Ish", "2", "Some would say that this is gaming.") + .AddOption("Moderate", "3", "It could pass as gaming") + .AddOption("Confirmed", "4", "We are gaming") + .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); + + // We use UpdateAsync to update the message and its original content and components. + await arg.UpdateAsync(x => + { + x.Content = $"Thank you {arg.User.Mention} for rating us {value}/5 on the gaming scale"; + x.Components = new ComponentBuilder().WithSelectMenu(menu).Build(); + }); + break; + } +} +``` diff --git a/docs/guides/interactions/message-components/images/image1.png b/docs/guides/interactions/message-components/images/image1.png new file mode 100644 index 000000000..a161d8a61 Binary files /dev/null and b/docs/guides/interactions/message-components/images/image1.png differ diff --git a/docs/guides/interactions/message-components/images/image2.png b/docs/guides/interactions/message-components/images/image2.png new file mode 100644 index 000000000..9303de91b Binary files /dev/null and b/docs/guides/interactions/message-components/images/image2.png differ diff --git a/docs/guides/interactions/message-components/images/image3.png b/docs/guides/interactions/message-components/images/image3.png new file mode 100644 index 000000000..7480e1da9 Binary files /dev/null and b/docs/guides/interactions/message-components/images/image3.png differ diff --git a/docs/guides/interactions/message-components/images/image4.png b/docs/guides/interactions/message-components/images/image4.png new file mode 100644 index 000000000..c54ab791f Binary files /dev/null and b/docs/guides/interactions/message-components/images/image4.png differ diff --git a/docs/guides/interactions/message-components/images/image5.png b/docs/guides/interactions/message-components/images/image5.png new file mode 100644 index 000000000..096b7587f Binary files /dev/null and b/docs/guides/interactions/message-components/images/image5.png differ diff --git a/docs/guides/interactions/message-components/images/image6.png b/docs/guides/interactions/message-components/images/image6.png new file mode 100644 index 000000000..1536096d0 Binary files /dev/null and b/docs/guides/interactions/message-components/images/image6.png differ diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md new file mode 100644 index 000000000..0a4ca26e9 --- /dev/null +++ b/docs/guides/introduction/intro.md @@ -0,0 +1,51 @@ +--- +uid: Guides.Introduction +title: Introduction to Discord.Net +--- + +# Introduction + +## Looking to get started? + +Welcome! Before you dive into this library, however, you should have +some decent understanding of the language +you are about to use. This library touches on +[Task-based Asynchronous Pattern] \(TAP), [polymorphism], [interface] +and many more advanced topics extensively. Please make sure that you +understand these topics to some extent before proceeding. With all +that being said, feel free to visit us on Discord at the link below +if you have any questions! + +An official collection of samples can be found +in [our GitHub repository]. + +> [!NOTE] +> Please note that you should *not* try to blindly copy paste +> the code. The examples are meant to be a template or a guide. + +[our GitHub repository]: https://github.com/RogueException/Discord.Net/tree/dev/samples +[Task-based Asynchronous Pattern]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap +[polymorphism]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/polymorphism +[interface]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/ + +## New to .NET/C#? + +All examples or snippets featured in this guide and all API +documentation will be written in C#. + +If you are new to the language, using this wrapper may prove to be +difficult, but don't worry! There are many resources online that can +help you get started in the wonderful world of .NET. Here are some +resources to get you started. + +- [C# Programming Guide (MSDN/Microsoft, Free)](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/) +- [C# Fundamentals For Absolute Beginners (Channel9/Microsoft, Free)](https://channel9.msdn.com/Series/C-Fundamentals-for-Absolute-Beginners) +- [C# Path (Pluralsight, Paid)](https://www.pluralsight.com/paths/csharp) + +## Still have questions? + +Please visit us at `#dotnet_discord-net` on the [Discord API] server. +Describe the problem in details to us, what you've done, and, +if any, the problematic code uploaded onto [Hastebin](https://hastebin.com). + +[Discord API]: https://discord.gg/jkrBmQR \ No newline at end of file diff --git a/docs/guides/migrating/migrating.md b/docs/guides/migrating/migrating.md deleted file mode 100644 index bc628a5f8..000000000 --- a/docs/guides/migrating/migrating.md +++ /dev/null @@ -1,61 +0,0 @@ -# Migrating from 0.9 - -**1.0.0 is the biggest breaking change the library has gone through, due to massive -changes in the design of the library.** - ->A medium to advanced understanding is recommended when working with this library. - -It is recommended to familiarize yourself with the entities in 1.0 before continuing. -Feel free to look through the library's source directly, look through IntelliSense, or -look through our hosted [API Documentation](xref:Discord). - -## Entities - -Most API models function _similarly_ to 0.9, however their names have been changed. -You should also keep in mind that we now separate different types of Channels and Users. - -Before proceeding, please read over @Terminology to understand the naming behind some objects. - -Below is a table that compares most common 0.9 entities to their 1.0 counterparts. - ->This should be used mostly for migration purposes. Please take some time to consider whether ->or not you are using the right "tool for the job" when working with 1.0 - -| 0.9 | 1.0 | Notice | -| --- | --- | ------ | -| Server | @Discord.WebSocket.SocketGuild | -| Channel | @Discord.WebSocket.SocketGuildChannel | Applies only to channels that are members of a Guild | -| Channel.IsPrivate | @Discord.WebSocket.SocketDMChannel -| ChannelType.Text | @Discord.WebSocket.SocketTextChannel | This applies only to Text Channels in Guilds -| ChannelType.Voice | @Discord.WebSocket.SocketVoiceChannel | This applies only to Voice Channels in Guilds -| User | @Discord.WebSocket.SocketGuildUser | This applies only to users belonging to a Guild* -| Profile | @Discord.WebSocket.SocketGuildUser -| Message | @Discord.WebSocket.SocketUserMessage - -\* To retrieve an @Discord.WebSocket.SocketGuildUser, you must retrieve the user from an @Discord.WebSocket.SocketGuild. - -## Event Registration - -Prior to 1.0, events were registered using the standard c# `Handler(EventArgs)` pattern. In 1.0, -events are delegates, but are still registered the same. - -For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) - -To hook an event into MessageReceived, we now use the following code: -[!code-csharp[Event Registration](samples/event.cs)] - -> **All Event Handlers in 1.0 MUST return Task!** - -If your event handler is marked as `async`, it will automatically return `Task`. However, -if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, -stick a `return Task.CompletedTask` at the bottom. - -[!code-csharp[Sync Event Registration](samples/sync_event.cs)] - -**Event handlers no longer require a sender.** The only arguments your event handler needs to accept -are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the -API docs before implementing it. - -## Async - -Nearly everything in 1.0 is an async Task. You should always await any tasks you invoke. diff --git a/docs/guides/migrating/samples/event.cs b/docs/guides/migrating/samples/event.cs deleted file mode 100644 index 8719942f2..000000000 --- a/docs/guides/migrating/samples/event.cs +++ /dev/null @@ -1,4 +0,0 @@ -_client.MessageReceived += async (msg) => -{ - await msg.Channel.SendMessageAsync(msg.Content); -} \ No newline at end of file diff --git a/docs/guides/migrating/samples/sync_event.cs b/docs/guides/migrating/samples/sync_event.cs deleted file mode 100644 index f4a55cdd3..000000000 --- a/docs/guides/migrating/samples/sync_event.cs +++ /dev/null @@ -1,5 +0,0 @@ -_client.Log += (msg) => -{ - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; -} \ No newline at end of file diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 2e3a61e19..968468416 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,27 +1,68 @@ -- name: Getting Started +- name: Introduction + topicUid: Guides.Introduction +- name: "Working with Guild Events" items: - - name: Installation - href: getting_started/installing.md - - name: Your First Bot - href: getting_started/intro.md - - name: Terminology - href: getting_started/terminology.md -- name: Basic Concepts + - name: Introduction + topicUid: Guides.GuildEvents.Intro + - name: Creating Events + topicUid: Guides.GuildEvents.Creating + - name: Getting Event Users + topicUid: Guides.GuildEvents.GettingUsers + - name: Modifying Events + topicUid: Guides.GuildEvents.Modifying +- name: Working with Commands items: - - name: Logging Data - href: concepts/logging.md - - name: Working with Events - href: concepts/events.md - - name: Managing Connections - href: concepts/connections.md - - name: Entities - href: concepts/entities.md -- name: The Command Service + - name: Introduction + topicUid: Guides.Commands.Intro + - name: TypeReaders + topicUid: Guides.Commands.TypeReaders + - name: Named Arguments + topicUid: Guides.Commands.NamedArguments + - name: Preconditions + topicUid: Guides.Commands.Preconditions + - name: Dependency Injection + topicUid: Guides.Commands.DI + - name: Post-execution Handling + topicUid: Guides.Commands.PostExecution +- name: Working with Slash Commands items: - - name: Command Guide - href: commands/commands.md -- name: Voice + - name: Introduction + topicUid: Guides.SlashCommands.Intro + - name: Creating slash commands + topicUid: Guides.SlashCommands.Creating + - name: Receiving and responding to slash commands + topicUid: Guides.SlashCommands.Receiving + - name: Slash command parameters + topicUid: Guides.SlashCommands.Parameters + - name: Ephemeral responses + topicUid: Guides.SlashCommands.Ephemeral + - name: Sub commands + topicUid: Guides.SlashCommands.SubCommand + - name: Slash command choices + topicUid: Guides.SlashCommands.Choices + - name: Slash commands Bulk Overwrites + topicUid: Guides.SlashCommands.BulkOverwrite +- name: Working with Context commands + items: + - name: Creating Context Commands + topicUid: Guides.ContextCommands.Creating + - name: Receiving Context Commands + topicUid: Guides.ContextCommands.Reveiving +- name: Working with Message Components items: - - name: Voice Guide - href: voice/sending-voice.md -- name: Migrating from 0.9 \ No newline at end of file + - name: Getting started + topicUid: Guides.MessageComponents.GettingStarted + - name: Responding to Components + topicUid: Guides.MessageComponents.Responding + - name: Buttons in depth + topicUid: Guides.MessageComponents.Buttons + - name: Select menus + topicUid: Guides.MessageComponents.SelectMenus + - name: Advanced Concepts + topicUid: Guides.MessageComponents.Advanced +- name: Emoji + topicUid: Guides.Emoji +- name: Voice + topicUid: Guides.Voice.SendingVoice +- name: Deployment + topicUid: Guides.Deployment diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs index 4cec67540..8803d3596 100644 --- a/docs/guides/voice/samples/joining_audio.cs +++ b/docs/guides/voice/samples/joining_audio.cs @@ -1,9 +1,10 @@ -[Command("join")] +// The command's Run Mode MUST be set to RunMode.Async, otherwise, being connected to a voice channel will block the gateway thread. +[Command("join", RunMode = RunMode.Async)] public async Task JoinChannel(IVoiceChannel channel = null) { // Get the audio channel - channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; - if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } + channel = channel ?? (Context.User as IGuildUser)?.VoiceChannel; + if (channel == null) { await Context.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. var audioClient = await channel.ConnectAsync(); diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 024a98b95..555adbca2 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -1,4 +1,5 @@ --- +uid: Guides.Voice.SendingVoice title: Sending Voice --- @@ -10,16 +11,16 @@ Information is not guaranteed to be accurate. ## Installing -Audio requires two native libraries, `libsodium` and `opus`. -Both of these libraries must be placed in the runtime directory of your -bot. (When developing on .NET Framework, this would be `bin/debug`, -when developing on .NET Core, this is where you execute `dotnet run` +Audio requires two native libraries, `libsodium` and `opus`. +Both of these libraries must be placed in the runtime directory of your +bot. (When developing on .NET Framework, this would be `bin/debug`, +when developing on .NET Core, this is where you execute `dotnet run` from; typically the same directory as your csproj). -For Windows Users, precompiled binaries are available for your -convienence [here](https://discord.foxbot.me/binaries/). +For Windows Users, precompiled binaries are available for your +convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives). -For Linux Users, you will need to compile [Sodium] and [Opus] from +For Linux Users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager. [Sodium]: https://download.libsodium.org/libsodium/releases/ @@ -27,7 +28,7 @@ source, or install them from your package manager. ## Joining a Channel -Joining a channel is the first step to sending audio, and will return +Joining a channel is the first step to sending audio, and will return an [IAudioClient] to send data with. To join a channel, simply await [ConnectAsync] on any instance of an @@ -35,67 +36,76 @@ To join a channel, simply await [ConnectAsync] on any instance of an [!code-csharp[Joining a Channel](samples/joining_audio.cs)] -The client will sustain a connection to this channel until it is +>[!WARNING] +>Commands which mutate voice states, such as those where you join/leave +>an audio channel, or send audio, should use [RunMode.Async]. RunMode.Async +>is necessary to prevent a feedback loop which will deadlock clients +>in their default configuration. If you know that you're running your +>commands in a different task than the gateway task, RunMode.Async is +>not required. + +The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to disconnect. -It should be noted that voice connections are created on a per-guild -basis; only one audio connection may be open by the bot in a single -guild. To switch channels within a guild, invoke [ConnectAsync] on +It should be noted that voice connections are created on a per-guild +basis; only one audio connection may be open by the bot in a single +guild. To switch channels within a guild, invoke [ConnectAsync] on another voice channel in the guild. [IAudioClient]: xref:Discord.Audio.IAudioClient -[ConnectAsync]: xref:Discord.IAudioChannel#Discord_IAudioChannel_ConnectAsync_Action_IAudioClient__ +[ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* +[RunMode.Async]: xref:Discord.Commands.RunMode ## Transmitting Audio ### With FFmpeg -[FFmpeg] is an open source, highly versatile AV-muxing tool. This is +[FFmpeg] is an open source, highly versatile AV-muxing tool. This is the recommended method of transmitting audio. -Before you begin, you will need to have a version of FFmpeg downloaded -and placed somewhere in your PATH (or alongside the bot, in the same -location as libsodium and opus). Windows binaries are available on +Before you begin, you will need to have a version of FFmpeg downloaded +and placed somewhere in your PATH (or alongside the bot, in the same +location as libsodium and opus). Windows binaries are available on [FFmpeg's download page]. [FFmpeg]: https://ffmpeg.org/ [FFmpeg's download page]: https://ffmpeg.org/download.html -First, you will need to create a Process that starts FFmpeg. An -example of how to do this is included below, though it is important +First, you will need to create a Process that starts FFmpeg. An +example of how to do this is included below, though it is important that you return PCM at 48000hz. >[!NOTE] ->As of the time of this writing, Discord.Audio struggles significantly ->with processing audio that is already opus-encoded; you will need to +>As of the time of this writing, Discord.Audio struggles significantly +>with processing audio that is already opus-encoded; you will need to >use the PCM write streams. [!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] -Next, to transmit audio from FFmpeg to Discord, you will need to -pull an [AudioOutStream] from your [IAudioClient]. Since we're using +Next, to transmit audio from FFmpeg to Discord, you will need to +pull an [AudioOutStream] from your [IAudioClient]. Since we're using PCM audio, use [IAudioClient.CreatePCMStream]. -The sample rate argument doesn't particularly matter, so long as it is -a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of +The sample rate argument doesn't particularly matter, so long as it is +a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of simplicity, I recommend using 1920. -Channels should be left at `2`, unless you specified a different value +Channels should be left at `2`, unless you specified a different value for `-ac 2` when creating FFmpeg. [AudioOutStream]: xref:Discord.Audio.AudioOutStream [IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ -Finally, audio will need to be piped from FFmpeg's stdout into your -AudioOutStream. This step can be as complex as you'd like it to be, but -for the majority of cases, you can just use [Stream.CopyToAsync], as +Finally, audio will need to be piped from FFmpeg's stdout into your +AudioOutStream. This step can be as complex as you'd like it to be, but +for the majority of cases, you can just use [Stream.CopyToAsync], as shown below. [Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx -If you are implementing a queue for sending songs, it's likely that -you will want to wait for audio to stop playing before continuing on -to the next song. You can await `AudioOutStream.FlushAsync` to wait for +If you are implementing a queue for sending songs, it's likely that +you will want to wait for audio to stop playing before continuing on +to the next song. You can await `AudioOutStream.FlushAsync` to wait for the audio client's internal buffer to clear out. -[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] \ No newline at end of file +[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] diff --git a/docs/index.md b/docs/index.md index ef9ecdfdd..1d0f5aaf7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,28 @@ +--- +uid: Root.Landing +title: Home +--- # Discord.Net Documentation -Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/). + -If this is your first time using Discord.Net, you should refer to the [Intro](guides/getting_started/intro.md) for tutorials. -More experienced users might refer to the [API Documentation](api/index.md) for a breakdown of the individuals objects in the library. +[![GitHub](https://img.shields.io/github/last-commit/discord-net/discord.net?style=plastic)](https://github.com/discord-net/Discord.Net) +[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) +[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) +[![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev) +[![Discord](https://discord.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) -For additional resources: - - [Discord API Guild](https://discord.gg/discord-api) - Look for `#dotnet_discord-net` - - [GitHub](https://github.com/RogueException/Discord.Net/tree/dev) - - [NuGet](https://www.nuget.org/packages/Discord.Net/) - - [MyGet Feed](https://www.myget.org/feed/Packages/discord-net) - Addons and nightly builds \ No newline at end of file +## What is Discord.Net? + +Discord.Net is an asynchronous, multi-platform .NET Library used to +interface with the [Discord API](https://discord.com/). + +## Where to begin? + +If this is your first time using Discord.Net, you should refer to the +[Intro](xref:Guides.Introduction) for tutorials. + +More experienced users might want to refer to the +[API Documentation](xref:API.Docs) for a breakdown of the individual +objects in the library. diff --git a/docs/langwordMapping.yml b/docs/langwordMapping.yml new file mode 100644 index 000000000..9ddd00cab --- /dev/null +++ b/docs/langwordMapping.yml @@ -0,0 +1,61 @@ +references: +- uid: langword_csharp_null + name.csharp: "null" + name.vb: "Nothing" +- uid: langword_vb_Nothing + name.csharp: "null" + name.vb: "Nothing" +- uid: langword_csharp_static + name.csharp: static + name.vb: Shared +- uid: langword_vb_Shared + name.csharp: static + name.vb: Shared +- uid: langword_csharp_virtual + name.csharp: virtual + name.vb: Overridable +- uid: langword_vb_Overridable + name.csharp: virtual + name.vb: Overridable +- uid: langword_csharp_true + name.csharp: "true" + name.vb: "True" +- uid: langword_vb_True + name.csharp: "true" + name.vb: "True" +- uid: langword_csharp_false + name.csharp: "false" + name.vb: "False" +- uid: langword_vb_False + name.csharp: "false" + name.vb: "False" +- uid: langword_csharp_abstract + name.csharp: abstract + name.vb: MustInherit +- uid: langword_vb_MustInherit + name.csharp: abstract + name.vb: MustInherit +- uid: langword_csharp_sealed + name.csharp: sealed + name.vb: NotInheritable +- uid: langword_vb_NotInheritable + name.csharp: sealed + name.vb: NotInheritable +- uid: langword_csharp_async + name.csharp: async + name.vb: Async +- uid: langword_vb_Async + name.csharp: async + name.vb: Async +- uid: langword_csharp_await + name.csharp: await + name.vb: Await +- uid: langword_vb_Await + name.csharp: await + name.vb: Await +- uid: langword_csharp_async/await + name.csharp: async/await + name.vb: Async/Await +- uid: langword_vb_Async/Await + name.csharp: async/await + name.vb: Async/Await \ No newline at end of file diff --git a/docs/marketing/logo/Logo.ai b/docs/marketing/logo/Logo.ai new file mode 100644 index 000000000..f145111c3 --- /dev/null +++ b/docs/marketing/logo/Logo.ai @@ -0,0 +1,1445 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[6 0 R 7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Logo + + + Adobe Illustrator CC 23.0 (Windows) + 2018-12-23T04:14+02:00 + 2018-12-23T04:14+01:00 + 2018-12-23T04:14+01:00 + + + + 256 + 128 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAgAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXmH50/l55m85Np C6MbdYrAXDTi4laMM03phKBUevEIczNJnjju+rmaTPHHd9WM6j+S2q6jfWsV1Pf2zrpdnplzd2k6 /V2dI1WaTi37x/T5nj9np4nKsnDImVtOSpEytOLj/nHTS5rBrP8AxHqagxSwrKvoK9JrZLepKRpX h6QCUoFjLRj4WyhpZD+W/wCVFt5Hur24h1e81E3kcMJS4b4QsCLGjGpYs/FB3AG9FFcVR/nLyJf+ YJ4Liz8z6rokkNxaz+jZT8Ld1t2YyRvGnBmEyPRqvSoU02IZVK778nxd3NlOfOnmmNbOOKI26akP Rn9Eg8rhDF+8MlPj3FcVVLb8rL1dKt7S685+YXu0gkhubuC89P1WliijMgWVbgoyGAOlG2Zm68ji q1PyjPqXM03m7zCbi4hlthPHeBHWJ1RUoSjnnH6ZYHpydzx+KgVTHS/y5h068iuYvMGuSpDIsiWk 18Xgovp/A0fH4lb0t67/ABNv8WKsuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV8of8rQ/MD/q+XP3r/TN/wDlcfc9R+TxfzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/ 1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/ NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5gf9Xy5+9f6Y /lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5g f9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+Tx fzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+ mP5XH3L+TxfzQ+jvy+v7zUPJekXt7K091PAHmmf7TNU7nNNqIgTIDz2qiI5JAcmQ5S0OxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxV2KvifOnexdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir6w/K/ /wAl/of/ADDD/iRzn9V/eF5fWf3sveyjKHGdirsVdirsVdirsVdirsVdirsVdirsVdirsVfE+dO9 i7FXoPkLyroGqaOlzfrbNctetE/1u6Fsot1SIlgBIjk1ZgCEYda9sw8+WUZUO7ucDU5pxlQuq6C9 3mOveXvzEje41HTYFTQXflaTymIH0iQoIJqaVNNxX2oMjKeTiqJDCeTMZ1EhJ7pfPfpH0Ht/U3p9 kj+87VA240pX/KrvTJS8etqZzGoravx+Pv8AJNNG/TRSY6pxBL1hUBQQvh8JYU6U3rl2Lj34nIwe Jvxpjlze7FUDZ33lkeZb628zahJYaZHpZktZIpZInF4ZxTiEV1kPoh+KMKFqbgVOa7V5ZCdA1s6n XZpRnQNCv1rfL8s82jWks8jTSOnL1WHFmUk8SR/q0zMwEmAJc/TknGCTaYZa3uxV7ZoX5d6ZNpOi 3Mukiezmso7jUJZaKCZIjIzJIhVxQNX4m6jNXPUEEi97dPk1UhKQve9niebR3DsVdir6w/K//wAl /of/ADDD/iRzn9V/eF5fWf3sveyjKHGdiqX+YdGg1vQNT0W4PGDU7SezlahNEuI2jY0BXs3iMVeU WH/OMXl6zupbga9qMrSEkRyLbSR7/VzwaOSJ1aMfVFASlAKD9kYq67/5xj0Ka8e5XXb4iWokinWK VCvrrcAFQEVv3kSO3IEM/J2DFsVev6dZJY6fbWSSSTJaxJCsszc5XEahQ0jn7TGlSe5xV5bP+XP5 vvqsk9v53a2snvpLmGFzLcGGBmcpHSkXq05KeLEKPs/EooyqBvPyw/O9rqwntPPhSG2kt5LmzLzA SLFGVkj9VllJ5MPtMhryqwPBQVXpPkXR/MGj+WLTT/MGpHVtUh5+reMS5Ks5KJ6jBGk4KQvNgCcV T/FXYq7FXYq7FXYq+J86d7F2KuxVeZpSnAuxT+Uk029sFIpZhS7FXYq7FXUAJIG564Fd02GFXYq7 FVYXl4oIE8gDKEYB23ULxAO/TiKfLBQY8IUcLJ2KuxV9Yflf/wCS/wBD/wCYYf8AEjnP6r+8Ly+s /vZe9lGUOM7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq+J86d7F2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KvrD8r//ACX+h/8AMMP+JHOf1X94Xl9Z/ey97KMocZ2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxVR+o2X/LPF/wC/0w8RTxHvd9Rsv+WeL/AIBf6Y8RXiPe76jZf8s8X/AL/THiK8R7 3fUbL/lni/4Bf6Y8RXiPe76jZf8ALPF/wC/0x4ivEe931Gy/5Z4v+AX+mPEV4j3u+o2X/LPF/wAA v9MeIrxHvd9Rsv8Alni/4Bf6Y8RXiPe76jZf8s8X/AL/AEx4ivEe931Gy/5Z4v8AgF/pjxFeI97v qNl/yzxf8Av9MeIrxHvd9Rsv+WeL/gF/pjxFeI97vqNl/wAs8X/AL/THiK8R73fUbL/lni/4Bf6Y 8RXiPe76jZf8s8X/AAC/0x4ivEe931Gy/wCWeL/gF/pjxFeI96qiIihUUKo6KBQD7sCG8VdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi qld3drZ2s15eTR21pbRtNcXEzBI440BZ3d2IVVVRUk9MVSj/AB55Hox/xFplEXm5+uW+ykRkMfj6 UnjP+yXxGKtz+evJMBkE/mHTYjEC0oe8gXgFlFuxar7UmYRmv7Xw9cVTpWVlDKQysKqw3BB7jFW8 VdirsVdirsVdirsVdiryvW/+cg/Lmka9f6Tc6dduNPna2lnj9M1ZDxchSw2G9N8zYaGUoggjdzYa KUo8QIeqZhOE7FXjf5uap+a0PmyKPyjFqB0+O0iVmtYfViNw8j8ixKsv2Sla9OubDTRxcPrq3P0w xcJ46tLtbl/ObUfPV5YeWfM0cOn/AFjglvPGBwCkMx9X6nIvBkicBQTv+3vTMM4yBbhnGQLRuueV P+ck5bemlearKKYVBaT0mBP1ktyCiyTgGiIoCW4gFfi5cxWwZ9+X+n+ebOzvv8W3qXc01y0lkisk jRQn9hnjit1Ir9kcTTx3oFWVYq7FXzl/zkdd+f283Wa+V5NRjtrO1hFy1n6oiDyPKzc2j2FQI6/t bbeOZ+niODpfF1rk52nA4Ol29o/LY6qfImivqsk8uoPbh55LolpzzJZfULfFXiR13zGz1xmuTjZ6 4zXJkuUtTsVfLXm+/wDzEl/MHVW06XWIIZtTlt7OS3+spHSOUwDgV+Ar8Pyzc4hj8MXXJ3GLw/DF 1dPqXNM6d2KuxV2KuxV2KoTV9MtdW0m90u7Ba0v4JbW4UUqY5kMbgVBH2W7jFWA2P/OPX5aWNzJd Wttdw3Ej+r6iXlwjCUmJjICrr8TNApJNdycVSvXf+cePLMmpW15olslv8RN489xcl6KUMPokFuJj EaqDUEBVoRvWceHrbKPD1eqaXYxWGmWljCixQ2kMcEcUfLgqxoFCryJagA2qa5AsXls//OP6Tas+ oDzPf2we/kvxBbVVYzIztwh9R5QhBevIgnl8S8TSiqDvf+ccPXu9Pu4vNuoQyWEttMtuAwt2NshT j6ccsbKh22D1FXofiqFWZ+VPy+1Py5pVhpdv5huJrOzjdGieMUZnlaQFCXZ0VQ3BV5EcR3O+TjIA cmYkAOTNMgwdir5sf87fzvt7OR00i11P/RJJbS7t9M1EJNOr2iuiRVMjek08qBjwVyvUUOKslvPz b/NWFL1l8t8JobwQR2Umn3zOqCFmgRpY2aFnvpFRUZGKwF/3nKm6rHl/Of8AO2W91S3TTLOCaC/k h0exlsL157yIPcRoitG4jKmSBUMvJeJJJHHfFUZq/wCVuiah5tmuGudSM19qD3Fw3owpC3rXyo3p cjyKfvuu+wJ9s2kdTIQrbYfodlHUyEK25fofQeat1rsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVY/wD8rD8gf9TNpX/Sdbf814q7/lYfkD/q ZtK/6Trb/mvFXf8AKw/IH/UzaV/0nW3/ADXirv8AlYfkD/qZtK/6Trb/AJrxV3/Kw/IH/UzaV/0n W3/NeKu/5WH5A/6mbSv+k62/5rxV3/Kw/IH/AFM2lf8ASdbf814q7/lYfkD/AKmbSv8ApOtv+a8V d/ysPyB/1M2lf9J1t/zXirv+Vh+QP+pm0r/pOtv+a8Vd/wArD8gf9TNpX/Sdbf8ANeKu/wCVh+QP +pm0r/pOtv8AmvFXf8rD8gf9TNpX/Sdbf814q7/lYfkD/qZtK/6Trb/mvFXf8rD8gf8AUzaV/wBJ 1t/zXirv+Vh+QP8AqZtK/wCk62/5rxVOrO8s722ju7OeO5tZhyinhdZI3XxVlJBHyxVWxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxV2KuxV+VeKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV+iv5A /wDkmvKf/MCv/EmxVn+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvyrxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2Kv0V/IH/AMk15T/5gV/4k2Ks/wAVdiqyaaGCGSeeRYoYlLyyuQqqqirM zHYADqcQFAQo1zRTUjULY0FTSaPYUU+PhIp/2Q8clwHuZcJ7nS65okRYS6hbRlAS4aaNaASekSan b958H+tt1x4D3LwnuRuRYuxV2KuxV2KuxV2KuxV2KvNNY/PzynpOuX+lXVlfudPnNvPcwxxMgda8 zQyK/FeJOynbMuOjlIAgjdyo6WRAO2789sxHFdirKPLWl+X7nTZZNSZRPzb0v3hQ8QB2qB1rmw0u HHKNy5+9z9LixyiTPmzzy5Z/84/L5Us7jzHBfR639X5XLQNO8Bfi9Dx/dlpOZXkqlUpxowPM5hHH IC62cM4zV9F2gt/zjAZv9y66qIqDhxEpP+8wp6nBh8Xq8ufHbnx4/u65BgwX8wR+X63lgvk31DEt so1As0rxmcGlUadIZCSN2+BV/lGKsTxV2KvTvyg0TyLfw6kfNoUQmGT6pJyZZBKrRU4cVfkeDPRS KE0qR1zP0+K4XQJv7HO0+MGF0Cbef61FbxardRW4CwpIVQDpt/bmLnAEyBycbOAJkDkgcqanYq9A 0vSNAOgRSXNvG8/oiRmFQ/xLyqzZucWDH4YJA5O4xYcfhixvTz/NM6d2KuxV+iv5A/8AkmvKf/MC v/EmxVn+KuxVDapp8GpaZd6dcV+r3sMlvNxpXhKhRqcgy9D3BwxlRtMTRthNr+SPkq1mkmh+tiR2 58jN0esZLU40NTENmqBU0A7ZB1cy3nUyLpPyQ8ktP68YuYXqDtIjigZWUUlSQfDwQA9dutSSX83N fzMmdWdpBZ2kFpbrwt7eNYoUqTREUKoqak0A75jk2baCbNsAm/KjU5NQa6XzPd26NdvdiGASKEVy xEcZeZwPtVqQRXcKMyRqBVcLkDOK5IW5/J7WZbiznj83XcRtHhk9ALO0LeivEpxNzzCnwD7Atuag gjUjf0/j5JGoH838fJm/lTQptC0K20ya9e/kg5VuXDCvJiwVQ7ysFWtAC5+eY+SfFK6poyS4jab5 Bg7FXYq7FXYq+dtY/LTRr/zbcSnVb4y6jqMlxOkVlF6DeverGwSRp0LJWYbgNsCabcc2kc5EeQ2H f5OxjmIjyGw/Q+NM1brnYq7FWyzEUJJHhja21irsVdirsVbqfu6Yq1irsVdirdTirWKuxV2Kv0V/ IH/yTXlP/mBX/iTYqz/FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX57f9C3fnb/1K0/8AyOtv +quKu/6Fu/O3/qVp/wDkdbf9VcVd/wBC3fnb/wBStP8A8jrb/qrirv8AoW787f8AqVp/+R1t/wBV cVd/0Ld+dv8A1K0//I62/wCquKu/6Fu/O3/qVp/+R1t/1VxV3/Qt352/9StP/wAjrb/qrirv+hbv zt/6laf/AJHW3/VXFXf9C3fnb/1K0/8AyOtv+quKu/6Fu/O3/qVp/wDkdbf9VcVd/wBC3fnb/wBS tP8A8jrb/qrirv8AoW787f8AqVp/+R1t/wBVcVd/0Ld+dv8A1K0//I62/wCquKu/6Fu/O3/qVp/+ R1t/1VxV3/Qt352/9StP/wAjrb/qrirv+hbvzt/6laf/AJHW3/VXFX2v+TuiapoX5YeXdI1a3Nrq NnaLHc27FWKOGJoSpZe/Y4qzLFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== + + + + uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7 + xmp.did:7b6ad08a-6c34-094a-99ed-6fd2184825ab + uuid:00f17b8b-7d1e-47af-92bc-2084415a76cb + proof:pdf + + xmp.iid:53573ae3-c43e-0d4a-8781-3ffb12417c5c + xmp.did:53573ae3-c43e-0d4a-8781-3ffb12417c5c + uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7 + proof:pdf + + + + + saved + xmp.iid:7cc70b61-b475-d44e-a574-e657edcb3f5b + 2018-10-23T21:23:47+02:00 + Adobe Illustrator CC 22.1 (Windows) + / + + + saved + xmp.iid:7b6ad08a-6c34-094a-99ed-6fd2184825ab + 2018-12-23T04:13:58+01:00 + Adobe Illustrator CC 23.0 (Windows) + / + + + + Basic RGB + Document + 1 + False + False + + 5000.000000 + 5000.000000 + Pixels + + + + + MyriadPro-Regular + Myriad Pro + Regular + Open Type + Version 2.106;PS 2.000;hotconv 1.0.70;makeotf.lib2.5.58329 + False + MyriadPro-Regular.otf + + + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Cold + 1 + + + + C=56 M=0 Y=20 K=0 + RGB + PROCESS + 101 + 200 + 208 + + + C=51 M=43 Y=0 K=0 + RGB + PROCESS + 131 + 139 + 197 + + + C=26 M=41 Y=0 K=0 + RGB + PROCESS + 186 + 155 + 201 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 5000.0 5000.0]/Type/Page>> endobj 10 0 obj <>stream +HI& u@fya?o/>-l_p4Oݾӟ__–f/Gc!-99/xc#}Som +vgG b-}P+a1jKe),t+==so/{9zk;BB1j; ra6:َR;%#̤-bv2a}__tt6ȚU]M=rKOӜr_e7u53OG#Oçh|s[-p۰3-vԷڏ2 +dNeKkBu~شQ23,W[=܎"pc;j1}ȉ~o +:ӏQ1K |dR8B0ǏN%~߉.o7{|E7;7͘4ffݗX#ߏjX["//-y9{JQU'uR9bOY^$ƐŰ( ~xqr"u$fh9hzڷ朄Esu@xJ~ߪdQ9#d.V$q,/='JZo&dbTX-lEBh1@~TU1Fg-r[X"{3R2EJ٘ M- +#ye^O'*ALvGgtu*ؙ0خ5MQ.=uD+1za Bb,4[8C"'hNoȃ'f򑽋Hm^!Q!4?;P,EGprPcJxh?j1bA4Pp<uB3WAh +XhY 4J݋^fû=XG^RԠ"/c!9VZ^'̔/G;DϳIF %1Ї+R"r6z/Crxg>{}?Ab%oZ/fETVߖV/BʋsCZX 1D.Kl]yv_K Yʐ0f\y.Լ8 6/۫5,Eozlh"xu$#U> (fbq +]$@JjN34ϭN0P/RPI(V bc=$OKS 4%;|(mDGs l7C)6H!3%1C5iTq4'S >2V̚0vj<;< $`"b2)+yg>pWoƝdՌ^`JdyFlL!Wt*2f͛Q9TNՀ7M]#?#?7Q~?tTp;wǯoGN/?ZGQ[s|ԇ?bv *ZPܓMrZA*.l}сtts5*@6\TcF500Càa T;GSnTBǵ|0#h)NE`,MB ^9f:Z kn1ЛMS'OT4T@a*߇bS}Uj4[@Sm9[e45WlD ik;Zkm,3ÌQ z^7d&|Xd@*X#H|ةC7ޭʥams?}W໬6"S\ ~84FuA {KKؽ7 cˤGsݟDfƩ/$<}yQ06WKOU5Uؚ7QR27`f03oO۱k}ݾܜ,Xz + bȏ=˓baLen>ZQ-*Z1X;%`\^ddE;o)> E8`5LG9)Ыnd4鱥2Nz%o6d`p]U +5rƀ0u1{{H@@/*{? 𧃕JMI^sɸǶH*&Sm>NB_0-2g5vV ?wZ88%lo jI =5~ 5X=hx7b)S=BtּdqLjvqu96&{4ҤQJ.x` !du(C IK%S_׎jQ.fAJ7rWzWJI(2aMm*x5Sȧ^4knumjl3s o-֤)!pOН-UmhA%`]WIg{hY~O/kJa`iJ^L>Mp!o)SPE@văΨ|=6(6< Ү_q"T= #, +6 탸CMx, +Sr4Y.ѴЧ૨mFG9scJNG0t_.8+9PA3sA5 %[3L<-vw8`qG ^3rMyM %^WSO+PEhNRU-׎oqAsg? +?"\l_Kp”Gʍ(mnDW7(dsCDf/݌[=bP̎֞D-:s7j;?uN6j'_$xէr+/^Q@$wu\9f_#, + m]lkFJ#ߢiIrZ}qf?U\5emw|& "j,lOQAu>ː6)f9$tl&jSlOp9(|So$}-5GlsS#Xj3Ԟ-܆ b +|tIz/lX3ˏ3ͮ]`=NWo i5&=6|?5a<,)4bCӦӔ,4',5b*Ai,hmA:39B׊+SX3'TUZ,TC4#, +)X.G'z.-az@ֿƞݲ"Bh@b1==7awxT6 x[݋Ġ{N:^HV f[І|HK%gJ\r$J)dTcΥOqiAS'9fJɊ?=c6;M)xyiw/WMUuJTklgB[k{ɞ.4Z:'LC*ȁZ=h$16;PEc$Nr||4{6F>%Tr HhαX -Ln9bo33u=spJ+ 8g@Yy*צ֤ t6nRSmU@WmMK@[BR@@'iTA`PKql;?ejhIZ)bWÊȐ- +N#%Jle"0MSL(x,*% XO\T/>#H+#|+.lAzLR~uz93`5b9 7(>[z@RG[%HF3`\y<^5{M6fk=m,?%(em+-z"4YG.Č +U ׎mɸq"cCV S a z`;}Z'Cl?ѿ"ָ ޼Y˟}u{)bO$lwܨ'Dk?vnF!;ǟ&dK02;FmQÝ5;QQ|E['[]reQ)˱v;0L~fƩ#/JJd-< A7"#Κ|}bҦ5=|c3) 5+rz5\~#WI9 S"5GlsC8 +hTR6gJXCnM̮ŞP듽r/3X3KBE^ + t1sIن[£5 Iv +$O˥Ǎ\3Z&]Ad˰wnu79/IņbTWW +Ĩ. ?Ҽȁ;O@XoE؜n^Qy%dQيyG1+ NL;ooꛨ[FfC[8W9hR ag '˳§bMRhl, R/L![%1V䕦ӄ\[E] [ߚMX:D1gT-X>.=wL[a7 z3f3S^:qH]GnJ2UAVP%z@mn;N%J߭C/vq{3ʯ&OcSu@^ mђF'w%GV# =D΂$VrNlcv}wut ƌ9}F3;ާE4g~\°O#5i c`e2DLp{A_^6r9`p(51&M e&\\f&\ 2>U4kMMj2Ql٤]U Vf-ghBZx/#՛m/q| Y1+{ +fyZh)(.(x6QS:*'$:(ꔥb Q,:G2AX1^ʏR)&~HYq WQߏO aKZS_Kg!!&ުu"Gu`aF|Lb6!TsԽ93f)?R%鐜⸱--9d DQWfdI!ZF{'uB@"'ą>>;8:kA*%)ŶKQώFyF#FS9cm{ F9(+j 4(p48-RԶ(G< 8HѱI]I1|G-K5ۗ|}6Ut2y8%M +(Te x:H]a\8T&( T"L;'JE$#Ux0#օmCzTq*3j0ё\>4˨"{C(:MBMs&m؁4#:bCUVU,Q(@ixNx OmU5"U+01FRMꎣW -m""*}doV/5#QI,$2%?=%=%JzJPJ]M`>zBY e$t4"ȡHƮP'GbN)u-*_Gau݊.<\r|}iÉc%"rjP@9YQzsʛbr<_{ usNZmf:kVI2O#jw'N"m <.le>]Xݓ9>sL„ 'w5,Ѿ2TZd\.t6Lv$&2ۖ:G_K_rLuw |gi}C.{1ӹW3F{8k:y9OleNz&8~{w8 ꣰q 7SlD>D#y,=a*1M$m׷|]综:O?r?|xͻ߼|5#/l +endstream endobj 14 0 obj <>stream +8;Z]"@?Q6c%#$]KIZ_P/XB]C0W3]EN8@I6tmd0fj["XSXe1maVEYDno;Kf]?8@ssL +TuX^&?fc:aqL=IKKsj4XYnu$Y/(.<@>dWI%)_7-<98H3"G/[%AWa[%^qU69)KTI(J +>]_Y`[*Qj?WWblGn<":9I&rR$D^&fb1I'`//'67j]%D7DeDZC0'C@Z/;j/2Lb6 +_cu:;1MZ;]D'=S)*RcnijF?dS,)HjtaEpI,cGcdjYSAtHJ"ZMZ;0Q#561OU8.X8[a +k(NHi&J.hN`dkh(TsPT`S<-.t!rQajS,/ZtmF3\E,@ohUd7TH38X7[uc%aJbIR5I) +2!,`7#U/7mp*kP(63t+sm*O-Fm)h*o]F[`s/?_aIiq^:M>72l0Die8n2W2o^(/Qe^ +,$GAnWbo0g'Ytq:M`OK,;<#A!&e>3&+\h7oBA%(/B]np0C_Cf]XB0=8]Q"Yp6sjOp +KTP*8f)RAD/AM~> +endstream endobj 15 0 obj [/Indexed/DeviceRGB 255 16 0 R] endobj 16 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 6 0 obj <> endobj 7 0 obj <> endobj 19 0 obj [/View/Design] endobj 20 0 obj <>>> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 5 0 obj <> endobj 21 0 obj <> endobj 22 0 obj <>stream +H|TmTg!dMjgf\ZnQԈUkEȧ+!$J"zȗaYDE* +buEni=9;a}s{܏ǜ0on?(teČdy9Qh*~B oÏ﴿K cN8[iBlb[!WK7w^>Iƫzz LUiuiZҠN>II)NW+4zN1$uRp&jx:3 J]2ux?8M*'pqQ4F=LMX.h糨FY@[%b1 >-6c6,ێbaX8]0S=?S#+@{"hH# @Sa/?ۋ ^Q3o {c"]FR 3P? g^HG94@R/ycxȽq}O)sUGRkbk`T+5Ơ .b̥X[9rﮎneLmrb,nW*L نWuheמּ9%)9:=&/ǩ+1[Ԗ|#kzMc;ͻCtB3]a˦vZSpt/ +s%lezP;m*A `Q' +$d0ҼBM E* ۆ iJt׃w6*Bjga7A=]4 P2}~0]Ŝ 4uuK| +- F ddg&Zb9"9E1盎0T*G]J7`xSPHǃ*:e)=QV%y!'KadEY#3">O>W3[4B 'U'XROoan"/ӽ &+,-,61r8nwZYKUu6&NtUzFYg7^sD2h-,͌/jr0Yњ({?,8ݚޡcSr(7G&E5F>+ݯNj>M|9' 5暺'Fc>FV3q]dE>fMf<;9)ء(2=]mW AXtXyyG{b o UEdүpa2OEl7Z= +,{XHlGb|uW?tό:n:;9kC4TX 4fs`}DRX"+x=a.ŧKrvrcv(cl\jеü?;26i(.X1kg5zK~(!-GTO` x#Q(:B&GmOR-ԓOdlvVQZuYp9溸"-,Too=Ž2ؘt6gfY +lvUJ"NŝKgs/NVcbSA>4B#w`%Rr_ +aGa-GPx xUCW -DejE-/'!|bob{ٺ&">܁||Vk@{V S붠u-W%7 +endstream endobj 13 0 obj <> endobj 12 0 obj [/ICCBased 23 0 R] endobj 23 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km +endstream endobj 11 0 obj <> endobj 24 0 obj <> endobj 25 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 17.0 +%%AI8_CreatorVersion: 23.0.0 +%%For: (Brian Ebeling) () +%%Title: (Discord.Ne Logo.ai) +%%CreationDate: 12/23/2018 4:14 AM +%%Canvassize: 16383 +%%BoundingBox: -2336 -3284 7564 1593 +%%HiResBoundingBox: -2335.22935779817 -3283.60582212756 7563.8555757689 1592.16169635493 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 13.0 +%AI12_BuildNumber: 530 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: -2335.22935779817 -3256.62385321101 2664.77064220183 1743.37614678899 +%AI3_TemplateBox: 250.5 -250.5 250.5 -250.5 +%AI3_TileBox: -120.867144663403 -1165.35896063288 450.332867543628 -347.678967957101 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 2 +%AI9_OpenToView: -2342.1743119266 1517.80733944954 0.378472222222222 3136 1318 18 0 0 46 117 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 77 +%%PageOrigin:-150 -550 +%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 26 0 obj <>stream +%%BoundingBox: -2336 -3284 7564 1593 +%%HiResBoundingBox: -2335.22935779817 -3283.60582212756 7563.8555757689 1592.16169635493 +%AI7_Thumbnail: 128 64 8 +%%BeginData: 4082 Hex Bytes +%0000330000660000990000CC0033000033330033660033990033CC0033FF +%0066000066330066660066990066CC0066FF009900009933009966009999 +%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 +%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 +%3333663333993333CC3333FF3366003366333366663366993366CC3366FF +%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 +%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 +%6600666600996600CC6600FF6633006633336633666633996633CC6633FF +%6666006666336666666666996666CC6666FF669900669933669966669999 +%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 +%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF +%9933009933339933669933999933CC9933FF996600996633996666996699 +%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 +%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF +%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 +%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 +%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF +%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC +%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 +%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 +%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 +%000011111111220000002200000022222222440000004400000044444444 +%550000005500000055555555770000007700000077777777880000008800 +%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB +%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF +%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF +%524C45FD0AFF7746A2525227525252275227FFFFFFA87D52A87D5252A87D +%A87DA852FD057DA8FD5EFF4646777D5252527D52522752A8FFFFA852277D +%7D525252A87D7D52FD067DA8FD5DFFA8A277A8A2A2A8FD7EFFA8FD71FFCB +%A8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CB +%A8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CB +%A8CBA8FD40FFFD407177FD3FFFFD3F716B9CFD3FFFFD407177FD3FFFFD40 +%71A2FD3FFFFD407177FD3FFFFD40719CFD3FFFFD0A7146FD277146FD0D71 +%77FD11FFA8FFA8FFA8FD29FFFD0A719C71717196FD13714C46FD0D71779C +%FD0D719CFD0EFF7D2727525252275227A8FFFFFFFD07A87DA8FD1BFFFD08 +%716B9CA2A27121F821212721212127FD0B7121527DF8FD0B7177CB9CCBFD +%0C7177FD0EFFA85252527D7D522752A8FFFFFF52FD0527F82727A8FD1AFF +%FD0971A2CBA2772127214C2127F82746FD0A71962177A24CFD0A716BA2A8 +%A2A29CFD0B719CFD1BFFFD07A87DA8FD1BFFFD087146A2A2A271A29CFD05 +%7146FD0C7127212721FD0B7177FFA8CBFD0C7177FD3FFFFD0C719C779CFD +%057196FD0E714CFD0C719C779CFD0D71A2FD3FFFFD30716BFD0F7177FD3F +%FFFD40719CFD3FFFFD407177FD3FFFFD40719CFD3FFFFD407177FD3FFFFD +%3F716B9CFD3FFFFD407177FD40FFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFF +%CBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFF +%CBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFA8FDFCFFFDFCFFFDFCFFFD48FFCB +%779C77FD4FFFA8527D7DA8FD047DA8FFFFFFA8FD1FFF7D464646FD0AFFCB +%A9FFA8FFA8FFA8FFA8FD0DFFA87777A9FD0CFF77A2A2FD1BFF7D27F82752 +%52F82727A8FFFFFF7D5252527D52275252A8FFCBFD14FFA246714CFD09FF +%A246717D27F8272752F8F82752FD0BFFA200214CFD0BFF7E47464DA8FD1A +%FFFD07A8527DA8FFFFFF7DFD0552275252A277A8FD08FF52FD087D52527D +%7D784D77FD09FF77474DA852527DA87D7D527D7DFD0BFF77222177FD0BFF +%A246714DFD22FFA9A8FD18FF52F827F82752522727F85227A8A2A8A8FD09 +%FFA87DA8A87EA2FD12FFA277777EFD0BFF7E774DA2FD3CFF527D52FD057D +%5252527DA8A27DA9FDFCFFFDFCFFFDFCFFFD8CFFFD417DFD3FFFFD40F827 +%FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD40F827FD3FFFFD40F852FD3F +%FFFD40F827FD3FFFFD40F852FD3CFF77A2A2FD40F827FD0EFFCBA2A8A8CB +%A8A8A2A8CBFD23FF7E46464DFD0AF821FD17F8272121FD0BF8002127FD0D +%F852FD0EFFA24D4C7777774C774DA9FFFFFFA877A27EA27E787EA2FD17FF +%A246774DFD09F821464C27A87D7D527D7DA87D52FD0BF82177A222FD0CF8 +%71714DFD0CF827FD0EFFCBA2A8A8CBA8A8A2A8FD04FF7D774D7777774C77 +%77FD17FF7D774C77FD09F8217246277DA87D7D52A8A8A852FD0BF821A2A8 +%4CFD0BF827717146FD0CF852FD22FFCBFD19FFA8A8A8FD09F8212121F828 +%21FD12F8214C7121FD0BF8004C4622FD0CF827FD3BFFA2A277A8FD0CF821 +%FD17F827FD0BF827FD0FF852FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD +%40F827FD3FFFFD40F852FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD41A8 +%FD3EFFFF +%%EndData + +endstream endobj 27 0 obj <>stream +%AI12_CompressedDataxk%u> HQu=B`zhXsQ.=jvqb ϯ?yWd%ʒ1 MqgdFFu]޵޿~]zÏrwˇ/޼;.?{Ѫ;ջo~xW?<7Vxw._=^ܿv⋘^%Cqȇ˟r;jꉋW߾\=~S󡕚 __٭"ƑJkE]J1D{*E/j/B uT|7o?ˇ<{w?J͛ܿ3wo?ؽ?:U?|{ës˻opICqE_^}|?|6,%8k'/Kx^^~]ZpܿOo5^R a Xkhm92fYNՐk}Y/M \,E??֛/|C\.zm!ZS^] \G].{0Vr[#sFiYq~|Ƿ/}Oʜ=~û_}U]?y|ƞ8Vq^pz`Zv}{ %?/QM6ooC6m94r%Z-nR-@?ڒ ڼplo}kKZc̦O߽(R9W|W/~_v<ËL-^7> W]_xFvz`?y?գ^?/~x5xݻ/ů=B/_|YWyo_X~y_o_fezKej[;ɏ_|i߽ˏ^|VBzk^^yso5v=a ˏoԢ)_3o~l|.'y{}%l‹[=V[-'n}w}/{n?|BOq{;/ng]ݗ|oFs#͏_I Ons=.&oSPҚ3Z^?{ +ox? ƒJMZ5[{|[ūxc?^~yO^k'O? 3~fdS9gso +yxI~goڨS?Q=Vɍ{iܷ-e?(_mGvyl9_׾Ov/W>7[ϔoo:-SO~7~|7kNo~ioTm޽[7<J?o۞zMV-&3Ͻc_g^?- a'_,T7߾vǵͷ_|}Vz/UNsG?'8\=o!GΟ WZCKהօԲ>UWc1klc*q:Eջ_'tmٛg/Z`:]0^~ſ/;n{vusĴ77`_/l5urk[ƻx)no+oS_ݷZ(ktm.K9gȗ*_|W\JQ.˵ݔ?>6X:e7޵ضJk.Ui}$ko}~կmXҰv2r\;O_?M,ewm7wVr?k^>X7wwkϵ]ߥ ^>޾}M?N?5nJ+aM;k;Ϯ\]ݭ]]]_]]]^~ծUWfrZ..oחW_zY.e.q7w죮mP6Ն9ۀl +X3lBmjT"&J vkqm&SIUM`VYhKv/->!0On]eY;6ZXCѽ.W&l$%x_鶖CNWiؚZY{\gыYlp Rj6hw6Ն۰__4lj$"e'K)~S29urvzVvq{H홑b#UFS|nNF*|#jbUxXlOFd櫴Fz[sϿiϩ2&͹ݬoO s_`',;Z*}7Ms/gN1}jeNYUX# M|ѿ6=n+]LHjCQf&.ek2}o}4]~2lVaʠ5v얱jmuFKYȚPV0d76ʆf,/jvF\:V_hvWvRZ,@-Na>''ZPv0':JE(=jJN"n %/i@ϫVOUʳRGJ7psjB0U#Se:D%|jܙ (j Ԕa}l{k~ŸC!ߝemܭ}mϙKn&@Z I1h65, X?mlO]5ZQ6hpTvx[ܬYn)ez.4Wx?i˶ MzmJ] wgS3)L6olNnNw'm+h;咝ƚR)@bSX^WP;owߖ\5UMriSGkV1VA89O>il O]yٕ7iz#MH{3)8~ogkݻӲ]kXOG똨E.֝)qT?)2~+r{5To_=TW8 {%*̿*\T .mB̕μnS wsd,;g]:ێr3I{W6?!yJukOI6't\g_]Q\ +ih_ma?>^V!7a|\T FYJ]pٸb6iȹ.g\O2|O￳vwO?ëϹamva_+7_w݇Wo?bvڠ(ٗ?W֑7zşT>b>U;Vlى{$=\"h;'ŇO<=tWr@vaJ Q8VJfmK9P`[35{l@*P/ZA,K/U_*mP\X]ތB7s+Q)і56b_L_;\.l_bfpal"ֳI[󆵢&k^ ĚVơ 7J(5qta>* {L}{NaZZ5ė[)h{((yAO$+ cʶ^=$Vm!LE51aÈ6}vYg)utO"&2SixaLj\vóiܕmzr|ܐ4\ca\pjl~MG?.+YikU4Rlr] ћ-ٚZMx k6Mig$6&lTǗa=ksgY ǒ֋f WAK|l_[ ngY]8kucmS EHqޙ| +U~ae.? +iv .oKWP*b3]+26qGVwsm;*%fY7l*UQ!Hdp,L !*W *"gtNUm!5¤oKYԔ}}Ul*7k¶TPoȋ-:c)kMoH4;:`-G 4^=B<*z +@TpF3 +kǶ_-ֳn#2-bQE/wߚ.:{pц"F mZ&M` ޮspݚ޽yt;YrG% 5ZF)`~z# f`glus4Xm9wR>#_,a} kŨ\J^ni[}oFR[NlR65=EJ6= &~Q'<ޙ۠f|CF6le! !YtK@mMRs3*jz +L=%0x!Φű:۶"JSXV`m[ g:21٧-(6*SL[LhIʔeC˩܎ 6fNR9f]ssU+.J蒃Xk}&z o6"6EWg:hlw<<ΙFmEYB]G=7 Kh}p˶bGN:K'6ߨW6ћ T ?JYf_aw:{M- yZκXGPw[+9Ukީd^t{ZOڅ6ʔBp^I6A)P(]8mcSeVW- r)ʹl^V9)&5OD e}SSJ*bQ߭ݟ;bT04z3SSJmjL1mooymFUC6{-VMt܏Yk# CRl;UĎޞH\mhmʼ;s(귉H^зW' N,rЊ1fh` w]_7yia6rk"{픋6֯FXDg=Z nIDzj=-̀ǔن*_/!;û!CYf +dbSch@{>̰q2̛F"G{2) yH TPH Uf5f̹Tu,v->OΪ\nWMAaVn$Ǿdrq|xlv.R}:-sMbӾ[m]ۆ`f-Ⲉ0Qh_2Tts(%dhUm"m+dU +YIēȦO +Xil碫D\B UVѰ)\^J)J]N%<ZZ0"߷%\-Ȭ.p"}f}L݈PLx)>/,WXFvdlwIOiq}> 9ա[uEek˂cɮsiuLVCEұyiLޥQXV`U*}MNG,*4b(' |IփE_9]0FzLf)U +~U#km)en-hLf."&GؠRhoR# #d,T6?E.nXPo % Z(-5~oht + G6qywtqǝlЯcOiG{MNb7m&|'RW ܫ&+)7D,3& uLQ*0 Y`)+wr!<{.Kd/R6F8rtH#v>B{`(ըzLK&Pݠ;Y 9ҹ ,O$t5{mq#F@*59lSڌ=L̴Ew]ADV7.CsA?&ۆ,CU?d_ +=7EY=0"Qr'oӶ1)G;JoQPN:ٻ{;l@uYu.p7*ΠW!*UdHd;^˜d#ͻR^#)p֓O4iJ MDei{pk&kSNjKJ>_QC^CMNT^[=2Er ؙ8d10D?a_E]D0l2%\(Y%'Bԑ[2ZpǶ b\j Q{exowXTa؊dWMx hb-fߖ671R!rv2i֩R`h0 $l?=Az=VD5ڽR!jUlx=]C񲶚⺍aKCd0):>HlhS|~aBPd] #bG0ynHrAq:S0]|Ub$ӀC Ҥ,ӓ{jcK4#rڞf#)bQA9rXR?)mce=TW-cV\B_mv +νt"o*U]UtYM)/flyd]3ukNjg +^?ц;w,8:ꊕg cHht/pngm'9p:Q xG]M]'g~axkIcTmqtUd7$pBH_%m;?zM$B nxS 7.U[lB@%;ޓsUyew&a#3۱Mi3%-̜x| 9L\Yܦh_8 N'Y!q:I,\GW(PBBT5-kt^c4Wښ&\pYa* e:5 HxZeŕ';LJ,nEZcpc񭬶T;]V&L45$j1X1a)GyDxn m 3QF +RauS2Sz`XAAߪxK2OWuރ"sG61V>jEwZ461B ĺaՙuv,5+m8%3FZג+b)n̴Ju&Jkt dYAծp[ufB|{Ln =FTt}T ' ;:rqD]][}x{$CZwpH4[,LE>EjՃ +xSډ<*Yc{U:ϧ ]4.1&=JQ +a \vݖJ`knfJwl:ܦ*CQCY8RUw`IA/(:!= \1)d#R@/(Hqp>q1pFYjXc5 ڶyg[ I( 6m!\6JmyjjZvXi@D̡̟C;"-vx6w\7t9%PZPݷՏr G˛؛mfEvbACl_ Os;[/XOxLqhfZ'&6Rm\$s4Ӣ4rF2CbS/K;`?NrʄFh 8=iL;̢0LYcm@#p4EjU2$8ЕZvo 8PfT&yU70 +̱;^R";&}ef+ =+q`Jt}e~E=Xd_("W@^o>y +3`T$239. g:<[ V;i2}V ok!ر)A0 t6~`IbQ^ (U0ȱ5)d>䊎 'F 7f߭kl0)999汈爱trst؃ Lr +>-(qlRR{@P㈇PP !ٌ)(cS9$6ęE)1]l}Jݍ05;[hQ"(89IOLO"Ɍ^`2M!fK_ 48H cMA>Ȼ8ͺJIR )G 0\$qďP¬ICG(b' +-1nUUn3rt^.mVcF?}&i6Mxftz +-əHJnD6@DS0 kNsܧٚs*?A(PoD~0uH,N0ȟ +RTTisBNx R  !}/:"{וB835y>f Zc&DҲi ]5ň̒MH*rzꙔ+m H@؀F/iBS̐Y!t=!eOo +8,pQ +<I5e4y+ ,M W,ɓט('ՍM#OχkI,/iY$r }λ +rDeNVR z\ty2/:s[pxǾlE^ᨧ]uU#@GS x0lի!6Lz'BwCUD˵n[ĦenP +K:M7qy&7u<;'tv7Bcp{(@D)8 +o4\j72_+t\n} ]蓱`'J.B;5>> +\ngJcׇ. W=(PwɈX)J K +_mP*р=r5+( &N0L9, 4d\e铅1l~thLu-7NPE"*VP>Lmבv囄uA|Y,uY}yGdU}7CXOnԩ,ClgqX-ָE+iپn(OFD_Avb1AĠ$Gqi0U +3Z) k~6q.!F,H38"W[Y^!v쩔t0Z[ٮ@ JCΎb +OAn`+z +?ZI ~]2Y\>[m (t> \a*Y4k=ণ+n NQ_7l]O~ IOp`,zT(͔] J@%{ݼ9QJȱ5JJW%V$X'Bئ؁`&x@d*c<4-0⻔ao +`ڣ^"AnwLyQG% +f͢J[^]XTMfqUE~;R ^#.uYȒ<yӄK# C6&DYT Svk_3$zd%OZ#/ZjFГ'!Y"hBscS*p}uQa:OaRWA`kT޷բ>;jL-8W]$R؍i= i9wfC^%>R_v׫!즒S2jNlrI)af%aA}#W-D/P&R;$Sd.-nph 5cpP68*D-2> +g_93JXaﲥ EǺ$(JPf)R :V `/2 DȢIaĶ9DU)% oUB6Cy7sR` d֚%I[q֖\$,"&?2i3fl q&Nʄr84EKyx! &sW/p%1 -ꔋM@ Jrfp +W +[Z73JΪOWQГ6ܝN"R&'dXTrAE3x|8,M:E Zb楒 6,$HQ$bi;w=?"4Ë_|xW\]]|?~t7LB^7)XHS\]i۔o#.@|Ks;!S(E#Hy d(Qn{ ;L0]dWa +7Lclfm%mgO=8^Q((S$GaseTO(U 1%A.ALtbUma20MsO@d\wR`(P"Oa +Yۤ(N iBek8T}cRx2z;AȮ٧x =V.osU=1kG-uwzttO@'k3?>NΙM'BϘ*V2Pdr%z6֕{H +bT4#UGd| ie|p+RɨǨdr! EGIw旵>bdpQ?Y"H_ 487Ąǂiҩ0vV,J|E`'%dTRM_IPlU`i^JT-}PZ{P#>*eoz64=6D6cFK ɽWuOq*N 锺Ht +8%-UF (1)!Ze/cŒ0{N21HL_>@Jw= +Lȑ%/DKF䫱*+@fO7ټA_e=B6$%P0$+9V5 #R):DO'A,!$LE4NS@.'NeB(ߦw124OV_O-x`ͷ1HFTyfrZMRnvŧ8# +[B3 +^t-yyÁ/%F-sPˊI%\h*O0tʌ@ƘlLBBɶer Ɔ\I{vS!EEu2/2A ,f$bEOL&I=Jnp@IΌӶH빛qo^ESxB%ǥ?"HRxɺAuGl:hbDĶĬvW)ԱJI&`!rMՕ#4 B{QA1ϒ A##{q[tq7&ٙ4U){J=Yw!ݦ̬;ixl@)WyKMm`{zWCK؜"@AoYH1E$eM̷Jdh 7K%BaGTLDLM qʀ&fApD{(BjI\KHT&+z>(Ψ;fKJo'aaK:W +s2 +RD +ɔ+9VB8%ON,sReyːL@6Us# .y6J´t*JK n\p(g+n#UkA.zodI9N!+ krڣH;<ðMdHĐ{r [4!z8i@O'`N6IW Xd"l2wxTBf{ΔXGҊ/ǖzmTN< ;W?zk6=t7&~S˟ʣ0n) ꛑ', PZB @{D` 4eHGnT![Җ<(At2VXhϹ4P +DA(h[2⩚:$>Ϭ) xݣ,cRq + +)XxTqĢ}J޺ h [#B$li@a(y +RY\j%J;Wa;6K +br&Dp"6C !97Υ4o`P2PigLi6.y-DY&]OI 1K> h7׎04J-m!Q׻7`f +f$OZ<{SȮv:TT 7P0o70* p@ j< .SNE~%*{BCzҚ,J,PR^#Vg>F+Pj;KMj!%^ f++_WP׷+8fW'T#TgVܦa礊{^L7c@z6G.ޥbéC,EW?- +<<ɺ83L(k9fT*c'Ei +^fF[f|D8[o C_\6M+(ƳnUԴ\&aW|'շph.J+J$&T]$[Jj +.)8mp+<,z)184O*4 G@mI``tOM/`~nœ>ymh) >͆=k,+㿂+׺zKQqU* >v _bM yLDNg 1,S@rq+)V@8yD +lݚ7Y2]<9D۠W6%dIU!-XVH!8:aє4O,.RsL +@@9>5F֮ه]r4CS-up>L^@+q%IA(\OGIy 3LC.E9DD[=>={_3 n'CtGlAMT@lSROF6 +~Z'`E`(% |B$7$ص`j`ݡP;+CG +Я^HDY%u,xlx>6ƹ&΃<8L̈́묇W`ӤJu!q3}Bpo-{iT_)YYP+x4z!{F`͕ޟJNɃ59<#yP-iЅ$6 zf[]'ᶯ6>뇨VP☙74+*!ISVP4Gv$zBIl09\ gN L]3{ZaZ=Y]:'\y-kUn!QP ׅ 5(aa!=OaW|@} ASH*4vgf>s R@drHs]LH1Wo +eZ:~V b%y-$iddVſR)rBXY_&kGSE1l|!a/ <}umJ}N$gI01߂{͜VǼ + EjOx_ʅzL*LЄF"Hls$Bw8&@tB!TԜ +]dVJ׊M$i3sq!/|2(PF@nᳱئ9H[L0Ӝ,(ׇr.%T3I>rV*%ɼ*:4/( 6qaKxo#NDQ9LG(U1%l6-RhO/P>դ [bLj"2]4#=эsx5g{Rg&ٖ3D ç E`A$&9Y s2=o].=O?>YY8z||s?/x}K{bV?o7_޾zo=~?<?O mZQ?䳱J e4A*kFm )Aϙkg +$ەpR\ @a`DU9rJh?m<%̚'A'XW-KMp5wQ/4ҧ"nHȟ;UBNߴIb-bTC⾢ք1I(G> l,ٱ0B^2qq<@#Gn;) ;>N@iA8MC&@!mN#AVk9_`{mƆ I}QK$*\>s X<3r񓰛$Ƕŭt8l&Ξ+SYfs³1sp3WJ*OhkQIϐcHFMOKԬ㜇\gӶ{޳hՕwlj})tm%g{e][\O[F-{r|2➅8~o;x22\i=+=RX K85YmLt@;t1De8J^&Lٕ4l ++-:avlE37$ﰣw=NJ`ZΑ2*ؼ:l3nGg5,愲,3cVJvYYOT+Uk3HLNZGo J zAP$;47c?h_ZI,OeCie] ğGGڌF +|GNBDz)$3l;np6JpPNCˏ) \yySy ysgVADe9HBRy_M~W-'ukqӖN~ɧ^6*n~]feo,^Tvi+]a+Q&B+@!cs6uc6:fQ6b]$?rkǐmŦرaϼr`AaZ ~b3^sqڧ<\<*yOjJW.kbX+53vh$& ꙕZĩ{>vv+5ڞfqP[cӐjq2MrܘE!Z&cG,BGViIeI z#StJ+;h/ѽ{/_M] c;2yLHO/Z99 +}|`'3):#3)}1{fZ;5E'͋13gV>fђ'PYЎd΍|wG{e@(K(Ƀ7^,NY79!{OȓQg2Hr%wŕyMfC +k3[PZ‘x}w^\iIBr;R$y2Nn(afG]Wb`JYR/ +xQz,w%)Yċg%& b"V2b0~ObJSG8lF{F4,Fz/l + 0 /BA8#LJpYӫC$.28=]3ۄ#K2Vv`fIdO1|R9p])Aa2\_D Η]l|' (C{_9 +pLԏG^KW{`'!cڎwRQP^aPLʻKx_NywӴlti\YvA }'H'.YA;:)uwʤ|َ񰈭S2JONjJ0访w ɠ[8:8{%Luo?G+"N(g +Os +N^{BD UR582\6ܤq{:WwcnQ|BtN8=RaT8CH_r()cWB?A(8:&ϘW)D,猹1; 5g\w;\J,zRlPʬ^'rgpX3WVM1Yo?eF=˙KN41|)CuRc}JZ952V]Lve:ge4\\ CDhsG jƄZaoOj2Ȏ=OxƎU%wDmQcA;Zֳ'!;s }V@]J<厉.F2s;O)jU*xQsZeR<&H{Z>3}EP.)H+W/9$ V;q +MW-dkR+ŎV?[)FV +[hϣ} 6V;cHI-bSnZcpD_iQa#p ---c$g,gEpN5K&NfkvJ,[{=)lmo]omN׆Wl'D&V)EvmRv GfXK9v̰xv33<)zirZ;9_9ez3ڗ 3g7" esG=X\chX[ی\9[0je>2w1+~ߙEJ|¾sŽt-$4IWq'y٪19Vkt++*ӄ܍ZEGU#mDOj;MյW +gȩŒPju ؁`$+%*8\ X +R!g$>9 Tn< Qeқ[r59e3UcȒ2IL`e. SRaLJ><JNxI!R8HD0,Ҟ$++•pJtopoa0g7 7|dG#D}D%[C;ОSOx +L"t[uc0nOy@kbZs}VO,/GoFt‰6=‘R׽ǍSOODN픜G4ȁOF)D$&x}:I30ƞs|JJLF%.ub#f%Tۥ'm#b' ztyǙ%\ZOГhD鈈qFUQF,jOԠWBO%EnFIz#Ƀ-{BOSzS*UwJwʅ=Q;FOY]c¥?J|+~"2c1yϞG,B+-iečTGyʏEUD݈+L.<ÞLZpZ;sҤRv +1VE>{G۩ Vr.m bx2LN侧 +5Q 卶Z`[y;w$Jz-ĄheDSd7;2JP09'!){Ir# [i& 6nIX(%ʑJr)mc|ӌ#,1" 3$ؓCOq3\7*H 1@ďD\ \y" +zw%ՑHH"&#.6 Jrq8WF4aH(BvDcj+?2Ɯ2Qmec$ JHS5n\͏ԕi6E9ižI1-^ԕ`/>x#UE+9Fzš%ƤjIJfM&hܢpr7Ey ,Y}ϧHX^6BE71m*PT*m3nˁqɬH`=z#VTkR=G۲1+ +;__Ƭ9BS+Jj\{ި +JtJI)h3,iz[q8QT^S"E?ş8&wƖH}'ITv7 25C1-G]&g (ϑ\34gZ9F.Mx{{svjD}xrɎQ*TDG{*EWGD;FDĊJ7\Ou9ڨ#dQk?`ӝWjD 4 +I?ϮD6jDOO\R6nDAJ@ +hǍ(_'\ktsFz 2oԈRb3"J\VD}X#"=.53gD7#5Oa/i#BT<f P{- "V"Dh6z y#BԹqDRuews? =U +eLJleeDlWBCb'BmIBQj"{F>6؈ #BT:U@⇶!:H˕<;RQ"*.1dD\w͕Qzڎс" 5OtChaV4P8#Ro$=o4AI `Q@@AT$lGD=>8WD5@xwNgA ߄R,L:D8*l5lg;ɔH+JH VN*(]QcyҲJ1%]?%J|Z$ YVuMW MFDA6|.tqjGQpQNBPr'V#*@`GnDE ,r8F'ٕ$W,i9ܞ$>xaOII|9IWv$cH8M$=I-8Yw, +R# yE;D北۔#cL`H9p#* GNe,yʍxVxƍ(MA KB:q#j;_6mӔ{d%T@S^BGM^Cw !J,8]8PVz>Et9.FC gG +.4]v_9M%&|B $Dz%6%er|ƘkO:-gg\2##e\x;A} e񛋠)ݥy s@P]<hU j)C y_N5H(q\^5H׻$pͯ2{q(ޯ_7zy-mό G7}j:Q#oMRRZ20j~sCcg|JZ6;u߭2ғKG=#چETAZe>?y7i>on}|ލ#6nonIͭHjǼ iX1l,U:#Buha=i/9Zw6@: hoHcakm~V랲*m +1Rc + ?-ǣIإQ|]ڕ\ 3}Xl6mZBb ИMBl{~W3i>f4{z,͎ߌ }vuӶFe1ɨne<}1)_Mt$݈%2uVi;&o6<.c-\Lz`s):d!Qwxca8|%MXN=MA_"y,c0LC0`w4ެ Sz4/h1EY1B5` 5Q$$iſ5.P,Aw.i)L/5+chb(>[Z#4B7a-ȦkjEl77-x$c DnYf +iuF﫝X l<\,J N_߿a=|\v$=>V;Rs=T/VGEj8oUgURZKQ櫣L9CH7 ~α?Cy)itK~ܓ77%"O7C{NBTG s?#!YVi$7L6Tsz|(%ONGvʮ?H=GTa7`7K#Rfj4u]ep?X~0."ak"֮W4cͦ' ?݉8Ô/"{"+BjX)\DU%yC3Cx._ghP― + +q`.מ%Kf'$q_<]s*ҀRc;𗨳0iL]fXObT48 RJP: u/ +=%vz:0x/J2:,txx?ֵ Zn[P)Z@۶$Ytu`G-u8* fd6H7J.m`R %S$Bv(iJc "~ Iv7?hKLjƥqO_Y#D}4۪)Wzi`biu9DRZ擸R`/ tm^O75+b{c}*=VuW9(s-J2>RtF)l#mE7}t`VW.t D|D ]2PKj,i;.A=:=dJ]mneNb}1ndlʉmq4 ,D?as\Co룒QD6\%EId 4֋Vt#OiŠ4u"9 !-xEu c^3/nK-`?91v ~MSy 胟`qa,=pqZLјr1Y=LHAVK@:D1߶֫ +hw -eX^iSϡ#f/+O3liBǓJ&8C؂O؄*{C0i,g;:qɺWǯu_VЃIF$CcSMhՓt+ȩi3%dk4z7>V&C'b?dd҅Ҷj{pqF0Aњqb1DWyGR nI%n6ИG{ o 2OZ"tWKQi ;cjrFN9o>ӐxU}a䪍 H/t"A*ǜQSIk4 +8Fm$a 0[3x@p5xO 閍~#]ԁ[DH=eЌZ9h ՛߼<\~D![G5rAk \]<т.BӋd=c=&9|BVXbH}r-Y{SZeW(ƈitX*#Hx&{TLk*y$r0[NJt#x:]VeF!H)ͥkU$zXѥDt]/>ACDIﱬ|XmW`|z+^'MZ (mpcs5Z jTTOƞdPV=u9h\N5An?Mim_ZI/3>'=7Hd:K}A 65ԇgIvC!/p{R|xH '̻??F T}xs cЂB% ZIQRTb"iVW>amD4b"SydP#Bj-#D#|:#QqxaNq^%h%5viN:6pLxZdDWM_J; ,-|G+Iו"c?'V}vV +3BKr9 Oe)Qoo4 +Rg0,l\@Z1=ƃ6Ϋ>ư5!g٫QL2+Iugf>ߕI}Ri<ͱd>8S(7dm0qRU'Z?ZRCQ|P_$^V<3:YH1OS,ZêHjFȁ<=FqybR#~Ŕ1g8=緜{~|)o T*r3&ޒ1?_ihT7F40"Ig,zA!,60-uuǘS6>HRH@"rl]c? +&Sue?μX`AQ-a$e4b.pێdx^|}pgS5ڑg\$ܦ6Fן>d*e 8>[PFY,xÆ ;%z+h2fq@ŭ3m\Ņ0;xQe 0ԅD7Hm}]l\Ą1ke)-Yސ^h2#Yot;/V7:Owuzdk n,XV~?Gdafwl+8=|f$T Fzk*/Q\*sp}U%eI-%{sw$Abi Jq*{"n"ECsI{Œj\JxGT킱苊r=2;0T+2ԙ9#fx8-Qc.|8TnpKߴd,3\'_rYD>qVk +Vv"f}NtiƑM%ޙT CXؐ hxY+:7ܨƄ*tbP Ml=k%1_*IץD:q:ҕ3b#*jC>p9hG2;ňBrtOX6&46X&rm!٩k^FtB0& v}s(@źG[hLB;?R c^ay34 ]( e{Ut &FP՘{^pY@҉E?~9Kbfw\A*YPFCls&Yr Ya-g3Jf&HODY/4o$Th9YF,mڞ F! ĒI2 o F^AaǬG$*?(mD4XkҲWabXY#cY/F-)bo{rz#*X,|4*e#>+Ty%rRC*-p>WX%{@@N۴ǓM)ˊ )!$Iɸ `OƿkvH#n$W-NFȩڛL|ރJ$xk}@ݎWG +:;Ffub)⇪Ey<ͬYؿs}6?0]$)/gnv:QlK eu2^t(K nȂWX;"Cv 8oSyâG'{]rz"IZ݋ZEH긫AJrUPheJˆG]cRH/y -K"- 8҃=G4lT@BhED'kǒ:T# Bxؒ {V1S.6 e^ ;cX_vY.L8R:1Gxش{dhٶ,S;Ǵ +f(NO#L [;MI_e],žL=A=ATF O땘RaēGd|F42|oHEBD5R/F2:X+8miwҿpD,йuiCX [eMisc;Uׇ+JzJcL=zZ0V5ci] AW 7aÀ5v:~cjI*ȽY *2pܶ i9p (o47t1dncRnvHY[͵u9ӵPLdZRSVtSc}0<*h;機VN|hC^UN6U Ąt?J`b5BJ%4!6Oz&%FB+,ʭv}bQX;z)S>=Xղy{yZ#+]8W &Uvbu@EsV׬,1 VW-2 N!xmp@ !Q/p3h~iճIJw媂.oJdzs吐+yAw gKj[ i\YI$(8Z촟aiԈFI~bFJڄ9P ^>>_NV#PqeIhׁꃕ:u8GA2++޳e*-}~F;ExZ:jyMA>bX1O E!2W{t9N=s0ȁʼnz ID=Yl ڮ%Q0H +Z> <C #n*K$yuA`jm(QwI@TO㵵ǂ[BC[7,<&1 y&#hhqkyi(3ղIRE&&!f["/_~G!C# ߤ6J3?nlF `Vm-+xLlW e*LdAm6* +j!5e0nPtQ]v2|*%;;*̥I#tgbSB/cA\b^w鋕qᬐ?^Ag61lyq2+_;Aatߢ}%q/'Fb^Fc 0xN1Rr nj@K%9aδ8osHN +2P.}LL@2hS ] K~ՄXž}I +0l5 )oEYc`D1|Rx 2D0ψElPzCAHkR +` xZB:V9y ݗK$qqQ4F$ oD̗Ϻ^T#tk_CԳp +}#X7= +8@M7Xr.q٩j&=_Pʰmܖ4Vٚxܫ\BR/=r/BakT)}*羦HbwqӀvSP-G>0NwlU.F-!ԲDJrMٲWfmS&&9CNN5;U-bw Hqbj9rB/1fCL78]FVqU.FyU>o9$dz/1,~G©pEg:MEJ\lZ;teЇ-\C-;-ou-ь0(Ya!t[.f|\Lu;_ :<&!z4SH0}E:NĞ _#Y=Q7% ѭZa!>fJ1F,`c)j09rǰ1~m>W3S{~ڠs8P^ %JḨ)Etïo +XR(e\%X +M b&qyL\%C0Jx<} Pe2cU4b:@;G:Fǵo&kЫ` +py9CC׏EБWv}ϒHOUyUKbG;%M %.C!aF.<>S;fsƫ\beu"턑r> x&.'9Lt( )Fh%\Pmz@ܪc\I+E5eZHPh+4T~$? 2F|Eē%rI_$<:`դ!IZt̬im_42چ] #sVT&-tĴ":as>VThSAdSe5]}ucZِж}gXabR:ZO"h߹rD0Փ򈑂߻&y4G0Ū Ww{+EQ՜S̈́%N|gOysv8֑3$1n& aQ[SܬA vH>h4ϕJU{ԥ |qK ɝzDK2<^*xvlf &ÿ|QPEbҢCuSAVF ?r-&͗|)`5=i5>jlv >. }2G6o.A}羰 YZ] +Ʉ)a6{h'ըK2GT\pSw$pw(FmG@rJ3l%X~"H ŕ1 lU9 BHf96GMb$Uȱ1 MĥFшU}Ed + c=,oa{ |U#Et't71ƪg^GĀDN|<rZ4v ;[Tw5@[JTM\2>K/e^kT OI1(7DEFYW\y2(Ir"s+)#-B}ďxOנL k~D?(FxdL{1z?6ot'_'ഴhaAftc[Kr{pDl6"w]FYNE'i7 ]=)R,h@T8?oLE#̨%GNH^sY\hK@" vE1S&޻joqbDT)5!sRodBRżl}M媪M,UqU<-O^sصzL{LYJ`iޭ=9Pj(_ x[m #?.m C!'ϠD-xTc>)P$UnW`8rRj+omQ}Q?%.rY|Aq1bl r6b5,V7C AbeA*udn j! Cqk%z!!(%X2!-1c`?#5eMjFQNĬ@TiumYj S,f[ %.Nxdʐs*nZC$QoNb25 n/gȕ>xF Dݤz oozT#_C1Dω OYP + /PHW~Nt]k&B'O#-eSyOaI +5=E9eo "^uBF\ -1mXۥ5+6 F=+FW)"*m_Ƹ:hTYO$2z:vo?Ue[+gn0hO1f"ֿ1E9 "b|(HۄSl_xŃk7MPt60:_$(9IG\6ťiUr) f}${$#"9tpP*ti[K5WUiěߞ [/Vt.ٸSYMZ-|/Dw>9d>H\xiK: +q-XKidJcXp*w"V( 󃚚g<@a썝f=2w&+Ds!IGh^%z¿PtuR}+hl;Hou?iPc T\bk ,/_nU(v= ZT_#.3oF)NZoJ]HeY鏓Ĉ:-|! amo#f/:<y$)>.-d5r23vlMR d`T-"׺4ӾOf~gUDGgJ}$H *vT<&Y1@BnL]eo b>P9_)WJZ[D5S¯+D ֭aW" yTz2Fnf)^u+lоT{LΜ;ijgP=*Cze7h| 7Ϡ٠+^w,  +OߙX8Sbnso$WѰ:.ŻY-cT S.M-DZMz2$X$ij>:ji0Lau{O޼T)apڦ!3!!~+vvm\Wŵ1KeRAÈ +[4fV@DjDf/! 4?HDB&Y\\b^)%DwZE{#5-5[#T?_!aJYdž+=x/Aj + ՖO|Hk7aH N!i`HIl(#0,@jk`Ee':7E3vLofjS`Xf 䡣;Bjm+-%߅jz(ECHr-'ba6ՅvxWbՑ1/_2 )}Xee^ $Nx悧?4rhM/zv*,0 !m)uiUU_ky{j3QMSt#(F[UO&9Şr꾾U>Tf2Vm0QʭͩX@S e`] =xYUa!&6n"PJDLN/MҩX־n% =xDx]Ha6w6ˌŻfL%+(hZÎ5 +yg +uE5KI6!E%. ClwdϣPt'N%OY?!}'3[g,Nй-@Waj\;x”Jf;/@k/| _P-lVF*VvZ2,u- +A|0ߏV- wCTh=ks+]XɌne|PpUHd{K.HF]1{0qLkpթ_8~Ǟr䮪MTaG-jqS Z AM#(qI+@,%WD l#Xg4q$#FkUq#@Wh7b0c1p})@4Si@*PZ!+9INGdBd4KEި)}魪ȳ,f8fHy|<76\W$TtMl|-q4LRiD , Qq$vO}Uyv)_%̀kMp|]rxe%0\Ttǚ@tU[[(D>B&{|wC4GxXwӃ̤%gQa$8ܣ#`IZ*EKaE}V b*HΎl0eÓ ާPI8FDh?!BD>CkhmqZʌ(I9$|xlϹXה҉#  9$ oU!(aKKEڛxѬ3m{-K j`%s89eaJɷ0;m8ާns@5WpsZ0$̦@koqme˷LN3 )&kơtY"UD5Y籮=쒌M0a]Ijb*cQW9Ote -ښatCD ]R{vKaU{<ê!&K_[S%ܻMph@X:V/":U`ˡxlaq 7H>s8=R>Y +:=W6>U0G>ԇYʥzI-zcq)uX.`3KOX MJ@Dd{%yѓs`PhMmfNZ܏އ_t6%2g#tSr[Q̀c,(If;cEکҒ&=bkyۜWi<>Lj8KUt92&cFH#+k@(d{~0#@eŐe֏pEtdXU4@#T의_ϗ˒ŝ.cm(˼[L?V uŻkq^cc'DO"ecɔsJ5Ӯ^n-8Rl[7V̫/_;)# 0U ,_*lX ,}}l@ *g >zu~ _FR/CM7?:}[(HK?tJ8no\2qp?N)ѳ-nG|WLnl={[}|SGI,RafLsnkD,e[CrF)ArBŠtD@CΒ&rrSxzD lD ^)IPF AelU\-.{}*4:DdDynODB^G411uOIJQ|Cq.DrFuf[bCqʤf%\јx(C2 zV[՜,  +Ÿ9́^b}088, e1ԫ3wDq75*]o-F4*+Ef.2Rt>>_KK.%bvS,A,H*yCp΁Ō6X h"sxHe ԣ9Tw(QI#V%_AY.("-Cض +6`՚΂i|mIJ@"N É7twS+M{v +}T–T(\&҂eRdDG Eni7us7[`:Lh?P4`N 1[ܚ1(d2DQI I4U Tg\ҏuM *sZZ$Qe-"b׷Kbb+mkvo':$יdGQVU!ӧ3] +2qaRY4+ AmlU'bEX4I?ԎtYhǘax˃?-1$ކY,*-uWY% K`S} +1vռ]K7Y^oa=1vLJ[/f#vC>7?~6%NhUA +8FA5=ZE:+L> "O^;"x(iG5r.3GHޕK`LnRU@d$ƿ_G]}?F8TdYJZEdPRĸn=H3RHp͂Ԝ8ŞYynʲtG'K$iBUH|c#9DeK9 %s jϔn$fU g٥􂔅A/_D)&B@ 1(<7^?n1w*>uFf"w3)ߩ)^-껏I2u˄%V׬-ic?Z@J +*`E^]􌀳|q>*r9vTMqT+$P/^6Uc.ĕ!;.EUjCa׀]1XV=xvDQJ А-*e H.S:-\ۨV;YF*IQeE5\vUź2.V'rrf`Cv@xAai_b.IG`kt#(a" IH( T\I#O+ Ĉ4pV i(A~J"sI 5_q@ww |"nF,QpڥԒ^)?n!>Ͳ{(50|IQ+O5\{cЮ]BE2T/CaU0.H"Zk%'vIde $bt)NcIǐ&I:aJ\ R0׿RsbѰ&mm-T%+A7,Of ˈ.qs m7fò4#@&[qd,RX'{k~,%.Bv»EQ)R ]GITx#3%3DE .c7&P#3]V򯕓:#v i2Jn=7bW61 .ɩ7uk7uҧfpJk43uԟ:T`&imOȡp_lnZa4P/kX1PpLwQ>/}5 +m:z섶B#KfbKw~}BbN}H[gݴhjW.!7as(5d5Yg3DZu$l4ZUG"˺'-ޯ;Wc:)֌Yr~pљQzLBi +Іk+ӃTZӲdT`eb *kCn&IK0;_ ՅXke7ح32aƲڏi,%i"15W|l[/e.X*#Hl[[iBC#!Ƕ6!(! +#/~)(tODfa=kK"'=ʟJ{Msc8ԟ[L;vAj~^o6FܺT_9,U*YsTnmb|~~CU}iRt=^>N+SaX5]D2:54f/+l^2H%ѡ1!n1{sEgPS8Ċ5Sdw 0QFrc$!iX{jbMR-IעrGϖ_ !veg\^ M^g + +k$4YIR 5*Y膧R:ѴRGFDS* )`LѠ*qsTJq5YnZa W0qNF0wAa:yEF꒕(J2TYA'zŀN"FO$F<ȯ {_u* ȃ/Tڕ7J[Ri梟cT~ԕƓ`HMʰ`lD"EtPeR}472E#+ +tU wi#$wiٶ>e3FXߓ,tO-(O\=~y +._l%c7$kgFl?2Rn-;-i;i;Xmȁ6 5퟾ xhK롢)[.UrwʷXeGV, KΉ:!K2ڶDB}2NވQ;rmA%O@1!(vk}Bu㴝oRa0E%ԫWbI6WL_J(qBTDJlRJc5ed+~@6p%`\|N +,-'zԖ!1aKgؐՒxXvUMVߩM޿#V|oyV̊=3VR=eFa$EzZtUy1zP6Wfuk[k?&^*y1Mvj0:v{U̖˿'M=>uSwAm8iDW"CcéAޒvԻvv~#؟<–JGUWёloPDWoİ![?(*Ky" BNGI:~fdf1a)8cSj5AqhJJ?* i&4O(4c}jqQANOT &Gd˴dTkY?Gd *v['Rxl&ۼ/f]<^az:ݵ8YeiuN ba c1۰MB)mF `foyj2W8[VƗoR( +,5㣫amA*뜏nm֪b#ui2mj#MKoEwx7VItWa^_0ŏQF"tY>32 p=IǵzG5t%Ay2lΙ]5*?#e +|+Ql z/l{e1A <%Mz ~-J|f:ߐXQ#.UI\*ouT) 3a lxef: +F1Ps<C3_M&՛qqfɓ7]&wC44=/D+ OeyqLy"mTHb$TT? &x$oFaؘsmB'2t?>(+LФ˺<<=damʦ0Z]}#^Iߓ_L%VY"N-y5mOyiJygqb4,Ca"io 2f{A1^(rN O.V^]AV?Fcw|KD"1oH1ԙ{Kx%B +B19"7_4Dp".u4^zUɥ+8))/ms9hkq"$' L絩Nf=y3P7BP]t9Z(b1T8&w#(ࣥ>'˥a9 +69{يzH|R'q~̃՚RRoxEzQDcM#LjR}EN#@)]V)gE1_$7֐Il}JJlܕ@K3wN%+نR"į!fWo  ݒ6JF$!QyKJid ,aI/:n&_UXdI_9d#K,_ck +C ϝe8sZ`, zRC :F߶Ỹxӊ˕=6nMiCLxa9'.>w??_>?~ $c~צ`q~PM\Q8.!g L0z`{E%sKx}3?Zw@hZݕ.cVVK "%nPi>c +Gl#-4Ķp{I"Ưx[ۏK (AP]8c(5eSuUD\}Dbr?^uHRnhFJNJ@Z?Y_Y%3MǩƥIjq_i$)ޔO2 6}%>d^$9 +ph7,1L΋3㚈V/U^B1HۃMB6P)ΐ%H-aio +JxVG!ķyoiNԒAڃA R@Gm;IcRl/ +J#@h!RT*9g;( CRj.ESK+Ba;h$ᢑxC@$; /~) "&51Wt>< O_RizNJU;`nArj.C4"GM7?;vvy^KH%g׆xY/ BoyxF1iw4-gQ\+3qcHU,:EطkoWP ]Wl\k# A2.gz=U˓9R]@a8}1XtZu |M3æ͈K]<2@夜UI9B)ngMB4#YfZ-%b~)s"|X55^/Z{~S쟑 E+@&x-/ +R%E4.**K9!UR%R--MD^< K0- 2!-uU.sDn^F2l_;4Q7K)3d(ty!~o3XZ.@J}]QaSAѯHR?gJ'gW?r35P6]ƿ s^INRZPmq.^ᴥ$Á- cշǍk6%*!m#J +Snu(1jFG7x@ċJ+Jgq'" wfu: +d:\eP(\:.J6~)EX(4GKdBsR ڍ9`xAK  J8v 5HޗhRn993k2)h aȾ[$ѣ̐r8+?~]#ߏJ Eĕ:e[U=ْCС nk63ң~5[70y`]?*='UFlA+68RSw]p~is@/BTCo<\(|U[lj5aXx'}"(O=Dg*KP%LJ\:xnA[5yU(8-d ُ=&g*b(.ŌtO -TN'BÙTu@W5^9R3Y'LbƏV4mD 9$,O:a4DZZ +yOYu_vcU{azl]˕iL-ĿyfO#x޵˦6-h\[y1<Ѕe4,^KÌq- +$$ȧ. 8u ++j.::\Q K$,5ՕY3O)_^Z)lTA|%!4ӏM^tMtj4Sj5J@M"9IE䃭0̤2CiHINavv|Əj-jEm/p6Ր{9Ҫ]PV`ēYqAGLɤ*9^s^II{F*}]Lp,S-"J=&?O r)-drt)~k'QPC`U8JIfMCG]7Mb#7IYm`?+J΢a:Xu)I:ʝ]L61ɭ}^2&3C.}Hټ?X +PkMvYd%GIYgbKxf['X]VGvI]4* C@9dr|;+}K`3:Rз>4hOٚ~+aT;ǿ&M '2IE0\_5)YD>%?^&|B^%x9 2@x}$@x80n7%[ rShi/+k$Dtřv]?Q>J5 +#Hw8lRf_ QpT,lKWLPh1BD9: Tff\uF&$u};LZ@jT0%e)YmRr$gDKO%#\Y?ӄzRHhL!9 +-swԄXQOQvX*H*x]+ UJ*Pt"T~ )"" Mb. <R* +\zVijꉱlMyh=x +K'ڷnZDY&)lY{*]\x}ИDBP\l4YUX|TQ2Ō4z7)oT U|(n esA($j)'Zf:rҫ5  cB o+ԋbg0B D %CAJ{rIY?E;7hpzp4LuZSe|]kcd\g˺qctFcأ ROGgm>1kr5ņ;cJzᵄB 7leU_GLԙTl\A,:r,>K/?G6Ԉ~9 6ﮌ Ɩ;ҥDG{>>4LN*@RӒ-5(PǴ7T<6]"d1m gN3fNZXI !xFV"9܅̸g.B\{z;!bl :]BPQ4|ta_lw1*E \؞;%F,) ULŒYhɭ lgAeg3,&ϾNJ|$>S6e6:Ṭ,99$B\mfNFJH@gqs!cQ(VՁQQH~9r=~o%<}IENĪXugD$Jr6A!BED}"'c3FO14"2C ",Z ?>dҵlZ`B7sLEsBI='Ek{fHFuߎ`Wy$:A^ڪ|r.[J5󰧝tx*tP7whl8_M% eIAjXXW }2HJ%Q*ce/c{NwU!zBݧ`MxmnE&wHA༙qZ +@mbi~ +w° [DP|KpÉ7dePw\ >zn)x ۦ7cA_%ôKcdmH{WN[TR3c@#M^=a8]|Jc>Wm+(ˌЛrJ>2vϊCT p!0Um'khN*5 SWw/exh-t7}k+2?jkv騌Ql=aT ;6AsF0b4 KTd +tdc͓eTyǖq"W0޴ei?5k?1)Jv$JbE B*՛.)dJzbOjN ;8RFӧX0TTQweL1ٻ͒eS,$A>֐.5پǖXRUR5]H) FYʙ(WϒakGxkZN71HKId Ov>!LxoT^'k\َ// !lfp!&|>ꢃ,2),.x^J"G>7"xPJ: PpLp90KWWTI H!:w$ B;*׺40˭y +;{QY9rz. TAJw/ss%&F$9"/K":v46F W/{ۗdYpK&"wa! aK+迣e IrvaŎlE +N0‚IPй>>5Gt"\E2JT#ByiK~~ $ˍ ř+'7Rlo鋬Uw!`$Qg\,BU#h Wj9)퀮A|跣x/ ^'瑫-|EJGt%ʎ-:8 GZ`LƆ9MDS=_W'EP)F(1l" +s0/B>kSttoG[y~ 4j{`&x ?Gzv |liʸgN5Lu#i;_+Vg5mKȟbiaD /\#A,o#n!(s@&Bpi.uYkRu'/9|Eb+! +gZr$x',":x.R1V"_+-# **7pQD,P+o +Rɸؑ +X dr~cWuobHkb/oNˁaA}&QlױPe淠/su%u-[!vP/k8Fi˭fn`b>Wղi9(Z$rO$ +]zsN*x˗tGRΚiD)V]dJf3Pa兝H/MkAwa'u7}@.3kzZnGl?IG0Wf(D?#&e +}z6o?9URE+(_WKBnLkWI$E^ƀy +v|Q7H+Hj&C +0$rhWsTֿqTn:|QyG'AQ A#pMߖO,zE[@" _PEE`{esA O}jI3XYgnVxmOt+&yç9R+AmS[7a$hW,u&Y CX]?ۉ7NR@ŭtb;e=K$svD[b7l7&U#ѻMj΄˒hLƋ4v"^+Fw_I&!,&j&Y+AFd'uZBro= z%^F4%$=cE#>mjP8 7&Y-K\4_@H2*֔6qz +uK +ƅ$ˈCo|+̺!UMA15/\XP簓$It <ť#lE_:oK)풰]TSa6' Ѵ_X*!k m5rؘdrKϼec IRzTK^U2616/*]/xh=mKsѲs ƅ+~WT`Z"ZC|תOf1M>[w+XZzմbsXչ8KWFXTG* $NDyL='}ukf La8/GNX[q^)6X/Jm/mY$Tj;[.f+lA Ǟ% &MtnH)"[=yo[YqsIv&-+ze9?K/5?-lQ)CH4i_!IWLk:0= s {bC9aǐe6n!<[N 0kziXqXM%H +; t!zj~FWG`eOLP:tǖMF1+iȔ륍sk= R^bF2zpWW!g3m=v Ѷת?LD$K4ͽQv\G_͚*m)\qit5 )WGNZK 'Zfz7c'V.QдAI@>T։/ "p7/Qd͌WC<SW Te1le}$54.7ō EH,;@!eNSJ#yܛd(ꝾUf}HܒWOlN^]D|SH +름ƮNoF]IzF*o.8d-63kVSAKUToe]ʗGﺼlNt4eGuJRKM<[⊭s4))-)qE'e(B55w" 2O?I'W@$"tT.f ?Sj +1T͇v"NxD.> +3SR!nAW_ F"6.}Gx.吕KYq& }@zsyjJDԱ`ILx^IsUQ%@F -ɭx'TH@ Xp?uɪXQp p֥ҿ}`^(y}S$] '[bX6i9w)fMwXzGqEYCA*{o aN׊eRqkTRO]8^_L'Z ̷ &X6Ǚ ++ $'S>PQ~EK:vPB:wKƋ_?ũ{DN|pId:qa,"O%JJ]k5)P6yqLd;[VJIp諛E!^ 8 M(8xV41N-_ -8u5mA9Q_UlH}) ФPu JTx ekRRv0🉀`}{r-/{x_~)DˍaDS$$˲#tF$~| tvJ^ɰeicPr٪,]yb¦!oC2Z?tFy]ݴ'NFYVgP }0{D)! PbeB)C!iJH8&WҪgFG(דBz9h'07PZJ0\q}QjUxv.=@9IYUF2iZ!6Zq"V0Յ(+A,NͅYL8 QXםčr),nPzHYbAs(SbQ08$7p"Y u2Ҽo[ p,8\顰oy0L0ȱbJ}rr0/93KRڈT +UHԄ?$ ,ɩx|-3MɑYfvOTrbKABs-:`DK9'PYW")M G< \9&Wma@?)%sBbvi gfkRyN#L|D{[}F[S!֫.9A/Z%9EKs܂krT \Y-j9@ '3GO:,j!?#g7$߁\[՟|viR*~`Yq檅"hQm8$Py|k MإieeOٴ'Qi3?Ȃ jl_]LG<`ʅ]  6Ȭ-Zv|l  vEC4lb.K61. Jص h" cGg,}a^Rhs?["")ZqhKC\FbcNQ .U?@K+I(֤kS`ZL>R:E$nz0";ٕCk +I=؁7 ?8\' Nh8s %"lQ.!^ š>jG6qI'|v oBY6FtsˣBq-N=pRUdDԀ#}Ȏ >\HkG񃁽!EܝLNa qfq@"Ijzu).ނJPGs̋;p ł9hVG%ׅ +T5pN9/~ln.8F|E9!d:ڱ|ۚ$C #!Z)] [$؈Ki`y,ņFApT4+FꀓQIf1y8W_BdMS\e.8#UP=+GD +,ҟ^ÕʠOR#sOΕb$=WCCƪe!S/?ka.q&μ\Ba4%K 9!FDe(UfWdõ;ԋ\9̋ oDbG"IgQdEV +E++F0983\fCb\ 9qd>t뇜YWB 4ט' 0&qQH䄋x*g}Mx*(_tUQ]9'jDxDN,RuPS"F K<gU~Tǭ[$>0ZgLV+ji/h*1x + KBa 1I6h?l͙cD@VcTڋ❄2>,j2$NvϿ ɅΙI-םX %EL?2>Z+e&IF1S9q 쎅<2$E2j < $P I[ -V,a`q vlЊ`EĢyMR!g&QR`)G9TPe}uᒕ#jYڌa7/䤢B韛+!8?c=!f8ou8/Ɂ300X?OL:@pX>8qGopܺJX!ըdJh@!_Uϳd<蠧p5ZUxX2jOahE9a~J +pbA= +5ܤF>m +E 9e#1+ $Գ佚hXԓ$#"W ,A*屍QVwu [?u'D@5hDJqi +6"P{gX)c< +\PB*toXWPA|^/ %/}4VRwD(omvԁq)c+ªZEqh(`Xbg=E癨,+ JNN]%>ި.g!qJE9$$Cj`~1VfFEK^շ5A!ťP=9^D"̮ o]ȑo;_\>O~Ó/~z'W_]^zqaɧ9:—' CO89?sL}Պ]9J;}mWBqr_>om~0vˋ[[pbR3?պWov7v 7<_Mfn:»}pq4yiy_~y~0 aڠa`h"p$ x)yfpi ~P:`WIl5G_\Qty> S$@Ԯ[.4=*5ƑD;*pl +AC4`1YL'x3F_6q5xH'('*Ӿ G^2JN<$V.|8F!F6q޷/lwP86[@VXdk/IN2Ev2]w;PUn$$b,gYxdʄHD6kC}^=5誝M EDhxP JQEX +DZ +UdjkC #0Aw_:{g\(%i0Z28rGlF0.!S*|ށ"Y'u¼#g3<`D9JBͽ`,CU#8,7 Fkv:~f1[L[@xt٨x0EDG[~v+> :V+uR>L%+ + !N<#ͤ+&e?2 Ya0Dp VE9ZoC09Lo0S %;̥<'yc*)#nf!0>Orj)þ2Y;0cXgx ,<+k2[8#2z%xx6A.%JF C, TM]z8/C9AC`A浱 g5UCw}YFpT GBgU]lHE+ +{\҃ `+{YcN@T`UVq9N0D Ґ%|Wmuc`+m2k;q(ph "Tr{۶f#x>סrvF8 1w@lO$L6t~g&M`FFIG!)%@~OӢs(sg\13ծQ42 +RMIN⧣ah(uqr| d +%3`> +{&qd,#o:oոC'0TԱ O!LW:<03nmT!Q]piE&K8ua\Wl̊} 6 } +X UC1|W^r!e +#PVd`Da"4?ΥAdn]+]֌h2W(v%f7 +B{LW`D&=e#a i6dKaj]ZEf;V`UX h_&~UW\UdcnG?pe`w ^pkU ~Y]iз)Q*:9Twp6S`pU(@b ^")G'@Yx G ՌsYBtqZ9>lwJU +*밹\XT_f ߐ:~z0d~IaD-= ј6fBU:kN,h'jADX)l$ BȒph,Wuܿjg'EEUqU8s !Rc⻲,'tM2pp2+Ht)Țf=P]Գ]zVʁuqc^J3|xK&#qP ~y9XѸ 'cWOL\Wj8Y*&P' =Մ]' +]´?(o6'c^E__rEOc@r@9`\YU}5.rpْT%WѺ"Ppbo]?waECW6 +.Ru͒ lF/j[/ӑ*#IHhK]=4"*g4K5AmsuzR Q +u] xq7ubAEBָ +ӻ>stream +Qwroƞ2:FyLy~ Pp̟ G[z(΀DSyZ;Isk N TDosR`ꚋɺ!f`Lu;0Q.Nd-},kuy8Yki++(X[#Ȧ $3݈xp"h01# ںQU >x;>R +pPs'#0:Y[ F [Ǡe ԔK'ss +U `.YچN6L# +Df3όS/[}U\wiDBP夾WvR-uF6w :<(FFskE5#^Zl@"ԯC:hYb鵎]BxV",((dy71Tu~z1;pQbidn;B/'ƥs *]\1CGEA`OtmR\f"A7_ _,y4- Xg0 E75RhS1Kź¢Vf]s;J+ǤIQYV{Rvad(B?&QdEQM+sxtpʼIU2ÈڀGT\C-)@TPd[dU7B a>'h8w&;.N^HDUĉ.R ^C0'a*ܽ$cdž)uP5Qj?1ՏUHӦh$-,]`@T1vҸoAgK]$XemԾBK 0>]u\QmSr5og3ܪ8 \=X:D/@澅XDSm\o^2D̓`7ZHNYHSHkw@WփW0Xz4*ꏓO^j~P&B;H{`@ 2䑉1+\o̷i`ܤ# cHcxnk"t&ZJS [CemME`쒇kJI`UF]حt~S(Jmޥ=nDc4{P lzްszYz*G[essndNCW$Q5Bo59uQ:Ԩn05u$hAn XxQcădK@qQK<9%!]D=@AMMau"0Ǖv'C-&r*8Qh Ca{yan*#eN,mկM};FKlV^r2[Ĭ:NSfP=m5:CR=\mFg ,96kpD7H8[* +4 ytW"vB!ϵm?8gz.kYt|q1WChOuF_FüQ,;<x;5Iyub)SCr8hINT ,8ݍ Yq( >ȷNqRo n_s7 +1%s-r1=> c얜ĹęR=*R=p΢(("oy%7"I!]] [3G{ HQZKj{(CqC>AT91g/fa9ɮschLDH쉸 WB?!u cRYqJҤa$.Y ;ˢ/ cx=hR#2`CycP0'M9G;ƞ"NGoym窹 03-I͝ ]܁La]Աh\;~ `\O/RPwغ +^#Tk3˳ EQXzS]xU$ƍqn+=kc '90xs#i|kE:Cڜ~JzFy4דX)NLH1=I]N%c\DI/ +j3Tp:q$\F ٨]$xGAu٨̄S*:fSLm="1z\VM ^Qrv44r|]7}l£䭒]8-=\9T;< o="6(>3\G_\eZ[v4 pq򦤰*9'h+QW`X-rl\[`َ8<^6K0z] +0NgT&@.% < +♭=`Rr 9c4JdRl.:yICodE,rѾ +3 :.S$`,2.Ԗl8v#OSN;k'u̓$ΐ d햺a"xuBÞ'[|"4tR&PG{0Ʈ=Ҧe妈<+ +$^/߇Yqy )c«~(n1y!k6[]{L4 F*TÉSD7fZs0YμdbwWݛ+[^ + Ä"b_vvK'o*H{HBsFљ2=)Q`'$9<HhḰsޡAVAuR逃%X +`\oV={SXsy}ϼpR$xi-q%"7JV0BШ;fv3W$fw,ӼS%'Z #(PÎ̤&E+~tCHap4k+y:}{"h1<L_"~s"FhE`=UWR Ca" >:; A Pgk޳e-Bi7+nLB+̅\2MSV''~=>AL{p*²(+nUǴO}䛹Op.Xjc! k5d  vSNo +ڃsPJ5q@.*w`d8Esfۛt;Z +j \M& #dI cPd7q1 WULz51)bMlNY bs𗩔l sXriK~눧R3)zM'Z$**2uRxluum]J'aL=N=/p]B:s3|;v7):J6CwcJ +ZTBC 9qIԝ:eJK'53m=I ÍC,HI>Sra-ټQP{:2!8QzH\Ѡ{!ޢ%I(13j䬍֢HRZ4{WjAd*h[qpbT; Ce]RHf\[Kc֪hyh:Uvjjg +ØgG>6sjؿ:6 D,.~k|wf&+9k5NrX1ľz`L /S øc&1Yu]sgf5*snٮA!يfSyߴwsIxicWFX Ѕ2!Ғb3z,>չt R T{!3S*C FU!TIUf:9({ +kYؙX,xjQܯD\$O' 剥{4S.L 7 lqvj8BwĚm3c?Թ;W<^ũM=zhxΡ?/l ]>j>z1Cp] UыƱP$fյJnnup ogXR xvf(=͊\#Ym{XJzEТێIbiLT2kSI`rv84V +H{ cnOK$^ ϩj ͙ݧH&3|GMi,n@Ύ~9;ҘEHTCicw&B 4K#(58q[,N/àB/C+]\1\UU<(+pSE-^5 mS=׍+[)){h k&/>VTKH :{$-=@3|E`tƇ +|0c$W`jRu;h9ָvs\NulmDF*=1u&-uaqvnjKwQ6r[3vZ<%'x 0 jg2Hc>1OsL;> wm1(VGGjIW +]9zn2ުu;0=|RRj 衙9/aQ"{3ZpDĨ$0T ȈE1:&+ִ&dZeja1M"C9 F176+LER"Ǭ 3hā뉈S]tCBD_mc0- 1y oLgkG 1i5+dtz;5g^8Xzk8#= +7m Zal 3"-'[5yh$&` ,+PU~oƖ{/*[B|wp[ǢLx+$ ip_I{l_rSFG(*t 1֊U۟lGLRbBbKCfRW%n]do(byn#e  3Z7l `M>E^夁=q5$ V Y \|,)_ٸ.K!bjI,74 HX'PB}!+hQש` +U+hƊY֙FC2jHcۘ9aư? ˆfv56Va§lO&+c7S" l#.5:?QK%Z rxr Z4)L.b2k nA +6Y͞EKJ pa;;W )gUi:34 +X +fbc[on;ߟ,.X`uwW͞LK.Vc\sD1}dus'JL :ᛴJFNyģC9+Is4LWVZ@H"*`RfkP"'@u x%qЭbP&[ݴL{še7.zK&FSC'Ŗ37aɈ ;a +#ZPm>$U=BXGYS[ۦyÒebiZdk,fi5 K% |V={ KƆ?4r7%z2엟L8{ađoX-Ϫ)3t]8-]eY +Ŭd%"ȓ|Fo&Y&s73z'۽KoZ̎=$פO^g -L>7TH8߹j +g WІ-=铻y7,:/-TC-q;[] g~Y~N\VZS)\".@ qaן<2G{h\_CM܀}Xzr +Jt q~6k +$c4Fo^[r,#Րj'RKl\$ u g{ Lzյ/pZo6G8ekO&8*\'KݙkagrrJ]}gЯ9[iog WWH ?<[g___'?ūc>}#j>t⋿7׿Ɛ_.;_?ç~ɞS&Fw5'MS[1󓟟_?7k\3oF},"?rG\kևyۣ_;w5'W'WGD_rNN7Kw5իW?=W )k{uJeu'?nّ)6aYN :9_{.^/..ߧ񍭸g'vjZO.^]Ϳ<|hm2;`g/.^\mt`XZ{}16N8i ;Om'np +leN6ɦw)>$c$p%MmFGلv]R{.:b!2}f[h_˗'o?ЄoOے-ٳۯoyی$wyŷDVr^ьI|1?-mޭi)^^~ۋ\~Φl/_^l7Th"1k%~ImQdNDWw'W_FDmfT>bٔ/[J|{#3}C SwMwmm0[4B 3J3n03qVm$țm!3TXW&h}#o@2o?ϱym|%?Mzy`h_ۂ&UۡV~mp{맙<<>yNw'ԷQr~ދhL{u|?O É[&|љD0]Hn_{-۽=l=a7ٛ-Inowbz{ ĕ-#3{}߻-p#!-OSaFmr$? o07@}w27.??Ώ6R-yW|}v;&k_|qzx4?_}sbigK?hson(!u#V'9`߬sYnA[)6y@뺶 n0w4-uS. +mwYw^)_;<:zuv߆yN޹I]Mr"w||ru&[߸Ύ׿dDү/7䅻pӣo.6qWS|Y@ijӍ"?NXH: ߥ뻴Զ i6bm#(tyIlnw`@bϿ~'z'ooy[.;Eoj-Z;C;0◗18N)zHq*>|VqwHqBS 0n\%ޒM|7noݴMЂߝ6?F?`yr~<vr~kAë/6I3yZrQ={op\wɨS2 9Z{ۘr~vq۩ b?7f??уϺ; R;7n!ט}z*oq Fۅܑ3lےb+%˓nvo9%ٷn3Y_A[TmB;S?[l{ 3Jw?[fbF!6?:(Nw$h\}wxrcGM%4y nG M~3qr[ƔkH. ܆4y,6]s0` 1_wu>ݸ߼CIQ~kssۻ:}0߶>m *x +r~R-U-]pu;/wv&LX{6wȲ֟ү'Ǜj;Z`jt[q76~)oRvʛ"=M{pʛ fS7;6lo۵S7;Ͷ+o71|GnO_.j~~;;S}ۣ_6sWSۘlK-o"ޕ']&@[7wҠm3]Woхv-F5gp7kL~c̊~w]^=2Ҷy/۝L;UK gq$:9-fl)C2Qmφmi{m1?w=a/rr^[h-GW?kCKwuxYWGo3ګ37۝ćqw(E3d% J>oG-7bߗ ON-cOOfݷy#7sfU=WcX_/ο^8k#?|r>}G}!$y.Na|uuuqnُ⟟?^a_;~'?u?͎Cnh{ι<{ ??z43ףfGEUifء-0 3G7_BkuB%ԁ+t"G=n{%0 DGq9á98SBu^yu_~DrwI姎 ?#|HP?a?9ugSGNx ckMC]\苃:0l]zKwk=>z>~F31]}0s{tTƻ1]]t_v#۪tOܪ?)==vA鱚NMO.]iQ$xۻ5xwiN?<%|ȌiVC(<~G|~y=!WlhYelSq#Å''HW'wv?<|W-||qq*^+ `Oq?>tߧLi*D g~~tqyLs(N_[)cx|_OiPOP8Èxy}-뻌fyJG4urȷ iCj V Db%A +ќ"2H=KH^ȑ0A<["HQ~呼Fށ*=Wka:zݧ֊2DHV|4HƸWaPbjh[' 'R?pJHr(f0azWt$hh) +;gH;`XFAI\( &+tA}nZ-C-DgduF؍!+ ˖UU4=)H<D9Ugѵ QXGKPѹќo4n:557OpGQ7Z/z{ C^Cg..37T]&3TpTYs a-L; DŽhv"wpf gMC.^֕%┨ m~'ѡ'䑶-šT>J ھaW#?Ct& FG0>D7Ũ SN)-4[ J5oi @vAuto-8 ^͎~m̆v͕-"uUAga; ?i:("=Z -meEgbߴY)sY"+ E*,앫iڨ47LKNz|=>ta_A=#}\2uPd I"ZI0*.c ' +GRv kiq z} ha#[~jPB8c՘D5@*HgחK:[Zեu9:kۨCt JJe赃vrѸsoxd#)L}No>f?"XL'qgC"v {B`[:tݫ *{|1’8i:7b,}е[6^H!~7B"{ĉ-F0O % 宄uhF`H4Xט4)GYo|3 nsZe&lCTg Y+@D$ȇ 'O 0T;c_i8U7op}pF΁2 oc1 d0[E9 +5skoa$&LGVD`D4 +gu,^ .AL 5ArDAAGDΠ$.KAOG4A*t!A($KJt)92HiӁYD{"V&Ήs Eb[yC?w/ЈLѾ1v{9fa,j60c"`@XE.)}~$+'uȳ ![ $#&/@`P!z5yҍkg7ACdY?M&@ W炱%2]SsB.80vb= m= K (P\ENUb\_S8tFi>"L DV9V'TG2Pn+tjc\˛գ=,+k}9ݙDē4ÙAGq^pK>pbt0k)3:@r]WMuJ#G]6qfd!fփ#f[W5qai9g9@7|* Ms+.,+ :e'TlϺ5âm00SLP<$d&e~zCK,қA4քHXBNB,BQ4Je Ħwes;WD bU3 Uڠ=A1dJxJBDv[[cWfxǎt?Jo93m3]"@ ꄈ+Ad,tm=S^O,tG-DU-b o!LJt8ld#]Hf⍚4I A#1H4`RBWqBBG ]Npzl@PJhK +ĒM7vۜYʄIan!\)^&@N[In'V #ۃE%"LN h8yDX0 XPDC2q3_B]=F7h[ UծYs:Kj`_\~W&&n ւٯf[_#xVN +>hE3 Ѽk[p>FO}Mgxx_cřu^yruP7|EONNK}:ݢ^ܫw2q4X9vp(dQ%]9p]ȧO]xʛ(zjbωFgy#?T,͌/쿄>cوKGD'D9Û N̤ 5_U@YՈ8Y wj`=:p? #>0?$ $_?;dtkm~|5}\7!6˛Ag(_xut%8_gjQN~pLEcEu6:yyB;]ɓãn×'Gd\]^s>u]|+f..!VV^ +|Vcg +*WN/O8i8yw)ҙ.8{%wGO&@ca䁬L`Z˫;q~js)nZr/oeq6 8^ {C_XLxz>>WnEiⵓwm~ + +=x%f;FXƎI[+MAŽCS;4CSo62T/./^̞wd^PM$@LJ\l_ Y~:=?^\W/=9xl+Yx}ۋCޏ.._]@OOzC;yB>fΖ.ptO7t ߳ 5ǽY+X,]yos:b9UBd al]'m"Ye}|uz,^\>`6{V'l_Z:\D.ǯs?֩c7dz}<Iա YDX4BbS 9#ėlTuNr>?ZF*@"bb_cŖZtz$- Ď]yeHDn=ВC1 ̄$ `Ms0ßW_y nCEloDxJ Ć$~$7.|ItG.Y)vBFْq=\s 8FA8ӻu`zBrG_e:Lwq`E΅$CkhA Q iH QWhtWh^dO>OO8|e<|6wj.#a *`/ӜdFF:D[ZEBx GV :ѦL1/Sy +Hb,P3LxЈG%\nPH~UƣW^)0)F2e4uX@}85.6>DU*]pOόt. }B@ԉHtQo ~ gmŭ%(4Kl~M)Ggce 9]f`ƽð #A>FI-ς{lP}bR8Z&Xtm#Ӥ+A"44@ hѰ-GL]H9TrKD2#}nHDYIHs5E42 F(Z"*,w4ؐu0W7 B|0]&M0"%dM HPdL Swk P;#Ô)*`hD?DRZr²`1?m"aA|8m/,z5sd~ +E`]eSe4ɮ + wG#!rO# Mp00H(< ȋzh7eOq8x4L W!DPBECp;3{-$1"7HcH#M"Kf1tq5L +3.G Yv79M9LM0} ̝9sZ7ե*TR&Q & u-tqR؁P&/G %KsZb>1E9>x a2="yJhan@y R7HAjv>r܈,Dg +43ZM  20/@Ca%=)HSo8(Bp j`pxa? IO0"Cw<>E' +_qhH9f$`Q4>px(ܜ4 j3 91^1_(H2 2^" 7xeHSLNv A$1w1[nt*06x,'x͝υn&=xDjo^AQBFMSre{!T,xH9a;{\0m9]@Kkdì0`$tJxx(G*@, H!׶⟙HCڨ7rQ~ oTD 2a ~!{ j؛|)2 ( 2`&5 l@V{({w?o@ँr П?>39R5E&Zt]~`j؞]VPUܔ+ȍ.($X/q-Vnm;0 =>HX/n~/"}gw@B'*ؼ.x?  oEƬ"aB@`7i>@&_a6?Ky  ]x-."o@ D=Z4e$X%.b0 +N`{wlx,1R+ 3T`ʿ܆Bj rƂ̮vrJ` +#=߃ $yz唂ʧHkWWt3+  ~Edy)^x/IMw~f,1wN@7;5Pjx]y$7N=~8W1tAL pET?0r `Lڢ‘ +#a3!{BMy?@NэLJG=ŜgvW * +/=ZGgC_ 1eٵ80EP BM0Kf|D +XEKKFPou$(ReDkG&S[S' P3I@3FD͟IGloH*&UfB!zg<XЇo%y c +P=Lf,V>/@H39Z F P![.-=?XwnV޿ֳJӄxTj^TTD.+膇ByANX̏]eNk`zS0?C8vx' &hgWEͱZv>5Pz>nt&H~{">{iχ +Qǘ, -K4NuN/y/΂C9Ԯ`|~y//zdgQwod%({xv ^saATa Z15#KnЩk\$q:erfRggZ ՓMIqO=nxn `sg9߶MrP(N=&np& t Ob +}bxM5Χq#҂߾ Z5ˍ^րƗܮG@ 02-=xlp9/zGN}6ts<h*!I/h<< H7vi k6#mJx>ۍ7Bvq<,x0/Y|I^/ZNDxR업'ŎOBED ޿dgxޮ/7 +*nDK-N/=_:o O5x7J3/t'4=NMz^.F[`[%R6}2ÕvO|yX@mHP(6cԗVg˂޽sm%Ϟ8T_S1sftҊӚݢ ]#MY[&Y]iPyੈ9_/+ ݒx+LbRC5jvNC~ +ҋTkJ R}r<_n<0TœL 2grzϳ$mV|V?)1 =?+w#ÅR.0] ]/SW,l!*ږ%@$q1&~sXA\N!K 3etz/@Q[7y(2 e­ga"<2-ٴ-d%`Tψ]+q~Eri } dA뎡[7F-+9a4d'wn+?\r|vt%8y ˙rXv za m1PfpbO9BLi[dߏ!/̊ŸT_ !H F; ^,?L[@_q N'"' +q<=~ś)υq!B}B+.B" W% \H".gPo"(PB +A!GE-~{|ӜW0T`>^,{@*_G8 ($fBHB&V?{0vćz= '%TP<j$P#!\5Q?!TpB B!!l%Y +.BB0-%427@K#G'kAG z5)# Bx?G=!?4v{jT2SWc7߹|{G42,[)(B:86}!`;p>9*v"o|ZU+J~VO6rN/Qt>L0/ Dnsx}EOQ6so\ ;+%\/KV^`GoU3HU:_ŅEdv%X4q;+fX)p58%8}65c&gÔ$"T!)lr6?0#t@;'wԎ&5GRp?3btFR,@gxI/IoutΎ~O0E)5)g9|=4gJT^U࿏l"#L zrTRգӃ aЈlT_ ULS);h+t)д! N)C2ߎrJv$'L<8|KUV5MY^# APOgk(jyx7+AlPnzMĺ-[=t4dUxn^Y Uh%8;Ϋl~mwM֞)H+_LM(*P\Cr擗a|㧩ի?/L u0 += [Ve#]Qt-PWI6SU`yyRhzެӑmJ'$!In]aRb<\\1۟C +48P[dPX&ĂLoĒlŀ73DO1 ^Rjc1wY+ùMUm~ŰS B3%.uOֿr|?`+aL% 6dk00+qj Aw_I` Gȧ<9" +6&3`4ٯiה;ï5q}_k2pvI _LH liW7d{gs z3 > 4~j~~L zS䄾 ;A.D줍kr5pWwi'?g}.MD,}|u>ݠ{֤[2EwՕ}.QwJ|׶]Y̞rv1mMhvZ6䏬Vqm<oַƚ\<)6Ѥx {3ٜ SA#29?Ck&f 7K>JmB3[⧿N( B̰Ik.+|^G !kZ!?ߚ^}nu?MYcҾJl}z3q}K%BgyawD7jx.ԲvMXokl|dt&֫ui Jk^OzZ{W~@5n|F mͼjB i+MT[lx]ךAюvl߮77+ِ\4Bg:0΋uvG+1]:CuwҮZ{^]vzP^uCW;^=F^zu.I>jSϯ+}5[~GB\G?^ߣ l17a &lՉCb40[6T6{C`a=agzn h;ߌa 7ŸT1k{k8ƅo5*őIġ44Ƣɱ|A_\L\T2޷ 4++m +.lLͧfgZ7+9><;\wlfRg:`e:ֆ`?ۀעX-Çd~,gc)usKp[Of=\ټX].h&2[kv5gpX/Fڔ͕+"Ç-{έmYkn˝&]oov}jI{g/o{}_Ú:|֑X!Gk91.w +miujJlNL_ Cg-tq;;SNp){9tk2' h0jvіyEx.bBľP?5Z]Cv*}כőw5hnswdp;3g1|nz#:R_MqYE~:juuul)LײW! Ď!n7i+8gOq0坉mLLz,1mIl8$u2;/eo6W)cd +6TRT^:V&xIW%n].bftoY_{=gJ5#3譒Wubl*`1g;X29c ;W|Mjs y=0ͯL~V?VB~VXag.v9VL4]O%Z͵RVyl}a , K8+ё ^(Yiʪ]5X[vg~ثտ\&}ُמW|fjj.\ qWCs~cݵk> / l6Q Lca&gof77wR97tZygs6x]je1X]m'Qu[FoaVWjwa~̩%N=NXW|լrS_:?3LWU%޶ﻬA,Ζ{9 'Ost}163&NU[3i5}1\-Ɏ&?_fOe:'x'XoLz}ҝu듓eҜ^/]f:C8lYcfy)y=o].jbHn |V>;wvs:= }~wӺ>.Mаms>l#_Nn7tJ{)hg~ģq9 +a5E.=[W+5'혟A4g~1*w1A3TKdAm& ڲR8&V]җ_+yJz'X茂.ؖA_l{q܆>Gh(q5%ڒ[1Ko>ن7wP{5?_Ƨe5"^ |rka/*"LIk/3vr܂l dwKXyd{W`/b:Y v7v=%8_+8>anHh{@)W0Â>\X!O]OOg +~u`H'u$XnΗ~ٓ0Rms[x9/ܚO{r]r,I:`-tG$Ȁǀ)eUȎ&=ҼMza72#wG*?\w q2(v,)jpS% $r粲\65+>۫n7BQ ++o6ތ 2ddN4NFg">s=z[AqX9@|@pڰCsl߬Z@T^t ; U$y֢DIlRK h`4aE;R"sWB&uZ,9RcA0\Y?F*Ś>\o\QwDKb!{>+ .4HD6A"tv3g +3-H.`ːD$nA>~Y uɖ]'6]:N Sz#I`Mھ!`'_-nI +O f ci +Yj'5Yi~̉pi} O;kvw5@2D]N3s`o{n|ZcD5j橭#ЯbDڂ^x)~10Fk~c{=Rge|=DU,1vgh8ba'}rS-Ӷٛ* +~íXėQ^4}j#R*U0`@MjAyTf ύ=w4|[3 g';u)ߤӈ2X^uݴlܐA_ ^g߾WK$ruaz lm̢!~fkL{`#MyJxB^R> +Zl!8c;u6o"vՔl^<&;&D}$ۥUMjP[1:GյL95߳GHH䓣h.bA-;2oZ?r6LUsE),HC>E{LK%*Q `m^79Ո|巛OKְ畇p|XNGe ̼axiDVFK'inT_/.*e)Gv0|vLtq.h0+1rz  f bӆcʨ1-vqB\Yr/WeI[Q7ҕ6B,"irf.72c+4 sZ ` ganjQF)^n0ؤ) \$5h6K˞l߮&m w`-Dw &1vn˺̺aȧ+ )(I(NL8ts_"i&kU\ 4^!o)dĞ{홦k~&(a{K^H0 =F5é1Aـ`L9aAoaT4ePR>E3$ё/  -RL=fcz1x+| M(uE3̜첐FyP +ڀ+zS.5I|Eɹ"a$6Dz (o4ZB>ᤙwS0 ȢE!2",XIlr_f Bh! @\ҝĤ!ͣ#Z +ie 5\z~_ױMm>\,@,yG-s }~5X˵F%5}+6c4n  Rve+x#)|F?$-K7S" J?k-xmuso:^)O%wN7pmҫO,dG} `Ԧj}0oze+1h8S^!4 +E?4@hX B۟`b0nd+?k=,-Btu!-(tD,3>HLr[Mxq'۝q@mg SӲmnھ17l8!9w3o)1k$s6x]X`vbTeB(kGsfGir4M$h?[8qCq\͇_gjtLԮGyjW;@wZ| P$(NHz?0Y&Bo9vW&M.}9k?&`_` ÙVA:O[hoL]0m v.;̢P&GvZߚyZ\7@5lWv#&M `{6,ܟN@7 P[>;QxCu~v`~3z]q +]Xfu?4JAopЯv/l}PprT;6?8X$[&l#lnwc.=m̘HmZc-lN7ӷiwo#S6)OuPoLx%X](ZH /\%v񇎎4P+R-H{N?DNGZN-6Z3~fߗ)r[ +Qd') LtUdm8$/V(ER$߾~JPIAK'@wLw0op̶`s|v`)\ Ж26;ݍo{ <1^J)O_)D?=FUs?N/&')${`3/qc Nvkd5g/rF˯lqN ůYKIJ+_H.kG4`5̔'ù-{.K,LPj6%c)[@mfA +VG\@G"kKX$Yls; p c[a=s#¹tŏ|e'еXxz|7Sfa8j}%mN.ж+rCCk (Yk6@I$zeg2?_`PɔK<20FZ_m/@|:}I͠unX_Bnts =!~n:a_@[>hf'd/@g1 +Ĺ=8P@`RhLK0] UE| Xc M[-pGY$W!^tcHi9rYVhu|S?^v8ws!R`NaVpn[)ٺ!lo+v^rFx '?g' e뾖s͈Piܭ2( WsƼ>Wvz:Gdz٠]v:PXB~i pGPp|"<_,0ĝqёMp"UCocʪXyC߼T9*ٷqݮiW'ѷxY)Ϸb.V .@oLczKا]4h\~(euq)\0bpBЯ1J͗=ROa֘ow'Ni|qO.Or' 쎷/)`C7m3:ߏO]hӷB;PI&_߃mϾz 0[- wF&09U00W̝ +`ܽx,t< 9!OXt_Kr "=GfOx=o蔰${Yw Յ4^:]_XF،.{$[~J>$2X 18@k~ >wO@wzZ$[͝hLMbSc0}- Zcy`A!D`j19P_:ץۉ󝁊!p8k8F ?JbP'" .Os* +UtU *k2 T6l>J&~y{Gjޞ0'71pm>> +ph-;|T@,wi +Di+GqFrPۚ"u|]1 x97>7̇zNRP G 51~ A% o6ЛMTk$ܔOŠ"ar.dMn{^3 1x.IB5Tk< +#Lx}_8-A=vA?Vns@}b Zs-l~T#sN`5P'RPxm[kC-ă + /6]=P?Ow+X\H?uOvp[Ƣ@NOldf=mEM%Q5 +}<}MV!Ci~S!*~<$.6m$t5s]O{NBH*#a~:ؘjS M7ׇ\)c17},zԱH?M=駯1AiD'|2#ɧޞNBajlsk0 B̅gvܧG%N6T 73*v4G@VU 7SԧR6 +vfCSR/Z1|΋1{t55u3XKMx#O1Z:XsiFyi,=Y V~m""! AP}ЙM) hG4Thq B 5P=-uuf0h ́]A`+cHCl dH*&INP&R9H~-wBEKP:FSZ%DSwW%e %D'R]N%ˈĸpb[eE7?eI?'miklߚ WzJ|@C(S_ z"ZnHQp5i%؞* WgĎYҦQ/h![qTO Z|lrCyo> Bӿ7<07)E<Ѫ6{$[=XPx 8\)$UkmȆPphF`#UeCfmP>tMfQk.i:WkQXHYR,K^I;H!JD,OXR9q%"%UI \w*b#W*UyވogkÚkv=UcYkҢ@a3Ak()[5zڟF]R "MrTCHsiNMMh. 8-$A+iήLT麒2Tk,p _%ӜF'┖z 8o8z4N +DZٳ +筂^IQU$2nʜYIi6 .nC8Gr$) +JQZLHi6vZ_!`}IWB/g BƟ;<;` ㉤ QAhV/U.Nӹgkqt1[<^8_Lٕ tÁH(#Wt#ؕcqS"6N-af#{t嚝M)+~=Y'Q pQ$ E]"A @{GL(p\\5)Te4ABv;V(ֱ,s/rPè+% 'zhWTK4P3/9r/3!D鶓p¸gq˟&e5,$ ZFͅ]ܠ,A%" Y}0#/nYߨޢKփGCDwlTX/tѯ9P/Xc/zK;>n. P'&& +ssϭf#'~}-J[#P6ֆJA(o"ҍ; #@y&~j<.@W+ +{쭎^zFX'H7؋PRbGwK7Fu{{I7>i0Awl.G6|Bu(WmDWM%UUjO&`WsWxe {s8thcg??-#\ݽB ٠TYފib{YyiG?%B(Ӝ5 +@]Br{[Sǚɣlozʉllk#9_:9G۩舫KmDl~7GI.? uY +$?ToгWHF(ÙR4:5&`T9m3~m7v::R08Qd +?ÒfP659)/14;E}7u,PdJd =v#:8Q<ə29CVu Tz;`.吅aoLdA({Ũ Nޞgsp^Ri&O>='G>O>|B:|.n||ssRL僢)||q~';>=Χ|t>^p #-Q8f+|y`}{7&޶8ۘ)MZbu9bbeTI]>m +U-86>NawF-ly'*(/r*2Vΐ@e;X!`E5]|sOJ-J^yspYAz =ࣥL"Iar]!&Riw +KŴ;%O1Lgo;[q#}Z!otF:G ܔ[f1)Eg9ShLj^YZ*uӝT9X*LG r3z8=Gzo?|4uLr(roWxP΢o Tq)w:1i%,o "^O#z=ffY^>':MDao530<i;w#M$}/2zާR%JIhIC)oׁ%.z (ۏw=X9k*@$,Af%2|*N +,Ut9 fکQe;e"[Tj"gE^rC{5L\#%E +)P +z\$,i=-nJi%l=./}$.VoPd@r*dDc:a彑_JFʌzoܺޔ/=+ @ **t!Xk ̇P%|zoP {#S]A0F"$ tWvM~;z054OȐ,`GtIY5=c6P|8@ PC LU0J9pU˥ tU{E(U ,;~u2V$r Rq6y|RfuO{vfHfh0,̩{nfޯagthgdgDTo̻iD̓rf6@fUBim"u,g^vcWR{PFd}P^:cW;};Ry x*0U`XB~qQ4,A?%q;!sBQW̳+ +}qQn=?^xKvtO^Զ:>u?ٛ2m}0Ӭfݨ,$$YBp$YB7-"Ixdǚy$޳d{J=%I'v5(TI]4Eל%a* E.aMuhs$g%y=E{{VRT'n{>=)O1?DF'f%Іcs\tĢJOspw&O+p>>HGy(w3Ĭk>h*z>>ɌۛsڳVxVbXV(׉}bY}yjb|>%I}b]I?' +&=i]H{,O +iON:KubZXt7Xe'SG!=,xWi;)4GF9g*Jp}!3C@o +ԆZ瘌Gj*g*QZ妓&SI̖_wrOJSeGv?dKDl_p#|k?zCЛl?"ᥭR73.6k33T +b)U0!:ga˻)Z1ۛ*;7!7u~<C&|sb31@ݙB!2:/5@ +%TPʢPGaL +LT|SAw{XUN-uoYH)Ki6*~ Q;4U\(z!7gm?jcjwڟ zs~EvtSU';}/EnaACm*d +QK5: JTdz5Tm-dRyADI6ߞzSi]@T}\c{=%]T70}etv~-4?TŒnYǽy|.6yPnբ갷 +MfK;RW@Pm1L+&`ädwh0rL4L-Ya% fژOu*ϰ<ηڈ {t7_.F0tA@݉Q-g읏S457#)WohRTwGR)Cy+ 5?َ.~Jwʑ 4MT:G" %]@ʒYRYKGVqv'RPbPd<$6]*uYJB>m*[4Gn)יv9PtE1NFrP-HoL ^x}Ukl"&Nޥs!߈B#˓[}'{ M6{/|a9Ȕ_ +Varm߸n4+XРG-:ե$>t.oy+:Lg9]Sx]-~J،5ogYҹI?9gj|3;0"aF6. ]XM`g?~%13\K$˩QչD٤DTn^uUB*(?,%N?)4@|D:$D0)jׯRբܤNIT(KI"S祢X~كb!㏺[ qo +2{ƭV]HтŜ)@i<K|JF>|//]C_<#~mm9 +!"UCyl$WP>ě병O-xxWzܳR.; J +/x(iuG0D,xxGߧ\',x(_퐒/x(SgO)x(EdCnؕ?i<+,maC >d_PȌ*5ueeF^WQK-x(ov342֗-VP^!:PPcT')x*o%RP]W %6sڡL|CA +[ksCjur2d2$-x(:&Fu?+ Ϭ_1|e`;N}m]m"c@D]mn= NWF"0l\cxy~:z3$zmkiG:5dd6LjXr6e&9c| ++@wLrx+ t g9ࠗN43[;.u4o ++JC” ;DÕׯWpi'gЃ>:܆H6KBhw($"d23Q.$r`rʫ- $9R CR1M:o6/lH>8tz$j@ ,2!Fr}Ȫc*Ωn@W;,4UY Qq|\<;g.9٥#?kzOսwEĪ[+wPg\[\l~`Ǿ <JDgP?n>eŊokS%7HjoUS{鸽2S;{}A +|۟͋|r (WYcNn>9@tsֿÜ-ǾXa3gهϮ,6ōpu}N17;>+w>_Dmل&1Sdeܖ}ؼ WjW.`^̰G^(k7@Ĝ0( /-C;=ٯ{pt[w^ȳ>|oi'jydZ/i_vGi_YN;=m|.Y}Kv90'rJMx^SZWkݱݬQNx>D7&3^f#ȮݾK/N<=PIU TkܴgLf"VDE5&Ϛܤ̔˾Etu-c;Ou|o:ZJJg+CNKI뻟vqh8?SS3>/qkg'_|v1wk]xzꋓ+:ٲ2>!,\bNW-QZF$4EџQ"4%5٧.,.NŸj_}w}<_2sgKeHCNVX6F/O8U^Z~_Hkҵ,9wq¨/O8jγy!K)wvi~r_?qxNqmEg#4O{9֟5\}WDr1|]7/ͅ/Wlͫc0؎9q4n;69+$gi}6_tk~qOm9 },Jr@˘IĂ *G ~A_<^&`y_FtSLE߮ Jg9)Ds)]~",VVH6K^3.Q2ufo繹*\^s[yo|yȷ}o"j?NS~~Kas,p.BuM{曝L}ľa7JB=C^kO.b%l~7_{T>Q?sp-_m}G6y}ه'i8\yXǯ_9ڧn,34w>?:xj%MvGr6?9r ϛwr~o?\ls[ݟ̯L x[EW[.Ko{/ǯ.xjknq=| $Ba}'}}?|ʑU~ޢڽEU "Y|sW>ľgwXJ\+#ѹኾߺ{~ze!)Yr)q {NItD'.xȇj6,n$LI+~})q΍"iGsDtVjǷ~~+φ$k7˪ό0#R\;vt/Nˇ~>aOS6B)sUǞDgZK\PgL*5Ei;ENu{$V/%/~_=}dzSG잻=}ӳsW._NOݟ@K\)_Yv,*O>Ou0UT)s7LKGOWze_nst'ꟷi`6볱֭J>b[g?<}9v/w%m ObMcz&߾Oܻǻ?g> ?~l3>Op+p]v^U1jRs孊[ZЗtT֬]_WmOddKV~; [?`H6"їv.RooN[5w~=K,}'v_ʳr|P7}q2ww^k(r)jُ?McaQB1.~+vrY(#22ECystҏwdi_$?,??}Yϟ?Ry'"vHW8~v9sqYuiJ][1cߚwܧ\F>ŝe _R_N}_k;Vk̅CjؾcoN-ԻZƑ[!/ӗ<=9-yóWVpS6y !։ 2e0o\7 `ԅZ>JY<3.wi5讔I'r`/ gľt/~V{HlfJ!|g$wZ?xچ +!UyQb}̚]_b<^+!'\ +0ۄ뫛,7A^#Ȳ_.xrJ[Fi |ͦ}xT/d$urQ6Ս\妫<3)ȑe\jk|#ϸt,f @{Š0ߴ'Oڝ_֪(jVxh"ws})h ukwת~Z_e +Ksڕ/i}yNsڵ'/#;O[׬mMaws~r'rc32v~sCȵߚwWGJ%%}2txGZcx-oOPOg}߮>Ν*asme@tN݌;?{iy󭧺Q/sx6&__{{sߟ񳵿|_k+y[ bz/5`zF2.y hգc/~UVI0lvG{Ϝ,<[O8Ε?IzD J%v9nܿ}Xݗ.])GzNb]8peKcmjskmiT񴱖W(_3Ҩ^]?u଺3YbuV-mi|=GZ_a?#f3׌4|?#lTEy_]7~mO]??+R[_WnDֳ^˜y׍ǻX?}t^:/!K1Q璙e +vG@n.Jw=}}J=e9х4]x>R1:Z I}i#г +yMϟ9cB=g;{>3#-~8;vKn/|~vzVt `v U SGȏujvu]0!sLYGmt35er6>)|z\>3_ϴ}1qS?~{&7QgvL2|2Fёkܘl}9XS8S4Αz$9v"o}~~W?}י?}G>Q-}ɧO+/_J:2G.o;:;۽yEsn?{Oo?ޓ_|Ǽ/O^<X-+/e+/R2̗\iI+vF {`s`[ףvMhlk@׷~l䟇b\uzŦŵ\1 "k[H5*;&KP^FoD;*rS/r4DsO`3Cj>Qg za/nBk& 銬-[u7B~#MBZ+*#xD"!Ԓ1h.ur85ufHTLjwYF촎E|YO:I蹉&cnOsR2P3X$"1ql-~)FϕrFiyJTUw˻\yF8-xOTI<,37)yvJL̽Xj@]qn\M`T*g8VnOHsUH eMy2=m$]:cЇ`ex;8#unFGOVI1̖GJc39YoMʁcskaTCWSqNUMN7.ĖuWL߯J X!rAXpQlox,Co+0WV +7=ju^RybH& +;+$$ I-$n&OCH. +^8~OeT.*Ge`mkcw'\望$ +y#`^.hv˂2 8=ې#T>#NDjϑ B~Tav@T7Jo^*M-9%*ܿ MG[b'TQlBQPSu , RYp6PF݂7O&IS!D')=OӨܸbe5yTTͅꊫ'Ra`G5퍓{o4i ׾:TTM&a;6oLjlӨ7vYyvꏫg_G8d=]-! Ct1+8t(1 Q;FmK@;zˇ `ƨ0@Ƴ@b]^qpm +82ɨf*B"R8 B3rlh?pݷgadP*+Te yMC]¯ap{K5srN6Es 8Cΐg!JmMR +V xb ׼o ?M({bu[\B[aD6)YM`hIT v6.)'#"y^A(5Il/p/0]y +6${3.uQ˘b)<Ȋ2xp ap[&RXW# Ě`;sO~BH.$e G0$[NeԠs5$ͺI1haȊjpfm":h +槱_knfTF~hy?;#QxLGéqW@wV/3w5{|&;eHB.^qbw>qxO +g![,($[h@ $-Y{ka1SXehoޔ۳1^V{!2$5/`BtE;a@/+'h8<7OCHѰոBtE~Zla๱>ȫ Ra Ɲfx7@\ + 6q.A6c |Uj+ Or&qi088`Dm{>y|y p?9 +W?Ck(TpUܮj0q9 +tAӞR?x crK˻**-!8Rl56Z/^,/rgS 2A=dvl4ǚD{1p z [b`XH()McfVlB +!Bzah}}pjr =?Fi$PX8-ď8~-~& IEU*BG9G2++ ҿpز j60ϛ!`JXmЪ,V+K sM;Y9B)$5,W n>缅`b4d,A7@ju'EQ9Y USUNQD6[NV}&-'A:7C5)T+bA^C lb9OcEX0LgFT  T +>#.dx&Kt'&\X&oOp9wG>y~cwo[0Z*Mwp`"kr2f_Q8G1$1Ppscd`oPID.FSRCi_ ;H(c}T%7 TQA͖DCpЈL0O&Lp1 G = +/d[/,NJ84) e2sJ[~!"ǐfӎcj R  +, +fV1 }"@Ȱ,ntҦ#!;psɲ>(Z8[Vodpn06,GW} Gpd Lz}GQA#TAOh{샘H1C#MiPaXЇ+(&C哧n?<\7!~wџpγ'_<9ҙ7`X~Ʌۗx]h7n7GZ%Ukf,ieŎR{ۭ/+%?}XG/ +"3[)vsN2z+bbBBQ5@ + b*0` z]Adnjh MԠl ,iezlXJ\ScLè72V?#2SWYsU|LTt>-(+XNlq\!s{{p$;juuYCt$bSC(ʊxx!fM4®/e,Ljtc49֙IX,-foap|"pW*qݢaK;*X%8Bqu|WovfZG>Cئh,QQ&7c6ރ'٠)!2H4dL$ZZ8P 1sr+0Wj,ap7zb8"$ \ F_aAѯչMK N5ZsenHrc0t.%QZ5ܠbd6*?]au^C]Fn#S, >(T qxcߥ3yXO护y^U}rjɇ j1`[bdh[ZM{k`;i^zkGWD7sL4*ٱJkP;NE}B +#5 +fMf1yj'̜VU-vjh ӽoV)֊Zj$0^52\ ڴeA6[am"#u=ʎd سD١r+ ;RaX㽂C%Ȏ^!+(;DZYAD 5U eAKi]K$Ѭ,T}ܣID&Rĩ 9$Aɇ MDpp6Շ JS^74Ő`ǣPuex1DlM|LlT|vi9欁ip AFH"5ZPeT g ^J AdΚ%LXY +QX05QLrltt3۸[2B[lm[PLk :jTw+)2IBւ I)=XlE$J)uZH4R%U])7vp+v:vD@a2˫*H۠Iq(z$*0C4ؠ&^F09,_PtBZǵ7&.u3`mHx#Zs=&Zr /\XQ%;gy{P +8)3-Op'n +@H!$d:RA.  )i @|񯬫c TβQ8㢗@HJ# X/bD^ $8Fj9X8EW攭lHoQĢݶ3fʨUm`מW%0N)33^. G*Sj` LLo0RQihCX8*50&Zm'LHIVF"1#='yD`Կ!@E+u!+4δD]+HI2<ͪiD90,Ș5jubC汶uk1 `j۸,ƴY(43c؆v8%)+5&%b1K1P(ehF`[!3#R6_a8c  ?ƠU! +#b} EY^XOj\C ĉ;#S4hV(ji EbDKIk¯3(;:#v<k&dN ["AmAcΊbre'6ɌzfXXꀷR0! + c{&Esc 5?(qnޏENp VwvPtTXa9؈5HP0G\ DkCWm͒z T$(̿ _K m@A9(iV#+؍ ]g1)&(lPe!:a-pkl{PFS2iJ%ihx)-;fBP38)\v=F$MOoa\T"/SJʹTY`M$3K,v#lLf"ou|9rF`X=2ԯ,a?@ шXBNrPU!g0,XbVF#NpsQu辌9o*DmPT,jf=G% z`aE JNɊ5 q@PqhFG |:}=wQyjG BgFX_X%[.6H¨Zӧhs&0DvPISDt<uyvv?F1u +yJw[0>YW]T猊-͵/wզmӬ*aT + 2[&kF&Q9X U ]FEKBGwh$8+VtJ=+6! MF`i@*f^{1qE/N*%$R]7 $$Ĉb7"%IMO`)r*g$Ų2$8¯Noe] _8M>[D3R4rJ[ 5dvݑ"9p[f8hQ +Ob[`TqwRH( ~J`\Nf7x"2o1:*k+0UK#(!)Vpb5#X>R6¸&p;u7<25d΃Y13,+'c +`UlД=bUj?J̪BCĶ x  كV/kkYc( Ъ1TQ +j,U3NKo?dљJM3ikDZa4bsVTq]S{Qa42RP K{ӹz%lF0%9-jWzjO0Ŝ` ETQC$r@g0iVP% 4}qHѴ1BC݋[{ ֽNi"'1 +Τ`Zvzb]b 2P )G XC ا s4(ix 6AGC(W' fA`c!]?0:1ļL9ڝK^HZ(S沒S8/rԃ.4bH8S ؃Ĭm2{ dmfjK$yy#f4b0ӊiZ܃ )NtUIƉ%t;.ȎLr3ʦ<4GZ6.,`89N0qB.@n'I[beز9)PrY:x8]JItȱ]߇4qX [78)%!Kݣ~`sFFZ! 40rľdžR +EB(V.GFg~DwDG"`[l;Չ(ڛ&6ԉ01f;$ *TmvlCԿs"6D$ agE﬷ٷ&[P[u." m{ɚ3<ϩEXvhBօZ.bȺzElд\Ě"."HgZ>bM1z>bq#"QHXڎkGTr7> mGa >"@ߑa饏tl$baM?}ĉ/f+p]HB1SOeE8V@Tl ȜXI#:UɇFC[dL$XTjuJS9rA#@A*c9|TJE38ȪoI-$<,Wh&`ϱIW%^ =NLo1tN1%%Fp/lIL2-4D}ic hS>x"IB8Ə؛Xt:R/JcHq-Q' ;J?d2hJZV˳G #O)d||8G!;/勳==$ +dh;ѯA Yكjyg{vg3Um:2 +i2$u]1zKg<-skЈ溸(04#8lKtx=gȔEY照j +&1Z1[,ExF)߇Zp#]_1tMB9H{*]R }Gc1#$h8h}J7*@\E,Ov Jc ^p:ug85 >.ԩT8U˞̓d^u^c^jskM$|/54_S ?gO;;nqx/5g陵n+{){,CTO*mix4r!&lBNfO@eq^%s_tQt`!d,/HeasKFQ9SpbcPYFPh=tIcCJY"vqF[D& #5卜i`?CV4sb~V#71 d@Ң,*+sz +] ""Ӳue.-S^H&hMYx"hNζbAYPmh&bm.Y$ۊlLĠY4mhW ƾmإ!Wkkֱ5*T3Rmmvjd}Xb6 c#,&UdRT +,XYZ‚oÂB3#\l'[7.qf ,/8j,hXbPL% mض, JX0m`[`DfmȺE `dQKX4k`y:75H8 b$&i g#,؈}&koJ}&&l+T2mLt"YwuXsq <7Յl 6rY5x_S/}M&k1zcC&/puS`sïmFa[a;] 9$Ѳ"l+gzlklU5@Ҵ&aqg{ܫ^mS`Q݆UѴQ*1SXqNl%d[PL DJmclEYzT/N_`5r{66 ĉv[N|HG +ZcdE"gŕ0Azp(4UD"l?omGP V@=)3GhNb2D2>g/wCTzcKuWi;FoL=,YkgdaKKzU4mX֣^XAykBIҷR Ħ + {?؁.~^p8tPqeׇ]F2 RAݦly֫u2>b11`ľ0*1 S@^“Aqr.3ļ2 VCD$`CL/Cp(*4CL/Q^xt!ˀxF0 Ax1v±lkHFjpyրB3ߩz58/űݵ̟y͒`*C5  + oRn9tKTh^ @1@=.yeag``^T?s,@:א U?BK)Yɋ٭9kd8-p钬p >--Sr0^CFRmv`!RVeAF,ۊU ^Fy&ۭ@gQ!ѹ_" ^ x5<IVxa U9-!lP!1j-jz-ƅ1d5ךG8e$Ck +WZ8W kWDn3DŽO]w U5 7iÙm@KBd<4K֓8%nXhT^, 5 +M6 *6WP^QfqmjGFUP`nW3k-(^OlTa"D݆37%nvl䔽oV$!*&`a h0^op2S+Ls#CH:m/볌tӆ5|~ä1k/]L(D6 eycq6D@HxB89~((@CΦ^@d+*bUP,x,|rĬ໤HK'oq!|DA F"o=%yiixs>әi mwm9 %7lGL (]828+Ё}X]BFЪqzH+iY $`G^wI" 4e /x.Ce. fcF**.:x$DKdpzW7 ]!U]X(w%f6X'#]%gb$ +YG@F ]^zM+ml&Qm݀53jv:Xm:(gr쮉[-.m2HۅyQaSAcIr·`2.!c*ޤ&02{YK5k@w1>طƙ.F2=N.ݰ#]vk1d$Tlυ.6F@h5qUk+0dӌLY֜bل1*Cф_(!bu0%f{M߽&ӌ]bq`ט`z'!viwإv><`YI؊V8x;oJ."bb>e^.EGikss@Cę1f K +&;!4ٱ5.Kj. T!ZPd/!u^j.:/TKM_(cZu^f%^Şf KOHb.Pei\#P]P]a&q-&: u`"u|0呺D%% +2{H]f\+Tɲ$BN%$qpXF +lմ EXR>WZ@]RC'l-.Vϡȓ#]%-Y#8]R s2\Gت0bMς!H|vޮG0%t-RϢXlGz K(tV G4=>W)Ղ,e)5>p +++ӓ[\3Mph˜6}LOs4ƶsE Yf^M1N7=<*ʏN9H!3Q^ WIW\Jaɣ_LL*2 œuxDp&Id!T%u7!/ U+KӬLx̢RAϱLӠ7(%CFOIXEBΐ^T : *{`%L^Tm S$ݫP_IuNyixk?6,VBհR7'>K% IdX [/A8MWmO/ö;ɞemRx0^B@L߳I&u}Yfb$SӍՅ)J]6T Z)͒ńe-$Xl7L2/!S_+ AB%ƹ"d~j"M!Yu){BApfyɠ=!n&If2 j#yLO˒ \q,vv@Y|g{gG 8W{؞"fIWOz t+$%pe>Kʷ.E^o7ɛ~GS;)fר./ltII [FW78IӨ +endstream endobj 29 0 obj <>stream +;. 6}n2+ lcO[ٳ"j퇢J *U]IT!w(8Sljx&MM )8_i E5$TM6 DvOjqtӨ7&e(۝h zQ[ncyЁA6Cn,d_jE RQK)ctd2#T79ہW# yU$YQmlb9$?1<){j*Jvݲ1pYQMS))p0E/ O9~ւ듉KM Mex E= ˈqbe)(mc) f^$;h]MAU7 R=SL|*IRdhx^-r#p%>"LdXc*7Qp!t-7%޷|e?[0BbYg(\A(\ +A5Sݹ;'J-ֳ"PH&)Lٸ"o'C FE$Jf:^12;n'$ch6HR1`/j Bܐ8qe>i&VbPYv#q,NTDש e)FSCj0';:&&3\s%\.bTΒ 37HD x_ZL00(wdذClP#Y@ fmHl :RrL.1.0"xFwR$#7'P*F +惼)16Kkp(/[ޝL3ϩ*l8uwPY KIlb^B#i:t Tb#I=MEٛ"6i-q[/9D\[+nIvZ}ӨFWqomܞ%lm۬Tn5UQ!7M8IB[&jlU9$T*B5HWLoLy$"_of},W 6ݯCj3Z̗'ך2a +D57`Q3ʰ 7K<,QU&p#R*R'j*$a </TTʆbCDD5Q=|aزH-&JEnJAUZ}#bveA5 9,ffU8RV)\~mÞXu ڧFky,HBC&,*]ÎSX|$j(G*;]QRHuM,ajyؙ7Qvz OsS_m8̱˾pROI2 +ysިR&""d 8H jNwgKQ9s_brΐq @Py*GxحC/p(}9D!b4ӵ`m<3}aGTJa%h8'ܙ2]t5dt9[Yk?5@-]DoS9 + +׌{rp>p`RCD>e@ҵl>IIBSS2#4Z IY +7`VFqE +t cIn||9v$shʝ#maֳ*۾itc=H7l4&h6Ep&gWԂM 2"u.[ 8,pYR((`dA\( +ɥlK6ɖ+yZyg=NIB("' + hNA"=d^f5x Ӹ\Jw +U""3Str!d Z\Ɋh1 9&kRJYFu*rk%Vd[0-$Z " )(Lrqʓ.q4[lKs[zO)"p?5 >c%.l8Ec4,|,X $ìf9f: +%Aփ}QV&lF) +LfjŜ 2 fLɔ3UlehdInt5~'L7QGImА={$FA9q޶RolPIU-/ۺQwqFUm;19knlQC/>T18KQ#B( a**V ;-& fET<(>kDx)$ ?`sJd)>Z RJ3x#1 j.8l=(7ogktYcxx`Kj[:7%-mSä&"(utIjsԢǠ26eeJqFS. ƩO2~"ᙧiId~bѴW9ML&ೄ+*%8e\49Y-*gA3IIkNHw0Xfe#DO)B-xis\N kő۱y$KfoI Ѣ7nC YJΕuùbՀJ?\I%YE?4a{=摵[Vfht_c^ZeDf?6p=OCtPmJYEFH2YsS3Q"h+Ջ 7+{!"3)Fd2[O /@!QN( +Lr)[o \#8&D9AJ3J S͠ +q?=ޛP BtsBPҬ&̧ݥ}w(:ҿKѮL/ iE^)5b.>oqφV &p?ӭ]Nh|#%h7OB$zZYs 3}2 +R1!k%C6b@65 l 7q'q ^"&7hl;h z5G|\9NRa8qr4>fmC| am _a"Njrz6t/FezD!IZM5= GEuq Mw I\^n &%>7"miG"N|Y"BS\iHAt| ae紗I6;ffKX7H c}IF)A+ +|#8&ҡ$` 7E;fzg2rW)ЅAL"2K7񹦢 r>tOn\ҧ[{v및}n |pFgUJ \}nDan0>v5*c5"x)%\%j#5+TOp'`+ybQiOƴGSԩ?S"Ot5G`c`jrcLL2>E9gd&]M[N+tto/o<.eM/E pi ǫ:%aw)@S„34gMPY jS"pq.JhFP#\>lr0xh4xLF'g8FB"+''uZj&&[O ,"2Eꐚ}v\4JF"Mܮ gnBy/|_xtqS]&%?L7sLh炍-Ҷ]NA ̭`'9ݮsJȋ͡N7~UJvI{M*y-]- x)upFp::e!0rZ;~$2QiQJ)z4^Lt3PHY϶7~[^N+4k k36ꑢ=I`ˍFMu}S1hɳg wX/ӯ@f Dn{l}>rUbμJp9ԊBˣ02-lu˛v89(ͬ57M^\+H *\kĜ&Q/T^ھXC5t+ڻeQ~EVυޙ˫˃CBAg9r9HknZO)O.4-K?MN Rr$*'|0GLQ^:d"G7B#)=4"J`24ZL 4Ip + + *r v]C,w:0)Vb~JW̷џr e4dJ'ZɛXA@ř`P/0? DBx +P62IPM8XӾζxV>>rGywwG>ֈkY:9 yJ]u3c܍1npiu͜F8}ͬ7Μ&/*,XhFjl"V~|}9]=+g]Z{Ff*n9U53Ӫ%fvBkod3Iy,q,n{̲}e3 S6AJ*9\\_u3_灱^cV> >t$H Iݓ `܌ܱțCV>p6߸)PUA{CFy>'VQǨ xzleGp7rΆ7:Y99w9#Bjvm ٳR*yLht=Wo>#U*3߲jTfrRXӄ ܞ ;ֵ[!G9U04~ʴ[Ɯ֗>~Je;+?w_㽬 +:da`GI1='Kc"v~mV(/F2 V 0#@`Ħwy%=UN|`|5{5f> r;Y)CщH]+-:n8Yj409hef)je&P!K&@^2 WJ!xW^2M 2w>^2'H1ʫѝ9VɲIwujg +颌\ MP)U/3VDŽg _2ge3.X{d3"i` UL$)e cG9Mzƹ9'_2LLF5\V45QnLq0hx +<4&P)j$leUhm*bn4j&4il8I<=$r "%0qܟX)T#gJFOLMӳ85t.bBHcQ‹ϫ.8!Ky2{&J_MI4@w;z_LĘ>FSyi)KFk*W'w8w)AO6F]O)3.(DGM%C #89J +PɳeI +˄v~.D&rUOX -|zG2X4E0NNۇL{(֯q@,SN 9Oa[ҦM} ?cW o=&u?ê4)AWnˡ9(&VYR\ Hă 輜&8Pt<#TPcl@SzP1,?_:><+TG$vbdxO O7+7>1<C}/`@_⽬*|nuL5Sg̻jufqYogz*ih ׊f9E396oIc$qyVKg/*ٜkz/s&I+g5b!M+(;?SmaҿaK'vp1\s8;ެ^r8&+Y!I9\7oT2P0I9@N0{-8&}NgRp:nWAg( Q}$ n԰qfD5?R{̴L1& 猿HfZĻX،C"L>qad&o4B9ܠyV4Jx诊y6|NU0y/evg'+SX<9*!uxgh~aLh[;'S;*͉užսv2[ؤ]n>Rd ? א*vQ|Wg$ԋbfeUҫbx56ukȷYͰUQRW$3Vbg 9S'yq5Fa:&:mǘ]4d֐t,4loZsƾ'e!'JWEy3^jf1hV#>cG#hY|*'VzYz~5x[Icu]Q~V#F#Ѵ'Z-?FR-G4G79~i6=<0;owo*F#cꆞ79"FwG4:_6}MgԉVj<.GKq]jF:htZV0%Y&/ӣՠZ+YuDbzf^yp /vlV\GXE;'< OD69 Pxi:Xc5mdv +C{:MPlȳXX.3 X\/k)"gxɃ(P޲tj,G8Lm2M\[*hNO+9ƵdX5n@8IlrVڡ<#Epė#ν\cj7={P1GuZ\\ez|,/q$ӹXs)Ge(#\].waN~uڟbU>j JH5 ȁ9 S +,[f+cΐ.QK+!%:)z4% +fm SZtߤ!^t( T79)>NoK05/ ̰2_\nvB[8пSQDKÁ&bm)3V4gf4 de=yH)tjo 2\o10AˠHff.p_5b6Uo,]s%AV)$|Jwm53gn 렑FZo'oɤR s*cuK&IxaG #fNIhJۈEQqzM3u՘dƿwon'o +_OCs-8햷nՓsOr )!1:fs8>Vw+U,_%CƏD|(%8ޏ^1(:b2 +.*"]DFAS'X0T`adff`2-1JO qenxPDrp$DAcO64F2GwbcJ[9c1;i@cb,˝~A1P F4E7,غ %6m.N'cSpE뀊yaN6"Gd ?7?]\Tk`,~lv|x ++d.-b1[{|Ml+GB8dAWd.{U/ +:!Z< Y`+2γ]h\]]T+|d6I+`Yq O;b[+w~X-WV`-u*گN`>o*nޞ$jɅˊ`{b@UP^tRd.#FkD +.-FfX!X3e|2t)roqHHS@` +"Ƞ{RNb)ck0%Z' H +cA?D.$(PZKFAQ Gr(H! +o\DŏQ,Dq3fY~w&"Ə~4.M"FUPg(A'rGA4q{Q 0"na:Wš +ύ(m:Cp1̖n0gidG)t8CsgXB*cڐCBo~CUjJJJei|ǝ1ɺ4p䬢!N'SʝQ7";C <9.yFTN@gD!gp3"b].(cx`}ܠ#7~nQW:8$;-ٔՠh0_JQj.X*X6-hdRrA:!Xc /P.&` ê ܄a;-j$ѴP#Ӣ |~WT& V(|M&A|*t 4x4~ѐ[㖌0so0p.E]yVδu*7v8=mpƅcx>NryvwO _; =FT D?P_~kw۷0pn=}򑎛N5G0Ei_lrm[A4T{i?~AYada&x!)nZ~+ t˗P'GJfa/0tVzgƍEg"W/?@ZWUg•H"7hv=+C[hz_䦪iT)k50 +ts4)nhF 2%ёy* +qj!1mA*ejg]#c|%(@9nF3q=bA̸[ +>Mܬ?MۛۇmGuh?2+gAT׿8biNiG0Q:܅b^*磮 R H=Uђ;/ŏb =C!VQ,\~2$`lQW)ݻ(z۠ë(*ƊPXUݕPԙ邫,Bl쀫QtqRbPN#WM.9h-ejhn*H~2X}Vm*Xmv5Ye#yV)W@1=p3ׇ8--B~hG6\)zŅUBO0g2Z*#A:ov7H9Ubs)a8׃9v +"@4NG"f94 +:PZaMoq/aJhp#mP*otd 9Hn\`J;5tOwWo~?V>EPT^ZoW8юH] Ջ"tsfC`2I+,1PT"?䂨0j#UqP͠Vamr7D$7zTDkf|;b @U(zv&u+ ^S|Ax;J;{ 76.}bhFz,hOіa.xĢ)Ju;u@!p۱SY|7s?ANm7'pSS&TGOm +zapY@VT7Q}yýd*(\䔑hm)OLȑo)cQfAd 8E'? 5v)@>l +  i착僾zl,,'c䁦(/A@#[@S# ?ܰ)o*ͅy0<*G$^jR1+Їåe8.iM?pGىnn?j~βyx>rqlRд(pRfC~tEK : Y2,e3bRxJ2tlݐR[zYx"+>ҎSH~c yC-}y x&j:2y%CIhf&%s#ɧs]aR\'XWCsQRh0wGIQQRjMnEPR? %Q?'J*jFI}%~q-r5xY]=q 3ҺqT.i32kQ(.z [ƨՇ8ǭ h8m63+Vގ(+M|Iot%cpowC6mFthhCaTEgW +2(b` HE4U=2MrL hR`/i)jąnk.6gUHwZD~ bZS%&+#9&WAi\M]i%'MS*xAPRQɂ6!&諧_ðR< +YbXi1Ԧ[^A@]k=[?o=5n9ʛ,R4X*0b1gi!"vF<, +0sτG/~=/S y]="GkT_bS ׆ؕ6DŮ!*v/SKU_b@صvTŮ*v Vrip(ՏSȗ):O]ˌ t'4Ɨ]k|-9_ +4df HJy5ljg4- R2]LJ{̨3%̈a&lAvQ, B>$d/\xWĕp%h#=/[!3B,$^nOa4w{bOL%_vքڌAb])~+@3XW/eV(rVqJ? + y4gG>Na]P4KE2R' ܅ 0ji"qh+^J^e:ȉ=S-W2+X[w%H \mJ^ѪĤV,󕼬2k?/e"t 7-/+ + p&n^I +ד;ؿMI}|A,UV'7Eb%ש n^tW+*Sts]fTLW~pdV9.eQTPted2?OvdPE֊vqr7Cd$AovٗUHtCK}L]SuT +Eno]/T5_K}\EVZgv*8vȠTM|wv boO;]F'AX>XotbM䁢c]+VNo%1.eÚ  +]vVVe=7Cs2 +]4ЋQt'cW芕nĪHBWM '}dOL7OK_~ࡕ/Oˌrjeìl#B0taiҔri~_KVY#W4FU˖WoX斗C@sf\_VO߉~1/usnH}~<3\ nG_Nkz3cظgO(ptvݐӢm{(ZrZѮo%DeJgҭU|D4oiqݹ:nɗ2ϘijiG{ǯ&fO6>bqve**\tnV|4<ŭ=3 9-J.?{CN+Dim+2!.i[\$ TJiYqvL/Fd7*t=aSZDEFMRzEecĺĔvA\@S~`;#M %]t¼mP}BZBh} me8ǠPg1ː -lIoaQ*D)-}vV { "d( s$"E8ʍ.Us @¤ۑ fMfM*JپYn˰kv}>3,\jM*\$,U4t-{FJn,(CQS 3κ\NNqW+voСP3w2/04׸6 lyEB| vh9{FϧQ'L$ +6wcth'`Xwp]S}HscZ=72RLcADei&KVuM?owpDnS2߁l-i6/ ,g,ziTQ:NhoC>V%|c>D6 ]t._(/!#oR-Y[/rc>n]Eq{zFqtZk. vzFXG;J({)cUB xPk 5s=P +"ׄ(@cmהh,vޡ&/u:Y̲X7p zG%~3JU [^+&Ȯ[DWV=P"hF@}'I: +eU l)Iλ|#̼%%@Yq|ύAw[fUwhy9Mw_|3D&84:&RZg !sS"onw:im@;1A|FN/CSLܡfTqt˞'{ָ~Vh6K灒ˇd;f~c[Q @{_ =18tw?>#ײ=(L1Q {E64 +2)Z[EPug Wvȝ;/Ҽ>iιc$flf#]9)SNb|)sn>앋nWSN!]~:!7 i/%X^}rys]Nvm\HYq!S3voxq\)c ʯMLF涹o-"Ǯ4+sV#"tdtJ`̞oMŻH#q3%n}/o(ۄy#SqRaԌܵrӣC)Qfz:|\qQz{|D "}?ʦv҇8_e훇ݷ-CXV#%BjE( gm"(y{ȨxNCFxI}Q?6zve18V#q&i#4چ!#qA $ͮ:o6MAc%ZT:GUFFmϔ)9`p z(;UTv#?tX +UˁnPJ!PVEmogLXw5#Ly.E'(sB7-{FPmLf3k9GӁ{7l gIY{}">_f(rYu~>9ȋd72<ljoz/K@@CzG|xpTrI|'n،8! +C|gFPEC{RGf`%La%"Gy4>ͻHB5yfkyS 11T,Q뢸gM7S0 80 =PE {FY)5 L#^WK 1Tb GC +'W x+Jͫ6KϞwR% +&?0mFC`~c}~됎m9HJ2 'GjM` +Ebzþڃ #p&h}g7ԍXaU +Yc OץisqŻx~}d|tt'K &v& +CRCt xE +Zݥ +O_(P a69rżUmU}A]eZEIc|eOD\{h̀;˜mkrdiwͬ leB;'#!1 uiHq%.`OȆ+JXk@%.5 zt~N!(M-epI-0=>ڨR[C[)?zhS$>wYVQXsHlm٘/;q W#7_}E`t>7 ٬aRJp!7ߌ<',&3Zn9kq +s"._nH|hɇ6-׵|qOd!wYh:H @F$`C>]5W3$4mk:Tqp>F86pR]ӡʴ'{[.ju'+\:tT T`Z> BHS"#hXHf(WQt,; 6S :\gH3Uԡ IgGY4ZcH4` j& +A@xG޿f~߼mi|+ E#T"Sto?L*[zd3Ϩ동gX{4<ɨ%vmp4hڡn8Znv"m +xD6 &A3FLYjoqy3f|-ņ 1LСv3w(G͔ >__?cZEj' /K}Ydn;T1=cq"8~}uhAYz\|q<K┆![š-o38:j]:ZP&:I'Q bvWOnn(GawAo7jdzk0&A_ :#x#aѾFʡ tgVgFh㰴ش|ڧTDFʍ IZz^"y#mJ6#o8SP־QZЪ"=,Z|}?/${:HN(WxxQ$}"аtLzm;&*є+:pi +Σ-Y +FbSe(35d;ܡle*2zU§@Ep>37MfY zam>APµdXÝEeA!S fAPǛg5p5)kP8N3zFNd3/htP)TP}^DOCI:ʿ=O6 %]:_2fIFmw͇3?,(JPx;KkY1 2r'D-}&&l +/j34w@}pW hT:gOs/Y #@;Ve:91vU\@%S;Ml,ȌQT"{Xki߲}[iaFfM$ aIj^@+R/SJ/rg22^(ecE1PF[)g{pLfh(C`esFA:*]\9 "!mOS  SR6piai>g>{Hn]P +M2N ܆o4ibdY G<ķo5'sq;uP'W/ ?V)h>FmLk"\|Px^+XгUL4b1tz$TwsjWy$T V`+*$'֒^-ݯЧPz7T`)ZS9Ɩhdm\^ :η9.z$rof]Fײ=EJ`30uʽN ii6%Z drm}(zw ?zekP퓎DG +h[D2E< eID7h# YΖBY6ѐ˺P-(M I׵@bHϷ^A~Dk;MZw(CPbwwٞNw +$bHaѲeYE"UtB'uA oƉc0B]<;2p e`c=˗\ , '''e/zoE*+@q5eaRQunKBAc6G=Hv3JhU si8/%(I(# LQ{mp7 o6mWٖE[f_>2Fad`q7z65+tO$oX'1+}F5YJñx`m-x-Ę+kۍO +fl0)XCy`U2Y餓یtw}Sq%aM7lpRno8XO.q&-+}`v3jղx*[ghXx=Fo9XzGP{ݢƍu3p9ysoJ0U7e^x6g}ɼ?f=nm"~>|C{([!qΌびon[OϨ>v6 (y CF7G*_&mmNEWf)(k.Q~oB3]6dیjǗ kaִt Q-&2M³͈ơ0f3:0D}_XXAֲٓu_،*3Zoj[;qkr_XXC,a~jLZJ@/-{0j=x. +]Pb`'jT5{7I>{F5ta3n++U6 T@!6i++hmz4q&<7Bš4)^@c<_N; +*rFQ啃Ut@Օ + %9_9MUAg^W,Wm=#XA܂lK"4+q,L=rvf^9Xa +GF6bP++_5SJsS}e`1[V=`s7 O`{l1Iʓ[ Vbܯl]Z%$<(JSe\z +l7eèbAl~21ȦS8O~MZ՚zP;D]IlFnw)Q7q :Tɪ͙yEM@s) +FOu|)+~)l 0y3%] ʀLOZ[5IeQTF}XGL@W +|kwoԉ{nF)Tj#PS4S4qK[DkF0n&wf΀ȉ\RQ3`A ==)\xD\L[j^~*7` ++}\)@R,xu)]ՠ ֚qكؔW'+dي"\ +4Q':+?ϴ׈w`!HuLLJԐ/޹|zf}&:L8*0HZևQ9n_3 p;Ȏy"a-nJ {.t#-.f:?@8 {d;im|.تk-3n2΂x8m;^u/_9>wӳf8:א`M{;}n r:3 +!\w3+qomWt͊{F^“)vÈrҌ啩`ˊv '+ڌ1Z"{Wֶ3g{Y6^V9+͖N-# :r+6)jyoDj-9,a!lF/I,}A7@,EjTB .擅'>ãӑJ9fwHtRoaVX(Bzy>-gIHi6R(L}a`u/LVzau7^Xw[$9 X<,jloYWF<{FCP$};ez=|a0L צ>?"J>ŲyFn<=rmJzN.zBJ@Zt_W3kʃP%kwcӹ?zT45fz*+hWUrXp8+)3B|^ HebEN`w6tl󤾻 >]{s+ sK [Cy +*cn&ǵpqe_FԪ)S9M=sCu{)<@7eci{_7Żүވ W9 7VURny~|נ&}[eRpbOyݴ-ьwu!FĹqFhWb>sV|M* +A)DÓޭ6ai܃=fL8̀zr @݆z失H"|Oqzm7if@j? Tu +]tz:ri:ƻ7Gvw,27>k{gьvرpʱZ|m孿mxun+?hWz3zv$\*NުAU ԪBES 5& }ud7gGz>ohDaaM5 җѹ,?KAQRԾ"E͎]EʖBҦZԘЍgG"F=;b} +ƾkM\{4*;WmoyF=ϏewBӛѳo*4QpЇm2ʏph~!S#:f&r׊ZQ1V\TW\5 /[eHpwjLw^D771kWpg@O#q"cړ6%sdpf$kLZ] pp$Gz[l8cS# jW3|Ux&gť; ՘^un*_ͳX\3%W}t\LtMHّf8;i=X%6z'v #bЪ,=WuGm}:=VZfYoծd|sZg۠$wRU̚t/mj3@ڄzjr3fX782s\e>>USIMk8 +*Z:J^[UjPU} +>U-|}]kuE_2-UzDm3y]Czn6y^pxt]zzW2H^#yТy= *GeVkF:@V!<, TN!m׽x!bH c]Ѫ3U6Yk7܍$dվ;}ra>V_/ڔ7w9e^m֯9:V{)AFfR9gXcyVK`oɑ{MȖcyF{E5/"ؾ">ǒ5X5X±zɱ2ӫBorNJد+upR7Ǻԑg|bm 4Na/}pX_ {:r'UgusQw9mȦ|s[c:j=VYx֨(>_wSB쪁<vԊ=K* YDiM9juț#WGLh"7T4/rOSX`v}~pZ]{7d{?!tqLEY٭c,MVȧq=d/NUd,*hԼeo*FfZmؽ#F1z/-Bٮ^}E({6όQʞ9 tl1q hdOq˞Sȟ?'.]5}E +{^w兜-_m{P|#&%6гػ& Y4њ6v1iLjE)4JW -` +=.I&l@jW"'mDkI˫Q: Y:DNW$[Fה!ck({dolg=h]JhS{C\{83XSκp>:Jއx64}ߟۛ`ffQ\\:- +b6Vʫ^st6wlLJNJ+kfcSyތaQ3he$0Ͱ䚔bɺЇc(b?hٝWlhfkwh~7.XhhR +& /GvyP^9{yޕhm(ItWbtTHUW=m=dO}lwٍ\5JQ\nj]F=bԶ`ΛUz8O=[Ci7&>,v}9CGԳzntl˛ѳkD2uZm "Dl Ei;Imhތ6k.BePy/)>#ͧERDUxtî-γoڛWB 8FUH1ZM(C,wlLʭ7k ŅP_l# r| +V925[e]ڮ-U169\w{vYQP8`?1Ua֬"Z ڬ<\b9 $6mY0+"Oү]d;)15E|VbKm*ۻ7GfUz.}N + R:_}ʞ]+{K#B3ˈFjۻ7GnEoO5j.Q/ +ne@"tʳ=0W'ez 9rϣ*̻ < @G{7Hr;?jrBAB\mr3tӯI]UnV BxF;rU*75,NsG=&4UFNJsht>7Ymޛ;F̊FSoU~MrM5Z7M0{O<=ʳZM{ˈOM{K}mݮ{yEJ!lV%_+VP /1_g~VuZOUmLO12/-x Я+m:$O{ij'7vvʝ2˓Rgd='ًblqNK2inrݻ7GUFyd>FϮ]u Mݹz?)5q@7l}l+2U\fHuzߪwongg.v.V壾:gLNTceA\fɚHv oLkx&oVmL* 7]dWudg1-O#ŗ)bnifv`&nkԼ{SM v#I9W-oyF7f`e<9o,~Ȓ֙Xkڮ~{8_ӛ4gMI̞R۾ѢؽǮB΃|=DBN{n&Us^*lVX]xw"!!gR.w/mzB2g@C#tP >"$ZG/0EI|F4 RR*th(>KF]K|biHSFg!OLRyUbDUudڑ2vu6ja2hYĿR-@DWMACd]Ru{3ěҾ~ )%NsBicOqLtg90O D)8&iF𷺲 u982ώ8V#TdbBKf% :XejzM) +*Q*pF]^ WHxi +mo=+0>?->0>LiX[.1>ۚi3,X$'"2zUABBd0<< 7bpDŽzj*tOh|֠t{q' S|V O%-li]mXAr^nt8v8A+&&Zy?|XZMdWs|RMЮg_t|Z٢z;]u|6{5*;>[ׇͅVѳ/%Yu#F4Vߟ  9&8*-$LjbKtZ$YN?uJ}W%DŽkꃫ[I,9oԁ$`;U|Z)і"L>(񌌸+cb3Ip@{7eq\r&=LG(~-&@>?·0)o(|d"k$Nȫo(PW +nR- o޵1Io8|1yToF)W_8_8|]r:V n{!}^龎s*wn*TQ{Flo;t*hl12Aq;Gf|l/Yof-!#q#o,}2sn}M{3zrN<&d>K"/D'^'`;h +TtMș_+x>^|UuRKke;/WwB#H۶^gO+*ip-3apʞ3gwF UlﵟlzPsT }jtXHw6)#Ѹ:״˟):γ2Ge=>Et3l%gqM 妁֬suRLe֞]+?8t^Z*&I&|{1t`j4SL,Z_9R'0Iq*f@|q:DgOc;DŽ&`i올 +4񬸷QȮ$:VLϯ4VVwQWQ=пWUR 3.zȢ8`\h@&0EIgMvFEpUu2:]&f(+JN6nmGߍ6Ys) m_>( + ^P&W')z+ON1L`\%m].=-;:Zitq[2vItPV\Hwdҿ([ޮ> +l\L1M(} p&|QJGU&GM+}wڰmb黉E-E..dʮ>o}3G=]X6~Li,pΪU1cCZۆ(K`{q'ۑB!:[ 3~::'2ZZ˒ZW[يϧ^԰ik +hTtiYH>4mv v ZyWV-WڻPkBk5ޕ޻Jnq"~ytʜ+%aJػR*ϻ"t:E]aJFt-jΕ5~\%+ٹBj[R5Um~s.~`7{oAUҵ ڕy`Oc{MZs_=:m"žpRJ9PUk!DBԫ! 쒷nt"`,}f? ~kXw *m +DEP (M.F{mAIa}a  F>ul{z`:K tu"A@NSꕔxr+Ra9`]ԉ +n- +1VX^. 6q!#{*D'YZr%fryCd$q%k,Iw߻d:M߄ZWV{'_<͏de?e'+h ?(A&/Q% +endstream endobj 8 0 obj [7 0 R 6 0 R] endobj 30 0 obj <> endobj xref +0 31 +0000000000 65535 f +0000000016 00000 n +0000000156 00000 n +0000040398 00000 n +0000000000 00000 f +0000050472 00000 n +0000050100 00000 n +0000050170 00000 n +0000228217 00000 n +0000040449 00000 n +0000040888 00000 n +0000055729 00000 n +0000053045 00000 n +0000052932 00000 n +0000048917 00000 n +0000049538 00000 n +0000049586 00000 n +0000050356 00000 n +0000050387 00000 n +0000050240 00000 n +0000050271 00000 n +0000050843 00000 n +0000051159 00000 n +0000053080 00000 n +0000055803 00000 n +0000056021 00000 n +0000057345 00000 n +0000061673 00000 n +0000127262 00000 n +0000192851 00000 n +0000228246 00000 n +trailer +<<790998C9BE9E5F4B86F18795B5F904FB>]>> +startxref +228431 +%%EOF diff --git a/docs/marketing/logo/PackageLogo.png b/docs/marketing/logo/PackageLogo.png new file mode 100644 index 000000000..047d6ad64 Binary files /dev/null and b/docs/marketing/logo/PackageLogo.png differ diff --git a/docs/marketing/logo/SVG/Combinationmark White Background.svg b/docs/marketing/logo/SVG/Combinationmark White Background.svg new file mode 100644 index 000000000..01c984d3b --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark White Background.svg @@ -0,0 +1 @@ +Combinationmark White Background \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Combinationmark White.svg b/docs/marketing/logo/SVG/Combinationmark White.svg new file mode 100644 index 000000000..83d4a8eb3 --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark White.svg @@ -0,0 +1 @@ +Combinationmark White \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Combinationmark.svg b/docs/marketing/logo/SVG/Combinationmark.svg new file mode 100644 index 000000000..c2e33c71b --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark.svg @@ -0,0 +1 @@ +Combinationmark \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Logomark Full Black.svg b/docs/marketing/logo/SVG/Logomark Full Black.svg new file mode 100644 index 000000000..c7856ad59 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Full Black.svg @@ -0,0 +1 @@ +Logomark Full Black \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Logomark Full Purple.svg b/docs/marketing/logo/SVG/Logomark Full Purple.svg new file mode 100644 index 000000000..33a442b91 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Full Purple.svg @@ -0,0 +1 @@ +Logomark Full Purple \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Logomark Purple.svg b/docs/marketing/logo/SVG/Logomark Purple.svg new file mode 100644 index 000000000..16d3789a1 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Purple.svg @@ -0,0 +1 @@ +Discord.Net Docs \ No newline at end of file diff --git a/docs/marketing/logo/SVG/Logomark White.svg b/docs/marketing/logo/SVG/Logomark White.svg new file mode 100644 index 000000000..a8249837f --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark White.svg @@ -0,0 +1 @@ +Logomark White \ No newline at end of file diff --git a/docs/toc.yml b/docs/toc.yml index c08e708bf..bea010c5a 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,6 +1,11 @@ - - name: Guides href: guides/ + topicUid: Guides.Introduction +- name: FAQ + href: faq/ + topicUid: FAQ.Basics.GetStarted - name: API Documentation href: api/ - homepage: api/index.md + topicUid: API.Docs +- name: Changelog + topicHref: ../CHANGELOG.md \ No newline at end of file diff --git a/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs index 50d467054..300784f4e 100644 --- a/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs +++ b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -192,7 +192,7 @@ namespace Discord.API internal override async Task DisconnectInternalAsync() { if (_webSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; diff --git a/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs index fd6b74ff1..90df8d1a7 100644 --- a/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs +++ b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -14,7 +14,7 @@ namespace Discord.Rpc /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the provider used to generate new websocket connections. + /// Gets or sets the provider used to generate new WebSocket connections. public WebSocketProvider WebSocketProvider { get; set; } public DiscordRpcConfig() @@ -24,7 +24,7 @@ namespace Discord.Rpc #else WebSocketProvider = () => { - throw new InvalidOperationException("The default websocket provider is not supported on this platform.\n" + + throw new InvalidOperationException("The default WebSocket provider is not supported on this platform.\n" + "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); }; #endif diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index 42e590aca..195411678 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -9,6 +9,7 @@ using Model = Discord.API.Rpc.Channel; namespace Discord.Rpc { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RpcDMChannel : RpcChannel, IRpcMessageChannel, IRpcPrivateChannel, IDMChannel { public IReadOnlyCollection CachedMessages { get; private set; } diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 1c9355047..9d484c25d 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -10,6 +10,7 @@ using Model = Discord.API.Rpc.Channel; namespace Discord.Rpc { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RpcGroupChannel : RpcChannel, IRpcMessageChannel, IRpcAudioChannel, IRpcPrivateChannel, IGroupChannel { public IReadOnlyCollection CachedMessages { get; private set; } diff --git a/samples/01_basic_ping_bot/01_basic_ping_bot.csproj b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj index 5484e3d55..128082edb 100644 --- a/samples/01_basic_ping_bot/01_basic_ping_bot.csproj +++ b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.1 diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 0fcc52b85..7fbe04993 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -1,42 +1,50 @@ using System; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.WebSocket; namespace _01_basic_ping_bot { - // This is a minimal, barebones example of using Discord.Net + // This is a minimal, bare-bones example of using Discord.Net // // If writing a bot with commands, we recommend using the Discord.Net.Commands // framework, rather than handling commands yourself, like we do in this sample. // // You can find samples of using the command framework: // - Here, under the 02_commands_framework sample - // - https://github.com/foxbot/DiscordBotBase - a barebones bot template + // - https://github.com/foxbot/DiscordBotBase - a bare-bones bot template // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library class Program { - private DiscordSocketClient _client; + private readonly DiscordSocketClient _client; // Discord.Net heavily utilizes TAP for async, so we create // an asynchronous context from the beginning. static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); + { + new Program().MainAsync().GetAwaiter().GetResult(); + } - public async Task MainAsync() + public Program() { + // It is recommended to Dispose of a client when you are finished + // using it, at the end of your app's lifetime. _client = new DiscordSocketClient(); _client.Log += LogAsync; _client.Ready += ReadyAsync; _client.MessageReceived += MessageReceivedAsync; + } + public async Task MainAsync() + { // Tokens should be considered secret data, and never hard-coded. await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await _client.StartAsync(); // Block the program until it is closed. - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } private Task LogAsync(LogMessage log) @@ -54,7 +62,7 @@ namespace _01_basic_ping_bot return Task.CompletedTask; } - // This is not the recommmended way to write a bot - consider + // This is not the recommended way to write a bot - consider // reading over the Commands Framework sample. private async Task MessageReceivedAsync(SocketMessage message) { diff --git a/samples/02_commands_framework/02_commands_framework.csproj b/samples/02_commands_framework/02_commands_framework.csproj index f479ee0b0..83a62f8d7 100644 --- a/samples/02_commands_framework/02_commands_framework.csproj +++ b/samples/02_commands_framework/02_commands_framework.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp2.0 + netcoreapp3.1 - + diff --git a/samples/02_commands_framework/Modules/PublicModule.cs b/samples/02_commands_framework/Modules/PublicModule.cs index f30dfd73f..18423f609 100644 --- a/samples/02_commands_framework/Modules/PublicModule.cs +++ b/samples/02_commands_framework/Modules/PublicModule.cs @@ -31,7 +31,7 @@ namespace _02_commands_framework.Modules [Command("userinfo")] public async Task UserInfoAsync(IUser user = null) { - user = user ?? Context.User; + user ??= Context.User; await ReplyAsync(user.ToString()); } @@ -59,5 +59,11 @@ namespace _02_commands_framework.Modules [Command("list")] public Task ListAsync(params string[] objects) => ReplyAsync("You listed: " + string.Join("; ", objects)); + + // Setting a custom ErrorMessage property will help clarify the precondition error + [Command("guild_only")] + [RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")] + public Task GuildOnlyCommand() + => ReplyAsync("Nothing to see here!"); } } diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index 3fed652d3..8a2f37dce 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Discord; @@ -15,28 +16,38 @@ namespace _02_commands_framework // // You can find samples of using the command framework: // - Here, under the 02_commands_framework sample - // - https://github.com/foxbot/DiscordBotBase - a barebones bot template + // - https://github.com/foxbot/DiscordBotBase - a bare-bones bot template // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library class Program { + // There is no need to implement IDisposable like before as we are + // using dependency injection, which handles calling Dispose for us. static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { - var services = ConfigureServices(); + // You should dispose a service provider created using ASP.NET + // when you are finished using it, at the end of your app's lifetime. + // If you use another dependency injection framework, you should inspect + // its documentation for the best way to do this. + using (var services = ConfigureServices()) + { + var client = services.GetRequiredService(); - var client = services.GetRequiredService(); + client.Log += LogAsync; + services.GetRequiredService().Log += LogAsync; - client.Log += LogAsync; - services.GetRequiredService().Log += LogAsync; + // Tokens should be considered secret data and never hard-coded. + // We can read from the environment variable to avoid hard coding. + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); - await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); - await client.StartAsync(); + // Here we initialize the logic required to register our commands. + await services.GetRequiredService().InitializeAsync(); - await services.GetRequiredService().InitializeAsync(); - - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); + } } private Task LogAsync(LogMessage log) @@ -46,7 +57,7 @@ namespace _02_commands_framework return Task.CompletedTask; } - private IServiceProvider ConfigureServices() + private ServiceProvider ConfigureServices() { return new ServiceCollection() .AddSingleton() diff --git a/samples/02_commands_framework/Services/CommandHandlingService.cs b/samples/02_commands_framework/Services/CommandHandlingService.cs index fc253eed3..5ec496f78 100644 --- a/samples/02_commands_framework/Services/CommandHandlingService.cs +++ b/samples/02_commands_framework/Services/CommandHandlingService.cs @@ -20,11 +20,16 @@ namespace _02_commands_framework.Services _discord = services.GetRequiredService(); _services = services; + // Hook CommandExecuted to handle post-command-execution logic. + _commands.CommandExecuted += CommandExecutedAsync; + // Hook MessageReceived so we can process each message to see + // if it qualifies as a command. _discord.MessageReceived += MessageReceivedAsync; } public async Task InitializeAsync() { + // Register modules that are public and inherit ModuleBase. await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); } @@ -36,14 +41,32 @@ namespace _02_commands_framework.Services // This value holds the offset where the prefix ends var argPos = 0; + // Perform prefix check. You may want to replace this with + // (!message.HasCharPrefix('!', ref argPos)) + // for a more traditional command format like !help. if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; var context = new SocketCommandContext(_discord, message); - var result = await _commands.ExecuteAsync(context, argPos, _services); + // Perform the execution of the command. In this method, + // the command service will perform precondition and parsing check + // then execute the command if one is matched. + await _commands.ExecuteAsync(context, argPos, _services); + // Note that normally a result will be returned by this format, but here + // we will handle the result in CommandExecutedAsync, + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + // command is unspecified when there was a search failure (command not found); we don't care about these errors + if (!command.IsSpecified) + return; + + // the command was successful, we don't care about this result, unless we want to log that a command succeeded. + if (result.IsSuccess) + return; - if (result.Error.HasValue && - result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors - await context.Channel.SendMessageAsync(result.ToString()); + // the command failed, let's notify the user that something happened. + await context.Channel.SendMessageAsync($"error: {result}"); } } } diff --git a/samples/03_sharded_client/03_sharded_client.csproj b/samples/03_sharded_client/03_sharded_client.csproj new file mode 100644 index 000000000..91cacef64 --- /dev/null +++ b/samples/03_sharded_client/03_sharded_client.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.1 + _03_sharded_client + + + + + + + + + + + + diff --git a/samples/03_sharded_client/Modules/PublicModule.cs b/samples/03_sharded_client/Modules/PublicModule.cs new file mode 100644 index 000000000..fad2ba98c --- /dev/null +++ b/samples/03_sharded_client/Modules/PublicModule.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Discord.Commands; + +namespace _03_sharded_client.Modules +{ + // Remember to make your module reference the ShardedCommandContext + public class PublicModule : ModuleBase + { + [Command("info")] + public async Task InfoAsync() + { + var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards! + This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; + await ReplyAsync(msg); + } + } +} diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs new file mode 100644 index 000000000..753f400a1 --- /dev/null +++ b/samples/03_sharded_client/Program.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using _03_sharded_client.Services; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; + +namespace _03_sharded_client +{ + // This is a minimal example of using Discord.Net's Sharded Client + // The provided DiscordShardedClient class simplifies having multiple + // DiscordSocketClient instances (or shards) to serve a large number of guilds. + class Program + { + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + public async Task MainAsync() + { + // You specify the amount of shards you'd like to have with the + // DiscordSocketConfig. Generally, it's recommended to + // have 1 shard per 1500-2000 guilds your bot is in. + var config = new DiscordSocketConfig + { + TotalShards = 2 + }; + + // You should dispose a service provider created using ASP.NET + // when you are finished using it, at the end of your app's lifetime. + // If you use another dependency injection framework, you should inspect + // its documentation for the best way to do this. + using (var services = ConfigureServices(config)) + { + var client = services.GetRequiredService(); + + // The Sharded Client does not have a Ready event. + // The ShardReady event is used instead, allowing for individual + // control per shard. + client.ShardReady += ReadyAsync; + client.Log += LogAsync; + + await services.GetRequiredService().InitializeAsync(); + + // Tokens should be considered secret data, and never hard-coded. + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); + + await Task.Delay(Timeout.Infinite); + } + } + + private ServiceProvider ConfigureServices(DiscordSocketConfig config) + { + return new ServiceCollection() + .AddSingleton(new DiscordShardedClient(config)) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + } + + + private Task ReadyAsync(DiscordSocketClient shard) + { + Console.WriteLine($"Shard Number {shard.ShardId} is connected and ready!"); + return Task.CompletedTask; + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + return Task.CompletedTask; + } + } +} diff --git a/samples/03_sharded_client/Services/CommandHandlingService.cs b/samples/03_sharded_client/Services/CommandHandlingService.cs new file mode 100644 index 000000000..adc91b12c --- /dev/null +++ b/samples/03_sharded_client/Services/CommandHandlingService.cs @@ -0,0 +1,72 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace _03_sharded_client.Services +{ + public class CommandHandlingService + { + private readonly CommandService _commands; + private readonly DiscordShardedClient _discord; + private readonly IServiceProvider _services; + + public CommandHandlingService(IServiceProvider services) + { + _commands = services.GetRequiredService(); + _discord = services.GetRequiredService(); + _services = services; + + _commands.CommandExecuted += CommandExecutedAsync; + _commands.Log += LogAsync; + _discord.MessageReceived += MessageReceivedAsync; + } + + public async Task InitializeAsync() + { + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task MessageReceivedAsync(SocketMessage rawMessage) + { + // Ignore system messages, or messages from other bots + if (!(rawMessage is SocketUserMessage message)) + return; + if (message.Source != MessageSource.User) + return; + + // This value holds the offset where the prefix ends + var argPos = 0; + if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) + return; + + // A new kind of command context, ShardedCommandContext can be utilized with the commands framework + var context = new ShardedCommandContext(_discord, message); + await _commands.ExecuteAsync(context, argPos, _services); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + // command is unspecified when there was a search failure (command not found); we don't care about these errors + if (!command.IsSpecified) + return; + + // the command was successful, we don't care about this result, unless we want to log that a command succeeded. + if (result.IsSuccess) + return; + + // the command failed, let's notify the user that something happened. + await context.Channel.SendMessageAsync($"error: {result.ToString()}"); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + + return Task.CompletedTask; + } + } +} diff --git a/samples/04_webhook_client/04_webhook_client.csproj b/samples/04_webhook_client/04_webhook_client.csproj new file mode 100644 index 000000000..c8d0c9ad3 --- /dev/null +++ b/samples/04_webhook_client/04_webhook_client.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp2.2 + _04_webhook_client + + + + + + + diff --git a/samples/04_webhook_client/Program.cs b/samples/04_webhook_client/Program.cs new file mode 100644 index 000000000..f3a50036c --- /dev/null +++ b/samples/04_webhook_client/Program.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.Webhook; +using System.Threading.Tasks; + +namespace _04_webhook_client +{ + // This is a minimal example of using Discord.Net's Webhook Client + // Webhooks are send-only components of Discord that allow you to make a POST request + // To a channel specific URL to send a message to that channel. + class Program + { + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + // The webhook url follows the format https://discord.com/api/webhooks/{id}/{token} + // Because anyone with the webhook URL can use your webhook + // you should NOT hard code the URL or ID + token into your application. + using (var client = new DiscordWebhookClient("https://discord.com/api/webhooks/123/abc123")) + { + var embed = new EmbedBuilder + { + Title = "Test Embed", + Description = "Test Description" + }; + + // Webhooks are able to send multiple embeds per message + // As such, your embeds must be passed as a collection. + await client.SendMessageAsync(text: "Send a message to this webhook!", embeds: new[] { embed.Build() }); + } + } + } +} diff --git a/samples/idn/Inspector.cs b/samples/idn/Inspector.cs new file mode 100644 index 000000000..1544c8d07 --- /dev/null +++ b/samples/idn/Inspector.cs @@ -0,0 +1,74 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Idn +{ + public static class Inspector + { + public static string Inspect(object value) + { + var builder = new StringBuilder(); + if (value != null) + { + var type = value.GetType().GetTypeInfo(); + builder.AppendLine($"[{type.Namespace}.{type.Name}]"); + builder.AppendLine($"{InspectProperty(value)}"); + + if (value is IEnumerable) + { + var items = (value as IEnumerable).Cast().ToArray(); + if (items.Length > 0) + { + builder.AppendLine(); + foreach (var item in items) + builder.AppendLine($"- {InspectProperty(item)}"); + } + } + else + { + var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.GetIndexParameters().Length == 0) + .GroupBy(x => x.Name) + .OrderBy(x => x.Key) + .ToArray(); + if (groups.Length > 0) + { + builder.AppendLine(); + int pad = groups.Max(x => x.Key.Length) + 1; + foreach (var group in groups) + builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}"); + } + } + } + else + builder.AppendLine("null"); + return builder.ToString(); + } + + private static string InspectProperty(object obj) + { + if (obj == null) + return "null"; + + var type = obj.GetType(); + + var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (debuggerDisplay != null) + return debuggerDisplay.GetValue(obj).ToString(); + + var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object)) + .FirstOrDefault(); + if (toString != null) + return obj.ToString(); + + var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (count != null) + return $"[{count.GetValue(obj)} Items]"; + + return obj.ToString(); + } + } +} diff --git a/samples/idn/Program.cs b/samples/idn/Program.cs new file mode 100644 index 000000000..abc315a2d --- /dev/null +++ b/samples/idn/Program.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Discord; +using Discord.WebSocket; +using System.Collections.Concurrent; +using System.Threading; +using System.Text; +using System.Diagnostics; + +namespace Idn +{ + public class Program + { + public static readonly string[] Imports = + { + "System", + "System.Collections.Generic", + "System.Linq", + "System.Threading.Tasks", + "System.Diagnostics", + "System.IO", + "Discord", + "Discord.Rest", + "Discord.WebSocket", + "idn" + }; + + static async Task Main(string[] args) + { + var token = File.ReadAllText("token.ignore"); + var client = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = LogSeverity.Debug }); + var logQueue = new ConcurrentQueue(); + var logCancelToken = new CancellationTokenSource(); + int presenceUpdates = 0; + + client.Log += msg => + { + logQueue.Enqueue(msg); + return Task.CompletedTask; + }; + Console.CancelKeyPress += (_ev, _s) => + { + logCancelToken.Cancel(); + }; + + var logTask = Task.Run(async () => + { + var fs = new FileStream("idn.log", FileMode.Append); + var logStringBuilder = new StringBuilder(200); + string logString = ""; + + byte[] helloBytes = Encoding.UTF8.GetBytes($"### new log session: {DateTime.Now} ###\n\n"); + await fs.WriteAsync(helloBytes); + + while (!logCancelToken.IsCancellationRequested) + { + if (logQueue.TryDequeue(out var msg)) + { + if (msg.Message?.IndexOf("PRESENCE_UPDATE)") > 0) + { + presenceUpdates++; + continue; + } + + _ = msg.ToString(builder: logStringBuilder); + logStringBuilder.AppendLine(); + logString = logStringBuilder.ToString(); + + Debug.Write(logString, "DNET"); + await fs.WriteAsync(Encoding.UTF8.GetBytes(logString)); + } + await fs.FlushAsync(); + try + { + await Task.Delay(100, logCancelToken.Token); + } + finally { } + } + + byte[] goodbyeBytes = Encoding.UTF8.GetBytes($"#!! end log session: {DateTime.Now} !!#\n\n\n"); + await fs.WriteAsync(goodbyeBytes); + await fs.DisposeAsync(); + }); + + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + var options = ScriptOptions.Default + .AddReferences(GetAssemblies().ToArray()) + .AddImports(Imports); + + var globals = new ScriptGlobals + { + Client = client, + PUCount = -1, + }; + + while (true) + { + Console.Write("> "); + string input = Console.ReadLine(); + + if (input == "quit!") + { + break; + } + + object eval; + try + { + globals.PUCount = presenceUpdates; + eval = await CSharpScript.EvaluateAsync(input, options, globals); + } + catch (Exception e) + { + eval = e; + } + Console.WriteLine(Inspector.Inspect(eval)); + } + + await client.StopAsync(); + client.Dispose(); + logCancelToken.Cancel(); + try + { await logTask; } + finally { Console.WriteLine("goodbye!"); } + } + + static IEnumerable GetAssemblies() + { + var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies(); + foreach (var a in Assemblies) + { + var asm = Assembly.Load(a); + yield return asm; + } + yield return Assembly.GetEntryAssembly(); + } + + public class ScriptGlobals + { + public DiscordSocketClient Client { get; set; } + public int PUCount { get; set; } + } + } +} diff --git a/samples/idn/idn.csproj b/samples/idn/idn.csproj new file mode 100644 index 000000000..f982ff86d --- /dev/null +++ b/samples/idn/idn.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/samples/idn/logview.ps1 b/samples/idn/logview.ps1 new file mode 100644 index 000000000..0857475f5 --- /dev/null +++ b/samples/idn/logview.ps1 @@ -0,0 +1 @@ +Get-Content .\bin\Debug\netcoreapp3.1\idn.log -Tail 3 -Wait \ No newline at end of file diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj index 5da3d506d..5fe98fc86 100644 --- a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj +++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -4,10 +4,10 @@ Discord.Net.Analyzers Discord.Analyzers A Discord.Net extension adding support for design-time analysis of the API usage. - netstandard1.3 + netstandard2.0;netstandard2.1 - + diff --git a/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs index 0760d019f..38d3f39d4 100644 --- a/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs +++ b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; @@ -24,6 +24,8 @@ namespace Discord.Analyzers public override void Initialize(AnalysisContext context) { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); } diff --git a/src/Discord.Net.Commands/AssemblyInfo.cs b/src/Discord.Net.Commands/AssemblyInfo.cs index c6b5997b4..bbbaca3be 100644 --- a/src/Discord.Net.Commands/AssemblyInfo.cs +++ b/src/Discord.Net.Commands/AssemblyInfo.cs @@ -1,3 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 6cd0abbb7..c4b78f534 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -2,14 +2,37 @@ using System; namespace Discord.Commands { - /// Provides aliases for a command. + /// + /// Marks the aliases for a command. + /// + /// + /// This attribute allows a command to have one or multiple aliases. In other words, the base command can have + /// multiple aliases when triggering the command itself, giving the end-user more freedom of choices when giving + /// hot-words to trigger the desired command. See the example for a better illustration. + /// + /// + /// In the following example, the command can be triggered with the base name, "stats", or either "stat" or + /// "info". + /// + /// [Command("stats")] + /// [Alias("stat", "info")] + /// public Task GetStatsAsync(IUser user) + /// { + /// // ...pull stats + /// } + /// + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class AliasAttribute : Attribute { - /// The aliases which have been defined for the command. + /// + /// Gets the aliases which have been defined for the command. + /// public string[] Aliases { get; } - /// Creates a new with the given aliases. + /// + /// Creates a new with the given aliases. + /// public AliasAttribute(params string[] aliases) { Aliases = aliases; diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index bfc04641a..d4d9ee3bb 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -2,17 +2,32 @@ using System; namespace Discord.Commands { + /// + /// Marks the execution information for a command. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { + /// + /// Gets the text that has been set to be recognized as a command. + /// public string Text { get; } + /// + /// Specifies the of the command. This affects how the command is executed. + /// public RunMode RunMode { get; set; } = RunMode.Default; public bool? IgnoreExtraArgs { get; } + /// public CommandAttribute() { Text = null; } + + /// + /// Initializes a new attribute with the specified name. + /// + /// The name of the command. public CommandAttribute(string text) { Text = text; diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index cc23a6d15..7dbe1a495 100644 --- a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -2,6 +2,14 @@ using System; namespace Discord.Commands { + /// + /// Prevents the marked module from being loaded automatically. + /// + /// + /// This attribute tells to ignore the marked module from being loaded + /// automatically (e.g. the method). If a non-public module marked + /// with this attribute is attempted to be loaded manually, the loading process will also fail. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class DontAutoLoadAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs index c982d93a1..72ca92fdf 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -1,9 +1,31 @@ using System; -namespace Discord.Commands { - - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class DontInjectAttribute : Attribute { - } - +namespace Discord.Commands +{ + /// + /// Prevents the marked property from being injected into a module. + /// + /// + /// This attribute prevents the marked member from being injected into its parent module. Useful when you have a + /// public property that you do not wish to invoke the library's dependency injection service. + /// + /// + /// In the following example, DatabaseService will not be automatically injected into the module and will + /// not throw an error message if the dependency fails to be resolved. + /// + /// public class MyModule : ModuleBase + /// { + /// [DontInject] + /// public DatabaseService DatabaseService; + /// public MyModule() + /// { + /// DatabaseService = DatabaseFactory.Generate(); + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class DontInjectAttribute : Attribute + { + } } diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index b1760d149..e1e38cff7 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -2,15 +2,26 @@ using System; namespace Discord.Commands { + /// + /// Marks the module as a command group. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class GroupAttribute : Attribute { + /// + /// Gets the prefix set for the module. + /// public string Prefix { get; } + /// public GroupAttribute() { Prefix = null; } + /// + /// Initializes a new with the provided prefix. + /// + /// The prefix of the module group. public GroupAttribute(string prefix) { Prefix = prefix; diff --git a/src/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/Discord.Net.Commands/Attributes/NameAttribute.cs index 4a4b2bfed..a6e1f2e5a 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,11 +3,21 @@ using System; namespace Discord.Commands { // Override public name of command/module + /// + /// Marks the public name of a command, module, or parameter. + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class NameAttribute : Attribute { + /// + /// Gets the name of the command. + /// public string Text { get; } + /// + /// Marks the public name of a command, module, or parameter with the provided name. + /// + /// The public name of the object. public NameAttribute(string text) { Text = text; diff --git a/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs new file mode 100644 index 000000000..e85717268 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Instructs the command system to treat command parameters of this type + /// as a collection of named arguments matching to its properties. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class NamedArgumentTypeAttribute : Attribute { } +} diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 17e2310d4..a44dcb6e4 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -1,20 +1,48 @@ using System; - using System.Reflection; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public class OverrideTypeReaderAttribute : Attribute + /// + /// Marks the to be read by the specified . + /// + /// + /// This attribute will override the to be used when parsing for the + /// desired type in the command. This is useful when one wishes to use a particular + /// without affecting other commands that are using the same target + /// type. + /// + /// If the given type reader does not inherit from , an + /// will be thrown. + /// + /// + /// + /// In this example, the will be read by a custom + /// , FriendlyTimeSpanTypeReader, instead of the + /// shipped by Discord.Net. + /// + /// [Command("time")] + /// public Task GetTimeAsync([OverrideTypeReader(typeof(FriendlyTimeSpanTypeReader))]TimeSpan time) + /// => ReplyAsync(time); + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); + /// + /// Gets the specified of the parameter. + /// public Type TypeReader { get; } + /// + /// The to be used with the parameter. + /// The given does not inherit from . public OverrideTypeReaderAttribute(Type overridenTypeReader) { if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) - throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); + throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}."); TypeReader = overridenTypeReader; } diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 3c5e8cf92..8ee46f9f9 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -3,9 +3,20 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// Requires the parameter to pass the specified precondition before execution can begin. + /// + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public abstract class ParameterPreconditionAttribute : Attribute { + /// + /// Checks whether the condition is met before execution of the command. + /// + /// The context of the command. + /// The parameter of the command being checked against. + /// The raw value of the parameter. + /// The service collection used for dependency injection. public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 367adebf0..37a08ba32 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -1,18 +1,39 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { + /// + /// Requires the module or class to pass the specified precondition before execution can begin. + /// + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { /// - /// Specify a group that this precondition belongs to. Preconditions of the same group require only one - /// of the preconditions to pass in order to be successful (A || B). Specifying = - /// or not at all will require *all* preconditions to pass, just like normal (A && B). + /// Specifies a group that this precondition belongs to. /// + /// + /// of the same group require only one of the preconditions to pass in order to + /// be successful (A || B). Specifying = null or not at all will + /// require *all* preconditions to pass, just like normal (A && B). + /// public string Group { get; set; } = null; + /// + /// When overridden in a derived class, uses the supplied string + /// as the error message if the precondition doesn't pass. + /// Setting this for a class that doesn't override + /// this property is a no-op. + /// + public virtual string ErrorMessage { get { return null; } set { } } + + /// + /// Checks if the has the sufficient permission to be executed. + /// + /// The context of the command. + /// The command being executed. + /// The service collection used for dependency injection. public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 104252799..5b3b5bd47 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,46 +1,59 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires that the bot has a specified permission in the channel a command is invoked in. + /// Requires the bot to have a specific permission in the channel a command is invoked in. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireBotPermissionAttribute : PreconditionAttribute { + /// + /// Gets the specified of the precondition. + /// public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// public ChannelPermission? ChannelPermission { get; } + /// + public override string ErrorMessage { get; set; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } /// - /// Require that the bot account has a specified GuildPermission + /// Requires the bot account to have a specific . /// - /// This precondition will always fail if the command is being invoked in a private channel. - /// The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the bot must have. Multiple permissions can be specified + /// by ORing the permissions together. + /// public RequireBotPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } /// - /// Require that the bot account has a specified ChannelPermission. + /// Requires that the bot account to have a specific . /// - /// The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. - /// - /// - /// [Command("permission")] - /// [RequireBotPermission(ChannelPermission.ManageMessages)] - /// public async Task Purge() - /// { - /// } - /// - /// + /// + /// The that the bot must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireBotPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; } + /// public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { IGuildUser guildUser = null; @@ -50,9 +63,9 @@ namespace Discord.Commands if (GuildPermission.HasValue) { if (guildUser == null) - return PreconditionResult.FromError("Command must be used in a guild channel"); + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}."); } if (ChannelPermission.HasValue) @@ -64,7 +77,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) - return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}."); } return PreconditionResult.FromSuccess(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 90af035e4..a27469c88 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,35 +1,50 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// + /// Defines the type of command context (i.e. where the command is being executed). + /// [Flags] public enum ContextType { + /// + /// Specifies the command to be executed within a guild. + /// Guild = 0x01, + /// + /// Specifies the command to be executed within a DM. + /// DM = 0x02, + /// + /// Specifies the command to be executed within a group. + /// Group = 0x04 } /// - /// Require that the command be invoked in a specified context. + /// Requires the command to be invoked in a specified context (e.g. in guild, DM). /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { - public ContextType Contexts { get; } - /// - /// Require that the command be invoked in a specified context. + /// Gets the context required to execute the command. /// + public ContextType Contexts { get; } + /// + public override string ErrorMessage { get; set; } + + /// Requires the command to be invoked in the specified context. /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. /// - /// - /// [Command("private_only")] + /// + /// [Command("secret")] /// [RequireContext(ContextType.DM | ContextType.Group)] - /// public async Task PrivateOnly() + /// public Task PrivateOnlyAsync() /// { + /// return ReplyAsync("shh, this command is a secret"); /// } /// /// @@ -38,12 +53,13 @@ namespace Discord.Commands Contexts = contexts; } + /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { bool isValid = false; if ((Contexts & ContextType.Guild) != 0) - isValid = isValid || context.Channel is IGuildChannel; + isValid = context.Channel is IGuildChannel; if ((Contexts & ContextType.DM) != 0) isValid = isValid || context.Channel is IDMChannel; if ((Contexts & ContextType.Group) != 0) @@ -52,7 +68,7 @@ namespace Discord.Commands if (isValid) return Task.FromResult(PreconditionResult.FromSuccess()); else - return Task.FromResult(PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}")); + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}.")); } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index 273c764bd..2a9647cd2 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -4,17 +4,42 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked in a channel marked NSFW + /// Requires the command to be invoked in a channel marked NSFW. /// + /// + /// The precondition will restrict the access of the command or module to be accessed within a guild channel + /// that has been marked as mature or NSFW. If the channel is not of type or the + /// channel is not marked as NSFW, the precondition will fail with an erroneous . + /// + /// + /// The following example restricts the command too-cool to an NSFW-enabled channel only. + /// + /// public class DankModule : ModuleBase + /// { + /// [Command("cool")] + /// public Task CoolAsync() + /// => ReplyAsync("I'm cool for everyone."); + /// + /// [RequireNsfw] + /// [Command("too-cool")] + /// public Task TooCoolAsync() + /// => ReplyAsync("You can only see this if you're cool enough."); + /// } + /// + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { + /// + public override string ErrorMessage { get; set; } + + /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { if (context.Channel is ITextChannel text && text.IsNsfw) return Task.FromResult(PreconditionResult.FromSuccess()); else - return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel.")); } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 93e3cbe18..c08e1e9da 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -4,23 +4,51 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked by the owner of the bot. + /// Requires the command to be invoked by the owner of the bot. /// - /// This precondition will only work if the bot is a bot account. + /// + /// This precondition will restrict the access of the command or module to the owner of the Discord application. + /// If the precondition fails to be met, an erroneous will be returned with the + /// message "Command can only be run by the owner of the bot." + /// + /// This precondition will only work if the account has a of + /// ;otherwise, this precondition will always fail. + /// + /// + /// + /// The following example restricts the command to a set of sensitive commands that only the owner of the bot + /// application should be able to access. + /// + /// [RequireOwner] + /// [Group("admin")] + /// public class AdminModule : ModuleBase + /// { + /// [Command("exit")] + /// public async Task ExitAsync() + /// { + /// Environment.Exit(0); + /// } + /// } + /// + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { + /// + public override string ErrorMessage { get; set; } + + /// public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { switch (context.Client.TokenType) { case TokenType.Bot: - var application = await context.Client.GetApplicationInfoAsync(); + var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false); if (context.User.Id != application.Owner.Id) - return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot."); return PreconditionResult.FromSuccess(); default: - return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); + return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); } } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 14121f35b..2908a18c1 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,47 +1,59 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires that the user invoking the command has a specified permission. + /// Requires the user invoking the command to have a specified permission. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireUserPermissionAttribute : PreconditionAttribute { + /// + /// Gets the specified of the precondition. + /// public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// public ChannelPermission? ChannelPermission { get; } + /// + public override string ErrorMessage { get; set; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } /// - /// Require that the user invoking the command has a specified GuildPermission + /// Requires that the user invoking the command to have a specific . /// - /// This precondition will always fail if the command is being invoked in a private channel. - /// The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireUserPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } /// - /// Require that the user invoking the command has a specified ChannelPermission. + /// Requires that the user invoking the command to have a specific . /// - /// The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. - /// - /// - /// [Command("permission")] - /// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)] - /// public async Task HasPermission() - /// { - /// await ReplyAsync("You can read messages and the message history!"); - /// } - /// - /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireUserPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; } - + + /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; @@ -49,9 +61,9 @@ namespace Discord.Commands if (GuildPermission.HasValue) { if (guildUser == null) - return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); + return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}.")); } if (ChannelPermission.HasValue) @@ -63,7 +75,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}.")); } return Task.FromResult(PreconditionResult.FromSuccess()); diff --git a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs index 353e96e41..75ffd2585 100644 --- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -2,14 +2,20 @@ using System; namespace Discord.Commands { - /// Sets priority of commands + /// + /// Sets priority of commands. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class PriorityAttribute : Attribute { - /// The priority which has been set for the command + /// + /// Gets the priority which has been set for the command. + /// public int Priority { get; } - /// Creates a new with the given priority. + /// + /// Initializes a new attribute with the given priority. + /// public PriorityAttribute(int priority) { Priority = priority; diff --git a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs index 56938f167..33e07f0d9 100644 --- a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -2,6 +2,9 @@ using System; namespace Discord.Commands { + /// + /// Marks the input to not be parsed by the parser. + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class RemainderAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs index c11f790a7..2fbe2bf4a 100644 --- a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs @@ -3,6 +3,9 @@ using System; namespace Discord.Commands { // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters + /// + /// Attaches remarks to your commands. + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class RemarksAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs index 641163408..57e9b0277 100644 --- a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs @@ -3,6 +3,9 @@ using System; namespace Discord.Commands { // Cosmetic Summary, for Groups and Commands + /// + /// Attaches a summary to your command. + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class SummaryAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 17a170775..1d946a33d 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -7,6 +7,7 @@ namespace Discord.Commands.Builders { public class CommandBuilder { + #region CommandBuilder private readonly List _preconditions; private readonly List _parameters; private readonly List _attributes; @@ -27,8 +28,9 @@ namespace Discord.Commands.Builders public IReadOnlyList Parameters => _parameters; public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; + #endregion - //Automatic + #region Automatic internal CommandBuilder(ModuleBuilder module) { Module = module; @@ -38,7 +40,9 @@ namespace Discord.Commands.Builders _attributes = new List(); _aliases = new List(); } - //User-defined + #endregion + + #region User-defined internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) : this(module) { @@ -118,6 +122,7 @@ namespace Discord.Commands.Builders return this; } + /// Only the last parameter in a command may have the Remainder or Multiple flag. internal CommandInfo Build(ModuleInfo info, CommandService service) { //Default name to primary alias @@ -131,7 +136,7 @@ namespace Discord.Commands.Builders var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); - + var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); @@ -139,5 +144,6 @@ namespace Discord.Commands.Builders return new CommandInfo(this, info, service); } + #endregion } } diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 6dc50db31..ddb62e797 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -7,6 +7,7 @@ namespace Discord.Commands.Builders { public class ModuleBuilder { + #region ModuleBuilder private readonly List _commands; private readonly List _submodules; private readonly List _preconditions; @@ -27,8 +28,9 @@ namespace Discord.Commands.Builders public IReadOnlyList Aliases => _aliases; internal TypeInfo TypeInfo { get; set; } + #endregion - //Automatic + #region Automatic internal ModuleBuilder(CommandService service, ModuleBuilder parent) { Service = service; @@ -40,7 +42,9 @@ namespace Discord.Commands.Builders _attributes = new List(); _aliases = new List(); } - //User-defined + #endregion + + #region User-defined internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) : this(service, parent) { @@ -132,5 +136,6 @@ namespace Discord.Commands.Builders public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent); + #endregion } } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 307874ca6..8c10ae806 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -34,7 +34,7 @@ namespace Discord.Commands } else if (IsLoadableModule(typeInfo)) { - await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); + await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.").ConfigureAwait(false); } } @@ -116,7 +116,7 @@ namespace Discord.Commands builder.AddAliases(alias.Aliases); break; case GroupAttribute group: - builder.Name = builder.Name ?? group.Prefix; + builder.Name ??= group.Prefix; builder.Group = group.Prefix; builder.AddAliases(group.Prefix); break; @@ -135,7 +135,8 @@ namespace Discord.Commands if (builder.Name == null) builder.Name = typeInfo.Name; - var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition); + // Get all methods (including from inherited members), that are valid commands + var validCommands = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(IsValidCommandDefinition); foreach (var method in validCommands) { @@ -157,7 +158,7 @@ namespace Discord.Commands case CommandAttribute command: builder.AddAliases(command.Text); builder.RunMode = command.RunMode; - builder.Name = builder.Name ?? command.Text; + builder.Name ??= command.Text; builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs; break; case NameAttribute name: @@ -280,7 +281,7 @@ namespace Discord.Commands } } - private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) + internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) { var readers = service.GetTypeReaders(paramType); TypeReader reader = null; @@ -290,7 +291,7 @@ namespace Discord.Commands return reader; } - //We dont have a cached type reader, create one + //We don't have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); service.AddTypeReader(paramType, reader, false); diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 8a59c247c..afe3a5af6 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -8,6 +8,7 @@ namespace Discord.Commands.Builders { public class ParameterBuilder { + #region ParameterBuilder private readonly List _preconditions; private readonly List _attributes; @@ -24,8 +25,9 @@ namespace Discord.Commands.Builders public IReadOnlyList Preconditions => _preconditions; public IReadOnlyList Attributes => _attributes; +#endregion - //Automatic + #region Automatic internal ParameterBuilder(CommandBuilder command) { _preconditions = new List(); @@ -33,7 +35,9 @@ namespace Discord.Commands.Builders Command = command; } - //User-defined + #endregion + + #region User-defined internal ParameterBuilder(CommandBuilder command, string name, Type type) : this(command) { @@ -50,17 +54,42 @@ namespace Discord.Commands.Builders if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); else if (type.IsArray) - type = ParameterType.GetElementType(); + DefaultValue = Array.CreateInstance(type.GetElementType(), 0); ParameterType = type; } private TypeReader GetReader(Type type) { - var readers = Command.Module.Service.GetTypeReaders(type); + var commands = Command.Module.Service; + if (type.GetTypeInfo().GetCustomAttribute() != null) + { + IsRemainder = true; + var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; + if (reader == null) + { + Type readerType; + try + { + readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex); + } + + reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); + commands.AddTypeReader(type, reader); + } + + return reader; + } + + + var readers = commands.GetTypeReaders(type); if (readers != null) return readers.FirstOrDefault().Value; else - return Command.Module.Service.GetDefaultTypeReader(type); + return commands.GetDefaultTypeReader(type); } public ParameterBuilder WithSummary(string summary) @@ -102,10 +131,11 @@ namespace Discord.Commands.Builders internal ParameterInfo Build(CommandInfo info) { - if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null) + if ((TypeReader ??= GetReader(ParameterType)) == null) throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified"); return new ParameterInfo(this, info, Command.Module.Service); } + #endregion } } diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 05bde56b1..393cdf97a 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -1,15 +1,27 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// The context of a command which may contain the client, user, guild, channel, and message. public class CommandContext : ICommandContext { + /// public IDiscordClient Client { get; } + /// public IGuild Guild { get; } + /// public IMessageChannel Channel { get; } + /// public IUser User { get; } + /// public IUserMessage Message { get; } + /// Indicates whether the channel that the command is executed in is a private channel. public bool IsPrivate => Channel is IPrivateChannel; - + + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. public CommandContext(IDiscordClient client, IUserMessage msg) { Client = client; diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index abfc14e1d..b487d8a1e 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -1,26 +1,51 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// Defines the type of error a command can throw. public enum CommandError { //Search + /// + /// Thrown when the command is unknown. + /// UnknownCommand = 1, //Parse + /// + /// Thrown when the command fails to be parsed. + /// ParseFailed, + /// + /// Thrown when the input text has too few or too many arguments. + /// BadArgCount, //Parse (Type Reader) //CastFailed, + /// + /// Thrown when the object cannot be found by the . + /// ObjectNotFound, + /// + /// Thrown when more than one object is matched by . + /// MultipleMatches, //Preconditions + /// + /// Thrown when the command fails to meet a 's conditions. + /// UnmetPrecondition, //Execute + /// + /// Thrown when an exception occurs mid-command execution. + /// Exception, //Runtime + /// + /// Thrown when the command is not successfully executed on runtime. + /// Unsuccessful } } diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index d5300841a..6c5ab0ae5 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -2,11 +2,24 @@ using System; namespace Discord.Commands { + /// + /// The exception that is thrown if another exception occurs during a command execution. + /// public class CommandException : Exception { + /// Gets the command that caused the exception. public CommandInfo Command { get; } + /// Gets the command context of the exception. public ICommandContext Context { get; } + /// + /// Initializes a new instance of the class using a + /// information, a context, and the exception that + /// interrupted the execution. + /// + /// The command information. + /// The context of the command. + /// The exception that interrupted the command execution. public CommandException(CommandInfo command, ICommandContext context, Exception ex) : base($"Error occurred executing {command.GetLogText(context)}.", ex) { diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index d922a2229..c15a33228 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -1,13 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { public struct CommandMatch { + /// The command that matches the search result. public CommandInfo Command { get; } + /// The alias of the command. public string Alias { get; } public CommandMatch(CommandInfo command, string alias) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 9ce4e1469..88698cda5 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Text; @@ -53,11 +53,27 @@ namespace Discord.Commands else c = '\0'; + //If we're processing an remainder parameter, ignore all other logic + if (curParam != null && curParam.IsRemainder && curPos != endPos) + { + argBuilder.Append(c); + continue; + } + //If this character is escaped, skip it if (isEscaping) { if (curPos != endPos) { + // if this character matches the quotation mark of the end of the string + // means that it should be escaped + // but if is not, then there is no reason to escape it then + if (c != matchQuote) + { + // if no reason to escape the next character, then re-add \ to the arg + argBuilder.Append('\\'); + } + argBuilder.Append(c); isEscaping = false; continue; @@ -70,13 +86,6 @@ namespace Discord.Commands continue; } - //If we're processing an remainder parameter, ignore all other logic - if (curParam != null && curParam.IsRemainder && curPos != endPos) - { - argBuilder.Append(c); - continue; - } - //If we're not currently processing one, are we starting the next argument yet? if (curPart == ParserPart.None) { @@ -94,7 +103,7 @@ namespace Discord.Commands argBuilder.Append(c); continue; } - + if (IsOpenQuote(aliasMap, c)) { curPart = ParserPart.QuotedParameter; @@ -127,7 +136,7 @@ namespace Discord.Commands else argBuilder.Append(c); } - + if (argString != null) { if (curParam == null) @@ -140,7 +149,7 @@ namespace Discord.Commands var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) - return ParseResult.FromError(typeReaderResult); + return ParseResult.FromError(typeReaderResult, curParam); if (curParam.IsMultiple) { @@ -163,15 +172,15 @@ namespace Discord.Commands { var typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess) - return ParseResult.FromError(typeReaderResult); + return ParseResult.FromError(typeReaderResult, curParam); argList.Add(typeReaderResult); } if (isEscaping) return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); if (curPart == ParserPart.QuotedParameter) - return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); - + return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete."); + //Add missing optionals for (int i = argList.Count; i < command.Parameters.Count; i++) { @@ -182,7 +191,7 @@ namespace Discord.Commands return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); } - + return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable()); } } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 7b7cffda2..f9552ef4b 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -11,19 +11,46 @@ using Discord.Logging; namespace Discord.Commands { - public class CommandService + /// + /// Provides a framework for building Discord commands. + /// + /// + /// + /// The service provides a framework for building Discord commands both dynamically via runtime builders or + /// statically via compile-time modules. To create a command module at compile-time, see + /// (most common); otherwise, see . + /// + /// + /// This service also provides several events for monitoring command usages; such as + /// for any command-related log events, and + /// for information about commands that have + /// been successfully executed. + /// + /// + public class CommandService : IDisposable { + #region CommandService + /// + /// Occurs when a command-related information is received. + /// public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); - public event Func CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _commandExecutedEvent = new AsyncEvent>(); + /// + /// Occurs when a command is executed. + /// + /// + /// This event is fired when a command has been executed, successfully or not. When a command fails to + /// execute during parsing or precondition stage, the CommandInfo may not be returned. + /// + public event Func, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent, ICommandContext, IResult, Task>>(); private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; private readonly ConcurrentDictionary _defaultTypeReaders; - private readonly ImmutableList> _entityTypeReaders; //TODO: Candidate for C#7 Tuple + private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; private readonly HashSet _moduleDefs; private readonly CommandMap _map; @@ -34,11 +61,35 @@ namespace Discord.Commands internal readonly LogManager _logManager; internal readonly IReadOnlyDictionary _quotationMarkAliasMap; + internal bool _isDisposed; + + /// + /// Represents all modules loaded within . + /// public IEnumerable Modules => _moduleDefs.Select(x => x); + + /// + /// Represents all commands loaded within . + /// public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); + + /// + /// Represents all loaded within . + /// public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); + /// + /// Initializes a new class. + /// public CommandService() : this(new CommandServiceConfig()) { } + + /// + /// Initializes a new class with the provided configuration. + /// + /// The configuration class. + /// + /// The cannot be set to . + /// public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; @@ -74,15 +125,16 @@ namespace Discord.Commands _defaultTypeReaders[typeof(string)] = new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); - var entityTypeReaders = ImmutableList.CreateBuilder>(); - entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IChannel), typeof(ChannelTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IRole), typeof(RoleTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IUser), typeof(UserTypeReader<>))); + var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>(); + entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>))); + entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>))); + entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>))); + entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>))); _entityTypeReaders = entityTypeReaders.ToImmutable(); } + #endregion - //Modules + #region Modules public async Task CreateModuleAsync(string primaryAlias, Action buildFunc) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -102,15 +154,42 @@ namespace Discord.Commands } /// - /// Add a command module from a type + /// Add a command module from a . /// - /// The type of module - /// An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null - /// A built module + /// + /// The following example registers the module MyModule to commandService. + /// + /// await commandService.AddModuleAsync<MyModule>(serviceProvider); + /// + /// + /// The type of module. + /// The for your dependency injection solution if using one; otherwise, pass null. + /// This module has already been added. + /// + /// The fails to be built; an invalid type may have been provided. + /// + /// + /// A task that represents the asynchronous operation for adding the module. The task result contains the + /// built module. + /// public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + + /// + /// Adds a command module from a . + /// + /// The type of module. + /// The for your dependency injection solution if using one; otherwise, pass null . + /// This module has already been added. + /// + /// The fails to be built; an invalid type may have been provided. + /// + /// + /// A task that represents the asynchronous operation for adding the module. The task result contains the + /// built module. + /// public async Task AddModuleAsync(Type type, IServiceProvider services) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; await _moduleLock.WaitAsync().ConfigureAwait(false); try @@ -135,14 +214,17 @@ namespace Discord.Commands } } /// - /// Add command modules from an assembly + /// Add command modules from an . /// - /// The assembly containing command modules - /// An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null - /// A collection of built modules + /// The containing command modules. + /// The for your dependency injection solution if using one; otherwise, pass null. + /// + /// A task that represents the asynchronous operation for adding the command modules. The task result + /// contains an enumerable collection of modules added. + /// public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; await _moduleLock.WaitAsync().ConfigureAwait(false); try @@ -175,7 +257,14 @@ namespace Discord.Commands return module; } - + /// + /// Removes the command module. + /// + /// The to be removed from the service. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the is successfully removed. + /// public async Task RemoveModuleAsync(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -188,7 +277,23 @@ namespace Discord.Commands _moduleLock.Release(); } } + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// public Task RemoveModuleAsync() => RemoveModuleAsync(typeof(T)); + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// public async Task RemoveModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -219,24 +324,31 @@ namespace Discord.Commands return true; } + #endregion - //Type Readers + #region Type Readers /// - /// Adds a custom to this for the supplied object type. - /// If is a , a will also be added. - /// If a default exists for , a warning will be logged and the default will be replaced. + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable will + /// also be added. + /// If a default exists for , a warning will be logged + /// and the default will be replaced. /// /// The object type to be read by the . - /// An instance of the to be added. + /// An instance of the to be added. public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); /// - /// Adds a custom to this for the supplied object type. - /// If is a , a for the value type will also be added. - /// If a default exists for , a warning will be logged and the default will be replaced. + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable for the + /// value type will also be added. + /// If a default exists for , a warning will be logged and + /// the default will be replaced. /// - /// A instance for the type to be read. - /// An instance of the to be added. + /// A instance for the type to be read. + /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { if (_defaultTypeReaders.ContainsKey(type)) @@ -245,21 +357,31 @@ namespace Discord.Commands AddTypeReader(type, reader, true); } /// - /// Adds a custom to this for the supplied object type. - /// If is a , a will also be added. + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable will + /// also be added. /// /// The object type to be read by the . - /// An instance of the to be added. - /// If should replace the default for if one exists. + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for + /// if it exists. + /// public void AddTypeReader(TypeReader reader, bool replaceDefault) => AddTypeReader(typeof(T), reader, replaceDefault); /// - /// Adds a custom to this for the supplied object type. - /// If is a , a for the value type will also be added. + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable for the + /// value type will also be added. /// - /// A instance for the type to be read. - /// An instance of the to be added. - /// If should replace the default for if one exists. + /// A instance for the type to be read. + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for if + /// it exists. + /// public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) { if (replaceDefault && HasDefaultTypeReader(type)) @@ -289,7 +411,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) return true; - return _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2)); + return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.EntityType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { @@ -316,23 +438,43 @@ namespace Discord.Commands _defaultTypeReaders[type] = reader; return reader; } + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType != null && underlyingType.IsEnum) + { + reader = NullableTypeReader.Create(underlyingType, EnumTypeReader.GetReader(underlyingType)); + _defaultTypeReaders[type] = reader; + return reader; + } //Is this an entity? for (int i = 0; i < _entityTypeReaders.Count; i++) { - if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1)) + if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) { - reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader; + reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; _defaultTypeReaders[type] = reader; return reader; } } return null; } + #endregion - //Execution + #region Execution + /// + /// Searches for the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The result containing the matching commands. public SearchResult Search(ICommandContext context, int argPos) => Search(context.Message.Content.Substring(argPos)); + /// + /// Searches for the command. + /// + /// The context of the command. + /// The command string. + /// The result containing the matching commands. public SearchResult Search(ICommandContext context, string input) => Search(input); public SearchResult Search(string input) @@ -346,22 +488,112 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } + /// + /// Executes the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// + /// A task that represents the asynchronous execution operation. The task result contains the result of the + /// command execution. + /// public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); + /// + /// Executes the command. + /// + /// The context of the command. + /// The command string. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// + /// A task that represents the asynchronous execution operation. The task result contains the result of the + /// command execution. + /// public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; var searchResult = Search(input); - if (!searchResult.IsSuccess) - return searchResult; - var commands = searchResult.Commands; + var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); + + if (validationResult is SearchResult result) + { + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, result).ConfigureAwait(false); + return result; + } + + if (validationResult is MatchResult matchResult) + { + return await HandleCommandPipeline(matchResult, context, services); + } + + return validationResult; + } + + private async Task HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) + { + if (!matchResult.IsSuccess) + return matchResult; + + if (matchResult.Pipeline is ParseResult parseResult) + { + var executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services); + + if (!executeResult.IsSuccess && !(executeResult is RuntimeResult || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult); + return executeResult; + } + + if (matchResult.Pipeline is PreconditionResult preconditionResult) + { + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); + } + + return matchResult; + } + + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + + /// + /// Validates and gets the best from a specified + /// + /// The SearchResult. + /// The context of the command. + /// The service provider to be used on the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// A task that represents the asynchronous validation operation. The task result contains the result of the + /// command validation as a or a if no matches were found. + public async Task ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + { + if (!matches.IsSuccess) + return matches; + + var commands = matches.Commands; var preconditionResults = new Dictionary(); - foreach (var match in commands) + foreach (var command in commands) { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); + preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider); } var successfulPreconditions = preconditionResults @@ -372,17 +604,16 @@ namespace Discord.Commands { //All preconditions failed, return the one from the highest priority command var bestCandidate = preconditionResults - .OrderByDescending(x => x.Key.Command.Priority) - .FirstOrDefault(x => !x.Value.IsSuccess); - return bestCandidate.Value; + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value); } - //If we get this far, at least one precondition was successful. + var parseResults = new Dictionary(); - var parseResultsDict = new Dictionary(); foreach (var pair in successfulPreconditions) { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false); if (parseResult.Error == CommandError.MultipleMatches) { @@ -397,46 +628,48 @@ namespace Discord.Commands } } - parseResultsDict[pair.Key] = parseResult; - } - - // Calculates the 'score' of a command given a parse result - float CalculateScore(CommandMatch match, ParseResult parseResult) - { - float argValuesScore = 0, paramValuesScore = 0; - - if (match.Command.Parameters.Count > 0) - { - var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; - var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; - - argValuesScore = argValuesSum / match.Command.Parameters.Count; - paramValuesScore = paramValuesSum / match.Command.Parameters.Count; - } - - var totalArgsScore = (argValuesScore + paramValuesScore) / 2; - return match.Command.Priority + totalArgsScore * 0.99f; + parseResults[pair.Key] = parseResult; } - //Order the parse results by their score so that we choose the most likely result to execute - var parseResults = parseResultsDict - .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + var weightedParseResults = parseResults + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); - var successfulParses = parseResults + var successfulParses = weightedParseResults .Where(x => x.Value.IsSuccess) .ToArray(); - if (successfulParses.Length == 0) + if(successfulParses.Length == 0) { - //All parses failed, return the one from the highest priority command, using score as a tie breaker var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); - return bestMatch.Value; + + return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); } - //If we get this far, at least one parse was successful. Execute the most likely overload. var chosenOverload = successfulParses[0]; - return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); + + return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); + } + #endregion + + #region Dispose + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _moduleLock?.Dispose(); + } + + _isDisposed = true; + } + } + + void IDisposable.Dispose() + { + Dispose(true); } + #endregion } } diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 00091634d..3c62063c8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -1,29 +1,62 @@ -using System; using System.Collections.Generic; namespace Discord.Commands { + /// + /// Represents a configuration class for . + /// public class CommandServiceConfig { - /// Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or builder. + /// + /// Gets or sets the default commands should have, if one is not specified on the + /// Command attribute or builder. + /// public RunMode DefaultRunMode { get; set; } = RunMode.Sync; + /// + /// Gets or sets the that separates an argument with another. + /// public char SeparatorChar { get; set; } = ' '; - /// Determines whether commands should be case-sensitive. + /// + /// Gets or sets whether commands should be case-sensitive. + /// public bool CaseSensitiveCommands { get; set; } = false; - /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// + /// Gets or sets the minimum log level severity that will be sent to the event. + /// public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// Determines whether RunMode.Sync commands should push exceptions up to the caller. + /// + /// Gets or sets whether commands should push exceptions up to the caller. + /// public bool ThrowOnError { get; set; } = true; - /// Collection of aliases that can wrap strings for command parsing. - /// represents the opening quotation mark and the value is the corresponding closing mark. + /// + /// Collection of aliases for matching pairs of string delimiters. + /// The dictionary stores the opening delimiter as a key, and the matching closing delimiter as the value. + /// If no value is supplied will be used, which contains + /// many regional equivalents. + /// Only values that are specified in this map will be used as string delimiters, so if " is removed then + /// it won't be used. + /// If this map is set to null or empty, the default delimiter of " will be used. + /// + /// + /// + /// QuotationMarkAliasMap = new Dictionary<char, char>() + /// { + /// {'\"', '\"' }, + /// {'“', '”' }, + /// {'「', '」' }, + /// } + /// + /// public Dictionary QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; - /// Determines whether extra parameters should be ignored. + /// + /// Gets or sets a value that indicates whether extra parameters should be ignored. + /// public bool IgnoreExtraArgs { get; set; } = false; } } diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index a754486dd..ec2795de2 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,19 +1,15 @@ + Discord.Net.Commands Discord.Commands A Discord.Net extension adding support for bot commands. - net46;netstandard1.3;netstandard2.0 - netstandard1.3;netstandard2.0 + net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 - - - - - - + \ No newline at end of file diff --git a/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs new file mode 100644 index 000000000..4c2262f9b --- /dev/null +++ b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Provides extension methods for the class. + /// + public static class CommandServiceExtensions + { + /// + /// Returns commands that can be executed under the current context. + /// + /// The set of commands to be checked against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static async Task> GetExecutableCommandsAsync(this ICollection commands, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + var tasks = commands.Select(async c => + { + var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); + return new { Command = c, PreconditionResult = result }; + }); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var result in results) + { + if (result.PreconditionResult.IsSuccess) + executableCommands.Add(result.Command); + } + + return executableCommands; + } + /// + /// Returns commands that can be executed under the current context. + /// + /// The desired command service class to check against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static Task> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, IServiceProvider provider) + => GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); + /// + /// Returns commands that can be executed under the current context. + /// + /// The module to be checked against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static async Task> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + + var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + executableCommands.AddRange(results.SelectMany(c => c)); + + return executableCommands; + } + } +} diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index a27c5f322..9aa83d418 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -2,39 +2,56 @@ using System; namespace Discord.Commands { + /// + /// Provides extension methods for that relates to commands. + /// public static class MessageExtensions { + /// + /// Gets whether the message starts with the provided character. + /// + /// The message to check against. + /// The char prefix. + /// References where the command starts. + /// + /// true if the message begins with the char ; otherwise false. + /// public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) { var text = msg.Content; - if (text.Length > 0 && text[0] == c) + if (!string.IsNullOrEmpty(text) && text[0] == c) { argPos = 1; return true; } return false; } + /// + /// Gets whether the message starts with the provided string. + /// public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { var text = msg.Content; - if (text.StartsWith(str, comparisonType)) + if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType)) { argPos = str.Length; return true; } return false; } + /// + /// Gets whether the message starts with the user's mention string. + /// public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) { var text = msg.Content; - if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; + if (string.IsNullOrEmpty(text) || text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; int endPos = text.IndexOf('>'); if (endPos == -1) return false; if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> " - ulong userId; - if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false; + if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out ulong userId)) return false; if (userId == user.Id) { argPos = endPos + 2; diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 604bfbc93..773c7c773 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -8,10 +8,16 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// + /// Provides the information of a command. + /// + /// + /// This object contains the information of a command. This can include the module of the command, various + /// descriptions regarding the command, and its . + /// [DebuggerDisplay("{Name,nq}")] public class CommandInfo { @@ -21,18 +27,63 @@ namespace Discord.Commands private readonly CommandService _commandService; private readonly Func _action; + /// + /// Gets the module that the command belongs in. + /// public ModuleInfo Module { get; } + /// + /// Gets the name of the command. If none is set, the first alias is used. + /// public string Name { get; } + /// + /// Gets the summary of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// public string Summary { get; } + /// + /// Gets the remarks of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// public string Remarks { get; } + /// + /// Gets the priority of the command. This is used when there are multiple overloads of the command. + /// public int Priority { get; } + /// + /// Indicates whether the command accepts a [] for its + /// parameter. + /// public bool HasVarArgs { get; } + /// + /// Indicates whether extra arguments should be ignored for this command. + /// public bool IgnoreExtraArgs { get; } + /// + /// Gets the that is being used for the command. + /// public RunMode RunMode { get; } + /// + /// Gets a list of aliases defined by the of the command. + /// public IReadOnlyList Aliases { get; } + /// + /// Gets a list of information about the parameters of the command. + /// public IReadOnlyList Parameters { get; } + /// + /// Gets a list of preconditions defined by the of the command. + /// public IReadOnlyList Preconditions { get; } + /// + /// Gets a list of attributes of the command. + /// public IReadOnlyList Attributes { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) @@ -72,7 +123,7 @@ namespace Discord.Commands public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; async Task CheckGroups(IEnumerable preconditions, string type) { @@ -100,11 +151,11 @@ namespace Discord.Commands return PreconditionGroupResult.FromSuccess(); } - var moduleResult = await CheckGroups(Module.Preconditions, "Module"); + var moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false); if (!moduleResult.IsSuccess) return moduleResult; - var commandResult = await CheckGroups(Preconditions, "Command"); + var commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false); if (!commandResult.IsSuccess) return commandResult; @@ -113,7 +164,7 @@ namespace Discord.Commands public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); @@ -124,7 +175,7 @@ namespace Discord.Commands return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false); } - + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) @@ -150,7 +201,7 @@ namespace Discord.Commands } public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; try { @@ -162,7 +213,10 @@ namespace Discord.Commands object argument = args[position]; var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); if (!result.IsSuccess) + { + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); return ExecuteResult.FromError(result); + } } switch (RunMode) @@ -221,6 +275,10 @@ namespace Discord.Commands var wrappedEx = new CommandException(this, context, ex); await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); + + var result = ExecuteResult.FromError(ex); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + if (Module.Service._throwOnError) { if (ex == originalEx) @@ -229,7 +287,7 @@ namespace Discord.Commands ExceptionDispatchInfo.Capture(ex).Throw(); } - return ExecuteResult.FromError(CommandError.Exception, ex.Message); + return result; } finally { @@ -248,11 +306,11 @@ namespace Discord.Commands foreach (object arg in argList) { if (i == argCount) - throw new InvalidOperationException("Command was invoked with too many parameters"); + throw new InvalidOperationException("Command was invoked with too many parameters."); array[i++] = arg; } if (i < argCount) - throw new InvalidOperationException("Command was invoked with too few parameters"); + throw new InvalidOperationException("Command was invoked with too few parameters."); if (HasVarArgs) { diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 7c144599b..7b9959efe 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -6,20 +6,59 @@ using Discord.Commands.Builders; namespace Discord.Commands { + /// + /// Provides the information of a module. + /// public class ModuleInfo { + /// + /// Gets the command service associated with this module. + /// public CommandService Service { get; } + /// + /// Gets the name of this module. + /// public string Name { get; } + /// + /// Gets the summary of this module. + /// public string Summary { get; } + /// + /// Gets the remarks of this module. + /// public string Remarks { get; } + /// + /// Gets the group name (main prefix) of this module. + /// public string Group { get; } + /// + /// Gets a read-only list of aliases associated with this module. + /// public IReadOnlyList Aliases { get; } + /// + /// Gets a read-only list of commands associated with this module. + /// public IReadOnlyList Commands { get; } + /// + /// Gets a read-only list of preconditions that apply to this module. + /// public IReadOnlyList Preconditions { get; } + /// + /// Gets a read-only list of attributes that apply to this module. + /// public IReadOnlyList Attributes { get; } + /// + /// Gets a read-only list of submodules associated with this module. + /// public IReadOnlyList Submodules { get; } + /// + /// Gets the parent module of this submodule if applicable. + /// public ModuleInfo Parent { get; } + /// + /// Gets a value that indicates whether this module is a submodule or not. + /// public bool IsSubmodule => Parent != null; internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 4a56415e5..a6ba9dfde 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -2,25 +2,56 @@ using Discord.Commands.Builders; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// + /// Provides the information of a parameter. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class ParameterInfo { private readonly TypeReader _reader; + /// + /// Gets the command that associates with this parameter. + /// public CommandInfo Command { get; } + /// + /// Gets the name of this parameter. + /// public string Name { get; } + /// + /// Gets the summary of this parameter. + /// public string Summary { get; } + /// + /// Gets a value that indicates whether this parameter is optional or not. + /// public bool IsOptional { get; } + /// + /// Gets a value that indicates whether this parameter is a remainder parameter or not. + /// public bool IsRemainder { get; } public bool IsMultiple { get; } + /// + /// Gets the type of the parameter. + /// public Type Type { get; } + /// + /// Gets the default value for this optional parameter if applicable. + /// public object DefaultValue { get; } + /// + /// Gets a read-only list of precondition that apply to this parameter. + /// public IReadOnlyList Preconditions { get; } + /// + /// Gets a read-only list of attributes that apply to this parameter. + /// public IReadOnlyList Attributes { get; } internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) @@ -44,7 +75,7 @@ namespace Discord.Commands public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; foreach (var precondition in Preconditions) { @@ -58,11 +89,11 @@ namespace Discord.Commands public async Task ParseAsync(ICommandContext context, string input, IServiceProvider services = null) { - services = services ?? EmptyServiceProvider.Instance; + services ??= EmptyServiceProvider.Instance; return await _reader.ReadAsync(context, input, services).ConfigureAwait(false); } public override string ToString() => Name; private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index bd3067718..16f469cde 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -23,6 +23,7 @@ namespace Discord.Commands _commands = ImmutableArray.Create(); } + /// Cannot add commands to the root node. public void AddCommand(CommandService service, string text, int index, CommandInfo command) { int nextSegment = NextSegment(text, index, service._separatorChar); diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 3e6fbbd9b..3eddc11d2 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -4,44 +4,81 @@ using Discord.Commands.Builders; namespace Discord.Commands { + /// + /// Provides a base class for a command module to inherit from. + /// public abstract class ModuleBase : ModuleBase { } + /// + /// Provides a base class for a command module to inherit from. + /// + /// A class that implements . public abstract class ModuleBase : IModuleBase where T : class, ICommandContext { + #region ModuleBase + /// + /// The underlying context of the command. + /// + /// + /// public T Context { get; private set; } /// - /// Sends a message to the source channel + /// Sends a message to the source channel. /// - /// Contents of the message; optional only if is specified - /// Specifies if Discord should read this message aloud using TTS - /// An embed to be displayed alongside the message - protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Contents of the message; optional only if is specified. + /// + /// Specifies if Discord should read this aloud using text-to-speech. + /// An embed to be displayed alongside the . + /// + /// Specifies if notifications are sent for mentioned users and roles in the . + /// If null, all mentioned roles and users will be notified. + /// + /// The request options for this request. + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); } - + /// + /// The method to execute before executing the command. + /// + /// The of the command to be executed. protected virtual void BeforeExecute(CommandInfo command) { } - + /// + /// The method to execute after executing the command. + /// + /// The of the command to be executed. protected virtual void AfterExecute(CommandInfo command) { } + /// + /// The method to execute when building the module. + /// + /// The used to create the module. + /// The builder used to build the module. protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) { } + #endregion - //IModuleBase + #region IModuleBase void IModuleBase.SetContext(ICommandContext context) { var newValue = context as T; - Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); + Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}."); } void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); + #endregion } } diff --git a/src/Discord.Net.Commands/MultiMatchHandling.cs b/src/Discord.Net.Commands/MultiMatchHandling.cs index 89dcf1c06..319e58edd 100644 --- a/src/Discord.Net.Commands/MultiMatchHandling.cs +++ b/src/Discord.Net.Commands/MultiMatchHandling.cs @@ -1,8 +1,13 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// + /// Specifies the behavior when multiple matches are found during the command parsing stage. + /// public enum MultiMatchHandling { + /// Indicates that when multiple results are found, an exception should be thrown. Exception, + /// Indicates that when multiple results are found, the best result should be chosen. Best } } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index cd7a9d744..cdbc59cef 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -6,19 +6,29 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// + /// This is shipped with Discord.Net and is used by default to parse any + /// implemented object within a command. The TypeReader will attempt to first parse the + /// input by mention, then the snowflake identifier, then by name; the highest candidate will be chosen as the + /// final output; otherwise, an erroneous is returned. + /// + /// The type to be checked; must implement . public class ChannelTypeReader : TypeReader where T : class, IChannel { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { var results = new Dictionary(); var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false); - ulong id; //By Mention (1.0) - if (MentionUtils.TryParseChannel(input, out id)) + if (MentionUtils.TryParseChannel(input, out ulong id)) AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); //By Id (0.9) diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index c097e6189..356d704c9 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -44,6 +44,7 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } + /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { object enumValue; @@ -53,14 +54,14 @@ namespace Discord.Commands if (_enumsByValue.TryGetValue(baseValue, out enumValue)) return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); else - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}")); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); } else { if (_enumsByName.TryGetValue(input.ToLower(), out enumValue)) return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); else - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}")); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); } } } diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index a87cfbe43..acec2f12d 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -4,15 +4,18 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class MessageTypeReader : TypeReader where T : class, IMessage { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - ulong id; - //By Id (1.0) - if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) { if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) return TypeReaderResult.FromSuccess(msg); diff --git a/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs new file mode 100644 index 000000000..0adf61046 --- /dev/null +++ b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal sealed class NamedArgumentTypeReader : TypeReader + where T : class, new() + { + private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo().DeclaredProperties + .Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) + .ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + + private readonly CommandService _commands; + + public NamedArgumentTypeReader(CommandService commands) + { + _commands = commands; + } + + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var result = new T(); + var state = ReadState.LookingForParameter; + int beginRead = 0, currentRead = 0; + + while (state != ReadState.End) + { + try + { + var prop = Read(out var arg); + var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); + if (propVal != null) + prop.SetMethod.Invoke(result, new[] { propVal }); + else + return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); + } + catch (Exception ex) + { + return TypeReaderResult.FromError(ex); + } + } + + return TypeReaderResult.FromSuccess(result); + + PropertyInfo Read(out string arg) + { + string currentParam = null; + char match = '\0'; + + for (; currentRead < input.Length; currentRead++) + { + var currentChar = input[currentRead]; + switch (state) + { + case ReadState.LookingForParameter: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = ReadState.InParameter; + } + break; + case ReadState.InParameter: + if (currentChar != ':') + continue; + else + { + currentParam = input.Substring(beginRead, currentRead - beginRead); + state = ReadState.LookingForArgument; + } + break; + case ReadState.LookingForArgument: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) + ? ReadState.InQuotedArgument + : ReadState.InArgument; + } + break; + case ReadState.InArgument: + if (!Char.IsWhiteSpace(currentChar)) + continue; + else + return GetPropAndValue(out arg); + case ReadState.InQuotedArgument: + if (currentChar != match) + continue; + else + return GetPropAndValue(out arg); + } + } + + if (currentParam == null) + throw new InvalidOperationException("No parameter name was read."); + + return GetPropAndValue(out arg); + + PropertyInfo GetPropAndValue(out string argv) + { + bool quoted = state == ReadState.InQuotedArgument; + state = (currentRead == (quoted ? input.Length - 1 : input.Length)) + ? ReadState.End + : ReadState.LookingForParameter; + + if (quoted) + { + argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); + currentRead++; + } + else + argv = input.Substring(beginRead, currentRead - beginRead); + + return _tProps[currentParam]; + } + } + + async Task ReadArgumentAsync(PropertyInfo prop, string arg) + { + var elemType = prop.PropertyType; + bool isCollection = false; + if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elemType = prop.PropertyType.GenericTypeArguments[0]; + isCollection = true; + } + + var overridden = prop.GetCustomAttribute(); + var reader = (overridden != null) + ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) + : (_commands.GetDefaultTypeReader(elemType) + ?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); + + if (reader != null) + { + if (isCollection) + { + var method = _readMultipleMethod.MakeGenericMethod(elemType); + var task = (Task)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); + return await task.ConfigureAwait(false); + } + else + return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); + } + return null; + } + } + + private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) + { + var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); + return (readResult.IsSuccess) + ? readResult.BestMatch + : null; + } + private static async Task ReadMultiple(TypeReader reader, ICommandContext context, IEnumerable args, IServiceProvider services) + { + var objs = new List(); + foreach (var arg in args) + { + var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); + if (read != null) + objs.Add((TObj)read); + } + return objs.ToImmutableArray(); + } + private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader) + .GetTypeInfo() + .DeclaredMethods + .Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); + + private enum ReadState + { + LookingForParameter, + InParameter, + LookingForArgument, + InArgument, + InQuotedArgument, + End + } + } +} diff --git a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs index 109689e15..f68bf6e2c 100644 --- a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -24,11 +24,12 @@ namespace Discord.Commands _baseTypeReader = baseTypeReader; } + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) return TypeReaderResult.FromSuccess(new T?()); - return await _baseTypeReader.ReadAsync(context, input, services); + return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index b19a6bd69..cb74139df 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -17,14 +17,16 @@ namespace Discord.Commands private readonly TryParseDelegate _tryParse; private readonly float _score; + /// must be within the range [0, 1]. public PrimitiveTypeReader() : this(PrimitiveParsers.Get(), 1) { } + /// must be within the range [0, 1]. public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { if (score < 0 || score > 1) - throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]"); + throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]."); _tryParse = tryParse; _score = score; @@ -34,7 +36,7 @@ namespace Discord.Commands { if (_tryParse(input, out T value)) return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}.")); } } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 508624103..4c9aaf4d8 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -6,20 +6,23 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class RoleTypeReader : TypeReader where T : class, IRole { + /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - ulong id; - if (context.Guild != null) { var results = new Dictionary(); var roles = context.Guild.Roles; //By Mention (1.0) - if (MentionUtils.TryParseRole(input, out id)) + if (MentionUtils.TryParseRole(input, out var id)) AddResult(results, context.Guild.GetRole(id) as T, 1.00f); //By Id (0.9) diff --git a/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs b/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs index 314fbb322..5448553b3 100644 --- a/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs @@ -6,29 +6,50 @@ namespace Discord.Commands { internal class TimeSpanTypeReader : TypeReader { - private static readonly string[] Formats = { - "%d'd'%h'h'%m'm'%s's'", //4d3h2m1s - "%d'd'%h'h'%m'm'", //4d3h2m - "%d'd'%h'h'%s's'", //4d3h 1s - "%d'd'%h'h'", //4d3h - "%d'd'%m'm'%s's'", //4d 2m1s - "%d'd'%m'm'", //4d 2m - "%d'd'%s's'", //4d 1s - "%d'd'", //4d - "%h'h'%m'm'%s's'", // 3h2m1s - "%h'h'%m'm'", // 3h2m - "%h'h'%s's'", // 3h 1s - "%h'h'", // 3h - "%m'm'%s's'", // 2m1s - "%m'm'", // 2m - "%s's'", // 1s + /// + /// TimeSpan try parse formats. + /// + private static readonly string[] Formats = + { + "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s + "%d'd'%h'h'%m'm'", // 4d3h2m + "%d'd'%h'h'%s's'", // 4d3h 1s + "%d'd'%h'h'", // 4d3h + "%d'd'%m'm'%s's'", // 4d 2m1s + "%d'd'%m'm'", // 4d 2m + "%d'd'%s's'", // 4d 1s + "%d'd'", // 4d + "%h'h'%m'm'%s's'", // 3h2m1s + "%h'h'%m'm'", // 3h2m + "%h'h'%s's'", // 3h 1s + "%h'h'", // 3h + "%m'm'%s's'", // 2m1s + "%m'm'", // 2m + "%s's'", // 1s }; + /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) - ? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) - : Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + if (string.IsNullOrEmpty(input)) + throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input)); + + var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign + if (isNegative) + { + input = input.Substring(1); + } + + if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) + { + return isNegative + ? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan)) + : Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)); + } + else + { + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + } } } } diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index af45a0aac..af780993d 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -1,10 +1,22 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { + /// + /// Defines a reader class that parses user input into a specified type. + /// public abstract class TypeReader { + /// + /// Attempts to parse the into the desired type. + /// + /// The context of the command. + /// The raw input of the command. + /// The service collection used for dependency injection. + /// + /// A task that represents the asynchronous parsing operation. The task result contains the parsing result. + /// public abstract Task ReadAsync(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 498a214e4..c0104e341 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -7,21 +7,25 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class UserTypeReader : TypeReader where T : class, IUser { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better IReadOnlyCollection guildUsers = ImmutableArray.Create(); - ulong id; if (context.Guild != null) guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); //By Mention (1.0) - if (MentionUtils.TryParseUser(input, out id)) + if (MentionUtils.TryParseUser(input, out var id)) { if (context.Guild != null) AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); @@ -45,8 +49,8 @@ namespace Discord.Commands string username = input.Substring(0, index); if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { - var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && - string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); + var channelUser = await channelUsers.FirstOrDefaultAsync(x => x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && @@ -59,7 +63,8 @@ namespace Discord.Commands { await channelUsers .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) - .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)); + .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)) + .ConfigureAwait(false); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); @@ -69,7 +74,8 @@ namespace Discord.Commands { await channelUsers .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) - .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)); + .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)) + .ConfigureAwait(false); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f); diff --git a/src/Discord.Net.Commands/Results/ExecuteResult.cs b/src/Discord.Net.Commands/Results/ExecuteResult.cs index bad39e230..055999596 100644 --- a/src/Discord.Net.Commands/Results/ExecuteResult.cs +++ b/src/Discord.Net.Commands/Results/ExecuteResult.cs @@ -1,16 +1,25 @@ -using System; +using System; using System.Diagnostics; namespace Discord.Commands { + /// + /// Contains information of the command's overall execution result. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct ExecuteResult : IResult { + /// + /// Gets the exception that may have occurred during the command execution. + /// public Exception Exception { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; private ExecuteResult(Exception exception, CommandError? error, string errorReason) @@ -20,15 +29,56 @@ namespace Discord.Commands ErrorReason = errorReason; } + /// + /// Initializes a new with no error, indicating a successful execution. + /// + /// + /// A that does not contain any errors. + /// public static ExecuteResult FromSuccess() => new ExecuteResult(null, null, null); + /// + /// Initializes a new with a specified and its + /// reason, indicating an unsuccessful execution. + /// + /// The type of error. + /// The reason behind the error. + /// + /// A that contains a and reason. + /// public static ExecuteResult FromError(CommandError error, string reason) => new ExecuteResult(null, error, reason); + /// + /// Initializes a new with a specified exception, indicating an unsuccessful + /// execution. + /// + /// The exception that caused the command execution to fail. + /// + /// A that contains the exception that caused the unsuccessful execution, along + /// with a of type Exception as well as the exception message as the + /// reason. + /// public static ExecuteResult FromError(Exception ex) => new ExecuteResult(ex, CommandError.Exception, ex.Message); + /// + /// Initializes a new with a specified result; this may or may not be an + /// successful execution depending on the and + /// specified. + /// + /// The result to inherit from. + /// + /// A that inherits the error type and reason. + /// public static ExecuteResult FromError(IResult result) => new ExecuteResult(null, result.Error, result.ErrorReason); - + + /// + /// Gets a string that indicates the execution result. + /// + /// + /// Success if is true; otherwise ": + /// ". + /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; } diff --git a/src/Discord.Net.Commands/Results/IResult.cs b/src/Discord.Net.Commands/Results/IResult.cs index 928d1139e..c11b58001 100644 --- a/src/Discord.Net.Commands/Results/IResult.cs +++ b/src/Discord.Net.Commands/Results/IResult.cs @@ -1,9 +1,31 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// + /// Contains information of the result related to a command. + /// public interface IResult { + /// + /// Describes the error type that may have occurred during the operation. + /// + /// + /// A indicating the type of error that may have occurred during the operation; + /// null if the operation was successful. + /// CommandError? Error { get; } + /// + /// Describes the reason for the error. + /// + /// + /// A string containing the error reason. + /// string ErrorReason { get; } + /// + /// Indicates whether the operation was successful or not. + /// + /// + /// true if the result is positive; otherwise false. + /// bool IsSuccess { get; } } } diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs new file mode 100644 index 000000000..fb266efa6 --- /dev/null +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -0,0 +1,47 @@ +using System; + +namespace Discord.Commands +{ + public class MatchResult : IResult + { + /// + /// Gets the command that may have matched during the command execution. + /// + public CommandMatch? Match { get; } + + /// + /// Gets on which pipeline stage the command may have matched or failed. + /// + public IResult? Pipeline { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + /// + public bool IsSuccess => !Error.HasValue; + + private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + { + Match = match; + Error = error; + Pipeline = pipeline; + ErrorReason = errorReason; + } + + public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) + => new MatchResult(match,pipeline,null, null); + public static MatchResult FromError(CommandError error, string reason) + => new MatchResult(null,null,error, reason); + public static MatchResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static MatchResult FromError(IResult result) + => new MatchResult(null, null,result.Error, result.ErrorReason); + public static MatchResult FromError(IResult pipeline, CommandError error, string reason) + => new MatchResult(null, pipeline, error, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + + } +} diff --git a/src/Discord.Net.Commands/Results/ParseResult.cs b/src/Discord.Net.Commands/Results/ParseResult.cs index 3a0692b2d..43351b6bf 100644 --- a/src/Discord.Net.Commands/Results/ParseResult.cs +++ b/src/Discord.Net.Commands/Results/ParseResult.cs @@ -4,23 +4,39 @@ using System.Diagnostics; namespace Discord.Commands { + /// + /// Contains information for the parsing result from the command service's parser. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct ParseResult : IResult { public IReadOnlyList ArgValues { get; } public IReadOnlyList ParamValues { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// + /// Provides information about the parameter that caused the parsing error. + /// + /// + /// A indicating the parameter info of the error that may have occurred during parsing; + /// null if the parsing was successful or the parsing error is not specific to a single parameter. + /// + public ParameterInfo ErrorParameter { get; } + + /// public bool IsSuccess => !Error.HasValue; - private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, string errorReason) + private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, string errorReason, ParameterInfo errorParamInfo) { ArgValues = argValues; ParamValues = paramValues; Error = error; ErrorReason = errorReason; + ErrorParameter = errorParamInfo; } public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) @@ -28,14 +44,14 @@ namespace Discord.Commands for (int i = 0; i < argValues.Count; i++) { if (argValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); } for (int i = 0; i < paramValues.Count; i++) { if (paramValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); } - return new ParseResult(argValues, paramValues, null, null); + return new ParseResult(argValues, paramValues, null, null, null); } public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) { @@ -49,15 +65,19 @@ namespace Discord.Commands for (int i = 0; i < paramValues.Count; i++) paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); } - return new ParseResult(argList, paramList, null, null); + return new ParseResult(argList, paramList, null, null, null); } public static ParseResult FromError(CommandError error, string reason) - => new ParseResult(null, null, error, reason); + => new ParseResult(null, null, error, reason, null); + public static ParseResult FromError(CommandError error, string reason, ParameterInfo parameterInfo) + => new ParseResult(null, null, error, reason, parameterInfo); public static ParseResult FromError(Exception ex) => FromError(CommandError.Exception, ex.Message); public static ParseResult FromError(IResult result) - => new ParseResult(null, null, result.Error, result.ErrorReason); + => new ParseResult(null, null, result.Error, result.ErrorReason, null); + public static ParseResult FromError(IResult result, ParameterInfo parameterInfo) + => new ParseResult(null, null, result.Error, result.ErrorReason, parameterInfo); public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})" : $"{Error}: {ErrorReason}"; diff --git a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs index ee650600a..7ecade4f8 100644 --- a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -15,7 +15,7 @@ namespace Discord.Commands PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); } - public static new PreconditionGroupResult FromSuccess() + public new static PreconditionGroupResult FromSuccess() => new PreconditionGroupResult(null, null, null); public static PreconditionGroupResult FromError(string reason, ICollection preconditions) => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index 01fc1a3fd..d8e399a01 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -3,29 +3,56 @@ using System.Diagnostics; namespace Discord.Commands { + /// + /// Represents a result type for command preconditions. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class PreconditionResult : IResult { + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; + /// + /// Initializes a new class with the command type + /// and reason. + /// + /// The type of failure. + /// The reason of failure. protected PreconditionResult(CommandError? error, string errorReason) { Error = error; ErrorReason = errorReason; } + /// + /// Returns a with no errors. + /// public static PreconditionResult FromSuccess() => new PreconditionResult(null, null); + /// + /// Returns a with and the + /// specified reason. + /// + /// The reason of failure. public static PreconditionResult FromError(string reason) => new PreconditionResult(CommandError.UnmetPrecondition, reason); public static PreconditionResult FromError(Exception ex) => new PreconditionResult(CommandError.Exception, ex.Message); + /// + /// Returns a with the specified type. + /// + /// The result of failure. public static PreconditionResult FromError(IResult result) => new PreconditionResult(result.Error, result.ErrorReason); + /// + /// Returns a string indicating whether the is successful. + /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; } diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs index 2a326a7a3..e4c86fc23 100644 --- a/src/Discord.Net.Commands/Results/RuntimeResult.cs +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -1,24 +1,30 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Text; namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class RuntimeResult : IResult { + /// + /// Initializes a new class with the type of error and reason. + /// + /// The type of failure, or null if none. + /// The reason of failure. protected RuntimeResult(CommandError? error, string reason) { Error = error; Reason = reason; } + /// public CommandError? Error { get; } + /// Describes the execution reason or result. public string Reason { get; } + /// public bool IsSuccess => !Error.HasValue; + /// string IResult.ErrorReason => Reason; public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs index 6a5878ea2..d1f1ea0d7 100644 --- a/src/Discord.Net.Commands/Results/SearchResult.cs +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -10,9 +10,12 @@ namespace Discord.Commands public string Text { get; } public IReadOnlyList Commands { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index e696dbc17..2fbdb45d6 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -27,10 +27,15 @@ namespace Discord.Commands { public IReadOnlyCollection Values { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; + + /// TypeReaderResult was not successful. public object BestMatch => IsSuccess ? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) : throw new InvalidOperationException("TypeReaderResult was not successful."); diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index ecb6a4b58..d6b49065b 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -1,9 +1,23 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// + /// Specifies the behavior of the command execution workflow. + /// + /// + /// public enum RunMode { + /// + /// The default behavior set in . + /// Default, + /// + /// Executes the command on the same thread as gateway one. + /// Sync, + /// + /// Executes the command on a different thread from the gateway one. + /// Async } } diff --git a/src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs b/src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs index 15a08b9b3..2612e99e5 100644 --- a/src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs +++ b/src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs @@ -1,19 +1,18 @@ -using System; using System.Collections.Generic; -using System.Text; -using System.Globalization; namespace Discord.Commands { /// - /// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig + /// Utility class which contains the default matching pairs of quotation marks for CommandServiceConfig /// internal static class QuotationAliasUtils { /// - /// Generates an IEnumerable of characters representing open-close pairs of - /// quotation punctuation. + /// A default map of open-close pairs of quotation marks. + /// Contains many regional and Unicode equivalents. + /// Used in the . /// + /// internal static Dictionary GetDefaultAliasMap { get diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index ec981cf52..062af0481 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -38,7 +37,7 @@ namespace Discord.Commands } catch (Exception ex) { - throw new Exception($"Failed to create \"{ownerType.FullName}\"", ex); + throw new Exception($"Failed to create \"{ownerType.FullName}\".", ex); } } @@ -46,12 +45,12 @@ namespace Discord.Commands { var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); if (constructors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\""); + throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"."); else if (constructors.Length > 1) - throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\""); + throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"."); return constructors[0]; } - private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo ownerType) + private static PropertyInfo[] GetProperties(TypeInfo ownerType) { var result = new List(); while (ownerType != ObjectTypeInfo) @@ -71,7 +70,7 @@ namespace Discord.Commands return commands; if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) return services; - var service = services?.GetService(memberType); + var service = services.GetService(memberType); if (service != null) return service; throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found."); diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index 116bc3850..b7c60f3d3 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Relay")] [assembly: InternalsVisibleTo("Discord.Net.Rest")] @@ -6,4 +6,5 @@ [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] [assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] -[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/Discord.Net.Core/Audio/AudioOutStream.cs b/src/Discord.Net.Core/Audio/AudioOutStream.cs index 7019ba8cd..cbc3167a2 100644 --- a/src/Discord.Net.Core/Audio/AudioOutStream.cs +++ b/src/Discord.Net.Core/Audio/AudioOutStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Discord.Audio @@ -7,8 +7,17 @@ namespace Discord.Audio { public override bool CanWrite => true; - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index 97820ea73..2287d47fa 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -11,10 +11,9 @@ namespace Discord.Audio public override bool CanSeek => false; public override bool CanWrite => false; - public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) - { - throw new InvalidOperationException("This stream does not accept headers"); - } + /// This stream does not accept headers. + public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) => + throw new InvalidOperationException("This stream does not accept headers."); public override void Write(byte[] buffer, int offset, int count) { WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); @@ -30,15 +29,30 @@ namespace Discord.Audio public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - public override long Length { get { throw new NotSupportedException(); } } + /// + /// Reading stream length is not supported. + public override long Length => + throw new NotSupportedException(); + + /// + /// Getting or setting this stream position is not supported. public override long Position { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 9be8ceef5..2fc52a529 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord.Audio @@ -15,11 +16,14 @@ namespace Discord.Audio /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } - /// Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. + /// Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. int Latency { get; } /// Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. int UdpLatency { get; } + /// Gets the current audio streams. + IReadOnlyDictionary GetStreams(); + Task StopAsync(); Task SetSpeakingAsync(bool value); diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 6bac241aa..d6535a4f1 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -2,10 +2,43 @@ using System; namespace Discord { + /// + /// Represents a class containing the strings related to various Content Delivery Networks (CDNs). + /// public static class CDN { + /// + /// Returns a team icon URL. + /// + /// The team identifier. + /// The icon identifier. + /// + /// A URL pointing to the team's icon. + /// + public static string GetTeamIconUrl(ulong teamId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}team-icons/{teamId}/{iconId}.jpg" : null; + + /// + /// Returns an application icon URL. + /// + /// The application identifier. + /// The icon identifier. + /// + /// A URL pointing to the application's icon. + /// public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; + + /// + /// Returns a user avatar URL. + /// + /// The user snowflake identifier. + /// The avatar identifier. + /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the user's avatar in the specified size. + /// public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { if (avatarId == null) @@ -13,47 +46,190 @@ namespace Discord string extension = FormatToExtension(format, avatarId); return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + + public static string GetGuildUserAvatarUrl(ulong userId, ulong guildId, string avatarId, ushort size, ImageFormat format) + { + if (avatarId == null) + return null; + string extension = FormatToExtension(format, avatarId); + return $"{DiscordConfig.CDNUrl}guilds/{guildId}/users/{userId}/avatars/{avatarId}.{extension}?size={size}"; + } + + /// + /// Returns a user banner URL. + /// + /// The user snowflake identifier. + /// The banner identifier. + /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the user's banner in the specified size. + /// + public static string GetUserBannerUrl(ulong userId, string bannerId, ushort size, ImageFormat format) + { + if (bannerId == null) + return null; + string extension = FormatToExtension(format, bannerId); + return $"{DiscordConfig.CDNUrl}banners/{userId}/{bannerId}.{extension}?size={size}"; + } + /// + /// Returns the default user avatar URL. + /// + /// The discriminator value of a user. + /// + /// A URL pointing to the user's default avatar when one isn't set. + /// public static string GetDefaultUserAvatarUrl(ushort discriminator) { return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; } + /// + /// Returns an icon URL. + /// + /// The guild snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the guild's icon. + /// public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; + /// + /// Returns a guild role's icon URL. + /// + /// The role identifier. + /// The icon hash. + /// + /// A URL pointing to the guild role's icon. + /// + public static string GetGuildRoleIconUrl(ulong roleId, string roleHash) + => roleHash != null ? $"{DiscordConfig.CDNUrl}role-icons/{roleId}/{roleHash}.png" : null; + /// + /// Returns a guild splash URL. + /// + /// The guild snowflake identifier. + /// The splash icon identifier. + /// + /// A URL pointing to the guild's splash. + /// public static string GetGuildSplashUrl(ulong guildId, string splashId) => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; + /// + /// Returns a guild discovery splash URL. + /// + /// The guild snowflake identifier. + /// The discovery splash icon identifier. + /// + /// A URL pointing to the guild's discovery splash. + /// + public static string GetGuildDiscoverySplashUrl(ulong guildId, string discoverySplashId) + => discoverySplashId != null ? $"{DiscordConfig.CDNUrl}discovery-splashes/{guildId}/{discoverySplashId}.jpg" : null; + /// + /// Returns a channel icon URL. + /// + /// The channel snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the channel's icon. + /// public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; + + /// + /// Returns a guild banner URL. + /// + /// The guild snowflake identifier. + /// The banner image identifier. + /// The format to return. + /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048 inclusive. + /// + /// A URL pointing to the guild's banner image. + /// + public static string GetGuildBannerUrl(ulong guildId, string bannerId, ImageFormat format, ushort? size = null) + { + if (string.IsNullOrEmpty(bannerId)) + return null; + string extension = FormatToExtension(format, bannerId); + return $"{DiscordConfig.CDNUrl}banners/{guildId}/{bannerId}.{extension}" + (size.HasValue ? $"?size={size}" : string.Empty); + } + /// + /// Returns an emoji URL. + /// + /// The emoji snowflake identifier. + /// Whether this emoji is animated. + /// + /// A URL pointing to the custom emote. + /// public static string GetEmojiUrl(ulong emojiId, bool animated) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; + /// + /// Returns a Rich Presence asset URL. + /// + /// The application identifier. + /// The asset identifier. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the asset image in the specified size. + /// public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { string extension = FormatToExtension(format, ""); return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; } + /// + /// Returns a Spotify album URL. + /// + /// The identifier for the album art (e.g. 6be8f4c8614ecf4f1dd3ebba8d8692d8ce4951ac). + /// + /// A URL pointing to the Spotify album art. + /// public static string GetSpotifyAlbumArtUrl(string albumArtId) => $"https://i.scdn.co/image/{albumArtId}"; + /// + /// Returns a Spotify direct URL for a track. + /// + /// The identifier for the track (e.g. 4uLU6hMCjMI75M1A2tKUQC). + /// + /// A URL pointing to the Spotify track. + /// public static string GetSpotifyDirectUrl(string trackId) => $"https://open.spotify.com/track/{trackId}"; + /// + /// Gets a stickers url based off the id and format. + /// + /// The id of the sticker. + /// The format of the sticker. + /// + /// A URL to the sticker. + /// + public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png) + => $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}"; + + private static string FormatToExtension(StickerFormatType format) + { + return format switch + { + StickerFormatType.None or StickerFormatType.Png or StickerFormatType.Apng => "png", // In the case of the Sticker endpoint, the sticker will be available as PNG if its format_type is PNG or APNG, and as Lottie if its format_type is LOTTIE. + StickerFormatType.Lottie => "lottie", + _ => throw new ArgumentException(nameof(format)), + }; + } + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; - switch (format) + return format switch { - case ImageFormat.Gif: - return "gif"; - case ImageFormat.Jpeg: - return "jpeg"; - case ImageFormat.Png: - return "png"; - case ImageFormat.WebP: - return "webp"; - default: - throw new ArgumentException(nameof(format)); - } + ImageFormat.Gif => "gif", + ImageFormat.Jpeg => "jpeg", + ImageFormat.Png => "png", + ImageFormat.WebP => "webp", + _ => throw new ArgumentException(nameof(format)), + }; } } } diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index ac1424339..d56eb38a0 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -1,11 +1,29 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// + /// Represents a context of a command. This may include the client, guild, channel, user, and message. + /// public interface ICommandContext { + /// + /// Gets the that the command is executed with. + /// IDiscordClient Client { get; } + /// + /// Gets the that the command is executed in. + /// IGuild Guild { get; } + /// + /// Gets the that the command is executed in. + /// IMessageChannel Channel { get; } + /// + /// Gets the who executed the command. + /// IUser User { get; } + /// + /// Gets the that the command is interpreted from. + /// IUserMessage Message { get; } } } diff --git a/src/Discord.Net.Core/ConnectionState.cs b/src/Discord.Net.Core/ConnectionState.cs index 42c505ccd..fadbc4065 100644 --- a/src/Discord.Net.Core/ConnectionState.cs +++ b/src/Discord.Net.Core/ConnectionState.cs @@ -1,10 +1,15 @@ -namespace Discord +namespace Discord { + /// Specifies the connection state of a client. public enum ConnectionState : byte { + /// The client has disconnected from Discord. Disconnected, + /// The client is connecting to Discord. Connecting, + /// The client has established a connection to Discord. Connected, + /// The client is disconnecting from Discord. Disconnecting } } diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index d4d450e1c..7dc55b1cf 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,15 +1,19 @@ - + + Discord.Net.Core Discord The core components for the Discord.Net library. - net46;netstandard1.3;netstandard2.0 - netstandard1.3;netstandard2.0 + net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 - + - + + + all + - + \ No newline at end of file diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 8c6cfc28c..d5951bd07 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -2,35 +2,180 @@ using System.Reflection; namespace Discord { + /// + /// Defines various behaviors of Discord.Net. + /// public class DiscordConfig { - public const int APIVersion = 6; + /// + /// Returns the API version Discord.Net uses. + /// + /// + /// An representing the API version that Discord.Net uses to communicate with Discord. + /// A list of available API version can be seen on the official + /// Discord API documentation + /// . + /// + public const int APIVersion = 9; + /// + /// Returns the Voice API version Discord.Net uses. + /// + /// + /// An representing the API version that Discord.Net uses to communicate with Discord's + /// voice server. + /// public const int VoiceAPIVersion = 3; + /// + /// Gets the Discord.Net version, including the build number. + /// + /// + /// A string containing the detailed version information, including its build number; Unknown when + /// the version fails to be fetched. + /// public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; - public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + /// + /// Gets the user agent that Discord.Net uses in its clients. + /// + /// + /// The user agent used in each Discord.Net request. + /// + public static string UserAgent { get; } = $"DiscordBot (https://github.com/discord-net/Discord.Net, v{Version})"; + /// + /// Returns the base Discord API URL. + /// + /// + /// The Discord API URL using . + /// + public static readonly string APIUrl = $"https://discord.com/api/v{APIVersion}/"; + /// + /// Returns the base Discord CDN URL. + /// + /// + /// The base Discord Content Delivery Network (CDN) URL. + /// public const string CDNUrl = "https://cdn.discordapp.com/"; + /// + /// Returns the base Discord invite URL. + /// + /// + /// The base Discord invite URL. + /// public const string InviteUrl = "https://discord.gg/"; + /// + /// Returns the default timeout for requests. + /// + /// + /// The amount of time it takes in milliseconds before a request is timed out. + /// public const int DefaultRequestTimeout = 15000; + /// + /// Returns the max length for a Discord message. + /// + /// + /// The maximum length of a message allowed by Discord. + /// public const int MaxMessageSize = 2000; + /// + /// Returns the max messages allowed to be in a request. + /// + /// + /// The maximum number of messages that can be gotten per-batch. + /// public const int MaxMessagesPerBatch = 100; + /// + /// Returns the max users allowed to be in a request. + /// + /// + /// The maximum number of users that can be gotten per-batch. + /// public const int MaxUsersPerBatch = 1000; + /// + /// Returns the max users allowed to be in a request for guild event users. + /// + /// + /// The maximum number of users that can be gotten per-batch. + /// + public const int MaxGuildEventUsersPerBatch = 100; + /// + /// Returns the max guilds allowed to be in a request. + /// + /// + /// The maximum number of guilds that can be gotten per-batch. + /// public const int MaxGuildsPerBatch = 100; + /// + /// Returns the max user reactions allowed to be in a request. + /// + /// + /// The maximum number of user reactions that can be gotten per-batch. + /// public const int MaxUserReactionsPerBatch = 100; + /// + /// Returns the max audit log entries allowed to be in a request. + /// + /// + /// The maximum number of audit log entries that can be gotten per-batch. + /// public const int MaxAuditLogEntriesPerBatch = 100; - /// Gets or sets how a request should act in the case of an error, by default. + /// + /// Gets or sets how a request should act in the case of an error, by default. + /// + /// + /// The currently set . + /// public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; - /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// + /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// + /// + /// The currently set for logging level. + /// public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// Gets or sets whether the initial log entry should be printed. + /// + /// Gets or sets whether the initial log entry should be printed. + /// + /// + /// If set to true, the library will attempt to print the current version of the library, as well as + /// the API version it uses on startup. + /// internal bool DisplayInitialLog { get; set; } = true; + + /// + /// Gets or sets whether or not rate-limits should use the system clock. + /// + /// + /// If set to false, we will use the X-RateLimit-Reset-After header + /// to determine when a rate-limit expires, rather than comparing the + /// X-RateLimit-Reset timestamp to the system time. + /// + /// This should only be changed to false if the system is known to have + /// a clock that is out of sync. Relying on the Reset-After header will + /// incur network lag. + /// + /// Regardless of this property, we still rely on the system's wall-clock + /// to determine if a bucket is rate-limited; we do not use any monotonic + /// clock. Your system will still need a stable clock. + /// + public bool UseSystemClock { get; set; } = true; + + /// + /// Gets or sets whether or not the internal experation check uses the system date + /// + snowflake date to check if an interaction can be responded to. + /// + /// + /// If set to then the CreatedAt property in an interaction + /// will be set to when it was received instead of the snowflakes date. + ///
+ /// This will still require a stable clock on your system. + ///
+ public bool UseInteractionSnowflakeDate { get; set; } = true; } } diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs new file mode 100644 index 000000000..5a5223b93 --- /dev/null +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a set of json error codes received by discord. + /// + public enum DiscordErrorCode + { + GeneralError = 0, + + #region UnknownXYZ (10XXX) + UnknownAccount = 10001, + UnknownApplication = 10002, + UnknownChannel = 10003, + UnknownGuild = 10004, + UnknownIntegration = 10005, + UnknownInvite = 10006, + UnknownMember = 10007, + UnknownMessage = 10008, + UnknownPermissionOverwrite = 10009, + UnknownProvider = 10010, + UnknownRole = 10011, + UnknownToken = 10012, + UnknownUser = 10013, + UnknownEmoji = 10014, + UnknownWebhook = 10015, + UnknownWebhookService = 10016, + UnknownSession = 10020, + UnknownBan = 10026, + UnknownSKU = 10027, + UnknownStoreListing = 10028, + UnknownEntitlement = 10029, + UnknownBuild = 10030, + UnknownLobby = 10031, + UnknownBranch = 10032, + UnknownStoreDirectoryLayout = 10033, + UnknownRedistributable = 10036, + UnknownGiftCode = 10038, + UnknownStream = 10049, + UnknownPremiumServerSubscribeCooldown = 10050, + UnknownGuildTemplate = 10057, + UnknownDiscoverableServerCategory = 10059, + UnknownSticker = 10060, + UnknownInteraction = 10062, + UnknownApplicationCommand = 10063, + UnknownApplicationCommandPermissions = 10066, + UnknownStageInstance = 10067, + UnknownGuildMemberVerificationForm = 10068, + UnknownGuildWelcomeScreen = 10069, + UnknownGuildScheduledEvent = 10070, + UnknownGuildScheduledEventUser = 10071, + #endregion + + #region General Actions (20XXX) + BotsCannotUse = 20001, + OnlyBotsCanUse = 20002, + CannotSendExplicitContent = 20009, + ApplicationActionUnauthorized = 20012, + ActionSlowmode = 20016, + OnlyOwnerAction = 20018, + AnnouncementEditRatelimit = 20022, + ChannelWriteRatelimit = 20028, + WordsNotAllowed = 20031, + GuildPremiumTooLow = 20035, + #endregion + + #region Numeric Limits Reached (30XXX) + MaximumGuildsReached = 30001, + MaximumFriendsReached = 30002, + MaximumPinsReached = 30003, + MaximumRecipientsReached = 30004, + MaximumGuildRolesReached = 30005, + MaximumWebhooksReached = 30007, + MaximumEmojisReached = 30008, + MaximumReactionsReached = 30010, + MaximumGuildChannelsReached = 30013, + MaximumAttachmentsReached = 30015, + MaximumInvitesReached = 30016, + MaximumAnimatedEmojisReached = 30018, + MaximumServerMembersReached = 30019, + MaximumServerCategoriesReached = 30030, + GuildTemplateAlreadyExists = 30031, + MaximumThreadMembersReached = 30033, + MaximumBansForNonGuildMembersReached = 30035, + MaximumBanFetchesReached = 30037, + MaximumUncompleteGuildScheduledEvents = 30038, + MaximumStickersReached = 30039, + MaximumPruneRequestReached = 30040, + MaximumGuildWigitsReached = 30042, + #endregion + + #region General Request Errors (40XXX) + TokenUnauthorized = 40001, + InvalidVerification = 40002, + OpeningDMTooFast = 40003, + RequestEntityTooLarge = 40005, + FeatureDisabled = 40006, + UserBanned = 40007, + TargetUserNotInVoice = 40032, + MessageAlreadyCrossposted = 40033, + ApplicationNameAlreadyExists = 40041, + #endregion + + #region Action Preconditions/Checks (50XXX) + MissingPermissions = 50001, + InvalidAccountType = 50002, + CannotExecuteForDM = 50003, + GuildWigitDisabled = 50004, + CannotEditOtherUsersMessage = 50005, + CannotSendEmptyMessage = 50006, + CannotSendMessageToUser = 50007, + CannotSendMessageToVoiceChannel = 50008, + ChannelVerificationTooHight = 50009, + OAuth2ApplicationDoesntHaveBot = 50010, + OAuth2ApplicationLimitReached = 50011, + InvalidOAuth2State = 50012, + InsufficientPermissions = 50013, + InvalidAuthenticationToken = 50014, + NoteTooLong = 50015, + ProvidedMessageDeleteCountOutOfBounds = 50016, + InvalidPinChannel = 50019, + InvalidInvite = 50020, + CannotExecuteOnSystemMessage = 50021, + CannotExecuteOnChannelType = 50024, + InvalidOAuth2Token = 50025, + MissingOAuth2Scope = 50026, + InvalidWebhookToken = 50027, + InvalidRole = 50028, + InvalidRecipients = 50033, + BulkDeleteMessageTooOld = 50034, + InvalidFormBody = 50035, + InviteAcceptedForGuildThatBotIsntIn = 50036, + InvalidAPIVersion = 50041, + FileUploadTooBig = 50045, + InvalidFileUpload = 50046, + CannotSelfRedeemGift = 50054, + PaymentSourceRequiredForGift = 50070, + CannotDeleteRequiredCommunityChannel = 50074, + InvalidSticker = 50081, + CannotExecuteOnArchivedThread = 50083, + InvalidThreadNotificationSettings = 50084, + BeforeValueEarlierThanThreadCreation = 50085, + ServerLocaleUnavailable = 50095, + ServerRequiresMonetization = 50097, + ServerRequiresBoosts = 50101, + + #endregion + + #region 2FA (60XXX) + Requires2FA = 60003, + #endregion + + #region User Searches (80XXX) + NoUsersWithTag = 80004, + #endregion + + #region Reactions (90XXX) + ReactionBlocked = 90001, + #endregion + + #region API Status (130XXX) + APIOverloaded = 130000, + #endregion + + #region Stage Errors (150XXX) + StageAlreadyOpened = 150006, + #endregion + + #region Reply and Thread Errors (160XXX) + CannotReplyWithoutReadMessageHistory = 160002, + MessageAlreadyContainsThread = 160004, + ThreadIsLocked = 160005, + MaximumActiveThreadsReached = 160006, + MaximumAnnouncementThreadsReached = 160007, + #endregion + + #region Sticker Uploads (170XXX) + InvalidJSONLottie = 170001, + LottieCantContainRasters = 170002, + StickerMaximumFramerateExceeded = 170003, + StickerMaximumFrameCountExceeded = 170004, + LottieMaximumDimentionsExceeded = 170005, + StickerFramerateBoundsExceeed = 170006, + StickerAnimationDurationTooLong = 170007, + #endregion + + #region Guild Scheduled Events + CannotUpdateFinishedEvent = 180000, + FailedStageCreation = 180002, + #endregion + } +} diff --git a/src/Discord.Net.Core/DiscordJsonError.cs b/src/Discord.Net.Core/DiscordJsonError.cs new file mode 100644 index 000000000..fdf82ea0c --- /dev/null +++ b/src/Discord.Net.Core/DiscordJsonError.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic parsed json error received from discord after performing a rest request. + /// + public struct DiscordJsonError + { + /// + /// Gets the json path of the error. + /// + public string Path { get; } + + /// + /// Gets a collection of errors associated with the specific property at the path. + /// + public IReadOnlyCollection Errors { get; } + + internal DiscordJsonError(string path, DiscordError[] errors) + { + Path = path; + Errors = errors.ToImmutableArray(); + } + } + + /// + /// Represents an error with a property. + /// + public struct DiscordError + { + /// + /// Gets the code of the error. + /// + public string Code { get; } + + /// + /// Gets the message describing what went wrong. + /// + public string Message { get; } + + internal DiscordError(string code, string message) + { + Code = code; + Message = message; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs b/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs new file mode 100644 index 000000000..15a79dff6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs @@ -0,0 +1,50 @@ +using System; + +namespace Discord +{ + /// + /// Flags for the property, that are ORd together. + /// These describe what the activity payload includes. + /// + [Flags] + public enum ActivityProperties + { + /// + /// Indicates that no actions on this activity can be taken. + /// + None = 0, + Instance = 1, + /// + /// Indicates that this activity can be joined. + /// + Join = 0b10, + /// + /// Indicates that this activity can be spectated. + /// + Spectate = 0b100, + /// + /// Indicates that a user may request to join an activity. + /// + JoinRequest = 0b1000, + /// + /// Indicates that a user can listen along in Spotify. + /// + Sync = 0b10000, + /// + /// Indicates that a user can play this song. + /// + Play = 0b100000, + /// + /// Indicates that a user is playing an activity in a voice channel with friends. + /// + PartyPrivacyFriends = 0b1000000, + /// + /// Indicates that a user is playing an activity in a voice channel. + /// + PartyPrivacyVoiceChannel = 0b10000000, + /// + /// Indicates that a user is playing an activity in a voice channel. + /// + Embedded = 0b10000000 + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index c7db7b247..1f67886eb 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -1,10 +1,33 @@ -namespace Discord +namespace Discord { + /// + /// Specifies a Discord user's activity type. + /// public enum ActivityType { + /// + /// The user is playing a game. + /// Playing = 0, + /// + /// The user is streaming online. + /// Streaming = 1, + /// + /// The user is listening to a song. + /// Listening = 2, - Watching = 3 + /// + /// The user is watching some form of media. + /// + Watching = 3, + /// + /// The user has set a custom status. + /// + CustomStatus = 4, + /// + /// The user is competing in a game. + /// + Competing = 5, } } diff --git a/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs new file mode 100644 index 000000000..7bd2664a2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for their custom status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CustomStatusGame : Game + { + internal CustomStatusGame() { } + + /// + /// Gets the emote, if it is set. + /// + /// + /// An containing the or set by the user. + /// + public IEmote Emote { get; internal set; } + + /// + /// Gets the timestamp of when this status was created. + /// + /// + /// A containing the time when this status was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the state of the status. + /// + public string State { get; internal set; } + + public override string ToString() + => $"{Emote} {State}"; + + private string DebuggerDisplay => $"{Name}"; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs b/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs new file mode 100644 index 000000000..80f128fa8 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/DefaultApplications.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum DefaultApplications : ulong + { + /// + /// Watch youtube together. + /// + Youtube = 880218394199220334, + + /// + /// Youtube development application. + /// + YoutubeDev = 880218832743055411, + + /// + /// Poker! + /// + Poker = 755827207812677713, + + /// + /// Betrayal: A Party Adventure. Betrayal is a social deduction game inspired by Werewolf, Town of Salem, and Among Us. + /// + Betrayal = 773336526917861400, + + /// + /// Sit back, relax, and do some fishing! + /// + Fishing = 814288819477020702, + + /// + /// The queens gambit. + /// + Chess = 832012774040141894, + + /// + /// Development version of chess. + /// + ChessDev = 832012586023256104, + + /// + /// LetterTile is a version of scrabble. + /// + LetterTile = 879863686565621790, + + /// + /// Find words in a jumble of letters in coffee. + /// + WordSnack = 879863976006127627, + + /// + /// It's like skribbl.io. + /// + DoodleCrew = 878067389634314250, + + /// + /// It's like cards against humanity. + /// + Awkword = 879863881349087252, + + /// + /// A word-search like game where you unscramble words and score points in a scrabble fashion. + /// + SpellCast = 852509694341283871, + + /// + /// Classic checkers + /// + Checkers = 832013003968348200, + + /// + /// The development version of poker. + /// + PokerDev = 763133495793942528, + + /// + /// SketchyArtist. + /// + SketchyArtist = 879864070101172255 + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index 179ad4eaa..8891e142c 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -2,19 +2,36 @@ using System.Diagnostics; namespace Discord { + /// + /// A user's game status. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Game : IActivity { + /// public string Name { get; internal set; } + /// public ActivityType Type { get; internal set; } + /// + public ActivityProperties Flags { get; internal set; } + /// + public string Details { get; internal set; } internal Game() { } - public Game(string name, ActivityType type = ActivityType.Playing) + /// + /// Creates a with the provided name and . + /// + /// The name of the game. + /// The type of activity. + public Game(string name, ActivityType type = ActivityType.Playing, ActivityProperties flags = ActivityProperties.None, string details = null) { Name = name; Type = type; + Flags = flags; + Details = details; } + /// Returns the name of the . public override string ToString() => Name; private string DebuggerDisplay => Name; } diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index 02c29ba41..7217bded3 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -1,14 +1,37 @@ namespace Discord { + /// + /// An asset for a object containing the text and image. + /// public class GameAsset { internal GameAsset() { } internal ulong? ApplicationId { get; set; } - + + /// + /// Gets the description of the asset. + /// + /// + /// A string containing the description of the asset. + /// public string Text { get; internal set; } + /// + /// Gets the image ID of the asset. + /// + /// + /// A string containing the unique image identifier of the asset. + /// public string ImageId { get; internal set; } - + + /// + /// Returns the image URL of the asset. + /// + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A string pointing to the image URL of the asset; null when the application ID does not exist. + /// public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; } diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs index 54e6deef4..0cfa9980d 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameParty.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -1,11 +1,26 @@ namespace Discord { + /// + /// Party information for a object. + /// public class GameParty { internal GameParty() { } + /// + /// Gets the ID of the party. + /// + /// + /// A string containing the unique identifier of the party. + /// public string Id { get; internal set; } public long Members { get; internal set; } + /// + /// Gets the party's current and maximum size. + /// + /// + /// A representing the capacity of the party. + /// public long Capacity { get; internal set; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs index e9d988ba9..595b8851c 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -1,9 +1,21 @@ -namespace Discord +namespace Discord { + /// + /// Party secret for a object. + /// public class GameSecrets { + /// + /// Gets the secret for a specific instanced match. + /// public string Match { get; } + /// + /// Gets the secret for joining a party. + /// public string Join { get; } + /// + /// Gets the secret for spectating a game. + /// public string Spectate { get; } internal GameSecrets(string match, string join, string spectate) @@ -13,4 +25,4 @@ Spectate = spectate; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs index 8c8c992fa..a41388afb 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -1,10 +1,19 @@ -using System; +using System; namespace Discord { + /// + /// Timestamps for a object. + /// public class GameTimestamps { + /// + /// Gets when the activity started. + /// public DateTimeOffset? Start { get; } + /// + /// Gets when the activity ends. + /// public DateTimeOffset? End { get; } internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) @@ -13,4 +22,4 @@ namespace Discord End = end; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs index 1f158217d..96704b826 100644 --- a/src/Discord.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -1,8 +1,40 @@ -namespace Discord +namespace Discord { + /// + /// A user's activity status, typically a . + /// public interface IActivity { + /// + /// Gets the name of the activity. + /// + /// + /// A string containing the name of the activity that the user is doing. + /// string Name { get; } + /// + /// Gets the type of the activity. + /// + /// + /// The type of activity. + /// ActivityType Type { get; } + /// + /// Gets the flags that are relevant to this activity. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of flags for this activity. + /// + ActivityProperties Flags { get; } + /// + /// Gets the details on what the player is currently doing. + /// + /// + /// A string describing what the player is doing. + /// + string Details { get; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs index fc3f68cf0..2da8d741c 100644 --- a/src/Discord.Net.Core/Entities/Activities/RichGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -2,20 +2,46 @@ using System.Diagnostics; namespace Discord { + /// + /// A user's Rich Presence status. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RichGame : Game { internal RichGame() { } - public string Details { get; internal set; } + /// + /// Gets the user's current party status. + /// public string State { get; internal set; } + /// + /// Gets the application ID for the game. + /// public ulong ApplicationId { get; internal set; } + /// + /// Gets the small image for the presence and their hover texts. + /// public GameAsset SmallAsset { get; internal set; } + /// + /// Gets the large image for the presence and their hover texts. + /// public GameAsset LargeAsset { get; internal set; } + /// + /// Gets the information for the current party of the player. + /// public GameParty Party { get; internal set; } + /// + /// Gets the secrets for Rich Presence joining and spectating. + /// public GameSecrets Secrets { get; internal set; } + /// + /// Gets the timestamps for start and/or end of the game. + /// public GameTimestamps Timestamps { get; internal set; } - + + /// + /// Returns the name of the Rich Presence. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} (Rich)"; } diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index 8b966d68b..4eab34fa2 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -4,22 +4,118 @@ using System.Diagnostics; namespace Discord { + /// + /// A user's activity for listening to a song on Spotify. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SpotifyGame : Game { + /// + /// Gets the song's artist(s). + /// + /// + /// A collection of string containing all artists featured in the track (e.g. Avicii; Rita Ora). + /// public IReadOnlyCollection Artists { get; internal set; } + /// + /// Gets the Spotify album title of the song. + /// + /// + /// A string containing the name of the album (e.g. AVĪCI (01)). + /// public string AlbumTitle { get; internal set; } + /// + /// Gets the track title of the song. + /// + /// + /// A string containing the name of the song (e.g. Lonely Together (feat. Rita Ora)). + /// public string TrackTitle { get; internal set; } + + /// + /// Gets the date when the track started playing. + /// + /// + /// A containing the start timestamp of the song. + /// + public DateTimeOffset? StartedAt { get; internal set; } + + /// + /// Gets the date when the track ends. + /// + /// + /// A containing the finish timestamp of the song. + /// + public DateTimeOffset? EndsAt { get; internal set; } + + /// + /// Gets the duration of the song. + /// + /// + /// A containing the duration of the song. + /// public TimeSpan? Duration { get; internal set; } + /// + /// Gets the elapsed duration of the song. + /// + /// + /// A containing the elapsed duration of the song. + /// + public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt; + + /// + /// Gets the remaining duration of the song. + /// + /// + /// A containing the remaining duration of the song. + /// + public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow; + + /// + /// Gets the track ID of the song. + /// + /// + /// A string containing the Spotify ID of the track (e.g. 7DoN0sCGIT9IcLrtBDm4f0). + /// public string TrackId { get; internal set; } + /// + /// Gets the session ID of the song. + /// + /// + /// The purpose of this property is currently unknown. + /// + /// + /// A string containing the session ID. + /// public string SessionId { get; internal set; } + /// + /// Gets the URL of the album art. + /// + /// + /// A URL pointing to the album art of the track (e.g. + /// https://i.scdn.co/image/ba2fd8823d42802c2f8738db0b33a4597f2f39e7). + /// public string AlbumArtUrl { get; internal set; } + /// + /// Gets the direct Spotify URL of the track. + /// + /// + /// A URL pointing directly to the track on Spotify. (e.g. + /// https://open.spotify.com/track/7DoN0sCGIT9IcLrtBDm4f0). + /// public string TrackUrl { get; internal set; } internal SpotifyGame() { } + /// + /// Gets the full information of the song. + /// + /// + /// A string containing the full information of the song (e.g. + /// Avicii, Rita Ora - Lonely Together (feat. Rita Ora) (3:08) + /// public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})"; private string DebuggerDisplay => $"{Name} (Spotify)"; } diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs index afbc24cd9..127ae0b7f 100644 --- a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -1,12 +1,23 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// A user's activity for streaming on services such as Twitch. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class StreamingGame : Game { + /// + /// Gets the URL of the stream. + /// public string Url { get; internal set; } + /// + /// Creates a new based on the on the stream URL. + /// + /// The name of the stream. + /// The URL of the stream. public StreamingGame(string name, string url) { Name = name; @@ -14,7 +25,10 @@ namespace Discord Type = ActivityType.Streaming; } + /// + /// Gets the name of the stream. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Url})"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/ApplicationFlags.cs b/src/Discord.Net.Core/Entities/ApplicationFlags.cs new file mode 100644 index 000000000..1ede4257d --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationFlags.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents public flags for an application. + /// + public enum ApplicationFlags + { + GatewayPresence = 1 << 12, + GatewayPresenceLimited = 1 << 13, + GatewayGuildMembers = 1 << 14, + GatewayGuildMembersLimited = 1 << 15, + VerificationPendingGuildLimit = 1 << 16, + Embedded = 1 << 17, + GatewayMessageContent = 1 << 18, + GatewayMessageContentLimited = 1 << 19 + } +} diff --git a/src/Discord.Net.Core/Entities/ApplicationInstallParams.cs b/src/Discord.Net.Core/Entities/ApplicationInstallParams.cs new file mode 100644 index 000000000..180592f1e --- /dev/null +++ b/src/Discord.Net.Core/Entities/ApplicationInstallParams.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents install parameters for an application. + /// + public class ApplicationInstallParams + { + /// + /// Gets the scopes to install this application. + /// + public IReadOnlyCollection Scopes { get; } + + /// + /// Gets the default permissions to install this application + /// + public GuildPermission? Permission { get; } + + internal ApplicationInstallParams(string[] scopes, GuildPermission? permission) + { + Scopes = scopes.ToImmutableArray(); + Permission = permission; + } + } +} diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index e5a4ff30a..5092b4e7f 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -1,50 +1,196 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { /// - /// The action type within a + /// Representing a type of action within an . /// public enum ActionType { + /// + /// this guild was updated. + /// GuildUpdated = 1, + /// + /// A channel was created. + /// ChannelCreated = 10, + /// + /// A channel was updated. + /// ChannelUpdated = 11, + /// + /// A channel was deleted. + /// ChannelDeleted = 12, + /// + /// A permission overwrite was created for a channel. + /// OverwriteCreated = 13, + /// + /// A permission overwrite was updated for a channel. + /// OverwriteUpdated = 14, + /// + /// A permission overwrite was deleted for a channel. + /// OverwriteDeleted = 15, + /// + /// A user was kicked from this guild. + /// Kick = 20, + /// + /// A prune took place in this guild. + /// Prune = 21, + /// + /// A user banned another user from this guild. + /// Ban = 22, + /// + /// A user unbanned another user from this guild. + /// Unban = 23, + /// + /// A guild member whose information was updated. + /// MemberUpdated = 24, + /// + /// A guild member's role collection was updated. + /// MemberRoleUpdated = 25, + /// + /// A guild member moved to a voice channel. + /// + MemberMoved = 26, + /// + /// A guild member disconnected from a voice channel. + /// + MemberDisconnected = 27, + /// + /// A bot was added to this guild. + /// + BotAdded = 28, + /// + /// A role was created in this guild. + /// RoleCreated = 30, + /// + /// A role was updated in this guild. + /// RoleUpdated = 31, + /// + /// A role was deleted from this guild. + /// RoleDeleted = 32, + /// + /// An invite was created in this guild. + /// InviteCreated = 40, + /// + /// An invite was updated in this guild. + /// InviteUpdated = 41, + /// + /// An invite was deleted from this guild. + /// InviteDeleted = 42, + /// + /// A Webhook was created in this guild. + /// WebhookCreated = 50, + /// + /// A Webhook was updated in this guild. + /// WebhookUpdated = 51, + /// + /// A Webhook was deleted from this guild. + /// WebhookDeleted = 52, + /// + /// An emoji was created in this guild. + /// EmojiCreated = 60, + /// + /// An emoji was updated in this guild. + /// EmojiUpdated = 61, + /// + /// An emoji was deleted from this guild. + /// EmojiDeleted = 62, - MessageDeleted = 72 + /// + /// A message was deleted from this guild. + /// + MessageDeleted = 72, + /// + /// Multiple messages were deleted from this guild. + /// + MessageBulkDeleted = 73, + /// + /// A message was pinned from this guild. + /// + MessagePinned = 74, + /// + /// A message was unpinned from this guild. + /// + MessageUnpinned = 75, + + /// + /// A integration was created + /// + IntegrationCreated = 80, + /// + /// A integration was updated + /// + IntegrationUpdated = 81, + /// + /// An integration was deleted + /// + IntegrationDeleted = 82, + /// + /// A stage instance was created. + /// + StageInstanceCreated = 83, + /// + /// A stage instance was updated. + /// + StageInstanceUpdated = 84, + /// + /// A stage instance was deleted. + /// + StageInstanceDeleted = 85, + + /// + /// A sticker was created. + /// + StickerCreated = 90, + /// + /// A sticker was updated. + /// + StickerUpdated = 91, + /// + /// A sticker was deleted. + /// + StickerDeleted = 92, + /// + /// A thread was created. + /// + ThreadCreate = 110, + /// + /// A thread was updated. + /// + ThreadUpdate = 111, + /// + /// A thread was deleted. + /// + ThreadDelete = 112 } } diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs index 47aaffb26..a99a14eda 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { /// - /// Represents data applied to an + /// Represents data applied to an . /// public interface IAuditLogData { } diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs index 150c59a42..15ae5fd89 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs @@ -7,28 +7,40 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents an entry in an audit log + /// Represents a generic audit log entry. /// public interface IAuditLogEntry : ISnowflakeEntity { /// - /// The action which occured to create this entry + /// Gets the action which occurred to create this entry. /// + /// + /// The type of action for this audit log entry. + /// ActionType Action { get; } /// - /// The data for this entry. May be if no data was available. + /// Gets the data for this entry. /// + /// + /// An for this audit log entry; null if no data is available. + /// IAuditLogData Data { get; } /// - /// The user responsible for causing the changes + /// Gets the user responsible for causing the changes. /// + /// + /// A user object. + /// IUser User { get; } /// - /// The reason behind the change. May be if no reason was provided. + /// Gets the reason behind the change. /// + /// + /// A string containing the reason for the change; null if none is provided. + /// string Reason { get; } } } diff --git a/src/Discord.Net.Core/Entities/CacheMode.cs b/src/Discord.Net.Core/Entities/CacheMode.cs index a047bd616..503a80479 100644 --- a/src/Discord.Net.Core/Entities/CacheMode.cs +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the cache mode that should be used. + /// public enum CacheMode { + /// + /// Allows the object to be downloaded if it does not exist in the current cache. + /// AllowDownload, + /// + /// Only allows the object to be pulled from the existing cache. + /// CacheOnly } } diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs index e9f069a50..e60bd5031 100644 --- a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -1,11 +1,31 @@ -namespace Discord +namespace Discord { + /// Defines the types of channels. public enum ChannelType { + /// The channel is a text channel. Text = 0, + /// The channel is a Direct Message channel. DM = 1, + /// The channel is a voice channel. Voice = 2, + /// The channel is a group channel. Group = 3, - Category = 4 + /// The channel is a category channel. + Category = 4, + /// The channel is a news channel. + News = 5, + /// The channel is a store channel. + Store = 6, + /// The channel is a temporary thread channel under a news channel. + NewsThread = 10, + /// The channel is a temporary thread channel under a text channel. + PublicThread = 11, + /// The channel is a private temporary thread channel under a text channel. + PrivateThread = 12, + /// The channel is a stage voice channel. + Stage = 13, + /// The channel is a guild directory used in hub servers. (Unreleased) + GuildDirectory = 14 } } diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index 5d8d5e621..efdf4ff42 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,9 +1,30 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the direction of where message(s) should be retrieved from. + /// + /// + /// This enum is used to specify the direction for retrieving messages. + /// + /// At the time of writing, is not yet implemented into + /// . + /// Attempting to use the method with will throw + /// a . + /// + /// public enum Direction { + /// + /// The message(s) should be retrieved before a message. + /// Before, + /// + /// The message(s) should be retrieved after a message. + /// After, + /// + /// The message(s) should be retrieved around a message. + /// Around } } diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 2ac6c8d52..339d6fffd 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -1,34 +1,40 @@ -namespace Discord +using System.Collections.Generic; + +namespace Discord { /// - /// Modify an IGuildChannel with the specified changes. + /// Properties that are used to modify an with the specified changes. /// - /// - /// - /// await (Context.Channel as ITextChannel)?.ModifyAsync(x => - /// { - /// x.Name = "do-not-enter"; - /// }); - /// - /// + /// public class GuildChannelProperties { /// - /// Set the channel to this name + /// Gets or sets the channel to this name. /// /// - /// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes. - /// It must match the following RegEx: [a-z0-9-_]{2,100} + /// This property defines the new name for this channel. + /// + /// When modifying an , the must be alphanumeric with + /// dashes. It must match the RegEx [a-z0-9-_]{2,100}. + /// /// - /// A BadRequest will be thrown if the name does not match the above RegEx. public Optional Name { get; set; } /// - /// Move the channel to the following position. This is 0-based! + /// Moves the channel to the following position. This property is zero-based. /// public Optional Position { get; set; } /// - /// Sets the category for this channel + /// Gets or sets the category ID for this channel. /// + /// + /// Setting this value to a category's snowflake identifier will change or set this channel's parent to the + /// specified channel; setting this value to will detach this channel from its parent if one + /// is set. + /// public Optional CategoryId { get; set; } + /// + /// Gets or sets the permission overwrites for this channel. + /// + public Optional> PermissionOverwrites { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index a152ff744..179f4b03e 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,15 +1,31 @@ using Discord.Audio; -using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic audio channel. + /// public interface IAudioChannel : IChannel { - /// Connects to this audio channel. + /// + /// Connects to this audio channel. + /// + /// Determines whether the client should deaf itself upon connection. + /// Determines whether the client should mute itself upon connection. + /// Determines whether the audio client is an external one or not. + /// + /// A task representing the asynchronous connection operation. The task result contains the + /// responsible for the connection. + /// Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false); - /// Disconnects from this audio channel. + /// + /// Disconnects from this audio channel. + /// + /// + /// A task representing the asynchronous operation for disconnecting from the audio channel. + /// Task DisconnectAsync(); } } diff --git a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs index c004cafd5..838908b68 100644 --- a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { + /// + /// Represents a generic category channel. + /// public interface ICategoryChannel : IGuildChannel { } diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index ea930e112..e2df86f2a 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -1,17 +1,53 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic channel. + /// public interface IChannel : ISnowflakeEntity { - /// Gets the name of this channel. + /// + /// Gets the name of this channel. + /// + /// + /// A string containing the name of this channel. + /// string Name { get; } - - /// Gets a collection of all users in this channel. + + /// + /// Gets a collection of users that are able to view the channel or are currently in this channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that is able to view this channel or is currently in this channel. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 3000 users, and the constant + /// is 1000, the request will be split into 3 individual requests; thus returning 53individual asynchronous + /// responses, hence the need of flattening. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - - /// Gets a user in this channel with the provided id. + + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user (e.g. 168693960628371456). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a user object that + /// represents the found user; null if none is found. + /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs index 1608d1543..f0ef7f3f3 100644 --- a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs @@ -1,13 +1,27 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic direct-message channel. + /// public interface IDMChannel : IMessageChannel, IPrivateChannel { - /// Gets the recipient of all messages in this channel. + /// + /// Gets the recipient of all messages in this channel. + /// + /// + /// A user object that represents the other user in this channel. + /// IUser Recipient { get; } - /// Closes this private channel, removing it from your channel list. + /// + /// Closes this private channel, removing it from your channel list. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous close operation. + /// Task CloseAsync(RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs index d6cb2c182..77af3450e 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -1,10 +1,19 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic private group channel. + /// public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel { - /// Leaves this group. + /// + /// Leaves this group. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// Task LeaveAsync(RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 6514d46cd..992bd71fc 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -4,45 +4,156 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic guild channel. + /// + /// + /// + /// public interface IGuildChannel : IChannel, IDeletable { - /// Gets the position of this channel in the guild's channel list, relative to others of the same type. + /// + /// Gets the position of this channel. + /// + /// + /// An representing the position of this channel in the guild's channel list relative to + /// others of the same type. + /// int Position { get; } - /// Gets the guild this channel is a member of. + /// + /// Gets the guild associated with this channel. + /// + /// + /// A guild object that this channel belongs to. + /// IGuild Guild { get; } - /// Gets the id of the guild this channel is a member of. + /// + /// Gets the guild ID associated with this channel. + /// + /// + /// An representing the guild snowflake identifier for the guild that this channel + /// belongs to. + /// ulong GuildId { get; } - /// Gets a collection of permission overwrites for this channel. + /// + /// Gets a collection of permission overwrites for this channel. + /// + /// + /// A collection of overwrites associated with this channel. + /// IReadOnlyCollection PermissionOverwrites { get; } - /// Creates a new invite to this channel. - /// The 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. - Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); - /// Returns a collection of all invites to this channel. - Task> GetInvitesAsync(RequestOptions options = null); - - /// Modifies this guild channel. + /// + /// Modifies this guild channel. + /// + /// + /// This method modifies the current guild channel with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Gets the permission overwrite for a specific role, or null if one does not exist. + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// OverwritePermissions? GetPermissionOverwrite(IRole role); - /// Gets the permission overwrite for a specific user, or null if one does not exist. + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// OverwritePermissions? GetPermissionOverwrite(IUser user); - /// Removes the permission overwrite for the given role, if one exists. + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); - /// Removes the permission overwrite for the given user, if one exists. + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); - /// Adds or updates the permission overwrite for the given role. + + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// + /// The following example fetches a role via and a channel via + /// . Next, it checks if an overwrite had already been set via + /// ; if not, it denies the role from sending any + /// messages to the channel. + /// + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the + /// channel. + /// Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); - /// Adds or updates the permission overwrite for the given user. + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// + /// The following example fetches a user via and a channel via + /// . Next, it checks if an overwrite had already been set via + /// ; if not, it denies the user from sending any + /// messages to the channel. + /// + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); - /// Gets a collection of all users in this channel. + /// + /// Gets a collection of users that are able to view the channel or are currently in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a user in this channel with the provided id. + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous get operation. The task result contains a guild user object that + /// represents the user; null if none is found. + /// new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index ef5a6fa7a..87dfb3460 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -5,38 +5,362 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic channel that can send and receive messages. + /// public interface IMessageChannel : IChannel { - /// Sends a message to this message channel. - Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - /// Sends a file to this text channel, with an optional caption. - Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a message to this message channel. + /// + /// + /// The following example sends a message with the current system time in RFC 1123 format to the channel and + /// deletes itself after 5 seconds. + /// + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// The following example uploads a local file called wumpus.txt along with the text + /// good discord boi to the channel. + /// + /// The following example uploads a local image called b1nzy.jpg embedded inside a rich embed to the + /// channel. + /// + /// + /// + /// This method sends a file as if you are uploading an attachment directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. + /// + /// + /// + /// This method sends a file as if you are uploading an attachment directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method sends a file as if you are uploading an attachment directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The attachment containing the file and description. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a collection of files to this message channel. + /// + /// + /// This method sends files as if you are uploading attachments directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// A collection of attachments to upload. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); - /// Sends a file to this text channel, with an optional caption. - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - - /// Gets a message from this message channel with the given id, or null if not found. + /// + /// Gets a message from this message channel. + /// + /// The snowflake identifier of the message. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the last N messages from this message channel. + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under . The + /// library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example downloads 300 messages and gets messages that belong to the user + /// 53905483156684800. + /// + /// + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under around + /// the message depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example gets 5 message prior to the message identifier 442012544660537354. + /// + /// The following example attempts to retrieve messageCount number of messages from the + /// beginning of the channel and prints them to the console. + /// + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under around + /// the message depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example gets 5 message prior to a specific message, oldMessage. + /// + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a collection of pinned messages in this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a collection of messages found in the pinned messages. + /// Task> GetPinnedMessagesAsync(RequestOptions options = null); - /// Deletes a message based on the message ID in this channel. + /// + /// Deletes a message. + /// + /// The snowflake identifier of the message that would be removed. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// Task DeleteMessageAsync(ulong messageId, RequestOptions options = null); /// Deletes a message based on the provided message in this channel. + /// The message that would be removed. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// Task DeleteMessageAsync(IMessage message, RequestOptions options = null); - /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + /// + /// Modifies a message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The snowflake identifier of the message that would be changed. + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null); + + /// + /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation that triggers the broadcast. + /// Task TriggerTypingAsync(RequestOptions options = null); - /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned object is disposed. + /// + /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned + /// object is disposed. + /// + /// + /// The following example keeps the client in the typing state until LongRunningAsync has finished. + /// + /// + /// The options to be used when sending the request. + /// + /// A disposable object that, upon its disposal, will stop the client from broadcasting its typing state in + /// this channel. + /// IDisposable EnterTypingState(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index c8d2bcaaf..511d2bf51 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -1,16 +1,130 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { /// - /// A type of guild channel that can be nested within a category. - /// Contains a CategoryId that is set to the parent category, if it is set. + /// Represents a type of guild channel that can be nested within a category. /// public interface INestedChannel : IGuildChannel { - /// Gets the parentid (category) of this channel in the guild's channel list. + /// + /// Gets the parent (category) ID of this channel in the guild's channel list. + /// + /// + /// A representing the snowflake identifier of the parent of this channel; + /// null if none is set. + /// ulong? CategoryId { get; } - /// Gets the parent channel (category) of this channel, if it is set. If unset, returns null. + /// + /// Gets the parent (category) channel of this channel. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Syncs the permissions of this nested channel with its parent's. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for syncing channel permissions with its parent's. + /// + Task SyncPermissionsAsync(RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. + /// + /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); + /// + /// + /// The 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, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// The id of the embedded application to open for this invite. + /// The 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, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// The application to open for this invite. + /// The 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, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. + /// + /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); + /// + /// + /// The id of the user whose stream to display for this invite. + /// The 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, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteToStreamAsync(IUser user, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + /// + /// Gets a collection of all invites to this channel. + /// B + /// + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. + /// + /// var invites = await channel.GetInvitesAsync(); + /// if (invites.Count == 0) return; + /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of invite metadata that are created for this channel. + /// + Task> GetInvitesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/INewsChannel.cs b/src/Discord.Net.Core/Entities/Channels/INewsChannel.cs new file mode 100644 index 000000000..a1223b48b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/INewsChannel.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + /// + /// Represents a generic news channel in a guild that can send and receive messages. + /// + public interface INewsChannel : ITextChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs index 9a3289794..cd2307c2d 100644 --- a/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs @@ -1,9 +1,18 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { + /// + /// Represents a generic channel that is private to select recipients. + /// public interface IPrivateChannel : IChannel { + /// + /// Gets the users that can access this channel. + /// + /// + /// A read-only collection of users that can access this channel. + /// IReadOnlyCollection Recipients { get; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/IStageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IStageChannel.cs new file mode 100644 index 000000000..5e0be5b7e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IStageChannel.cs @@ -0,0 +1,114 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic Stage Channel. + /// + public interface IStageChannel : IVoiceChannel + { + /// + /// Gets the topic of the Stage instance. + /// + /// + /// If the stage isn't live then this property will be set to . + /// + string Topic { get; } + + /// + /// Gets the of the current stage. + /// + /// + /// If the stage isn't live then this property will be set to . + /// + StagePrivacyLevel? PrivacyLevel { get; } + + /// + /// Gets whether or not stage discovery is disabled. + /// + bool? IsDiscoverableDisabled { get; } + + /// + /// Gets whether or not the stage is live. + /// + bool IsLive { get; } + + /// + /// Starts the stage, creating a stage instance. + /// + /// The topic for the stage/ + /// The privacy level of the stage. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous start operation. + /// + Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null); + + /// + /// Modifies the current stage instance. + /// + /// The properties to modify the stage instance with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modify operation. + /// + Task ModifyInstanceAsync(Action func, RequestOptions options = null); + + /// + /// Stops the stage, deleting the stage instance. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous stop operation. + /// + Task StopStageAsync(RequestOptions options = null); + + /// + /// Indicates that the bot would like to speak within a stage channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous request to speak operation. + /// + Task RequestToSpeakAsync(RequestOptions options = null); + + /// + /// Makes the current user become a speaker within a stage. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous speaker modify operation. + /// + Task BecomeSpeakerAsync(RequestOptions options = null); + + /// + /// Makes the current user a listener. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous stop operation. + /// + Task StopSpeakingAsync(RequestOptions options = null); + + /// + /// Makes a user a speaker within a stage. + /// + /// The user to make the speaker. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous move operation. + /// + Task MoveToSpeakerAsync(IGuildUser user, RequestOptions options = null); + + /// + /// Removes a user from speaking. + /// + /// The user to remove from speaking. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous remove operation. + /// + Task RemoveFromSpeakerAsync(IGuildUser user, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 89e10e65e..ae0fe674b 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -5,30 +5,149 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic channel in a guild that can send and receive messages. + /// public interface ITextChannel : IMessageChannel, IMentionable, INestedChannel { - /// Checks if the channel is NSFW. + /// + /// Gets a value that indicates whether the channel is NSFW. + /// + /// + /// true if the channel has the NSFW flag enabled; otherwise false. + /// bool IsNsfw { get; } - /// Gets the current topic for this text channel. + /// + /// Gets the current topic for this text channel. + /// + /// + /// A string representing the topic set in the channel; null if none is set. + /// string Topic { get; } - /// Gets the current slow-mode delay for this channel. 0 if disabled. + /// + /// Gets the current slow-mode delay for this channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// int SlowModeInterval { get; } - /// Bulk deletes multiple messages. + /// + /// Bulk-deletes multiple messages. + /// + /// + /// The following example gets 250 messages from the channel and deletes them. + /// + /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); + /// await textChannel.DeleteMessagesAsync(messages); + /// + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); - /// Bulk deletes multiple messages. + /// + /// Bulk-deletes multiple messages. + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The snowflake identifier of the messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); - /// Modifies this text channel. + /// + /// Modifies this text channel. + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// Task ModifyAsync(Action func, RequestOptions options = null); - - /// Creates a webhook in this text channel. + + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); - /// Gets the webhook in this text channel with the provided id, or null if not found. + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); - /// Gets the webhooks for this text channel. + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// Task> GetWebhooksAsync(RequestOptions options = null); + + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// Whether non-moderators can add other non-moderators to a thread; only available when creating a private thread + /// The amount of seconds a user has to wait before sending another message (0-21600) + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, + IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs new file mode 100644 index 000000000..50e46efa6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a thread channel inside of a guild. + /// + public interface IThreadChannel : ITextChannel + { + /// + /// Gets the type of the current thread channel. + /// + ThreadType Type { get; } + + /// + /// Gets whether or not the current user has joined this thread. + /// + bool HasJoined { get; } + + /// + /// Gets whether or not the current thread is archived. + /// + bool IsArchived { get; } + + /// + /// Gets the duration of time before the thread is automatically archived after no activity. + /// + ThreadArchiveDuration AutoArchiveDuration { get; } + + /// + /// Gets the timestamp when the thread's archive status was last changed, used for calculating recent activity. + /// + DateTimeOffset ArchiveTimestamp { get; } + + /// + /// Gets whether or not the current thread is locked. + /// + bool IsLocked { get; } + + /// + /// Gets an approximate count of users in a thread, stops counting after 50. + /// + int MemberCount { get; } + + /// + /// Gets an approximate count of messages in a thread, stops counting after 50. + /// + int MessageCount { get; } + + /// + /// Joins the current thread. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous join operation. + /// + Task JoinAsync(RequestOptions options = null); + + /// + /// Leaves the current thread. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// + Task LeaveAsync(RequestOptions options = null); + + /// + /// Adds a user to this thread. + /// + /// The to add. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of adding a member to a thread. + /// + Task AddUserAsync(IGuildUser user, RequestOptions options = null); + + /// + /// Removes a user from this thread. + /// + /// The to remove from this thread. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of removing a user from this thread. + /// + Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 2e345bfda..1d36a41b9 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -3,14 +3,37 @@ using System.Threading.Tasks; namespace Discord { - public interface IVoiceChannel : INestedChannel, IAudioChannel + /// + /// Represents a generic voice channel in a guild. + /// + public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable { - /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. + /// + /// Gets the bit-rate that the clients in this voice channel are requested to use. + /// + /// + /// An representing the bit-rate (bps) that this voice channel defines and requests the + /// client(s) to use. + /// int Bitrate { get; } - /// Gets the max amount of users allowed to be connected to this channel at one time. + /// + /// Gets the max number of users allowed to be connected to this channel at once. + /// + /// + /// An representing the maximum number of users that are allowed to be connected to this + /// channel at once; null if a limit is not set. + /// int? UserLimit { get; } - /// Modifies this voice channel. + /// + /// Modifies this voice channel. + /// + /// The properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs index 31f814334..ffd90dae6 100644 --- a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -1,12 +1,28 @@ -namespace Discord +namespace Discord { + /// + /// Provides properties that are used to reorder an . + /// public class ReorderChannelProperties { - /// The id of the channel to apply this position to. + /// + /// Gets the ID of the channel to apply this position to. + /// + /// + /// A representing the snowflake identifier of this channel. + /// public ulong Id { get; } - /// The new zero-based position of this channel. + /// + /// Gets the new zero-based position of this channel. + /// + /// + /// An representing the new position of this channel. + /// public int Position { get; } + /// Initializes a new instance of the class used to reorder a channel. + /// Sets the ID of the channel to apply this position to. + /// Sets the new zero-based position of this channel. public ReorderChannelProperties(ulong id, int position) { Id = id; diff --git a/src/Discord.Net.Core/Entities/Channels/StageInstanceProperties.cs b/src/Discord.Net.Core/Entities/Channels/StageInstanceProperties.cs new file mode 100644 index 000000000..35201fe0f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/StageInstanceProperties.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Represents properties to use when modifying a stage instance. + /// + public class StageInstanceProperties + { + /// + /// Gets or sets the topic of the stage. + /// + public Optional Topic { get; set; } + + /// + /// Gets or sets the privacy level of the stage. + /// + public Optional PrivacyLevel { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs b/src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs new file mode 100644 index 000000000..0582a3e52 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/StagePrivacyLevel.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Represents the privacy level of a stage. + /// + public enum StagePrivacyLevel + { + /// + /// The Stage instance is visible publicly, such as on Stage Discovery. + /// + Public = 1, + /// + /// The Stage instance is visible to only guild members. + /// + GuildOnly = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index 87adccb85..2dceb025c 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -2,26 +2,57 @@ using System; namespace Discord { - /// + /// + /// Provides properties that are used to modify an with the specified changes. + /// + /// public class TextChannelProperties : GuildChannelProperties { /// - /// What the topic of the channel should be set to. + /// Gets or sets the topic of the channel. /// + /// + /// Setting this value to any string other than null or will set the + /// channel topic or description to the desired value. + /// public Optional Topic { get; set; } /// - /// Should this channel be flagged as NSFW? + /// Gets or sets whether this channel should be flagged as NSFW. /// + /// + /// Setting this value to true will mark the channel as NSFW (Not Safe For Work) and will prompt the + /// user about its possibly mature nature before they may view the channel; setting this value to false will + /// remove the NSFW indicator. + /// public Optional IsNsfw { get; set; } /// - /// What the slow-mode ratelimit for this channel should be set to; 0 will disable slow-mode. + /// Gets or sets the slow-mode ratelimit in seconds for this channel. /// /// - /// This value must fall within [0, 120] - /// - /// Users with will be exempt from slow-mode. + /// Setting this value to anything above zero will require each user to wait X seconds before + /// sending another message; setting this value to 0 will disable slow-mode for this channel. + /// + /// Users with or + /// will be exempt from slow-mode. + /// /// - /// Throws ArgummentOutOfRange if the value does not fall within [0, 120] + /// Thrown if the value does not fall within [0, 21600]. public Optional SlowModeInterval { get; set; } + + /// + /// Gets or sets whether or not the thread is archived. + /// + public Optional Archived { get; set; } + + /// + /// Gets or sets whether or not the thread is locked. + /// + public Optional Locked { get; set; } + + /// + /// Gets or sets the auto archive duration. + /// + public Optional AutoArchiveDuration { get; set; } + } } diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs new file mode 100644 index 000000000..2c8a0652c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs @@ -0,0 +1,34 @@ +namespace Discord +{ + /// + /// Represents the thread auto archive duration. + /// + public enum ThreadArchiveDuration + { + /// + /// One hour (60 minutes). + /// + OneHour = 60, + + /// + /// One day (1440 minutes). + /// + OneDay = 1440, + + /// + /// Three days (4320 minutes). + /// + /// This option is explicitly available to nitro users. + /// + /// + ThreeDays = 4320, + + /// + /// One week (10080 minutes). + /// + /// This option is explicitly available to nitro users. + /// + /// + OneWeek = 10080 + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadType.cs b/src/Discord.Net.Core/Entities/Channels/ThreadType.cs new file mode 100644 index 000000000..379128d21 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadType.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Represents types of threads. + /// + public enum ThreadType + { + /// + /// Represents a temporary sub-channel within a GUILD_NEWS channel. + /// + NewsThread = 10, + + /// + /// Represents a temporary sub-channel within a GUILD_TEXT channel. + /// + PublicThread = 11, + + /// + /// Represents a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission + /// + PrivateThread = 12 + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs index 81dd8063e..251a45c3d 100644 --- a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs @@ -1,15 +1,21 @@ -namespace Discord +namespace Discord { - /// + /// + /// Provides properties that are used to modify an with the specified changes. + /// public class VoiceChannelProperties : GuildChannelProperties { /// - /// The bitrate of the voice connections in this channel. Must be greater than 8000 + /// Gets or sets the bitrate of the voice connections in this channel. Must be greater than 8000. /// public Optional Bitrate { get; set; } /// - /// The maximum number of users that can be present in a channel. + /// Gets or sets the maximum number of users that can be present in a channel, or null if none. /// public Optional UserLimit { get; set; } + /// + /// Gets or sets the channel voice region id, automatic when set to . + /// + public Optional RTCRegion { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index c2dfc31ad..15c20148e 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -1,39 +1,5976 @@ -namespace Discord +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Discord { /// - /// A unicode emoji + /// A Unicode emoji. /// public class Emoji : IEmote { - // TODO: need to constrain this to unicode-only emojis somehow + /// + public string Name { get; } /// - /// The unicode representation of this emote. + /// Gets the Unicode representation of this emoji. /// - public string Name { get; } - + /// + /// A string that resolves to . + /// public override string ToString() => Name; /// - /// Creates a unicode emoji. + /// Initializes a new class with the provided Unicode. /// - /// The pure UTF-8 encoding of an emoji + /// The pure UTF-8 encoding of an emoji. public Emoji(string unicode) { Name = unicode; } + /// + /// Determines whether the specified emoji is equal to the current one. + /// + /// The object to compare with the current object. public override bool Equals(object other) { - if (other == null) return false; - if (other == this) return true; + if (other == null) + return false; - var otherEmoji = other as Emoji; - if (otherEmoji == null) return false; + if (other == this) + return true; - return string.Equals(Name, otherEmoji.Name); + return other is Emoji otherEmoji && string.Equals(Name, otherEmoji.Name); } + /// Tries to parse an from its raw format. + /// The raw encoding of an emoji. For example: :heart: or ❤ + /// An emoji. + public static bool TryParse(string text, out Emoji result) + { + result = null; + if (string.IsNullOrWhiteSpace(text)) + return false; + + if (NamesAndUnicodes.ContainsKey(text)) + result = new Emoji(NamesAndUnicodes[text]); + + if (Unicodes.Contains(text)) + result = new Emoji(text); + + return result != null; + } + + /// Parse an from its raw format. + /// The raw encoding of an emoji. For example: :heart: or ❤ + /// String is not emoji or unicode! + public static Emoji Parse(string emojiStr) + { + if (!TryParse(emojiStr, out var emoji)) + throw new FormatException("String is not emoji name or unicode!"); + + return emoji; + } + + /// public override int GetHashCode() => Name.GetHashCode(); + + private static IReadOnlyDictionary NamesAndUnicodes { get; } = new Dictionary + { + [",:("] = "\uD83D\uDE13", + [",:)"] = "\uD83D\uDE05", + [",:-("] = "\uD83D\uDE13", + [",:-)"] = "\uD83D\uDE05", + [",=("] = "\uD83D\uDE13", + [",=)"] = "\uD83D\uDE05", + [",=-("] = "\uD83D\uDE13", + [",=-)"] = "\uD83D\uDE05", + ["0:)"] = "\uD83D\uDE07", + ["0:-)"] = "\uD83D\uDE07", + ["0=)"] = "\uD83D\uDE07", + ["0=-)"] = "\uD83D\uDE07", + ["8-)"] = "\uD83D\uDE0E", + [":$"] = "\uD83D\uDE12", + [":'("] = "\uD83D\uDE22", + [":')"] = "\uD83D\uDE02", + [":'-("] = "\uD83D\uDE22", + [":'-)"] = "\uD83D\uDE02", + [":'-D"] = "\uD83D\uDE02", + [":'D"] = "\uD83D\uDE02", + [":("] = "\uD83D\uDE26", + [":)"] = "\uD83D\uDE42", + [":*"] = "\uD83D\uDE17", + [":+1:"] = "\uD83D\uDC4D", + [":+1::skin-tone-1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":+1::skin-tone-2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":+1::skin-tone-3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":+1::skin-tone-4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":+1::skin-tone-5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":+1_tone1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":+1_tone2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":+1_tone3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":+1_tone4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":+1_tone5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":,'("] = "\uD83D\uDE2D", + [":,'-("] = "\uD83D\uDE2D", + [":,("] = "\uD83D\uDE22", + [":,)"] = "\uD83D\uDE02", + [":,-("] = "\uD83D\uDE22", + [":,-)"] = "\uD83D\uDE02", + [":,-D"] = "\uD83D\uDE02", + [":,D"] = "\uD83D\uDE02", + [":-$"] = "\uD83D\uDE12", + [":-("] = "\uD83D\uDE26", + [":-)"] = "\uD83D\uDE42", + [":-*"] = "\uD83D\uDE17", + [":-/"] = "\uD83D\uDE15", + [":-1:"] = "\uD83D\uDC4E", + [":-1::skin-tone-1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":-1::skin-tone-2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":-1::skin-tone-3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":-1::skin-tone-4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":-1::skin-tone-5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":-@"] = "\uD83D\uDE21", + [":-D"] = "\uD83D\uDE04", + [":-O"] = "\uD83D\uDE2E", + [":-P"] = "\uD83D\uDE1B", + [":-S"] = "\uD83D\uDE12", + [":-Z"] = "\uD83D\uDE12", + [":-\")"] = "\uD83D\uDE0A", + [":-\\"] = "\uD83D\uDE15", + [":-o"] = "\uD83D\uDE2E", + [":-|"] = "\uD83D\uDE10", + [":100:"] = "\uD83D\uDCAF", + [":1234:"] = "\uD83D\uDD22", + [":8ball:"] = "\uD83C\uDFB1", + [":@"] = "\uD83D\uDE21", + [":D"] = "\uD83D\uDE04", + [":O"] = "\uD83D\uDE2E", + [":P"] = "\uD83D\uDE1B", + [":\")"] = "\uD83D\uDE0A", + [":_1_tone1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":_1_tone2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":_1_tone3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":_1_tone4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":_1_tone5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":a:"] = "\uD83C\uDD70️", + [":ab:"] = "\uD83C\uDD8E", + [":abacus:"] = "\uD83E\uDDEE", + [":abc:"] = "\uD83D\uDD24", + [":abcd:"] = "\uD83D\uDD21", + [":accept:"] = "\uD83C\uDE51", + [":adhesive_bandage:"] = "\uD83E\uDE79", + [":admission_tickets:"] = "\uD83C\uDF9F️", + [":adult:"] = "\uD83E\uDDD1", + [":adult::skin-tone-1:"] = "\uD83E\uDDD1\uD83C\uDFFB", + [":adult::skin-tone-2:"] = "\uD83E\uDDD1\uD83C\uDFFC", + [":adult::skin-tone-3:"] = "\uD83E\uDDD1\uD83C\uDFFD", + [":adult::skin-tone-4:"] = "\uD83E\uDDD1\uD83C\uDFFE", + [":adult::skin-tone-5:"] = "\uD83E\uDDD1\uD83C\uDFFF", + [":adult_dark_skin_tone:"] = "\uD83E\uDDD1\uD83C\uDFFF", + [":adult_light_skin_tone:"] = "\uD83E\uDDD1\uD83C\uDFFB", + [":adult_medium_dark_skin_tone:"] = "\uD83E\uDDD1\uD83C\uDFFE", + [":adult_medium_light_skin_tone:"] = "\uD83E\uDDD1\uD83C\uDFFC", + [":adult_medium_skin_tone:"] = "\uD83E\uDDD1\uD83C\uDFFD", + [":adult_tone1:"] = "\uD83E\uDDD1\uD83C\uDFFB", + [":adult_tone2:"] = "\uD83E\uDDD1\uD83C\uDFFC", + [":adult_tone3:"] = "\uD83E\uDDD1\uD83C\uDFFD", + [":adult_tone4:"] = "\uD83E\uDDD1\uD83C\uDFFE", + [":adult_tone5:"] = "\uD83E\uDDD1\uD83C\uDFFF", + [":aerial_tramway:"] = "\uD83D\uDEA1", + [":airplane:"] = "✈️", + [":airplane_arriving:"] = "\uD83D\uDEEC", + [":airplane_departure:"] = "\uD83D\uDEEB", + [":airplane_small:"] = "\uD83D\uDEE9️", + [":alarm_clock:"] = "⏰", + [":alembic:"] = "⚗️", + [":alien:"] = "\uD83D\uDC7D", + [":ambulance:"] = "\uD83D\uDE91", + [":amphora:"] = "\uD83C\uDFFA", + [":anchor:"] = "⚓", + [":angel:"] = "\uD83D\uDC7C", + [":angel::skin-tone-1:"] = "\uD83D\uDC7C\uD83C\uDFFB", + [":angel::skin-tone-2:"] = "\uD83D\uDC7C\uD83C\uDFFC", + [":angel::skin-tone-3:"] = "\uD83D\uDC7C\uD83C\uDFFD", + [":angel::skin-tone-4:"] = "\uD83D\uDC7C\uD83C\uDFFE", + [":angel::skin-tone-5:"] = "\uD83D\uDC7C\uD83C\uDFFF", + [":angel_tone1:"] = "\uD83D\uDC7C\uD83C\uDFFB", + [":angel_tone2:"] = "\uD83D\uDC7C\uD83C\uDFFC", + [":angel_tone3:"] = "\uD83D\uDC7C\uD83C\uDFFD", + [":angel_tone4:"] = "\uD83D\uDC7C\uD83C\uDFFE", + [":angel_tone5:"] = "\uD83D\uDC7C\uD83C\uDFFF", + [":anger:"] = "\uD83D\uDCA2", + [":anger_right:"] = "\uD83D\uDDEF️", + [":angry:"] = "\uD83D\uDE20", + [":anguished:"] = "\uD83D\uDE27", + [":ant:"] = "\uD83D\uDC1C", + [":apple:"] = "\uD83C\uDF4E", + [":aquarius:"] = "♒", + [":archery:"] = "\uD83C\uDFF9", + [":aries:"] = "♈", + [":arrow_backward:"] = "◀️", + [":arrow_double_down:"] = "⏬", + [":arrow_double_up:"] = "⏫", + [":arrow_down:"] = "⬇️", + [":arrow_down_small:"] = "\uD83D\uDD3D", + [":arrow_forward:"] = "▶️", + [":arrow_heading_down:"] = "⤵️", + [":arrow_heading_up:"] = "⤴️", + [":arrow_left:"] = "⬅️", + [":arrow_lower_left:"] = "↙️", + [":arrow_lower_right:"] = "↘️", + [":arrow_right:"] = "➡️", + [":arrow_right_hook:"] = "↪️", + [":arrow_up:"] = "⬆️", + [":arrow_up_down:"] = "↕️", + [":arrow_up_small:"] = "\uD83D\uDD3C", + [":arrow_upper_left:"] = "↖️", + [":arrow_upper_right:"] = "↗️", + [":arrows_clockwise:"] = "\uD83D\uDD03", + [":arrows_counterclockwise:"] = "\uD83D\uDD04", + [":art:"] = "\uD83C\uDFA8", + [":articulated_lorry:"] = "\uD83D\uDE9B", + [":asterisk:"] = "*️⃣", + [":astonished:"] = "\uD83D\uDE32", + [":athletic_shoe:"] = "\uD83D\uDC5F", + [":atm:"] = "\uD83C\uDFE7", + [":atom:"] = "⚛️", + [":atom_symbol:"] = "⚛️", + [":auto_rickshaw:"] = "\uD83D\uDEFA", + [":avocado:"] = "\uD83E\uDD51", + [":axe:"] = "\uD83E\uDE93", + [":b:"] = "\uD83C\uDD71️", + [":baby:"] = "\uD83D\uDC76", + [":baby::skin-tone-1:"] = "\uD83D\uDC76\uD83C\uDFFB", + [":baby::skin-tone-2:"] = "\uD83D\uDC76\uD83C\uDFFC", + [":baby::skin-tone-3:"] = "\uD83D\uDC76\uD83C\uDFFD", + [":baby::skin-tone-4:"] = "\uD83D\uDC76\uD83C\uDFFE", + [":baby::skin-tone-5:"] = "\uD83D\uDC76\uD83C\uDFFF", + [":baby_bottle:"] = "\uD83C\uDF7C", + [":baby_chick:"] = "\uD83D\uDC24", + [":baby_symbol:"] = "\uD83D\uDEBC", + [":baby_tone1:"] = "\uD83D\uDC76\uD83C\uDFFB", + [":baby_tone2:"] = "\uD83D\uDC76\uD83C\uDFFC", + [":baby_tone3:"] = "\uD83D\uDC76\uD83C\uDFFD", + [":baby_tone4:"] = "\uD83D\uDC76\uD83C\uDFFE", + [":baby_tone5:"] = "\uD83D\uDC76\uD83C\uDFFF", + [":back:"] = "\uD83D\uDD19", + [":back_of_hand:"] = "\uD83E\uDD1A", + [":back_of_hand::skin-tone-1:"] = "\uD83E\uDD1A\uD83C\uDFFB", + [":back_of_hand::skin-tone-2:"] = "\uD83E\uDD1A\uD83C\uDFFC", + [":back_of_hand::skin-tone-3:"] = "\uD83E\uDD1A\uD83C\uDFFD", + [":back_of_hand::skin-tone-4:"] = "\uD83E\uDD1A\uD83C\uDFFE", + [":back_of_hand::skin-tone-5:"] = "\uD83E\uDD1A\uD83C\uDFFF", + [":back_of_hand_tone1:"] = "\uD83E\uDD1A\uD83C\uDFFB", + [":back_of_hand_tone2:"] = "\uD83E\uDD1A\uD83C\uDFFC", + [":back_of_hand_tone3:"] = "\uD83E\uDD1A\uD83C\uDFFD", + [":back_of_hand_tone4:"] = "\uD83E\uDD1A\uD83C\uDFFE", + [":back_of_hand_tone5:"] = "\uD83E\uDD1A\uD83C\uDFFF", + [":bacon:"] = "\uD83E\uDD53", + [":badger:"] = "\uD83E\uDDA1", + [":badminton:"] = "\uD83C\uDFF8", + [":bagel:"] = "\uD83E\uDD6F", + [":baggage_claim:"] = "\uD83D\uDEC4", + [":baguette_bread:"] = "\uD83E\uDD56", + [":ballet_shoes:"] = "\uD83E\uDE70", + [":balloon:"] = "\uD83C\uDF88", + [":ballot_box:"] = "\uD83D\uDDF3️", + [":ballot_box_with_ballot:"] = "\uD83D\uDDF3️", + [":ballot_box_with_check:"] = "☑️", + [":bamboo:"] = "\uD83C\uDF8D", + [":banana:"] = "\uD83C\uDF4C", + [":bangbang:"] = "‼️", + [":banjo:"] = "\uD83E\uDE95", + [":bank:"] = "\uD83C\uDFE6", + [":bar_chart:"] = "\uD83D\uDCCA", + [":barber:"] = "\uD83D\uDC88", + [":baseball:"] = "⚾", + [":basket:"] = "\uD83E\uDDFA", + [":basketball:"] = "\uD83C\uDFC0", + [":basketball_player:"] = "⛹️", + [":basketball_player::skin-tone-1:"] = "⛹\uD83C\uDFFB", + [":basketball_player::skin-tone-2:"] = "⛹\uD83C\uDFFC", + [":basketball_player::skin-tone-3:"] = "⛹\uD83C\uDFFD", + [":basketball_player::skin-tone-4:"] = "⛹\uD83C\uDFFE", + [":basketball_player::skin-tone-5:"] = "⛹\uD83C\uDFFF", + [":basketball_player_tone1:"] = "⛹\uD83C\uDFFB", + [":basketball_player_tone2:"] = "⛹\uD83C\uDFFC", + [":basketball_player_tone3:"] = "⛹\uD83C\uDFFD", + [":basketball_player_tone4:"] = "⛹\uD83C\uDFFE", + [":basketball_player_tone5:"] = "⛹\uD83C\uDFFF", + [":bat:"] = "\uD83E\uDD87", + [":bath:"] = "\uD83D\uDEC0", + [":bath::skin-tone-1:"] = "\uD83D\uDEC0\uD83C\uDFFB", + [":bath::skin-tone-2:"] = "\uD83D\uDEC0\uD83C\uDFFC", + [":bath::skin-tone-3:"] = "\uD83D\uDEC0\uD83C\uDFFD", + [":bath::skin-tone-4:"] = "\uD83D\uDEC0\uD83C\uDFFE", + [":bath::skin-tone-5:"] = "\uD83D\uDEC0\uD83C\uDFFF", + [":bath_tone1:"] = "\uD83D\uDEC0\uD83C\uDFFB", + [":bath_tone2:"] = "\uD83D\uDEC0\uD83C\uDFFC", + [":bath_tone3:"] = "\uD83D\uDEC0\uD83C\uDFFD", + [":bath_tone4:"] = "\uD83D\uDEC0\uD83C\uDFFE", + [":bath_tone5:"] = "\uD83D\uDEC0\uD83C\uDFFF", + [":bathtub:"] = "\uD83D\uDEC1", + [":battery:"] = "\uD83D\uDD0B", + [":beach:"] = "\uD83C\uDFD6️", + [":beach_umbrella:"] = "⛱️", + [":beach_with_umbrella:"] = "\uD83C\uDFD6️", + [":bear:"] = "\uD83D\uDC3B", + [":bearded_person:"] = "\uD83E\uDDD4", + [":bearded_person::skin-tone-1:"] = "\uD83E\uDDD4\uD83C\uDFFB", + [":bearded_person::skin-tone-2:"] = "\uD83E\uDDD4\uD83C\uDFFC", + [":bearded_person::skin-tone-3:"] = "\uD83E\uDDD4\uD83C\uDFFD", + [":bearded_person::skin-tone-4:"] = "\uD83E\uDDD4\uD83C\uDFFE", + [":bearded_person::skin-tone-5:"] = "\uD83E\uDDD4\uD83C\uDFFF", + [":bearded_person_dark_skin_tone:"] = "\uD83E\uDDD4\uD83C\uDFFF", + [":bearded_person_light_skin_tone:"] = "\uD83E\uDDD4\uD83C\uDFFB", + [":bearded_person_medium_dark_skin_tone:"] = "\uD83E\uDDD4\uD83C\uDFFE", + [":bearded_person_medium_light_skin_tone:"] = "\uD83E\uDDD4\uD83C\uDFFC", + [":bearded_person_medium_skin_tone:"] = "\uD83E\uDDD4\uD83C\uDFFD", + [":bearded_person_tone1:"] = "\uD83E\uDDD4\uD83C\uDFFB", + [":bearded_person_tone2:"] = "\uD83E\uDDD4\uD83C\uDFFC", + [":bearded_person_tone3:"] = "\uD83E\uDDD4\uD83C\uDFFD", + [":bearded_person_tone4:"] = "\uD83E\uDDD4\uD83C\uDFFE", + [":bearded_person_tone5:"] = "\uD83E\uDDD4\uD83C\uDFFF", + [":bed:"] = "\uD83D\uDECF️", + [":bee:"] = "\uD83D\uDC1D", + [":beer:"] = "\uD83C\uDF7A", + [":beers:"] = "\uD83C\uDF7B", + [":beetle:"] = "\uD83D\uDC1E", + [":beginner:"] = "\uD83D\uDD30", + [":bell:"] = "\uD83D\uDD14", + [":bellhop:"] = "\uD83D\uDECE️", + [":bellhop_bell:"] = "\uD83D\uDECE️", + [":bento:"] = "\uD83C\uDF71", + [":beverage_box:"] = "\uD83E\uDDC3", + [":bicyclist:"] = "\uD83D\uDEB4", + [":bicyclist::skin-tone-1:"] = "\uD83D\uDEB4\uD83C\uDFFB", + [":bicyclist::skin-tone-2:"] = "\uD83D\uDEB4\uD83C\uDFFC", + [":bicyclist::skin-tone-3:"] = "\uD83D\uDEB4\uD83C\uDFFD", + [":bicyclist::skin-tone-4:"] = "\uD83D\uDEB4\uD83C\uDFFE", + [":bicyclist::skin-tone-5:"] = "\uD83D\uDEB4\uD83C\uDFFF", + [":bicyclist_tone1:"] = "\uD83D\uDEB4\uD83C\uDFFB", + [":bicyclist_tone2:"] = "\uD83D\uDEB4\uD83C\uDFFC", + [":bicyclist_tone3:"] = "\uD83D\uDEB4\uD83C\uDFFD", + [":bicyclist_tone4:"] = "\uD83D\uDEB4\uD83C\uDFFE", + [":bicyclist_tone5:"] = "\uD83D\uDEB4\uD83C\uDFFF", + [":bike:"] = "\uD83D\uDEB2", + [":bikini:"] = "\uD83D\uDC59", + [":billed_cap:"] = "\uD83E\uDDE2", + [":biohazard:"] = "☣️", + [":biohazard_sign:"] = "☣️", + [":bird:"] = "\uD83D\uDC26", + [":birthday:"] = "\uD83C\uDF82", + [":black_circle:"] = "⚫", + [":black_heart:"] = "\uD83D\uDDA4", + [":black_joker:"] = "\uD83C\uDCCF", + [":black_large_square:"] = "⬛", + [":black_medium_small_square:"] = "◾", + [":black_medium_square:"] = "◼️", + [":black_nib:"] = "✒️", + [":black_small_square:"] = "▪️", + [":black_square_button:"] = "\uD83D\uDD32", + [":blond_haired_man:"] = "\uD83D\uDC71\u200D♂️", + [":blond_haired_man::skin-tone-1:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♂️", + [":blond_haired_man::skin-tone-2:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♂️", + [":blond_haired_man::skin-tone-3:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♂️", + [":blond_haired_man::skin-tone-4:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♂️", + [":blond_haired_man::skin-tone-5:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♂️", + [":blond_haired_man_dark_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♂️", + [":blond_haired_man_light_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♂️", + [":blond_haired_man_medium_dark_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♂️", + [":blond_haired_man_medium_light_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♂️", + [":blond_haired_man_medium_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♂️", + [":blond_haired_man_tone1:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♂️", + [":blond_haired_man_tone2:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♂️", + [":blond_haired_man_tone3:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♂️", + [":blond_haired_man_tone4:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♂️", + [":blond_haired_man_tone5:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♂️", + [":blond_haired_person:"] = "\uD83D\uDC71", + [":blond_haired_person::skin-tone-1:"] = "\uD83D\uDC71\uD83C\uDFFB", + [":blond_haired_person::skin-tone-2:"] = "\uD83D\uDC71\uD83C\uDFFC", + [":blond_haired_person::skin-tone-3:"] = "\uD83D\uDC71\uD83C\uDFFD", + [":blond_haired_person::skin-tone-4:"] = "\uD83D\uDC71\uD83C\uDFFE", + [":blond_haired_person::skin-tone-5:"] = "\uD83D\uDC71\uD83C\uDFFF", + [":blond_haired_person_tone1:"] = "\uD83D\uDC71\uD83C\uDFFB", + [":blond_haired_person_tone2:"] = "\uD83D\uDC71\uD83C\uDFFC", + [":blond_haired_person_tone3:"] = "\uD83D\uDC71\uD83C\uDFFD", + [":blond_haired_person_tone4:"] = "\uD83D\uDC71\uD83C\uDFFE", + [":blond_haired_person_tone5:"] = "\uD83D\uDC71\uD83C\uDFFF", + [":blond_haired_woman:"] = "\uD83D\uDC71\u200D♀️", + [":blond_haired_woman::skin-tone-1:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♀️", + [":blond_haired_woman::skin-tone-2:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♀️", + [":blond_haired_woman::skin-tone-3:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♀️", + [":blond_haired_woman::skin-tone-4:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♀️", + [":blond_haired_woman::skin-tone-5:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♀️", + [":blond_haired_woman_dark_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♀️", + [":blond_haired_woman_light_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♀️", + [":blond_haired_woman_medium_dark_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♀️", + [":blond_haired_woman_medium_light_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♀️", + [":blond_haired_woman_medium_skin_tone:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♀️", + [":blond_haired_woman_tone1:"] = "\uD83D\uDC71\uD83C\uDFFB\u200D♀️", + [":blond_haired_woman_tone2:"] = "\uD83D\uDC71\uD83C\uDFFC\u200D♀️", + [":blond_haired_woman_tone3:"] = "\uD83D\uDC71\uD83C\uDFFD\u200D♀️", + [":blond_haired_woman_tone4:"] = "\uD83D\uDC71\uD83C\uDFFE\u200D♀️", + [":blond_haired_woman_tone5:"] = "\uD83D\uDC71\uD83C\uDFFF\u200D♀️", + [":blossom:"] = "\uD83C\uDF3C", + [":blowfish:"] = "\uD83D\uDC21", + [":blue_book:"] = "\uD83D\uDCD8", + [":blue_car:"] = "\uD83D\uDE99", + [":blue_circle:"] = "\uD83D\uDD35", + [":blue_heart:"] = "\uD83D\uDC99", + [":blue_square:"] = "\uD83D\uDFE6", + [":blush:"] = "\uD83D\uDE0A", + [":boar:"] = "\uD83D\uDC17", + [":bomb:"] = "\uD83D\uDCA3", + [":bone:"] = "\uD83E\uDDB4", + [":book:"] = "\uD83D\uDCD6", + [":bookmark:"] = "\uD83D\uDD16", + [":bookmark_tabs:"] = "\uD83D\uDCD1", + [":books:"] = "\uD83D\uDCDA", + [":boom:"] = "\uD83D\uDCA5", + [":boot:"] = "\uD83D\uDC62", + [":bottle_with_popping_cork:"] = "\uD83C\uDF7E", + [":bouquet:"] = "\uD83D\uDC90", + [":bow:"] = "\uD83D\uDE47", + [":bow::skin-tone-1:"] = "\uD83D\uDE47\uD83C\uDFFB", + [":bow::skin-tone-2:"] = "\uD83D\uDE47\uD83C\uDFFC", + [":bow::skin-tone-3:"] = "\uD83D\uDE47\uD83C\uDFFD", + [":bow::skin-tone-4:"] = "\uD83D\uDE47\uD83C\uDFFE", + [":bow::skin-tone-5:"] = "\uD83D\uDE47\uD83C\uDFFF", + [":bow_and_arrow:"] = "\uD83C\uDFF9", + [":bow_tone1:"] = "\uD83D\uDE47\uD83C\uDFFB", + [":bow_tone2:"] = "\uD83D\uDE47\uD83C\uDFFC", + [":bow_tone3:"] = "\uD83D\uDE47\uD83C\uDFFD", + [":bow_tone4:"] = "\uD83D\uDE47\uD83C\uDFFE", + [":bow_tone5:"] = "\uD83D\uDE47\uD83C\uDFFF", + [":bowl_with_spoon:"] = "\uD83E\uDD63", + [":bowling:"] = "\uD83C\uDFB3", + [":boxing_glove:"] = "\uD83E\uDD4A", + [":boxing_gloves:"] = "\uD83E\uDD4A", + [":boy:"] = "\uD83D\uDC66", + [":boy::skin-tone-1:"] = "\uD83D\uDC66\uD83C\uDFFB", + [":boy::skin-tone-2:"] = "\uD83D\uDC66\uD83C\uDFFC", + [":boy::skin-tone-3:"] = "\uD83D\uDC66\uD83C\uDFFD", + [":boy::skin-tone-4:"] = "\uD83D\uDC66\uD83C\uDFFE", + [":boy::skin-tone-5:"] = "\uD83D\uDC66\uD83C\uDFFF", + [":boy_tone1:"] = "\uD83D\uDC66\uD83C\uDFFB", + [":boy_tone2:"] = "\uD83D\uDC66\uD83C\uDFFC", + [":boy_tone3:"] = "\uD83D\uDC66\uD83C\uDFFD", + [":boy_tone4:"] = "\uD83D\uDC66\uD83C\uDFFE", + [":boy_tone5:"] = "\uD83D\uDC66\uD83C\uDFFF", + [":brain:"] = "\uD83E\uDDE0", + [":bread:"] = "\uD83C\uDF5E", + [":breast_feeding:"] = "\uD83E\uDD31", + [":breast_feeding::skin-tone-1:"] = "\uD83E\uDD31\uD83C\uDFFB", + [":breast_feeding::skin-tone-2:"] = "\uD83E\uDD31\uD83C\uDFFC", + [":breast_feeding::skin-tone-3:"] = "\uD83E\uDD31\uD83C\uDFFD", + [":breast_feeding::skin-tone-4:"] = "\uD83E\uDD31\uD83C\uDFFE", + [":breast_feeding::skin-tone-5:"] = "\uD83E\uDD31\uD83C\uDFFF", + [":breast_feeding_dark_skin_tone:"] = "\uD83E\uDD31\uD83C\uDFFF", + [":breast_feeding_light_skin_tone:"] = "\uD83E\uDD31\uD83C\uDFFB", + [":breast_feeding_medium_dark_skin_tone:"] = "\uD83E\uDD31\uD83C\uDFFE", + [":breast_feeding_medium_light_skin_tone:"] = "\uD83E\uDD31\uD83C\uDFFC", + [":breast_feeding_medium_skin_tone:"] = "\uD83E\uDD31\uD83C\uDFFD", + [":breast_feeding_tone1:"] = "\uD83E\uDD31\uD83C\uDFFB", + [":breast_feeding_tone2:"] = "\uD83E\uDD31\uD83C\uDFFC", + [":breast_feeding_tone3:"] = "\uD83E\uDD31\uD83C\uDFFD", + [":breast_feeding_tone4:"] = "\uD83E\uDD31\uD83C\uDFFE", + [":breast_feeding_tone5:"] = "\uD83E\uDD31\uD83C\uDFFF", + [":bricks:"] = "\uD83E\uDDF1", + [":bride_with_veil:"] = "\uD83D\uDC70", + [":bride_with_veil::skin-tone-1:"] = "\uD83D\uDC70\uD83C\uDFFB", + [":bride_with_veil::skin-tone-2:"] = "\uD83D\uDC70\uD83C\uDFFC", + [":bride_with_veil::skin-tone-3:"] = "\uD83D\uDC70\uD83C\uDFFD", + [":bride_with_veil::skin-tone-4:"] = "\uD83D\uDC70\uD83C\uDFFE", + [":bride_with_veil::skin-tone-5:"] = "\uD83D\uDC70\uD83C\uDFFF", + [":bride_with_veil_tone1:"] = "\uD83D\uDC70\uD83C\uDFFB", + [":bride_with_veil_tone2:"] = "\uD83D\uDC70\uD83C\uDFFC", + [":bride_with_veil_tone3:"] = "\uD83D\uDC70\uD83C\uDFFD", + [":bride_with_veil_tone4:"] = "\uD83D\uDC70\uD83C\uDFFE", + [":bride_with_veil_tone5:"] = "\uD83D\uDC70\uD83C\uDFFF", + [":bridge_at_night:"] = "\uD83C\uDF09", + [":briefcase:"] = "\uD83D\uDCBC", + [":briefs:"] = "\uD83E\uDE72", + [":broccoli:"] = "\uD83E\uDD66", + [":broken_heart:"] = "\uD83D\uDC94", + [":broom:"] = "\uD83E\uDDF9", + [":brown_circle:"] = "\uD83D\uDFE4", + [":brown_heart:"] = "\uD83E\uDD0E", + [":brown_square:"] = "\uD83D\uDFEB", + [":bug:"] = "\uD83D\uDC1B", + [":building_construction:"] = "\uD83C\uDFD7️", + [":bulb:"] = "\uD83D\uDCA1", + [":bullettrain_front:"] = "\uD83D\uDE85", + [":bullettrain_side:"] = "\uD83D\uDE84", + [":burrito:"] = "\uD83C\uDF2F", + [":bus:"] = "\uD83D\uDE8C", + [":busstop:"] = "\uD83D\uDE8F", + [":bust_in_silhouette:"] = "\uD83D\uDC64", + [":busts_in_silhouette:"] = "\uD83D\uDC65", + [":butter:"] = "\uD83E\uDDC8", + [":butterfly:"] = "\uD83E\uDD8B", + [":cactus:"] = "\uD83C\uDF35", + [":cake:"] = "\uD83C\uDF70", + [":calendar:"] = "\uD83D\uDCC6", + [":calendar_spiral:"] = "\uD83D\uDDD3️", + [":call_me:"] = "\uD83E\uDD19", + [":call_me::skin-tone-1:"] = "\uD83E\uDD19\uD83C\uDFFB", + [":call_me::skin-tone-2:"] = "\uD83E\uDD19\uD83C\uDFFC", + [":call_me::skin-tone-3:"] = "\uD83E\uDD19\uD83C\uDFFD", + [":call_me::skin-tone-4:"] = "\uD83E\uDD19\uD83C\uDFFE", + [":call_me::skin-tone-5:"] = "\uD83E\uDD19\uD83C\uDFFF", + [":call_me_hand:"] = "\uD83E\uDD19", + [":call_me_hand::skin-tone-1:"] = "\uD83E\uDD19\uD83C\uDFFB", + [":call_me_hand::skin-tone-2:"] = "\uD83E\uDD19\uD83C\uDFFC", + [":call_me_hand::skin-tone-3:"] = "\uD83E\uDD19\uD83C\uDFFD", + [":call_me_hand::skin-tone-4:"] = "\uD83E\uDD19\uD83C\uDFFE", + [":call_me_hand::skin-tone-5:"] = "\uD83E\uDD19\uD83C\uDFFF", + [":call_me_hand_tone1:"] = "\uD83E\uDD19\uD83C\uDFFB", + [":call_me_hand_tone2:"] = "\uD83E\uDD19\uD83C\uDFFC", + [":call_me_hand_tone3:"] = "\uD83E\uDD19\uD83C\uDFFD", + [":call_me_hand_tone4:"] = "\uD83E\uDD19\uD83C\uDFFE", + [":call_me_hand_tone5:"] = "\uD83E\uDD19\uD83C\uDFFF", + [":call_me_tone1:"] = "\uD83E\uDD19\uD83C\uDFFB", + [":call_me_tone2:"] = "\uD83E\uDD19\uD83C\uDFFC", + [":call_me_tone3:"] = "\uD83E\uDD19\uD83C\uDFFD", + [":call_me_tone4:"] = "\uD83E\uDD19\uD83C\uDFFE", + [":call_me_tone5:"] = "\uD83E\uDD19\uD83C\uDFFF", + [":calling:"] = "\uD83D\uDCF2", + [":camel:"] = "\uD83D\uDC2B", + [":camera:"] = "\uD83D\uDCF7", + [":camera_with_flash:"] = "\uD83D\uDCF8", + [":camping:"] = "\uD83C\uDFD5️", + [":cancer:"] = "♋", + [":candle:"] = "\uD83D\uDD6F️", + [":candy:"] = "\uD83C\uDF6C", + [":canned_food:"] = "\uD83E\uDD6B", + [":canoe:"] = "\uD83D\uDEF6", + [":capital_abcd:"] = "\uD83D\uDD20", + [":capricorn:"] = "♑", + [":card_box:"] = "\uD83D\uDDC3️", + [":card_file_box:"] = "\uD83D\uDDC3️", + [":card_index:"] = "\uD83D\uDCC7", + [":card_index_dividers:"] = "\uD83D\uDDC2️", + [":carousel_horse:"] = "\uD83C\uDFA0", + [":carrot:"] = "\uD83E\uDD55", + [":cartwheel:"] = "\uD83E\uDD38", + [":cartwheel::skin-tone-1:"] = "\uD83E\uDD38\uD83C\uDFFB", + [":cartwheel::skin-tone-2:"] = "\uD83E\uDD38\uD83C\uDFFC", + [":cartwheel::skin-tone-3:"] = "\uD83E\uDD38\uD83C\uDFFD", + [":cartwheel::skin-tone-4:"] = "\uD83E\uDD38\uD83C\uDFFE", + [":cartwheel::skin-tone-5:"] = "\uD83E\uDD38\uD83C\uDFFF", + [":cartwheel_tone1:"] = "\uD83E\uDD38\uD83C\uDFFB", + [":cartwheel_tone2:"] = "\uD83E\uDD38\uD83C\uDFFC", + [":cartwheel_tone3:"] = "\uD83E\uDD38\uD83C\uDFFD", + [":cartwheel_tone4:"] = "\uD83E\uDD38\uD83C\uDFFE", + [":cartwheel_tone5:"] = "\uD83E\uDD38\uD83C\uDFFF", + [":cat2:"] = "\uD83D\uDC08", + [":cat:"] = "\uD83D\uDC31", + [":cd:"] = "\uD83D\uDCBF", + [":chains:"] = "⛓️", + [":chair:"] = "\uD83E\uDE91", + [":champagne:"] = "\uD83C\uDF7E", + [":champagne_glass:"] = "\uD83E\uDD42", + [":chart:"] = "\uD83D\uDCB9", + [":chart_with_downwards_trend:"] = "\uD83D\uDCC9", + [":chart_with_upwards_trend:"] = "\uD83D\uDCC8", + [":checkered_flag:"] = "\uD83C\uDFC1", + [":cheese:"] = "\uD83E\uDDC0", + [":cheese_wedge:"] = "\uD83E\uDDC0", + [":cherries:"] = "\uD83C\uDF52", + [":cherry_blossom:"] = "\uD83C\uDF38", + [":chess_pawn:"] = "♟️", + [":chestnut:"] = "\uD83C\uDF30", + [":chicken:"] = "\uD83D\uDC14", + [":child:"] = "\uD83E\uDDD2", + [":child::skin-tone-1:"] = "\uD83E\uDDD2\uD83C\uDFFB", + [":child::skin-tone-2:"] = "\uD83E\uDDD2\uD83C\uDFFC", + [":child::skin-tone-3:"] = "\uD83E\uDDD2\uD83C\uDFFD", + [":child::skin-tone-4:"] = "\uD83E\uDDD2\uD83C\uDFFE", + [":child::skin-tone-5:"] = "\uD83E\uDDD2\uD83C\uDFFF", + [":child_dark_skin_tone:"] = "\uD83E\uDDD2\uD83C\uDFFF", + [":child_light_skin_tone:"] = "\uD83E\uDDD2\uD83C\uDFFB", + [":child_medium_dark_skin_tone:"] = "\uD83E\uDDD2\uD83C\uDFFE", + [":child_medium_light_skin_tone:"] = "\uD83E\uDDD2\uD83C\uDFFC", + [":child_medium_skin_tone:"] = "\uD83E\uDDD2\uD83C\uDFFD", + [":child_tone1:"] = "\uD83E\uDDD2\uD83C\uDFFB", + [":child_tone2:"] = "\uD83E\uDDD2\uD83C\uDFFC", + [":child_tone3:"] = "\uD83E\uDDD2\uD83C\uDFFD", + [":child_tone4:"] = "\uD83E\uDDD2\uD83C\uDFFE", + [":child_tone5:"] = "\uD83E\uDDD2\uD83C\uDFFF", + [":children_crossing:"] = "\uD83D\uDEB8", + [":chipmunk:"] = "\uD83D\uDC3F️", + [":chocolate_bar:"] = "\uD83C\uDF6B", + [":chopsticks:"] = "\uD83E\uDD62", + [":christmas_tree:"] = "\uD83C\uDF84", + [":church:"] = "⛪", + [":cinema:"] = "\uD83C\uDFA6", + [":circus_tent:"] = "\uD83C\uDFAA", + [":city_dusk:"] = "\uD83C\uDF06", + [":city_sunrise:"] = "\uD83C\uDF07", + [":city_sunset:"] = "\uD83C\uDF07", + [":cityscape:"] = "\uD83C\uDFD9️", + [":cl:"] = "\uD83C\uDD91", + [":clap:"] = "\uD83D\uDC4F", + [":clap::skin-tone-1:"] = "\uD83D\uDC4F\uD83C\uDFFB", + [":clap::skin-tone-2:"] = "\uD83D\uDC4F\uD83C\uDFFC", + [":clap::skin-tone-3:"] = "\uD83D\uDC4F\uD83C\uDFFD", + [":clap::skin-tone-4:"] = "\uD83D\uDC4F\uD83C\uDFFE", + [":clap::skin-tone-5:"] = "\uD83D\uDC4F\uD83C\uDFFF", + [":clap_tone1:"] = "\uD83D\uDC4F\uD83C\uDFFB", + [":clap_tone2:"] = "\uD83D\uDC4F\uD83C\uDFFC", + [":clap_tone3:"] = "\uD83D\uDC4F\uD83C\uDFFD", + [":clap_tone4:"] = "\uD83D\uDC4F\uD83C\uDFFE", + [":clap_tone5:"] = "\uD83D\uDC4F\uD83C\uDFFF", + [":clapper:"] = "\uD83C\uDFAC", + [":classical_building:"] = "\uD83C\uDFDB️", + [":clinking_glass:"] = "\uD83E\uDD42", + [":clipboard:"] = "\uD83D\uDCCB", + [":clock1030:"] = "\uD83D\uDD65", + [":clock10:"] = "\uD83D\uDD59", + [":clock1130:"] = "\uD83D\uDD66", + [":clock11:"] = "\uD83D\uDD5A", + [":clock1230:"] = "\uD83D\uDD67", + [":clock12:"] = "\uD83D\uDD5B", + [":clock130:"] = "\uD83D\uDD5C", + [":clock1:"] = "\uD83D\uDD50", + [":clock230:"] = "\uD83D\uDD5D", + [":clock2:"] = "\uD83D\uDD51", + [":clock330:"] = "\uD83D\uDD5E", + [":clock3:"] = "\uD83D\uDD52", + [":clock430:"] = "\uD83D\uDD5F", + [":clock4:"] = "\uD83D\uDD53", + [":clock530:"] = "\uD83D\uDD60", + [":clock5:"] = "\uD83D\uDD54", + [":clock630:"] = "\uD83D\uDD61", + [":clock6:"] = "\uD83D\uDD55", + [":clock730:"] = "\uD83D\uDD62", + [":clock7:"] = "\uD83D\uDD56", + [":clock830:"] = "\uD83D\uDD63", + [":clock8:"] = "\uD83D\uDD57", + [":clock930:"] = "\uD83D\uDD64", + [":clock9:"] = "\uD83D\uDD58", + [":clock:"] = "\uD83D\uDD70️", + [":closed_book:"] = "\uD83D\uDCD5", + [":closed_lock_with_key:"] = "\uD83D\uDD10", + [":closed_umbrella:"] = "\uD83C\uDF02", + [":cloud:"] = "☁️", + [":cloud_lightning:"] = "\uD83C\uDF29️", + [":cloud_rain:"] = "\uD83C\uDF27️", + [":cloud_snow:"] = "\uD83C\uDF28️", + [":cloud_tornado:"] = "\uD83C\uDF2A️", + [":cloud_with_lightning:"] = "\uD83C\uDF29️", + [":cloud_with_rain:"] = "\uD83C\uDF27️", + [":cloud_with_snow:"] = "\uD83C\uDF28️", + [":cloud_with_tornado:"] = "\uD83C\uDF2A️", + [":clown:"] = "\uD83E\uDD21", + [":clown_face:"] = "\uD83E\uDD21", + [":clubs:"] = "♣️", + [":coat:"] = "\uD83E\uDDE5", + [":cocktail:"] = "\uD83C\uDF78", + [":coconut:"] = "\uD83E\uDD65", + [":coffee:"] = "☕", + [":coffin:"] = "⚰️", + [":cold_face:"] = "\uD83E\uDD76", + [":cold_sweat:"] = "\uD83D\uDE30", + [":comet:"] = "☄️", + [":compass:"] = "\uD83E\uDDED", + [":compression:"] = "\uD83D\uDDDC️", + [":computer:"] = "\uD83D\uDCBB", + [":confetti_ball:"] = "\uD83C\uDF8A", + [":confounded:"] = "\uD83D\uDE16", + [":confused:"] = "\uD83D\uDE15", + [":congratulations:"] = "㊗️", + [":construction:"] = "\uD83D\uDEA7", + [":construction_site:"] = "\uD83C\uDFD7️", + [":construction_worker:"] = "\uD83D\uDC77", + [":construction_worker::skin-tone-1:"] = "\uD83D\uDC77\uD83C\uDFFB", + [":construction_worker::skin-tone-2:"] = "\uD83D\uDC77\uD83C\uDFFC", + [":construction_worker::skin-tone-3:"] = "\uD83D\uDC77\uD83C\uDFFD", + [":construction_worker::skin-tone-4:"] = "\uD83D\uDC77\uD83C\uDFFE", + [":construction_worker::skin-tone-5:"] = "\uD83D\uDC77\uD83C\uDFFF", + [":construction_worker_tone1:"] = "\uD83D\uDC77\uD83C\uDFFB", + [":construction_worker_tone2:"] = "\uD83D\uDC77\uD83C\uDFFC", + [":construction_worker_tone3:"] = "\uD83D\uDC77\uD83C\uDFFD", + [":construction_worker_tone4:"] = "\uD83D\uDC77\uD83C\uDFFE", + [":construction_worker_tone5:"] = "\uD83D\uDC77\uD83C\uDFFF", + [":control_knobs:"] = "\uD83C\uDF9B️", + [":convenience_store:"] = "\uD83C\uDFEA", + [":cookie:"] = "\uD83C\uDF6A", + [":cooking:"] = "\uD83C\uDF73", + [":cool:"] = "\uD83C\uDD92", + [":cop:"] = "\uD83D\uDC6E", + [":cop::skin-tone-1:"] = "\uD83D\uDC6E\uD83C\uDFFB", + [":cop::skin-tone-2:"] = "\uD83D\uDC6E\uD83C\uDFFC", + [":cop::skin-tone-3:"] = "\uD83D\uDC6E\uD83C\uDFFD", + [":cop::skin-tone-4:"] = "\uD83D\uDC6E\uD83C\uDFFE", + [":cop::skin-tone-5:"] = "\uD83D\uDC6E\uD83C\uDFFF", + [":cop_tone1:"] = "\uD83D\uDC6E\uD83C\uDFFB", + [":cop_tone2:"] = "\uD83D\uDC6E\uD83C\uDFFC", + [":cop_tone3:"] = "\uD83D\uDC6E\uD83C\uDFFD", + [":cop_tone4:"] = "\uD83D\uDC6E\uD83C\uDFFE", + [":cop_tone5:"] = "\uD83D\uDC6E\uD83C\uDFFF", + [":copyright:"] = "©️", + [":corn:"] = "\uD83C\uDF3D", + [":couch:"] = "\uD83D\uDECB️", + [":couch_and_lamp:"] = "\uD83D\uDECB️", + [":couple:"] = "\uD83D\uDC6B", + [":couple_mm:"] = "\uD83D\uDC68\u200D❤️\u200D\uD83D\uDC68", + [":couple_with_heart:"] = "\uD83D\uDC91", + [":couple_with_heart_mm:"] = "\uD83D\uDC68\u200D❤️\u200D\uD83D\uDC68", + [":couple_with_heart_woman_man:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC68", + [":couple_with_heart_ww:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC69", + [":couple_ww:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC69", + [":couplekiss:"] = "\uD83D\uDC8F", + [":couplekiss_mm:"] = "\uD83D\uDC68\u200D❤️\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", + [":couplekiss_ww:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC8B\u200D\uD83D\uDC69", + [":cow2:"] = "\uD83D\uDC04", + [":cow:"] = "\uD83D\uDC2E", + [":cowboy:"] = "\uD83E\uDD20", + [":crab:"] = "\uD83E\uDD80", + [":crayon:"] = "\uD83D\uDD8D️", + [":credit_card:"] = "\uD83D\uDCB3", + [":crescent_moon:"] = "\uD83C\uDF19", + [":cricket:"] = "\uD83E\uDD97", + [":cricket_bat_ball:"] = "\uD83C\uDFCF", + [":cricket_game:"] = "\uD83C\uDFCF", + [":crocodile:"] = "\uD83D\uDC0A", + [":croissant:"] = "\uD83E\uDD50", + [":cross:"] = "✝️", + [":crossed_flags:"] = "\uD83C\uDF8C", + [":crossed_swords:"] = "⚔️", + [":crown:"] = "\uD83D\uDC51", + [":cruise_ship:"] = "\uD83D\uDEF3️", + [":cry:"] = "\uD83D\uDE22", + [":crying_cat_face:"] = "\uD83D\uDE3F", + [":crystal_ball:"] = "\uD83D\uDD2E", + [":cucumber:"] = "\uD83E\uDD52", + [":cup_with_straw:"] = "\uD83E\uDD64", + [":cupcake:"] = "\uD83E\uDDC1", + [":cupid:"] = "\uD83D\uDC98", + [":curling_stone:"] = "\uD83E\uDD4C", + [":curly_loop:"] = "➰", + [":currency_exchange:"] = "\uD83D\uDCB1", + [":curry:"] = "\uD83C\uDF5B", + [":custard:"] = "\uD83C\uDF6E", + [":customs:"] = "\uD83D\uDEC3", + [":cut_of_meat:"] = "\uD83E\uDD69", + [":cyclone:"] = "\uD83C\uDF00", + [":dagger:"] = "\uD83D\uDDE1️", + [":dagger_knife:"] = "\uD83D\uDDE1️", + [":dancer:"] = "\uD83D\uDC83", + [":dancer::skin-tone-1:"] = "\uD83D\uDC83\uD83C\uDFFB", + [":dancer::skin-tone-2:"] = "\uD83D\uDC83\uD83C\uDFFC", + [":dancer::skin-tone-3:"] = "\uD83D\uDC83\uD83C\uDFFD", + [":dancer::skin-tone-4:"] = "\uD83D\uDC83\uD83C\uDFFE", + [":dancer::skin-tone-5:"] = "\uD83D\uDC83\uD83C\uDFFF", + [":dancer_tone1:"] = "\uD83D\uDC83\uD83C\uDFFB", + [":dancer_tone2:"] = "\uD83D\uDC83\uD83C\uDFFC", + [":dancer_tone3:"] = "\uD83D\uDC83\uD83C\uDFFD", + [":dancer_tone4:"] = "\uD83D\uDC83\uD83C\uDFFE", + [":dancer_tone5:"] = "\uD83D\uDC83\uD83C\uDFFF", + [":dancers:"] = "\uD83D\uDC6F", + [":dango:"] = "\uD83C\uDF61", + [":dark_sunglasses:"] = "\uD83D\uDD76️", + [":dart:"] = "\uD83C\uDFAF", + [":dash:"] = "\uD83D\uDCA8", + [":date:"] = "\uD83D\uDCC5", + [":deaf_man:"] = "\uD83E\uDDCF\u200D♂️", + [":deaf_man::skin-tone-1:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♂️", + [":deaf_man::skin-tone-2:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♂️", + [":deaf_man::skin-tone-3:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♂️", + [":deaf_man::skin-tone-4:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♂️", + [":deaf_man::skin-tone-5:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♂️", + [":deaf_man_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♂️", + [":deaf_man_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♂️", + [":deaf_man_medium_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♂️", + [":deaf_man_medium_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♂️", + [":deaf_man_medium_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♂️", + [":deaf_man_tone1:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♂️", + [":deaf_man_tone2:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♂️", + [":deaf_man_tone3:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♂️", + [":deaf_man_tone4:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♂️", + [":deaf_man_tone5:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♂️", + [":deaf_person:"] = "\uD83E\uDDCF", + [":deaf_person::skin-tone-1:"] = "\uD83E\uDDCF\uD83C\uDFFB", + [":deaf_person::skin-tone-2:"] = "\uD83E\uDDCF\uD83C\uDFFC", + [":deaf_person::skin-tone-3:"] = "\uD83E\uDDCF\uD83C\uDFFD", + [":deaf_person::skin-tone-4:"] = "\uD83E\uDDCF\uD83C\uDFFE", + [":deaf_person::skin-tone-5:"] = "\uD83E\uDDCF\uD83C\uDFFF", + [":deaf_person_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFF", + [":deaf_person_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFB", + [":deaf_person_medium_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFE", + [":deaf_person_medium_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFC", + [":deaf_person_medium_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFD", + [":deaf_person_tone1:"] = "\uD83E\uDDCF\uD83C\uDFFB", + [":deaf_person_tone2:"] = "\uD83E\uDDCF\uD83C\uDFFC", + [":deaf_person_tone3:"] = "\uD83E\uDDCF\uD83C\uDFFD", + [":deaf_person_tone4:"] = "\uD83E\uDDCF\uD83C\uDFFE", + [":deaf_person_tone5:"] = "\uD83E\uDDCF\uD83C\uDFFF", + [":deaf_woman:"] = "\uD83E\uDDCF\u200D♀️", + [":deaf_woman::skin-tone-1:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♀️", + [":deaf_woman::skin-tone-2:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♀️", + [":deaf_woman::skin-tone-3:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♀️", + [":deaf_woman::skin-tone-4:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♀️", + [":deaf_woman::skin-tone-5:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♀️", + [":deaf_woman_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♀️", + [":deaf_woman_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♀️", + [":deaf_woman_medium_dark_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♀️", + [":deaf_woman_medium_light_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♀️", + [":deaf_woman_medium_skin_tone:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♀️", + [":deaf_woman_tone1:"] = "\uD83E\uDDCF\uD83C\uDFFB\u200D♀️", + [":deaf_woman_tone2:"] = "\uD83E\uDDCF\uD83C\uDFFC\u200D♀️", + [":deaf_woman_tone3:"] = "\uD83E\uDDCF\uD83C\uDFFD\u200D♀️", + [":deaf_woman_tone4:"] = "\uD83E\uDDCF\uD83C\uDFFE\u200D♀️", + [":deaf_woman_tone5:"] = "\uD83E\uDDCF\uD83C\uDFFF\u200D♀️", + [":deciduous_tree:"] = "\uD83C\uDF33", + [":deer:"] = "\uD83E\uDD8C", + [":department_store:"] = "\uD83C\uDFEC", + [":derelict_house_building:"] = "\uD83C\uDFDA️", + [":desert:"] = "\uD83C\uDFDC️", + [":desert_island:"] = "\uD83C\uDFDD️", + [":desktop:"] = "\uD83D\uDDA5️", + [":desktop_computer:"] = "\uD83D\uDDA5️", + [":detective:"] = "\uD83D\uDD75️", + [":detective::skin-tone-1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":detective::skin-tone-2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":detective::skin-tone-3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":detective::skin-tone-4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":detective::skin-tone-5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":detective_tone1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":detective_tone2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":detective_tone3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":detective_tone4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":detective_tone5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":diamond_shape_with_a_dot_inside:"] = "\uD83D\uDCA0", + [":diamonds:"] = "♦️", + [":disappointed:"] = "\uD83D\uDE1E", + [":disappointed_relieved:"] = "\uD83D\uDE25", + [":dividers:"] = "\uD83D\uDDC2️", + [":diving_mask:"] = "\uD83E\uDD3F", + [":diya_lamp:"] = "\uD83E\uDE94", + [":dizzy:"] = "\uD83D\uDCAB", + [":dizzy_face:"] = "\uD83D\uDE35", + [":dna:"] = "\uD83E\uDDEC", + [":do_not_litter:"] = "\uD83D\uDEAF", + [":dog2:"] = "\uD83D\uDC15", + [":dog:"] = "\uD83D\uDC36", + [":dollar:"] = "\uD83D\uDCB5", + [":dolls:"] = "\uD83C\uDF8E", + [":dolphin:"] = "\uD83D\uDC2C", + [":door:"] = "\uD83D\uDEAA", + [":double_vertical_bar:"] = "⏸️", + [":doughnut:"] = "\uD83C\uDF69", + [":dove:"] = "\uD83D\uDD4A️", + [":dove_of_peace:"] = "\uD83D\uDD4A️", + [":dragon:"] = "\uD83D\uDC09", + [":dragon_face:"] = "\uD83D\uDC32", + [":dress:"] = "\uD83D\uDC57", + [":dromedary_camel:"] = "\uD83D\uDC2A", + [":drool:"] = "\uD83E\uDD24", + [":drooling_face:"] = "\uD83E\uDD24", + [":drop_of_blood:"] = "\uD83E\uDE78", + [":droplet:"] = "\uD83D\uDCA7", + [":drum:"] = "\uD83E\uDD41", + [":drum_with_drumsticks:"] = "\uD83E\uDD41", + [":duck:"] = "\uD83E\uDD86", + [":dumpling:"] = "\uD83E\uDD5F", + [":dvd:"] = "\uD83D\uDCC0", + [":e_mail:"] = "\uD83D\uDCE7", + [":eagle:"] = "\uD83E\uDD85", + [":ear:"] = "\uD83D\uDC42", + [":ear::skin-tone-1:"] = "\uD83D\uDC42\uD83C\uDFFB", + [":ear::skin-tone-2:"] = "\uD83D\uDC42\uD83C\uDFFC", + [":ear::skin-tone-3:"] = "\uD83D\uDC42\uD83C\uDFFD", + [":ear::skin-tone-4:"] = "\uD83D\uDC42\uD83C\uDFFE", + [":ear::skin-tone-5:"] = "\uD83D\uDC42\uD83C\uDFFF", + [":ear_of_rice:"] = "\uD83C\uDF3E", + [":ear_tone1:"] = "\uD83D\uDC42\uD83C\uDFFB", + [":ear_tone2:"] = "\uD83D\uDC42\uD83C\uDFFC", + [":ear_tone3:"] = "\uD83D\uDC42\uD83C\uDFFD", + [":ear_tone4:"] = "\uD83D\uDC42\uD83C\uDFFE", + [":ear_tone5:"] = "\uD83D\uDC42\uD83C\uDFFF", + [":ear_with_hearing_aid:"] = "\uD83E\uDDBB", + [":ear_with_hearing_aid::skin-tone-1:"] = "\uD83E\uDDBB\uD83C\uDFFB", + [":ear_with_hearing_aid::skin-tone-2:"] = "\uD83E\uDDBB\uD83C\uDFFC", + [":ear_with_hearing_aid::skin-tone-3:"] = "\uD83E\uDDBB\uD83C\uDFFD", + [":ear_with_hearing_aid::skin-tone-4:"] = "\uD83E\uDDBB\uD83C\uDFFE", + [":ear_with_hearing_aid::skin-tone-5:"] = "\uD83E\uDDBB\uD83C\uDFFF", + [":ear_with_hearing_aid_dark_skin_tone:"] = "\uD83E\uDDBB\uD83C\uDFFF", + [":ear_with_hearing_aid_light_skin_tone:"] = "\uD83E\uDDBB\uD83C\uDFFB", + [":ear_with_hearing_aid_medium_dark_skin_tone:"] = "\uD83E\uDDBB\uD83C\uDFFE", + [":ear_with_hearing_aid_medium_light_skin_tone:"] = "\uD83E\uDDBB\uD83C\uDFFC", + [":ear_with_hearing_aid_medium_skin_tone:"] = "\uD83E\uDDBB\uD83C\uDFFD", + [":ear_with_hearing_aid_tone1:"] = "\uD83E\uDDBB\uD83C\uDFFB", + [":ear_with_hearing_aid_tone2:"] = "\uD83E\uDDBB\uD83C\uDFFC", + [":ear_with_hearing_aid_tone3:"] = "\uD83E\uDDBB\uD83C\uDFFD", + [":ear_with_hearing_aid_tone4:"] = "\uD83E\uDDBB\uD83C\uDFFE", + [":ear_with_hearing_aid_tone5:"] = "\uD83E\uDDBB\uD83C\uDFFF", + [":earth_africa:"] = "\uD83C\uDF0D", + [":earth_americas:"] = "\uD83C\uDF0E", + [":earth_asia:"] = "\uD83C\uDF0F", + [":egg:"] = "\uD83E\uDD5A", + [":eggplant:"] = "\uD83C\uDF46", + [":eight:"] = "8️⃣", + [":eight_pointed_black_star:"] = "✴️", + [":eight_spoked_asterisk:"] = "✳️", + [":eject:"] = "⏏️", + [":eject_symbol:"] = "⏏️", + [":electric_plug:"] = "\uD83D\uDD0C", + [":elephant:"] = "\uD83D\uDC18", + [":elf:"] = "\uD83E\uDDDD", + [":elf::skin-tone-1:"] = "\uD83E\uDDDD\uD83C\uDFFB", + [":elf::skin-tone-2:"] = "\uD83E\uDDDD\uD83C\uDFFC", + [":elf::skin-tone-3:"] = "\uD83E\uDDDD\uD83C\uDFFD", + [":elf::skin-tone-4:"] = "\uD83E\uDDDD\uD83C\uDFFE", + [":elf::skin-tone-5:"] = "\uD83E\uDDDD\uD83C\uDFFF", + [":elf_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFF", + [":elf_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFB", + [":elf_medium_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFE", + [":elf_medium_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFC", + [":elf_medium_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFD", + [":elf_tone1:"] = "\uD83E\uDDDD\uD83C\uDFFB", + [":elf_tone2:"] = "\uD83E\uDDDD\uD83C\uDFFC", + [":elf_tone3:"] = "\uD83E\uDDDD\uD83C\uDFFD", + [":elf_tone4:"] = "\uD83E\uDDDD\uD83C\uDFFE", + [":elf_tone5:"] = "\uD83E\uDDDD\uD83C\uDFFF", + [":email:"] = "\uD83D\uDCE7", + [":end:"] = "\uD83D\uDD1A", + [":england:"] = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F", + [":envelope:"] = "✉️", + [":envelope_with_arrow:"] = "\uD83D\uDCE9", + [":euro:"] = "\uD83D\uDCB6", + [":european_castle:"] = "\uD83C\uDFF0", + [":european_post_office:"] = "\uD83C\uDFE4", + [":evergreen_tree:"] = "\uD83C\uDF32", + [":exclamation:"] = "❗", + [":expecting_woman:"] = "\uD83E\uDD30", + [":expecting_woman::skin-tone-1:"] = "\uD83E\uDD30\uD83C\uDFFB", + [":expecting_woman::skin-tone-2:"] = "\uD83E\uDD30\uD83C\uDFFC", + [":expecting_woman::skin-tone-3:"] = "\uD83E\uDD30\uD83C\uDFFD", + [":expecting_woman::skin-tone-4:"] = "\uD83E\uDD30\uD83C\uDFFE", + [":expecting_woman::skin-tone-5:"] = "\uD83E\uDD30\uD83C\uDFFF", + [":expecting_woman_tone1:"] = "\uD83E\uDD30\uD83C\uDFFB", + [":expecting_woman_tone2:"] = "\uD83E\uDD30\uD83C\uDFFC", + [":expecting_woman_tone3:"] = "\uD83E\uDD30\uD83C\uDFFD", + [":expecting_woman_tone4:"] = "\uD83E\uDD30\uD83C\uDFFE", + [":expecting_woman_tone5:"] = "\uD83E\uDD30\uD83C\uDFFF", + [":exploding_head:"] = "\uD83E\uDD2F", + [":expressionless:"] = "\uD83D\uDE11", + [":eye:"] = "\uD83D\uDC41️", + [":eye_in_speech_bubble:"] = "\uD83D\uDC41\u200D\uD83D\uDDE8", + [":eyeglasses:"] = "\uD83D\uDC53", + [":eyes:"] = "\uD83D\uDC40", + [":face_palm:"] = "\uD83E\uDD26", + [":face_palm::skin-tone-1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":face_palm::skin-tone-2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":face_palm::skin-tone-3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":face_palm::skin-tone-4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":face_palm::skin-tone-5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":face_palm_tone1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":face_palm_tone2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":face_palm_tone3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":face_palm_tone4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":face_palm_tone5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":face_vomiting:"] = "\uD83E\uDD2E", + [":face_with_cowboy_hat:"] = "\uD83E\uDD20", + [":face_with_hand_over_mouth:"] = "\uD83E\uDD2D", + [":face_with_head_bandage:"] = "\uD83E\uDD15", + [":face_with_monocle:"] = "\uD83E\uDDD0", + [":face_with_raised_eyebrow:"] = "\uD83E\uDD28", + [":face_with_rolling_eyes:"] = "\uD83D\uDE44", + [":face_with_symbols_over_mouth:"] = "\uD83E\uDD2C", + [":face_with_thermometer:"] = "\uD83E\uDD12", + [":facepalm:"] = "\uD83E\uDD26", + [":facepalm::skin-tone-1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":facepalm::skin-tone-2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":facepalm::skin-tone-3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":facepalm::skin-tone-4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":facepalm::skin-tone-5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":facepalm_tone1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":facepalm_tone2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":facepalm_tone3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":facepalm_tone4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":facepalm_tone5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":factory:"] = "\uD83C\uDFED", + [":fairy:"] = "\uD83E\uDDDA", + [":fairy::skin-tone-1:"] = "\uD83E\uDDDA\uD83C\uDFFB", + [":fairy::skin-tone-2:"] = "\uD83E\uDDDA\uD83C\uDFFC", + [":fairy::skin-tone-3:"] = "\uD83E\uDDDA\uD83C\uDFFD", + [":fairy::skin-tone-4:"] = "\uD83E\uDDDA\uD83C\uDFFE", + [":fairy::skin-tone-5:"] = "\uD83E\uDDDA\uD83C\uDFFF", + [":fairy_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFF", + [":fairy_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFB", + [":fairy_medium_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFE", + [":fairy_medium_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFC", + [":fairy_medium_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFD", + [":fairy_tone1:"] = "\uD83E\uDDDA\uD83C\uDFFB", + [":fairy_tone2:"] = "\uD83E\uDDDA\uD83C\uDFFC", + [":fairy_tone3:"] = "\uD83E\uDDDA\uD83C\uDFFD", + [":fairy_tone4:"] = "\uD83E\uDDDA\uD83C\uDFFE", + [":fairy_tone5:"] = "\uD83E\uDDDA\uD83C\uDFFF", + [":falafel:"] = "\uD83E\uDDC6", + [":fallen_leaf:"] = "\uD83C\uDF42", + [":family:"] = "\uD83D\uDC6A", + [":family_man_boy:"] = "\uD83D\uDC68\u200D\uD83D\uDC66", + [":family_man_boy_boy:"] = "\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + [":family_man_girl:"] = "\uD83D\uDC68\u200D\uD83D\uDC67", + [":family_man_girl_boy:"] = "\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + [":family_man_girl_girl:"] = "\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + [":family_man_woman_boy:"] = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66", + [":family_mmb:"] = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC66", + [":family_mmbb:"] = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + [":family_mmg:"] = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67", + [":family_mmgb:"] = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + [":family_mmgg:"] = "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + [":family_mwbb:"] = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + [":family_mwg:"] = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67", + [":family_mwgb:"] = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + [":family_mwgg:"] = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + [":family_woman_boy:"] = "\uD83D\uDC69\u200D\uD83D\uDC66", + [":family_woman_boy_boy:"] = "\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + [":family_woman_girl:"] = "\uD83D\uDC69\u200D\uD83D\uDC67", + [":family_woman_girl_boy:"] = "\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + [":family_woman_girl_girl:"] = "\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + [":family_wwb:"] = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66", + [":family_wwbb:"] = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + [":family_wwg:"] = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67", + [":family_wwgb:"] = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + [":family_wwgg:"] = "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + [":fast_forward:"] = "⏩", + [":fax:"] = "\uD83D\uDCE0", + [":fearful:"] = "\uD83D\uDE28", + [":feet:"] = "\uD83D\uDC3E", + [":female_sign:"] = "♀️", + [":fencer:"] = "\uD83E\uDD3A", + [":fencing:"] = "\uD83E\uDD3A", + [":ferris_wheel:"] = "\uD83C\uDFA1", + [":ferry:"] = "⛴️", + [":field_hockey:"] = "\uD83C\uDFD1", + [":file_cabinet:"] = "\uD83D\uDDC4️", + [":file_folder:"] = "\uD83D\uDCC1", + [":film_frames:"] = "\uD83C\uDF9E️", + [":film_projector:"] = "\uD83D\uDCFD️", + [":fingers_crossed:"] = "\uD83E\uDD1E", + [":fingers_crossed::skin-tone-1:"] = "\uD83E\uDD1E\uD83C\uDFFB", + [":fingers_crossed::skin-tone-2:"] = "\uD83E\uDD1E\uD83C\uDFFC", + [":fingers_crossed::skin-tone-3:"] = "\uD83E\uDD1E\uD83C\uDFFD", + [":fingers_crossed::skin-tone-4:"] = "\uD83E\uDD1E\uD83C\uDFFE", + [":fingers_crossed::skin-tone-5:"] = "\uD83E\uDD1E\uD83C\uDFFF", + [":fingers_crossed_tone1:"] = "\uD83E\uDD1E\uD83C\uDFFB", + [":fingers_crossed_tone2:"] = "\uD83E\uDD1E\uD83C\uDFFC", + [":fingers_crossed_tone3:"] = "\uD83E\uDD1E\uD83C\uDFFD", + [":fingers_crossed_tone4:"] = "\uD83E\uDD1E\uD83C\uDFFE", + [":fingers_crossed_tone5:"] = "\uD83E\uDD1E\uD83C\uDFFF", + [":fire:"] = "\uD83D\uDD25", + [":fire_engine:"] = "\uD83D\uDE92", + [":fire_extinguisher:"] = "\uD83E\uDDEF", + [":firecracker:"] = "\uD83E\uDDE8", + [":fireworks:"] = "\uD83C\uDF86", + [":first_place:"] = "\uD83E\uDD47", + [":first_place_medal:"] = "\uD83E\uDD47", + [":first_quarter_moon:"] = "\uD83C\uDF13", + [":first_quarter_moon_with_face:"] = "\uD83C\uDF1B", + [":fish:"] = "\uD83D\uDC1F", + [":fish_cake:"] = "\uD83C\uDF65", + [":fishing_pole_and_fish:"] = "\uD83C\uDFA3", + [":fist:"] = "✊", + [":fist::skin-tone-1:"] = "✊\uD83C\uDFFB", + [":fist::skin-tone-2:"] = "✊\uD83C\uDFFC", + [":fist::skin-tone-3:"] = "✊\uD83C\uDFFD", + [":fist::skin-tone-4:"] = "✊\uD83C\uDFFE", + [":fist::skin-tone-5:"] = "✊\uD83C\uDFFF", + [":fist_tone1:"] = "✊\uD83C\uDFFB", + [":fist_tone2:"] = "✊\uD83C\uDFFC", + [":fist_tone3:"] = "✊\uD83C\uDFFD", + [":fist_tone4:"] = "✊\uD83C\uDFFE", + [":fist_tone5:"] = "✊\uD83C\uDFFF", + [":five:"] = "5️⃣", + [":flag_ac:"] = "\uD83C\uDDE6\uD83C\uDDE8", + [":flag_ad:"] = "\uD83C\uDDE6\uD83C\uDDE9", + [":flag_ae:"] = "\uD83C\uDDE6\uD83C\uDDEA", + [":flag_af:"] = "\uD83C\uDDE6\uD83C\uDDEB", + [":flag_ag:"] = "\uD83C\uDDE6\uD83C\uDDEC", + [":flag_ai:"] = "\uD83C\uDDE6\uD83C\uDDEE", + [":flag_al:"] = "\uD83C\uDDE6\uD83C\uDDF1", + [":flag_am:"] = "\uD83C\uDDE6\uD83C\uDDF2", + [":flag_ao:"] = "\uD83C\uDDE6\uD83C\uDDF4", + [":flag_aq:"] = "\uD83C\uDDE6\uD83C\uDDF6", + [":flag_ar:"] = "\uD83C\uDDE6\uD83C\uDDF7", + [":flag_as:"] = "\uD83C\uDDE6\uD83C\uDDF8", + [":flag_at:"] = "\uD83C\uDDE6\uD83C\uDDF9", + [":flag_au:"] = "\uD83C\uDDE6\uD83C\uDDFA", + [":flag_aw:"] = "\uD83C\uDDE6\uD83C\uDDFC", + [":flag_ax:"] = "\uD83C\uDDE6\uD83C\uDDFD", + [":flag_az:"] = "\uD83C\uDDE6\uD83C\uDDFF", + [":flag_ba:"] = "\uD83C\uDDE7\uD83C\uDDE6", + [":flag_bb:"] = "\uD83C\uDDE7\uD83C\uDDE7", + [":flag_bd:"] = "\uD83C\uDDE7\uD83C\uDDE9", + [":flag_be:"] = "\uD83C\uDDE7\uD83C\uDDEA", + [":flag_bf:"] = "\uD83C\uDDE7\uD83C\uDDEB", + [":flag_bg:"] = "\uD83C\uDDE7\uD83C\uDDEC", + [":flag_bh:"] = "\uD83C\uDDE7\uD83C\uDDED", + [":flag_bi:"] = "\uD83C\uDDE7\uD83C\uDDEE", + [":flag_bj:"] = "\uD83C\uDDE7\uD83C\uDDEF", + [":flag_bl:"] = "\uD83C\uDDE7\uD83C\uDDF1", + [":flag_black:"] = "\uD83C\uDFF4", + [":flag_bm:"] = "\uD83C\uDDE7\uD83C\uDDF2", + [":flag_bn:"] = "\uD83C\uDDE7\uD83C\uDDF3", + [":flag_bo:"] = "\uD83C\uDDE7\uD83C\uDDF4", + [":flag_bq:"] = "\uD83C\uDDE7\uD83C\uDDF6", + [":flag_br:"] = "\uD83C\uDDE7\uD83C\uDDF7", + [":flag_bs:"] = "\uD83C\uDDE7\uD83C\uDDF8", + [":flag_bt:"] = "\uD83C\uDDE7\uD83C\uDDF9", + [":flag_bv:"] = "\uD83C\uDDE7\uD83C\uDDFB", + [":flag_bw:"] = "\uD83C\uDDE7\uD83C\uDDFC", + [":flag_by:"] = "\uD83C\uDDE7\uD83C\uDDFE", + [":flag_bz:"] = "\uD83C\uDDE7\uD83C\uDDFF", + [":flag_ca:"] = "\uD83C\uDDE8\uD83C\uDDE6", + [":flag_cc:"] = "\uD83C\uDDE8\uD83C\uDDE8", + [":flag_cd:"] = "\uD83C\uDDE8\uD83C\uDDE9", + [":flag_cf:"] = "\uD83C\uDDE8\uD83C\uDDEB", + [":flag_cg:"] = "\uD83C\uDDE8\uD83C\uDDEC", + [":flag_ch:"] = "\uD83C\uDDE8\uD83C\uDDED", + [":flag_ci:"] = "\uD83C\uDDE8\uD83C\uDDEE", + [":flag_ck:"] = "\uD83C\uDDE8\uD83C\uDDF0", + [":flag_cl:"] = "\uD83C\uDDE8\uD83C\uDDF1", + [":flag_cm:"] = "\uD83C\uDDE8\uD83C\uDDF2", + [":flag_cn:"] = "\uD83C\uDDE8\uD83C\uDDF3", + [":flag_co:"] = "\uD83C\uDDE8\uD83C\uDDF4", + [":flag_cp:"] = "\uD83C\uDDE8\uD83C\uDDF5", + [":flag_cr:"] = "\uD83C\uDDE8\uD83C\uDDF7", + [":flag_cu:"] = "\uD83C\uDDE8\uD83C\uDDFA", + [":flag_cv:"] = "\uD83C\uDDE8\uD83C\uDDFB", + [":flag_cw:"] = "\uD83C\uDDE8\uD83C\uDDFC", + [":flag_cx:"] = "\uD83C\uDDE8\uD83C\uDDFD", + [":flag_cy:"] = "\uD83C\uDDE8\uD83C\uDDFE", + [":flag_cz:"] = "\uD83C\uDDE8\uD83C\uDDFF", + [":flag_de:"] = "\uD83C\uDDE9\uD83C\uDDEA", + [":flag_dg:"] = "\uD83C\uDDE9\uD83C\uDDEC", + [":flag_dj:"] = "\uD83C\uDDE9\uD83C\uDDEF", + [":flag_dk:"] = "\uD83C\uDDE9\uD83C\uDDF0", + [":flag_dm:"] = "\uD83C\uDDE9\uD83C\uDDF2", + [":flag_do:"] = "\uD83C\uDDE9\uD83C\uDDF4", + [":flag_dz:"] = "\uD83C\uDDE9\uD83C\uDDFF", + [":flag_ea:"] = "\uD83C\uDDEA\uD83C\uDDE6", + [":flag_ec:"] = "\uD83C\uDDEA\uD83C\uDDE8", + [":flag_ee:"] = "\uD83C\uDDEA\uD83C\uDDEA", + [":flag_eg:"] = "\uD83C\uDDEA\uD83C\uDDEC", + [":flag_eh:"] = "\uD83C\uDDEA\uD83C\uDDED", + [":flag_er:"] = "\uD83C\uDDEA\uD83C\uDDF7", + [":flag_es:"] = "\uD83C\uDDEA\uD83C\uDDF8", + [":flag_et:"] = "\uD83C\uDDEA\uD83C\uDDF9", + [":flag_eu:"] = "\uD83C\uDDEA\uD83C\uDDFA", + [":flag_fi:"] = "\uD83C\uDDEB\uD83C\uDDEE", + [":flag_fj:"] = "\uD83C\uDDEB\uD83C\uDDEF", + [":flag_fk:"] = "\uD83C\uDDEB\uD83C\uDDF0", + [":flag_fm:"] = "\uD83C\uDDEB\uD83C\uDDF2", + [":flag_fo:"] = "\uD83C\uDDEB\uD83C\uDDF4", + [":flag_fr:"] = "\uD83C\uDDEB\uD83C\uDDF7", + [":flag_ga:"] = "\uD83C\uDDEC\uD83C\uDDE6", + [":flag_gb:"] = "\uD83C\uDDEC\uD83C\uDDE7", + [":flag_gd:"] = "\uD83C\uDDEC\uD83C\uDDE9", + [":flag_ge:"] = "\uD83C\uDDEC\uD83C\uDDEA", + [":flag_gf:"] = "\uD83C\uDDEC\uD83C\uDDEB", + [":flag_gg:"] = "\uD83C\uDDEC\uD83C\uDDEC", + [":flag_gh:"] = "\uD83C\uDDEC\uD83C\uDDED", + [":flag_gi:"] = "\uD83C\uDDEC\uD83C\uDDEE", + [":flag_gl:"] = "\uD83C\uDDEC\uD83C\uDDF1", + [":flag_gm:"] = "\uD83C\uDDEC\uD83C\uDDF2", + [":flag_gn:"] = "\uD83C\uDDEC\uD83C\uDDF3", + [":flag_gp:"] = "\uD83C\uDDEC\uD83C\uDDF5", + [":flag_gq:"] = "\uD83C\uDDEC\uD83C\uDDF6", + [":flag_gr:"] = "\uD83C\uDDEC\uD83C\uDDF7", + [":flag_gs:"] = "\uD83C\uDDEC\uD83C\uDDF8", + [":flag_gt:"] = "\uD83C\uDDEC\uD83C\uDDF9", + [":flag_gu:"] = "\uD83C\uDDEC\uD83C\uDDFA", + [":flag_gw:"] = "\uD83C\uDDEC\uD83C\uDDFC", + [":flag_gy:"] = "\uD83C\uDDEC\uD83C\uDDFE", + [":flag_hk:"] = "\uD83C\uDDED\uD83C\uDDF0", + [":flag_hm:"] = "\uD83C\uDDED\uD83C\uDDF2", + [":flag_hn:"] = "\uD83C\uDDED\uD83C\uDDF3", + [":flag_hr:"] = "\uD83C\uDDED\uD83C\uDDF7", + [":flag_ht:"] = "\uD83C\uDDED\uD83C\uDDF9", + [":flag_hu:"] = "\uD83C\uDDED\uD83C\uDDFA", + [":flag_ic:"] = "\uD83C\uDDEE\uD83C\uDDE8", + [":flag_id:"] = "\uD83C\uDDEE\uD83C\uDDE9", + [":flag_ie:"] = "\uD83C\uDDEE\uD83C\uDDEA", + [":flag_il:"] = "\uD83C\uDDEE\uD83C\uDDF1", + [":flag_im:"] = "\uD83C\uDDEE\uD83C\uDDF2", + [":flag_in:"] = "\uD83C\uDDEE\uD83C\uDDF3", + [":flag_io:"] = "\uD83C\uDDEE\uD83C\uDDF4", + [":flag_iq:"] = "\uD83C\uDDEE\uD83C\uDDF6", + [":flag_ir:"] = "\uD83C\uDDEE\uD83C\uDDF7", + [":flag_is:"] = "\uD83C\uDDEE\uD83C\uDDF8", + [":flag_it:"] = "\uD83C\uDDEE\uD83C\uDDF9", + [":flag_je:"] = "\uD83C\uDDEF\uD83C\uDDEA", + [":flag_jm:"] = "\uD83C\uDDEF\uD83C\uDDF2", + [":flag_jo:"] = "\uD83C\uDDEF\uD83C\uDDF4", + [":flag_jp:"] = "\uD83C\uDDEF\uD83C\uDDF5", + [":flag_ke:"] = "\uD83C\uDDF0\uD83C\uDDEA", + [":flag_kg:"] = "\uD83C\uDDF0\uD83C\uDDEC", + [":flag_kh:"] = "\uD83C\uDDF0\uD83C\uDDED", + [":flag_ki:"] = "\uD83C\uDDF0\uD83C\uDDEE", + [":flag_km:"] = "\uD83C\uDDF0\uD83C\uDDF2", + [":flag_kn:"] = "\uD83C\uDDF0\uD83C\uDDF3", + [":flag_kp:"] = "\uD83C\uDDF0\uD83C\uDDF5", + [":flag_kr:"] = "\uD83C\uDDF0\uD83C\uDDF7", + [":flag_kw:"] = "\uD83C\uDDF0\uD83C\uDDFC", + [":flag_ky:"] = "\uD83C\uDDF0\uD83C\uDDFE", + [":flag_kz:"] = "\uD83C\uDDF0\uD83C\uDDFF", + [":flag_la:"] = "\uD83C\uDDF1\uD83C\uDDE6", + [":flag_lb:"] = "\uD83C\uDDF1\uD83C\uDDE7", + [":flag_lc:"] = "\uD83C\uDDF1\uD83C\uDDE8", + [":flag_li:"] = "\uD83C\uDDF1\uD83C\uDDEE", + [":flag_lk:"] = "\uD83C\uDDF1\uD83C\uDDF0", + [":flag_lr:"] = "\uD83C\uDDF1\uD83C\uDDF7", + [":flag_ls:"] = "\uD83C\uDDF1\uD83C\uDDF8", + [":flag_lt:"] = "\uD83C\uDDF1\uD83C\uDDF9", + [":flag_lu:"] = "\uD83C\uDDF1\uD83C\uDDFA", + [":flag_lv:"] = "\uD83C\uDDF1\uD83C\uDDFB", + [":flag_ly:"] = "\uD83C\uDDF1\uD83C\uDDFE", + [":flag_ma:"] = "\uD83C\uDDF2\uD83C\uDDE6", + [":flag_mc:"] = "\uD83C\uDDF2\uD83C\uDDE8", + [":flag_md:"] = "\uD83C\uDDF2\uD83C\uDDE9", + [":flag_me:"] = "\uD83C\uDDF2\uD83C\uDDEA", + [":flag_mf:"] = "\uD83C\uDDF2\uD83C\uDDEB", + [":flag_mg:"] = "\uD83C\uDDF2\uD83C\uDDEC", + [":flag_mh:"] = "\uD83C\uDDF2\uD83C\uDDED", + [":flag_mk:"] = "\uD83C\uDDF2\uD83C\uDDF0", + [":flag_ml:"] = "\uD83C\uDDF2\uD83C\uDDF1", + [":flag_mm:"] = "\uD83C\uDDF2\uD83C\uDDF2", + [":flag_mn:"] = "\uD83C\uDDF2\uD83C\uDDF3", + [":flag_mo:"] = "\uD83C\uDDF2\uD83C\uDDF4", + [":flag_mp:"] = "\uD83C\uDDF2\uD83C\uDDF5", + [":flag_mq:"] = "\uD83C\uDDF2\uD83C\uDDF6", + [":flag_mr:"] = "\uD83C\uDDF2\uD83C\uDDF7", + [":flag_ms:"] = "\uD83C\uDDF2\uD83C\uDDF8", + [":flag_mt:"] = "\uD83C\uDDF2\uD83C\uDDF9", + [":flag_mu:"] = "\uD83C\uDDF2\uD83C\uDDFA", + [":flag_mv:"] = "\uD83C\uDDF2\uD83C\uDDFB", + [":flag_mw:"] = "\uD83C\uDDF2\uD83C\uDDFC", + [":flag_mx:"] = "\uD83C\uDDF2\uD83C\uDDFD", + [":flag_my:"] = "\uD83C\uDDF2\uD83C\uDDFE", + [":flag_mz:"] = "\uD83C\uDDF2\uD83C\uDDFF", + [":flag_na:"] = "\uD83C\uDDF3\uD83C\uDDE6", + [":flag_nc:"] = "\uD83C\uDDF3\uD83C\uDDE8", + [":flag_ne:"] = "\uD83C\uDDF3\uD83C\uDDEA", + [":flag_nf:"] = "\uD83C\uDDF3\uD83C\uDDEB", + [":flag_ng:"] = "\uD83C\uDDF3\uD83C\uDDEC", + [":flag_ni:"] = "\uD83C\uDDF3\uD83C\uDDEE", + [":flag_nl:"] = "\uD83C\uDDF3\uD83C\uDDF1", + [":flag_no:"] = "\uD83C\uDDF3\uD83C\uDDF4", + [":flag_np:"] = "\uD83C\uDDF3\uD83C\uDDF5", + [":flag_nr:"] = "\uD83C\uDDF3\uD83C\uDDF7", + [":flag_nu:"] = "\uD83C\uDDF3\uD83C\uDDFA", + [":flag_nz:"] = "\uD83C\uDDF3\uD83C\uDDFF", + [":flag_om:"] = "\uD83C\uDDF4\uD83C\uDDF2", + [":flag_pa:"] = "\uD83C\uDDF5\uD83C\uDDE6", + [":flag_pe:"] = "\uD83C\uDDF5\uD83C\uDDEA", + [":flag_pf:"] = "\uD83C\uDDF5\uD83C\uDDEB", + [":flag_pg:"] = "\uD83C\uDDF5\uD83C\uDDEC", + [":flag_ph:"] = "\uD83C\uDDF5\uD83C\uDDED", + [":flag_pk:"] = "\uD83C\uDDF5\uD83C\uDDF0", + [":flag_pl:"] = "\uD83C\uDDF5\uD83C\uDDF1", + [":flag_pm:"] = "\uD83C\uDDF5\uD83C\uDDF2", + [":flag_pn:"] = "\uD83C\uDDF5\uD83C\uDDF3", + [":flag_pr:"] = "\uD83C\uDDF5\uD83C\uDDF7", + [":flag_ps:"] = "\uD83C\uDDF5\uD83C\uDDF8", + [":flag_pt:"] = "\uD83C\uDDF5\uD83C\uDDF9", + [":flag_pw:"] = "\uD83C\uDDF5\uD83C\uDDFC", + [":flag_py:"] = "\uD83C\uDDF5\uD83C\uDDFE", + [":flag_qa:"] = "\uD83C\uDDF6\uD83C\uDDE6", + [":flag_re:"] = "\uD83C\uDDF7\uD83C\uDDEA", + [":flag_ro:"] = "\uD83C\uDDF7\uD83C\uDDF4", + [":flag_rs:"] = "\uD83C\uDDF7\uD83C\uDDF8", + [":flag_ru:"] = "\uD83C\uDDF7\uD83C\uDDFA", + [":flag_rw:"] = "\uD83C\uDDF7\uD83C\uDDFC", + [":flag_sa:"] = "\uD83C\uDDF8\uD83C\uDDE6", + [":flag_sb:"] = "\uD83C\uDDF8\uD83C\uDDE7", + [":flag_sc:"] = "\uD83C\uDDF8\uD83C\uDDE8", + [":flag_sd:"] = "\uD83C\uDDF8\uD83C\uDDE9", + [":flag_se:"] = "\uD83C\uDDF8\uD83C\uDDEA", + [":flag_sg:"] = "\uD83C\uDDF8\uD83C\uDDEC", + [":flag_sh:"] = "\uD83C\uDDF8\uD83C\uDDED", + [":flag_si:"] = "\uD83C\uDDF8\uD83C\uDDEE", + [":flag_sj:"] = "\uD83C\uDDF8\uD83C\uDDEF", + [":flag_sk:"] = "\uD83C\uDDF8\uD83C\uDDF0", + [":flag_sl:"] = "\uD83C\uDDF8\uD83C\uDDF1", + [":flag_sm:"] = "\uD83C\uDDF8\uD83C\uDDF2", + [":flag_sn:"] = "\uD83C\uDDF8\uD83C\uDDF3", + [":flag_so:"] = "\uD83C\uDDF8\uD83C\uDDF4", + [":flag_sr:"] = "\uD83C\uDDF8\uD83C\uDDF7", + [":flag_ss:"] = "\uD83C\uDDF8\uD83C\uDDF8", + [":flag_st:"] = "\uD83C\uDDF8\uD83C\uDDF9", + [":flag_sv:"] = "\uD83C\uDDF8\uD83C\uDDFB", + [":flag_sx:"] = "\uD83C\uDDF8\uD83C\uDDFD", + [":flag_sy:"] = "\uD83C\uDDF8\uD83C\uDDFE", + [":flag_sz:"] = "\uD83C\uDDF8\uD83C\uDDFF", + [":flag_ta:"] = "\uD83C\uDDF9\uD83C\uDDE6", + [":flag_tc:"] = "\uD83C\uDDF9\uD83C\uDDE8", + [":flag_td:"] = "\uD83C\uDDF9\uD83C\uDDE9", + [":flag_tf:"] = "\uD83C\uDDF9\uD83C\uDDEB", + [":flag_tg:"] = "\uD83C\uDDF9\uD83C\uDDEC", + [":flag_th:"] = "\uD83C\uDDF9\uD83C\uDDED", + [":flag_tj:"] = "\uD83C\uDDF9\uD83C\uDDEF", + [":flag_tk:"] = "\uD83C\uDDF9\uD83C\uDDF0", + [":flag_tl:"] = "\uD83C\uDDF9\uD83C\uDDF1", + [":flag_tm:"] = "\uD83C\uDDF9\uD83C\uDDF2", + [":flag_tn:"] = "\uD83C\uDDF9\uD83C\uDDF3", + [":flag_to:"] = "\uD83C\uDDF9\uD83C\uDDF4", + [":flag_tr:"] = "\uD83C\uDDF9\uD83C\uDDF7", + [":flag_tt:"] = "\uD83C\uDDF9\uD83C\uDDF9", + [":flag_tv:"] = "\uD83C\uDDF9\uD83C\uDDFB", + [":flag_tw:"] = "\uD83C\uDDF9\uD83C\uDDFC", + [":flag_tz:"] = "\uD83C\uDDF9\uD83C\uDDFF", + [":flag_ua:"] = "\uD83C\uDDFA\uD83C\uDDE6", + [":flag_ug:"] = "\uD83C\uDDFA\uD83C\uDDEC", + [":flag_um:"] = "\uD83C\uDDFA\uD83C\uDDF2", + [":flag_us:"] = "\uD83C\uDDFA\uD83C\uDDF8", + [":flag_uy:"] = "\uD83C\uDDFA\uD83C\uDDFE", + [":flag_uz:"] = "\uD83C\uDDFA\uD83C\uDDFF", + [":flag_va:"] = "\uD83C\uDDFB\uD83C\uDDE6", + [":flag_vc:"] = "\uD83C\uDDFB\uD83C\uDDE8", + [":flag_ve:"] = "\uD83C\uDDFB\uD83C\uDDEA", + [":flag_vg:"] = "\uD83C\uDDFB\uD83C\uDDEC", + [":flag_vi:"] = "\uD83C\uDDFB\uD83C\uDDEE", + [":flag_vn:"] = "\uD83C\uDDFB\uD83C\uDDF3", + [":flag_vu:"] = "\uD83C\uDDFB\uD83C\uDDFA", + [":flag_wf:"] = "\uD83C\uDDFC\uD83C\uDDEB", + [":flag_white:"] = "\uD83C\uDFF3️", + [":flag_ws:"] = "\uD83C\uDDFC\uD83C\uDDF8", + [":flag_xk:"] = "\uD83C\uDDFD\uD83C\uDDF0", + [":flag_ye:"] = "\uD83C\uDDFE\uD83C\uDDEA", + [":flag_yt:"] = "\uD83C\uDDFE\uD83C\uDDF9", + [":flag_za:"] = "\uD83C\uDDFF\uD83C\uDDE6", + [":flag_zm:"] = "\uD83C\uDDFF\uD83C\uDDF2", + [":flag_zw:"] = "\uD83C\uDDFF\uD83C\uDDFC", + [":flags:"] = "\uD83C\uDF8F", + [":flame:"] = "\uD83D\uDD25", + [":flamingo:"] = "\uD83E\uDDA9", + [":flan:"] = "\uD83C\uDF6E", + [":flashlight:"] = "\uD83D\uDD26", + [":fleur_de_lis:"] = "⚜️", + [":floppy_disk:"] = "\uD83D\uDCBE", + [":flower_playing_cards:"] = "\uD83C\uDFB4", + [":flushed:"] = "\uD83D\uDE33", + [":flying_disc:"] = "\uD83E\uDD4F", + [":flying_saucer:"] = "\uD83D\uDEF8", + [":fog:"] = "\uD83C\uDF2B️", + [":foggy:"] = "\uD83C\uDF01", + [":foot:"] = "\uD83E\uDDB6", + [":foot::skin-tone-1:"] = "\uD83E\uDDB6\uD83C\uDFFB", + [":foot::skin-tone-2:"] = "\uD83E\uDDB6\uD83C\uDFFC", + [":foot::skin-tone-3:"] = "\uD83E\uDDB6\uD83C\uDFFD", + [":foot::skin-tone-4:"] = "\uD83E\uDDB6\uD83C\uDFFE", + [":foot::skin-tone-5:"] = "\uD83E\uDDB6\uD83C\uDFFF", + [":foot_dark_skin_tone:"] = "\uD83E\uDDB6\uD83C\uDFFF", + [":foot_light_skin_tone:"] = "\uD83E\uDDB6\uD83C\uDFFB", + [":foot_medium_dark_skin_tone:"] = "\uD83E\uDDB6\uD83C\uDFFE", + [":foot_medium_light_skin_tone:"] = "\uD83E\uDDB6\uD83C\uDFFC", + [":foot_medium_skin_tone:"] = "\uD83E\uDDB6\uD83C\uDFFD", + [":foot_tone1:"] = "\uD83E\uDDB6\uD83C\uDFFB", + [":foot_tone2:"] = "\uD83E\uDDB6\uD83C\uDFFC", + [":foot_tone3:"] = "\uD83E\uDDB6\uD83C\uDFFD", + [":foot_tone4:"] = "\uD83E\uDDB6\uD83C\uDFFE", + [":foot_tone5:"] = "\uD83E\uDDB6\uD83C\uDFFF", + [":football:"] = "\uD83C\uDFC8", + [":footprints:"] = "\uD83D\uDC63", + [":fork_and_knife:"] = "\uD83C\uDF74", + [":fork_and_knife_with_plate:"] = "\uD83C\uDF7D️", + [":fork_knife_plate:"] = "\uD83C\uDF7D️", + [":fortune_cookie:"] = "\uD83E\uDD60", + [":fountain:"] = "⛲", + [":four:"] = "4️⃣", + [":four_leaf_clover:"] = "\uD83C\uDF40", + [":fox:"] = "\uD83E\uDD8A", + [":fox_face:"] = "\uD83E\uDD8A", + [":frame_photo:"] = "\uD83D\uDDBC️", + [":frame_with_picture:"] = "\uD83D\uDDBC️", + [":free:"] = "\uD83C\uDD93", + [":french_bread:"] = "\uD83E\uDD56", + [":fried_shrimp:"] = "\uD83C\uDF64", + [":fries:"] = "\uD83C\uDF5F", + [":frog:"] = "\uD83D\uDC38", + [":frowning2:"] = "☹️", + [":frowning:"] = "\uD83D\uDE26", + [":fuelpump:"] = "⛽", + [":full_moon:"] = "\uD83C\uDF15", + [":full_moon_with_face:"] = "\uD83C\uDF1D", + [":funeral_urn:"] = "⚱️", + [":game_die:"] = "\uD83C\uDFB2", + [":garlic:"] = "\uD83E\uDDC4", + [":gay_pride_flag:"] = "\uD83C\uDFF3️\u200D\uD83C\uDF08", + [":gear:"] = "⚙️", + [":gem:"] = "\uD83D\uDC8E", + [":gemini:"] = "♊", + [":genie:"] = "\uD83E\uDDDE", + [":ghost:"] = "\uD83D\uDC7B", + [":gift:"] = "\uD83C\uDF81", + [":gift_heart:"] = "\uD83D\uDC9D", + [":giraffe:"] = "\uD83E\uDD92", + [":girl:"] = "\uD83D\uDC67", + [":girl::skin-tone-1:"] = "\uD83D\uDC67\uD83C\uDFFB", + [":girl::skin-tone-2:"] = "\uD83D\uDC67\uD83C\uDFFC", + [":girl::skin-tone-3:"] = "\uD83D\uDC67\uD83C\uDFFD", + [":girl::skin-tone-4:"] = "\uD83D\uDC67\uD83C\uDFFE", + [":girl::skin-tone-5:"] = "\uD83D\uDC67\uD83C\uDFFF", + [":girl_tone1:"] = "\uD83D\uDC67\uD83C\uDFFB", + [":girl_tone2:"] = "\uD83D\uDC67\uD83C\uDFFC", + [":girl_tone3:"] = "\uD83D\uDC67\uD83C\uDFFD", + [":girl_tone4:"] = "\uD83D\uDC67\uD83C\uDFFE", + [":girl_tone5:"] = "\uD83D\uDC67\uD83C\uDFFF", + [":glass_of_milk:"] = "\uD83E\uDD5B", + [":globe_with_meridians:"] = "\uD83C\uDF10", + [":gloves:"] = "\uD83E\uDDE4", + [":goal:"] = "\uD83E\uDD45", + [":goal_net:"] = "\uD83E\uDD45", + [":goat:"] = "\uD83D\uDC10", + [":goggles:"] = "\uD83E\uDD7D", + [":golf:"] = "⛳", + [":golfer:"] = "\uD83C\uDFCC️", + [":golfer::skin-tone-1:"] = "\uD83C\uDFCC\uD83C\uDFFB", + [":golfer::skin-tone-2:"] = "\uD83C\uDFCC\uD83C\uDFFC", + [":golfer::skin-tone-3:"] = "\uD83C\uDFCC\uD83C\uDFFD", + [":golfer::skin-tone-4:"] = "\uD83C\uDFCC\uD83C\uDFFE", + [":golfer::skin-tone-5:"] = "\uD83C\uDFCC\uD83C\uDFFF", + [":gorilla:"] = "\uD83E\uDD8D", + [":grandma:"] = "\uD83D\uDC75", + [":grandma::skin-tone-1:"] = "\uD83D\uDC75\uD83C\uDFFB", + [":grandma::skin-tone-2:"] = "\uD83D\uDC75\uD83C\uDFFC", + [":grandma::skin-tone-3:"] = "\uD83D\uDC75\uD83C\uDFFD", + [":grandma::skin-tone-4:"] = "\uD83D\uDC75\uD83C\uDFFE", + [":grandma::skin-tone-5:"] = "\uD83D\uDC75\uD83C\uDFFF", + [":grandma_tone1:"] = "\uD83D\uDC75\uD83C\uDFFB", + [":grandma_tone2:"] = "\uD83D\uDC75\uD83C\uDFFC", + [":grandma_tone3:"] = "\uD83D\uDC75\uD83C\uDFFD", + [":grandma_tone4:"] = "\uD83D\uDC75\uD83C\uDFFE", + [":grandma_tone5:"] = "\uD83D\uDC75\uD83C\uDFFF", + [":grapes:"] = "\uD83C\uDF47", + [":green_apple:"] = "\uD83C\uDF4F", + [":green_book:"] = "\uD83D\uDCD7", + [":green_circle:"] = "\uD83D\uDFE2", + [":green_heart:"] = "\uD83D\uDC9A", + [":green_salad:"] = "\uD83E\uDD57", + [":green_square:"] = "\uD83D\uDFE9", + [":grey_exclamation:"] = "❕", + [":grey_question:"] = "❔", + [":grimacing:"] = "\uD83D\uDE2C", + [":grin:"] = "\uD83D\uDE01", + [":grinning:"] = "\uD83D\uDE00", + [":guard:"] = "\uD83D\uDC82", + [":guard::skin-tone-1:"] = "\uD83D\uDC82\uD83C\uDFFB", + [":guard::skin-tone-2:"] = "\uD83D\uDC82\uD83C\uDFFC", + [":guard::skin-tone-3:"] = "\uD83D\uDC82\uD83C\uDFFD", + [":guard::skin-tone-4:"] = "\uD83D\uDC82\uD83C\uDFFE", + [":guard::skin-tone-5:"] = "\uD83D\uDC82\uD83C\uDFFF", + [":guard_tone1:"] = "\uD83D\uDC82\uD83C\uDFFB", + [":guard_tone2:"] = "\uD83D\uDC82\uD83C\uDFFC", + [":guard_tone3:"] = "\uD83D\uDC82\uD83C\uDFFD", + [":guard_tone4:"] = "\uD83D\uDC82\uD83C\uDFFE", + [":guard_tone5:"] = "\uD83D\uDC82\uD83C\uDFFF", + [":guardsman:"] = "\uD83D\uDC82", + [":guardsman::skin-tone-1:"] = "\uD83D\uDC82\uD83C\uDFFB", + [":guardsman::skin-tone-2:"] = "\uD83D\uDC82\uD83C\uDFFC", + [":guardsman::skin-tone-3:"] = "\uD83D\uDC82\uD83C\uDFFD", + [":guardsman::skin-tone-4:"] = "\uD83D\uDC82\uD83C\uDFFE", + [":guardsman::skin-tone-5:"] = "\uD83D\uDC82\uD83C\uDFFF", + [":guardsman_tone1:"] = "\uD83D\uDC82\uD83C\uDFFB", + [":guardsman_tone2:"] = "\uD83D\uDC82\uD83C\uDFFC", + [":guardsman_tone3:"] = "\uD83D\uDC82\uD83C\uDFFD", + [":guardsman_tone4:"] = "\uD83D\uDC82\uD83C\uDFFE", + [":guardsman_tone5:"] = "\uD83D\uDC82\uD83C\uDFFF", + [":guide_dog:"] = "\uD83E\uDDAE", + [":guitar:"] = "\uD83C\uDFB8", + [":gun:"] = "\uD83D\uDD2B", + [":haircut:"] = "\uD83D\uDC87", + [":haircut::skin-tone-1:"] = "\uD83D\uDC87\uD83C\uDFFB", + [":haircut::skin-tone-2:"] = "\uD83D\uDC87\uD83C\uDFFC", + [":haircut::skin-tone-3:"] = "\uD83D\uDC87\uD83C\uDFFD", + [":haircut::skin-tone-4:"] = "\uD83D\uDC87\uD83C\uDFFE", + [":haircut::skin-tone-5:"] = "\uD83D\uDC87\uD83C\uDFFF", + [":haircut_tone1:"] = "\uD83D\uDC87\uD83C\uDFFB", + [":haircut_tone2:"] = "\uD83D\uDC87\uD83C\uDFFC", + [":haircut_tone3:"] = "\uD83D\uDC87\uD83C\uDFFD", + [":haircut_tone4:"] = "\uD83D\uDC87\uD83C\uDFFE", + [":haircut_tone5:"] = "\uD83D\uDC87\uD83C\uDFFF", + [":hamburger:"] = "\uD83C\uDF54", + [":hammer:"] = "\uD83D\uDD28", + [":hammer_and_pick:"] = "⚒️", + [":hammer_and_wrench:"] = "\uD83D\uDEE0️", + [":hammer_pick:"] = "⚒️", + [":hamster:"] = "\uD83D\uDC39", + [":hand_splayed:"] = "\uD83D\uDD90️", + [":hand_splayed::skin-tone-1:"] = "\uD83D\uDD90\uD83C\uDFFB", + [":hand_splayed::skin-tone-2:"] = "\uD83D\uDD90\uD83C\uDFFC", + [":hand_splayed::skin-tone-3:"] = "\uD83D\uDD90\uD83C\uDFFD", + [":hand_splayed::skin-tone-4:"] = "\uD83D\uDD90\uD83C\uDFFE", + [":hand_splayed::skin-tone-5:"] = "\uD83D\uDD90\uD83C\uDFFF", + [":hand_splayed_tone1:"] = "\uD83D\uDD90\uD83C\uDFFB", + [":hand_splayed_tone2:"] = "\uD83D\uDD90\uD83C\uDFFC", + [":hand_splayed_tone3:"] = "\uD83D\uDD90\uD83C\uDFFD", + [":hand_splayed_tone4:"] = "\uD83D\uDD90\uD83C\uDFFE", + [":hand_splayed_tone5:"] = "\uD83D\uDD90\uD83C\uDFFF", + [":hand_with_index_and_middle_finger_crossed:"] = "\uD83E\uDD1E", + [":hand_with_index_and_middle_finger_crossed::skin-tone-1:"] = "\uD83E\uDD1E\uD83C\uDFFB", + [":hand_with_index_and_middle_finger_crossed::skin-tone-2:"] = "\uD83E\uDD1E\uD83C\uDFFC", + [":hand_with_index_and_middle_finger_crossed::skin-tone-3:"] = "\uD83E\uDD1E\uD83C\uDFFD", + [":hand_with_index_and_middle_finger_crossed::skin-tone-4:"] = "\uD83E\uDD1E\uD83C\uDFFE", + [":hand_with_index_and_middle_finger_crossed::skin-tone-5:"] = "\uD83E\uDD1E\uD83C\uDFFF", + [":hand_with_index_and_middle_fingers_crossed_tone1:"] = "\uD83E\uDD1E\uD83C\uDFFB", + [":hand_with_index_and_middle_fingers_crossed_tone2:"] = "\uD83E\uDD1E\uD83C\uDFFC", + [":hand_with_index_and_middle_fingers_crossed_tone3:"] = "\uD83E\uDD1E\uD83C\uDFFD", + [":hand_with_index_and_middle_fingers_crossed_tone4:"] = "\uD83E\uDD1E\uD83C\uDFFE", + [":hand_with_index_and_middle_fingers_crossed_tone5:"] = "\uD83E\uDD1E\uD83C\uDFFF", + [":handbag:"] = "\uD83D\uDC5C", + [":handball:"] = "\uD83E\uDD3E", + [":handball::skin-tone-1:"] = "\uD83E\uDD3E\uD83C\uDFFB", + [":handball::skin-tone-2:"] = "\uD83E\uDD3E\uD83C\uDFFC", + [":handball::skin-tone-3:"] = "\uD83E\uDD3E\uD83C\uDFFD", + [":handball::skin-tone-4:"] = "\uD83E\uDD3E\uD83C\uDFFE", + [":handball::skin-tone-5:"] = "\uD83E\uDD3E\uD83C\uDFFF", + [":handball_tone1:"] = "\uD83E\uDD3E\uD83C\uDFFB", + [":handball_tone2:"] = "\uD83E\uDD3E\uD83C\uDFFC", + [":handball_tone3:"] = "\uD83E\uDD3E\uD83C\uDFFD", + [":handball_tone4:"] = "\uD83E\uDD3E\uD83C\uDFFE", + [":handball_tone5:"] = "\uD83E\uDD3E\uD83C\uDFFF", + [":handshake:"] = "\uD83E\uDD1D", + [":hankey:"] = "\uD83D\uDCA9", + [":hash:"] = "#️⃣", + [":hatched_chick:"] = "\uD83D\uDC25", + [":hatching_chick:"] = "\uD83D\uDC23", + [":head_bandage:"] = "\uD83E\uDD15", + [":headphones:"] = "\uD83C\uDFA7", + [":hear_no_evil:"] = "\uD83D\uDE49", + [":heart:"] = "❤️", + [":heart_decoration:"] = "\uD83D\uDC9F", + [":heart_exclamation:"] = "❣️", + [":heart_eyes:"] = "\uD83D\uDE0D", + [":heart_eyes_cat:"] = "\uD83D\uDE3B", + [":heartbeat:"] = "\uD83D\uDC93", + [":heartpulse:"] = "\uD83D\uDC97", + [":hearts:"] = "♥️", + [":heavy_check_mark:"] = "✔️", + [":heavy_division_sign:"] = "➗", + [":heavy_dollar_sign:"] = "\uD83D\uDCB2", + [":heavy_heart_exclamation_mark_ornament:"] = "❣️", + [":heavy_minus_sign:"] = "➖", + [":heavy_multiplication_x:"] = "✖️", + [":heavy_plus_sign:"] = "➕", + [":hedgehog:"] = "\uD83E\uDD94", + [":helicopter:"] = "\uD83D\uDE81", + [":helmet_with_cross:"] = "⛑️", + [":helmet_with_white_cross:"] = "⛑️", + [":herb:"] = "\uD83C\uDF3F", + [":hibiscus:"] = "\uD83C\uDF3A", + [":high_brightness:"] = "\uD83D\uDD06", + [":high_heel:"] = "\uD83D\uDC60", + [":hiking_boot:"] = "\uD83E\uDD7E", + [":hindu_temple:"] = "\uD83D\uDED5", + [":hippopotamus:"] = "\uD83E\uDD9B", + [":hockey:"] = "\uD83C\uDFD2", + [":hole:"] = "\uD83D\uDD73️", + [":homes:"] = "\uD83C\uDFD8️", + [":honey_pot:"] = "\uD83C\uDF6F", + [":horse:"] = "\uD83D\uDC34", + [":horse_racing:"] = "\uD83C\uDFC7", + [":horse_racing::skin-tone-1:"] = "\uD83C\uDFC7\uD83C\uDFFB", + [":horse_racing::skin-tone-2:"] = "\uD83C\uDFC7\uD83C\uDFFC", + [":horse_racing::skin-tone-3:"] = "\uD83C\uDFC7\uD83C\uDFFD", + [":horse_racing::skin-tone-4:"] = "\uD83C\uDFC7\uD83C\uDFFE", + [":horse_racing::skin-tone-5:"] = "\uD83C\uDFC7\uD83C\uDFFF", + [":horse_racing_tone1:"] = "\uD83C\uDFC7\uD83C\uDFFB", + [":horse_racing_tone2:"] = "\uD83C\uDFC7\uD83C\uDFFC", + [":horse_racing_tone3:"] = "\uD83C\uDFC7\uD83C\uDFFD", + [":horse_racing_tone4:"] = "\uD83C\uDFC7\uD83C\uDFFE", + [":horse_racing_tone5:"] = "\uD83C\uDFC7\uD83C\uDFFF", + [":hospital:"] = "\uD83C\uDFE5", + [":hot_dog:"] = "\uD83C\uDF2D", + [":hot_face:"] = "\uD83E\uDD75", + [":hot_pepper:"] = "\uD83C\uDF36️", + [":hotdog:"] = "\uD83C\uDF2D", + [":hotel:"] = "\uD83C\uDFE8", + [":hotsprings:"] = "♨️", + [":hourglass:"] = "⌛", + [":hourglass_flowing_sand:"] = "⏳", + [":house:"] = "\uD83C\uDFE0", + [":house_abandoned:"] = "\uD83C\uDFDA️", + [":house_buildings:"] = "\uD83C\uDFD8️", + [":house_with_garden:"] = "\uD83C\uDFE1", + [":hugging:"] = "\uD83E\uDD17", + [":hugging_face:"] = "\uD83E\uDD17", + [":hushed:"] = "\uD83D\uDE2F", + [":ice_cream:"] = "\uD83C\uDF68", + [":ice_cube:"] = "\uD83E\uDDCA", + [":ice_skate:"] = "⛸️", + [":icecream:"] = "\uD83C\uDF66", + [":id:"] = "\uD83C\uDD94", + [":ideograph_advantage:"] = "\uD83C\uDE50", + [":imp:"] = "\uD83D\uDC7F", + [":inbox_tray:"] = "\uD83D\uDCE5", + [":incoming_envelope:"] = "\uD83D\uDCE8", + [":infinity:"] = "♾️", + [":information_desk_person:"] = "\uD83D\uDC81", + [":information_desk_person::skin-tone-1:"] = "\uD83D\uDC81\uD83C\uDFFB", + [":information_desk_person::skin-tone-2:"] = "\uD83D\uDC81\uD83C\uDFFC", + [":information_desk_person::skin-tone-3:"] = "\uD83D\uDC81\uD83C\uDFFD", + [":information_desk_person::skin-tone-4:"] = "\uD83D\uDC81\uD83C\uDFFE", + [":information_desk_person::skin-tone-5:"] = "\uD83D\uDC81\uD83C\uDFFF", + [":information_desk_person_tone1:"] = "\uD83D\uDC81\uD83C\uDFFB", + [":information_desk_person_tone2:"] = "\uD83D\uDC81\uD83C\uDFFC", + [":information_desk_person_tone3:"] = "\uD83D\uDC81\uD83C\uDFFD", + [":information_desk_person_tone4:"] = "\uD83D\uDC81\uD83C\uDFFE", + [":information_desk_person_tone5:"] = "\uD83D\uDC81\uD83C\uDFFF", + [":information_source:"] = "ℹ️", + [":innocent:"] = "\uD83D\uDE07", + [":interrobang:"] = "⁉️", + [":iphone:"] = "\uD83D\uDCF1", + [":island:"] = "\uD83C\uDFDD️", + [":izakaya_lantern:"] = "\uD83C\uDFEE", + [":jack_o_lantern:"] = "\uD83C\uDF83", + [":japan:"] = "\uD83D\uDDFE", + [":japanese_castle:"] = "\uD83C\uDFEF", + [":japanese_goblin:"] = "\uD83D\uDC7A", + [":japanese_ogre:"] = "\uD83D\uDC79", + [":jeans:"] = "\uD83D\uDC56", + [":jigsaw:"] = "\uD83E\uDDE9", + [":joy:"] = "\uD83D\uDE02", + [":joy_cat:"] = "\uD83D\uDE39", + [":joystick:"] = "\uD83D\uDD79️", + [":juggler:"] = "\uD83E\uDD39", + [":juggler::skin-tone-1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":juggler::skin-tone-2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":juggler::skin-tone-3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":juggler::skin-tone-4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":juggler::skin-tone-5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":juggler_tone1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":juggler_tone2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":juggler_tone3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":juggler_tone4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":juggler_tone5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":juggling:"] = "\uD83E\uDD39", + [":juggling::skin-tone-1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":juggling::skin-tone-2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":juggling::skin-tone-3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":juggling::skin-tone-4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":juggling::skin-tone-5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":juggling_tone1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":juggling_tone2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":juggling_tone3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":juggling_tone4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":juggling_tone5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":kaaba:"] = "\uD83D\uDD4B", + [":kangaroo:"] = "\uD83E\uDD98", + [":karate_uniform:"] = "\uD83E\uDD4B", + [":kayak:"] = "\uD83D\uDEF6", + [":key2:"] = "\uD83D\uDDDD️", + [":key:"] = "\uD83D\uDD11", + [":keyboard:"] = "⌨️", + [":keycap_asterisk:"] = "*️⃣", + [":keycap_ten:"] = "\uD83D\uDD1F", + [":kimono:"] = "\uD83D\uDC58", + [":kiss:"] = "\uD83D\uDC8B", + [":kiss_mm:"] = "\uD83D\uDC68\u200D❤️\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", + [":kiss_woman_man:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", + [":kiss_ww:"] = "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC8B\u200D\uD83D\uDC69", + [":kissing:"] = "\uD83D\uDE17", + [":kissing_cat:"] = "\uD83D\uDE3D", + [":kissing_closed_eyes:"] = "\uD83D\uDE1A", + [":kissing_heart:"] = "\uD83D\uDE18", + [":kissing_smiling_eyes:"] = "\uD83D\uDE19", + [":kite:"] = "\uD83E\uDE81", + [":kiwi:"] = "\uD83E\uDD5D", + [":kiwifruit:"] = "\uD83E\uDD5D", + [":knife:"] = "\uD83D\uDD2A", + [":koala:"] = "\uD83D\uDC28", + [":koko:"] = "\uD83C\uDE01", + [":knot:"] = "\uD83E\uDEA2", + [":lab_coat:"] = "\uD83E\uDD7C", + [":label:"] = "\uD83C\uDFF7️", + [":lacrosse:"] = "\uD83E\uDD4D", + [":large_blue_diamond:"] = "\uD83D\uDD37", + [":large_orange_diamond:"] = "\uD83D\uDD36", + [":last_quarter_moon:"] = "\uD83C\uDF17", + [":last_quarter_moon_with_face:"] = "\uD83C\uDF1C", + [":latin_cross:"] = "✝️", + [":laughing:"] = "\uD83D\uDE06", + [":leafy_green:"] = "\uD83E\uDD6C", + [":leaves:"] = "\uD83C\uDF43", + [":ledger:"] = "\uD83D\uDCD2", + [":left_facing_fist:"] = "\uD83E\uDD1B", + [":left_facing_fist::skin-tone-1:"] = "\uD83E\uDD1B\uD83C\uDFFB", + [":left_facing_fist::skin-tone-2:"] = "\uD83E\uDD1B\uD83C\uDFFC", + [":left_facing_fist::skin-tone-3:"] = "\uD83E\uDD1B\uD83C\uDFFD", + [":left_facing_fist::skin-tone-4:"] = "\uD83E\uDD1B\uD83C\uDFFE", + [":left_facing_fist::skin-tone-5:"] = "\uD83E\uDD1B\uD83C\uDFFF", + [":left_facing_fist_tone1:"] = "\uD83E\uDD1B\uD83C\uDFFB", + [":left_facing_fist_tone2:"] = "\uD83E\uDD1B\uD83C\uDFFC", + [":left_facing_fist_tone3:"] = "\uD83E\uDD1B\uD83C\uDFFD", + [":left_facing_fist_tone4:"] = "\uD83E\uDD1B\uD83C\uDFFE", + [":left_facing_fist_tone5:"] = "\uD83E\uDD1B\uD83C\uDFFF", + [":left_fist:"] = "\uD83E\uDD1B", + [":left_fist::skin-tone-1:"] = "\uD83E\uDD1B\uD83C\uDFFB", + [":left_fist::skin-tone-2:"] = "\uD83E\uDD1B\uD83C\uDFFC", + [":left_fist::skin-tone-3:"] = "\uD83E\uDD1B\uD83C\uDFFD", + [":left_fist::skin-tone-4:"] = "\uD83E\uDD1B\uD83C\uDFFE", + [":left_fist::skin-tone-5:"] = "\uD83E\uDD1B\uD83C\uDFFF", + [":left_fist_tone1:"] = "\uD83E\uDD1B\uD83C\uDFFB", + [":left_fist_tone2:"] = "\uD83E\uDD1B\uD83C\uDFFC", + [":left_fist_tone3:"] = "\uD83E\uDD1B\uD83C\uDFFD", + [":left_fist_tone4:"] = "\uD83E\uDD1B\uD83C\uDFFE", + [":left_fist_tone5:"] = "\uD83E\uDD1B\uD83C\uDFFF", + [":left_luggage:"] = "\uD83D\uDEC5", + [":left_right_arrow:"] = "↔️", + [":left_speech_bubble:"] = "\uD83D\uDDE8️", + [":leftwards_arrow_with_hook:"] = "↩️", + [":leg:"] = "\uD83E\uDDB5", + [":leg::skin-tone-1:"] = "\uD83E\uDDB5\uD83C\uDFFB", + [":leg::skin-tone-2:"] = "\uD83E\uDDB5\uD83C\uDFFC", + [":leg::skin-tone-3:"] = "\uD83E\uDDB5\uD83C\uDFFD", + [":leg::skin-tone-4:"] = "\uD83E\uDDB5\uD83C\uDFFE", + [":leg::skin-tone-5:"] = "\uD83E\uDDB5\uD83C\uDFFF", + [":leg_dark_skin_tone:"] = "\uD83E\uDDB5\uD83C\uDFFF", + [":leg_light_skin_tone:"] = "\uD83E\uDDB5\uD83C\uDFFB", + [":leg_medium_dark_skin_tone:"] = "\uD83E\uDDB5\uD83C\uDFFE", + [":leg_medium_light_skin_tone:"] = "\uD83E\uDDB5\uD83C\uDFFC", + [":leg_medium_skin_tone:"] = "\uD83E\uDDB5\uD83C\uDFFD", + [":leg_tone1:"] = "\uD83E\uDDB5\uD83C\uDFFB", + [":leg_tone2:"] = "\uD83E\uDDB5\uD83C\uDFFC", + [":leg_tone3:"] = "\uD83E\uDDB5\uD83C\uDFFD", + [":leg_tone4:"] = "\uD83E\uDDB5\uD83C\uDFFE", + [":leg_tone5:"] = "\uD83E\uDDB5\uD83C\uDFFF", + [":lemon:"] = "\uD83C\uDF4B", + [":leo:"] = "♌", + [":leopard:"] = "\uD83D\uDC06", + [":level_slider:"] = "\uD83C\uDF9A️", + [":levitate:"] = "\uD83D\uDD74️", + [":levitate::skin-tone-1:"] = "\uD83D\uDD74\uD83C\uDFFB", + [":levitate::skin-tone-2:"] = "\uD83D\uDD74\uD83C\uDFFC", + [":levitate::skin-tone-3:"] = "\uD83D\uDD74\uD83C\uDFFD", + [":levitate::skin-tone-4:"] = "\uD83D\uDD74\uD83C\uDFFE", + [":levitate::skin-tone-5:"] = "\uD83D\uDD74\uD83C\uDFFF", + [":levitate_tone1:"] = "\uD83D\uDD74\uD83C\uDFFB", + [":levitate_tone2:"] = "\uD83D\uDD74\uD83C\uDFFC", + [":levitate_tone3:"] = "\uD83D\uDD74\uD83C\uDFFD", + [":levitate_tone4:"] = "\uD83D\uDD74\uD83C\uDFFE", + [":levitate_tone5:"] = "\uD83D\uDD74\uD83C\uDFFF", + [":liar:"] = "\uD83E\uDD25", + [":libra:"] = "♎", + [":lifter:"] = "\uD83C\uDFCB️", + [":lifter::skin-tone-1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":lifter::skin-tone-2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":lifter::skin-tone-3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":lifter::skin-tone-4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":lifter::skin-tone-5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":lifter_tone1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":lifter_tone2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":lifter_tone3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":lifter_tone4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":lifter_tone5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":light_rail:"] = "\uD83D\uDE88", + [":link:"] = "\uD83D\uDD17", + [":linked_paperclips:"] = "\uD83D\uDD87️", + [":lion:"] = "\uD83E\uDD81", + [":lion_face:"] = "\uD83E\uDD81", + [":lips:"] = "\uD83D\uDC44", + [":lipstick:"] = "\uD83D\uDC84", + [":lizard:"] = "\uD83E\uDD8E", + [":llama:"] = "\uD83E\uDD99", + [":lobster:"] = "\uD83E\uDD9E", + [":lock:"] = "\uD83D\uDD12", + [":lock_with_ink_pen:"] = "\uD83D\uDD0F", + [":lollipop:"] = "\uD83C\uDF6D", + [":loop:"] = "➿", + [":loud_sound:"] = "\uD83D\uDD0A", + [":loudspeaker:"] = "\uD83D\uDCE2", + [":love_hotel:"] = "\uD83C\uDFE9", + [":love_letter:"] = "\uD83D\uDC8C", + [":love_you_gesture:"] = "\uD83E\uDD1F", + [":love_you_gesture::skin-tone-1:"] = "\uD83E\uDD1F\uD83C\uDFFB", + [":love_you_gesture::skin-tone-2:"] = "\uD83E\uDD1F\uD83C\uDFFC", + [":love_you_gesture::skin-tone-3:"] = "\uD83E\uDD1F\uD83C\uDFFD", + [":love_you_gesture::skin-tone-4:"] = "\uD83E\uDD1F\uD83C\uDFFE", + [":love_you_gesture::skin-tone-5:"] = "\uD83E\uDD1F\uD83C\uDFFF", + [":love_you_gesture_dark_skin_tone:"] = "\uD83E\uDD1F\uD83C\uDFFF", + [":love_you_gesture_light_skin_tone:"] = "\uD83E\uDD1F\uD83C\uDFFB", + [":love_you_gesture_medium_dark_skin_tone:"] = "\uD83E\uDD1F\uD83C\uDFFE", + [":love_you_gesture_medium_light_skin_tone:"] = "\uD83E\uDD1F\uD83C\uDFFC", + [":love_you_gesture_medium_skin_tone:"] = "\uD83E\uDD1F\uD83C\uDFFD", + [":love_you_gesture_tone1:"] = "\uD83E\uDD1F\uD83C\uDFFB", + [":love_you_gesture_tone2:"] = "\uD83E\uDD1F\uD83C\uDFFC", + [":love_you_gesture_tone3:"] = "\uD83E\uDD1F\uD83C\uDFFD", + [":love_you_gesture_tone4:"] = "\uD83E\uDD1F\uD83C\uDFFE", + [":love_you_gesture_tone5:"] = "\uD83E\uDD1F\uD83C\uDFFF", + [":low_brightness:"] = "\uD83D\uDD05", + [":lower_left_ballpoint_pen:"] = "\uD83D\uDD8A️", + [":lower_left_crayon:"] = "\uD83D\uDD8D️", + [":lower_left_fountain_pen:"] = "\uD83D\uDD8B️", + [":lower_left_paintbrush:"] = "\uD83D\uDD8C️", + [":luggage:"] = "\uD83E\uDDF3", + [":lying_face:"] = "\uD83E\uDD25", + [":m:"] = "Ⓜ️", + [":mag:"] = "\uD83D\uDD0D", + [":mag_right:"] = "\uD83D\uDD0E", + [":mage:"] = "\uD83E\uDDD9", + [":mage::skin-tone-1:"] = "\uD83E\uDDD9\uD83C\uDFFB", + [":mage::skin-tone-2:"] = "\uD83E\uDDD9\uD83C\uDFFC", + [":mage::skin-tone-3:"] = "\uD83E\uDDD9\uD83C\uDFFD", + [":mage::skin-tone-4:"] = "\uD83E\uDDD9\uD83C\uDFFE", + [":mage::skin-tone-5:"] = "\uD83E\uDDD9\uD83C\uDFFF", + [":mage_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFF", + [":mage_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFB", + [":mage_medium_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFE", + [":mage_medium_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFC", + [":mage_medium_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFD", + [":mage_tone1:"] = "\uD83E\uDDD9\uD83C\uDFFB", + [":mage_tone2:"] = "\uD83E\uDDD9\uD83C\uDFFC", + [":mage_tone3:"] = "\uD83E\uDDD9\uD83C\uDFFD", + [":mage_tone4:"] = "\uD83E\uDDD9\uD83C\uDFFE", + [":mage_tone5:"] = "\uD83E\uDDD9\uD83C\uDFFF", + [":magnet:"] = "\uD83E\uDDF2", + [":mahjong:"] = "\uD83C\uDC04", + [":mailbox:"] = "\uD83D\uDCEB", + [":mailbox_closed:"] = "\uD83D\uDCEA", + [":mailbox_with_mail:"] = "\uD83D\uDCEC", + [":mailbox_with_no_mail:"] = "\uD83D\uDCED", + [":male_dancer:"] = "\uD83D\uDD7A", + [":male_dancer::skin-tone-1:"] = "\uD83D\uDD7A\uD83C\uDFFB", + [":male_dancer::skin-tone-2:"] = "\uD83D\uDD7A\uD83C\uDFFC", + [":male_dancer::skin-tone-3:"] = "\uD83D\uDD7A\uD83C\uDFFD", + [":male_dancer::skin-tone-4:"] = "\uD83D\uDD7A\uD83C\uDFFE", + [":male_dancer::skin-tone-5:"] = "\uD83D\uDD7A\uD83C\uDFFF", + [":male_dancer_tone1:"] = "\uD83D\uDD7A\uD83C\uDFFB", + [":male_dancer_tone2:"] = "\uD83D\uDD7A\uD83C\uDFFC", + [":male_dancer_tone3:"] = "\uD83D\uDD7A\uD83C\uDFFD", + [":male_dancer_tone4:"] = "\uD83D\uDD7A\uD83C\uDFFE", + [":male_dancer_tone5:"] = "\uD83D\uDD7A\uD83C\uDFFF", + [":male_sign:"] = "♂️", + [":man:"] = "\uD83D\uDC68", + [":man::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB", + [":man::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC", + [":man::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD", + [":man::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE", + [":man::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF", + [":man_artist:"] = "\uD83D\uDC68\u200D\uD83C\uDFA8", + [":man_artist::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":man_artist::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":man_artist::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":man_artist::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":man_artist::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":man_artist_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":man_artist_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":man_artist_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":man_artist_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":man_artist_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":man_artist_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":man_artist_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":man_artist_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":man_artist_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":man_artist_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":man_astronaut:"] = "\uD83D\uDC68\u200D\uD83D\uDE80", + [":man_astronaut::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE80", + [":man_astronaut::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE80", + [":man_astronaut::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE80", + [":man_astronaut::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE80", + [":man_astronaut::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE80", + [":man_astronaut_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE80", + [":man_astronaut_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE80", + [":man_astronaut_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE80", + [":man_astronaut_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE80", + [":man_astronaut_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE80", + [":man_astronaut_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE80", + [":man_astronaut_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE80", + [":man_astronaut_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE80", + [":man_astronaut_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE80", + [":man_astronaut_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE80", + [":man_bald:"] = "\uD83D\uDC68\u200D\uD83E\uDDB2", + [":man_bald::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":man_bald::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":man_bald::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":man_bald::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":man_bald::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":man_bald_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":man_bald_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":man_bald_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":man_bald_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":man_bald_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":man_bald_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":man_bald_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":man_bald_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":man_bald_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":man_bald_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":man_biking:"] = "\uD83D\uDEB4\u200D♂️", + [":man_biking::skin-tone-1:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♂️", + [":man_biking::skin-tone-2:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♂️", + [":man_biking::skin-tone-3:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♂️", + [":man_biking::skin-tone-4:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♂️", + [":man_biking::skin-tone-5:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♂️", + [":man_biking_dark_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♂️", + [":man_biking_light_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♂️", + [":man_biking_medium_dark_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♂️", + [":man_biking_medium_light_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♂️", + [":man_biking_medium_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♂️", + [":man_biking_tone1:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♂️", + [":man_biking_tone2:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♂️", + [":man_biking_tone3:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♂️", + [":man_biking_tone4:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♂️", + [":man_biking_tone5:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♂️", + [":man_bouncing_ball:"] = "⛹️\u200D♂️", + [":man_bouncing_ball::skin-tone-1:"] = "⛹\uD83C\uDFFB\u200D♂️", + [":man_bouncing_ball::skin-tone-2:"] = "⛹\uD83C\uDFFC\u200D♂️", + [":man_bouncing_ball::skin-tone-3:"] = "⛹\uD83C\uDFFD\u200D♂️", + [":man_bouncing_ball::skin-tone-4:"] = "⛹\uD83C\uDFFE\u200D♂️", + [":man_bouncing_ball::skin-tone-5:"] = "⛹\uD83C\uDFFF\u200D♂️", + [":man_bouncing_ball_dark_skin_tone:"] = "⛹\uD83C\uDFFF\u200D♂️", + [":man_bouncing_ball_light_skin_tone:"] = "⛹\uD83C\uDFFB\u200D♂️", + [":man_bouncing_ball_medium_dark_skin_tone:"] = "⛹\uD83C\uDFFE\u200D♂️", + [":man_bouncing_ball_medium_light_skin_tone:"] = "⛹\uD83C\uDFFC\u200D♂️", + [":man_bouncing_ball_medium_skin_tone:"] = "⛹\uD83C\uDFFD\u200D♂️", + [":man_bouncing_ball_tone1:"] = "⛹\uD83C\uDFFB\u200D♂️", + [":man_bouncing_ball_tone2:"] = "⛹\uD83C\uDFFC\u200D♂️", + [":man_bouncing_ball_tone3:"] = "⛹\uD83C\uDFFD\u200D♂️", + [":man_bouncing_ball_tone4:"] = "⛹\uD83C\uDFFE\u200D♂️", + [":man_bouncing_ball_tone5:"] = "⛹\uD83C\uDFFF\u200D♂️", + [":man_bowing:"] = "\uD83D\uDE47\u200D♂️", + [":man_bowing::skin-tone-1:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♂️", + [":man_bowing::skin-tone-2:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♂️", + [":man_bowing::skin-tone-3:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♂️", + [":man_bowing::skin-tone-4:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♂️", + [":man_bowing::skin-tone-5:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♂️", + [":man_bowing_dark_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♂️", + [":man_bowing_light_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♂️", + [":man_bowing_medium_dark_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♂️", + [":man_bowing_medium_light_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♂️", + [":man_bowing_medium_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♂️", + [":man_bowing_tone1:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♂️", + [":man_bowing_tone2:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♂️", + [":man_bowing_tone3:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♂️", + [":man_bowing_tone4:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♂️", + [":man_bowing_tone5:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♂️", + [":man_cartwheeling:"] = "\uD83E\uDD38\u200D♂️", + [":man_cartwheeling::skin-tone-1:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♂️", + [":man_cartwheeling::skin-tone-2:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♂️", + [":man_cartwheeling::skin-tone-3:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♂️", + [":man_cartwheeling::skin-tone-4:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♂️", + [":man_cartwheeling::skin-tone-5:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♂️", + [":man_cartwheeling_dark_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♂️", + [":man_cartwheeling_light_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♂️", + [":man_cartwheeling_medium_dark_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♂️", + [":man_cartwheeling_medium_light_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♂️", + [":man_cartwheeling_medium_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♂️", + [":man_cartwheeling_tone1:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♂️", + [":man_cartwheeling_tone2:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♂️", + [":man_cartwheeling_tone3:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♂️", + [":man_cartwheeling_tone4:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♂️", + [":man_cartwheeling_tone5:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♂️", + [":man_climbing:"] = "\uD83E\uDDD7\u200D♂️", + [":man_climbing::skin-tone-1:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♂️", + [":man_climbing::skin-tone-2:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♂️", + [":man_climbing::skin-tone-3:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♂️", + [":man_climbing::skin-tone-4:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♂️", + [":man_climbing::skin-tone-5:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♂️", + [":man_climbing_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♂️", + [":man_climbing_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♂️", + [":man_climbing_medium_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♂️", + [":man_climbing_medium_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♂️", + [":man_climbing_medium_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♂️", + [":man_climbing_tone1:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♂️", + [":man_climbing_tone2:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♂️", + [":man_climbing_tone3:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♂️", + [":man_climbing_tone4:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♂️", + [":man_climbing_tone5:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♂️", + [":man_construction_worker:"] = "\uD83D\uDC77\u200D♂️", + [":man_construction_worker::skin-tone-1:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♂️", + [":man_construction_worker::skin-tone-2:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♂️", + [":man_construction_worker::skin-tone-3:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♂️", + [":man_construction_worker::skin-tone-4:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♂️", + [":man_construction_worker::skin-tone-5:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♂️", + [":man_construction_worker_dark_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♂️", + [":man_construction_worker_light_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♂️", + [":man_construction_worker_medium_dark_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♂️", + [":man_construction_worker_medium_light_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♂️", + [":man_construction_worker_medium_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♂️", + [":man_construction_worker_tone1:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♂️", + [":man_construction_worker_tone2:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♂️", + [":man_construction_worker_tone3:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♂️", + [":man_construction_worker_tone4:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♂️", + [":man_construction_worker_tone5:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♂️", + [":man_cook:"] = "\uD83D\uDC68\u200D\uD83C\uDF73", + [":man_cook::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF73", + [":man_cook::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF73", + [":man_cook::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF73", + [":man_cook::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF73", + [":man_cook::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF73", + [":man_cook_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF73", + [":man_cook_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF73", + [":man_cook_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF73", + [":man_cook_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF73", + [":man_cook_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF73", + [":man_cook_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF73", + [":man_cook_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF73", + [":man_cook_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF73", + [":man_cook_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF73", + [":man_cook_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF73", + [":man_curly_haired:"] = "\uD83D\uDC68\u200D\uD83E\uDDB1", + [":man_curly_haired::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":man_curly_haired::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":man_curly_haired::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":man_curly_haired::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":man_curly_haired::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":man_curly_haired_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":man_curly_haired_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":man_curly_haired_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":man_curly_haired_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":man_curly_haired_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":man_curly_haired_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":man_curly_haired_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":man_curly_haired_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":man_curly_haired_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":man_curly_haired_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":man_dancing:"] = "\uD83D\uDD7A", + [":man_dancing::skin-tone-1:"] = "\uD83D\uDD7A\uD83C\uDFFB", + [":man_dancing::skin-tone-2:"] = "\uD83D\uDD7A\uD83C\uDFFC", + [":man_dancing::skin-tone-3:"] = "\uD83D\uDD7A\uD83C\uDFFD", + [":man_dancing::skin-tone-4:"] = "\uD83D\uDD7A\uD83C\uDFFE", + [":man_dancing::skin-tone-5:"] = "\uD83D\uDD7A\uD83C\uDFFF", + [":man_dancing_tone1:"] = "\uD83D\uDD7A\uD83C\uDFFB", + [":man_dancing_tone2:"] = "\uD83D\uDD7A\uD83C\uDFFC", + [":man_dancing_tone3:"] = "\uD83D\uDD7A\uD83C\uDFFD", + [":man_dancing_tone4:"] = "\uD83D\uDD7A\uD83C\uDFFE", + [":man_dancing_tone5:"] = "\uD83D\uDD7A\uD83C\uDFFF", + [":man_detective:"] = "\uD83D\uDD75️\u200D♂️", + [":man_detective::skin-tone-1:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♂️", + [":man_detective::skin-tone-2:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♂️", + [":man_detective::skin-tone-3:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♂️", + [":man_detective::skin-tone-4:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♂️", + [":man_detective::skin-tone-5:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♂️", + [":man_detective_dark_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♂️", + [":man_detective_light_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♂️", + [":man_detective_medium_dark_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♂️", + [":man_detective_medium_light_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♂️", + [":man_detective_medium_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♂️", + [":man_detective_tone1:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♂️", + [":man_detective_tone2:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♂️", + [":man_detective_tone3:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♂️", + [":man_detective_tone4:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♂️", + [":man_detective_tone5:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♂️", + [":man_elf:"] = "\uD83E\uDDDD\u200D♂️", + [":man_elf::skin-tone-1:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♂️", + [":man_elf::skin-tone-2:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♂️", + [":man_elf::skin-tone-3:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♂️", + [":man_elf::skin-tone-4:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♂️", + [":man_elf::skin-tone-5:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♂️", + [":man_elf_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♂️", + [":man_elf_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♂️", + [":man_elf_medium_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♂️", + [":man_elf_medium_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♂️", + [":man_elf_medium_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♂️", + [":man_elf_tone1:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♂️", + [":man_elf_tone2:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♂️", + [":man_elf_tone3:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♂️", + [":man_elf_tone4:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♂️", + [":man_elf_tone5:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♂️", + [":man_facepalming:"] = "\uD83E\uDD26\u200D♂️", + [":man_facepalming::skin-tone-1:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♂️", + [":man_facepalming::skin-tone-2:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♂️", + [":man_facepalming::skin-tone-3:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♂️", + [":man_facepalming::skin-tone-4:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♂️", + [":man_facepalming::skin-tone-5:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♂️", + [":man_facepalming_dark_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♂️", + [":man_facepalming_light_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♂️", + [":man_facepalming_medium_dark_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♂️", + [":man_facepalming_medium_light_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♂️", + [":man_facepalming_medium_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♂️", + [":man_facepalming_tone1:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♂️", + [":man_facepalming_tone2:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♂️", + [":man_facepalming_tone3:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♂️", + [":man_facepalming_tone4:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♂️", + [":man_facepalming_tone5:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♂️", + [":man_factory_worker:"] = "\uD83D\uDC68\u200D\uD83C\uDFED", + [":man_factory_worker::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFED", + [":man_factory_worker::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFED", + [":man_factory_worker::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFED", + [":man_factory_worker::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFED", + [":man_factory_worker::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFED", + [":man_factory_worker_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFED", + [":man_factory_worker_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFED", + [":man_factory_worker_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFED", + [":man_factory_worker_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFED", + [":man_factory_worker_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFED", + [":man_factory_worker_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFED", + [":man_factory_worker_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFED", + [":man_factory_worker_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFED", + [":man_factory_worker_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFED", + [":man_factory_worker_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFED", + [":man_fairy:"] = "\uD83E\uDDDA\u200D♂️", + [":man_fairy::skin-tone-1:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♂️", + [":man_fairy::skin-tone-2:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♂️", + [":man_fairy::skin-tone-3:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♂️", + [":man_fairy::skin-tone-4:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♂️", + [":man_fairy::skin-tone-5:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♂️", + [":man_fairy_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♂️", + [":man_fairy_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♂️", + [":man_fairy_medium_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♂️", + [":man_fairy_medium_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♂️", + [":man_fairy_medium_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♂️", + [":man_fairy_tone1:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♂️", + [":man_fairy_tone2:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♂️", + [":man_fairy_tone3:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♂️", + [":man_fairy_tone4:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♂️", + [":man_fairy_tone5:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♂️", + [":man_farmer:"] = "\uD83D\uDC68\u200D\uD83C\uDF3E", + [":man_farmer::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":man_farmer::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":man_farmer::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":man_farmer::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":man_farmer::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":man_farmer_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":man_farmer_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":man_farmer_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":man_farmer_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":man_farmer_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":man_farmer_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":man_farmer_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":man_farmer_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":man_farmer_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":man_farmer_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":man_firefighter:"] = "\uD83D\uDC68\u200D\uD83D\uDE92", + [":man_firefighter::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE92", + [":man_firefighter::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE92", + [":man_firefighter::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE92", + [":man_firefighter::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE92", + [":man_firefighter::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE92", + [":man_firefighter_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE92", + [":man_firefighter_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE92", + [":man_firefighter_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE92", + [":man_firefighter_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE92", + [":man_firefighter_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE92", + [":man_firefighter_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDE92", + [":man_firefighter_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDE92", + [":man_firefighter_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDE92", + [":man_firefighter_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDE92", + [":man_firefighter_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDE92", + [":man_frowning:"] = "\uD83D\uDE4D\u200D♂️", + [":man_frowning::skin-tone-1:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♂️", + [":man_frowning::skin-tone-2:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♂️", + [":man_frowning::skin-tone-3:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♂️", + [":man_frowning::skin-tone-4:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♂️", + [":man_frowning::skin-tone-5:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♂️", + [":man_frowning_dark_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♂️", + [":man_frowning_light_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♂️", + [":man_frowning_medium_dark_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♂️", + [":man_frowning_medium_light_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♂️", + [":man_frowning_medium_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♂️", + [":man_frowning_tone1:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♂️", + [":man_frowning_tone2:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♂️", + [":man_frowning_tone3:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♂️", + [":man_frowning_tone4:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♂️", + [":man_frowning_tone5:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♂️", + [":man_genie:"] = "\uD83E\uDDDE\u200D♂️", + [":man_gesturing_no:"] = "\uD83D\uDE45\u200D♂️", + [":man_gesturing_no::skin-tone-1:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♂️", + [":man_gesturing_no::skin-tone-2:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♂️", + [":man_gesturing_no::skin-tone-3:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♂️", + [":man_gesturing_no::skin-tone-4:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♂️", + [":man_gesturing_no::skin-tone-5:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♂️", + [":man_gesturing_no_dark_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♂️", + [":man_gesturing_no_light_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♂️", + [":man_gesturing_no_medium_dark_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♂️", + [":man_gesturing_no_medium_light_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♂️", + [":man_gesturing_no_medium_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♂️", + [":man_gesturing_no_tone1:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♂️", + [":man_gesturing_no_tone2:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♂️", + [":man_gesturing_no_tone3:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♂️", + [":man_gesturing_no_tone4:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♂️", + [":man_gesturing_no_tone5:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♂️", + [":man_gesturing_ok:"] = "\uD83D\uDE46\u200D♂️", + [":man_gesturing_ok::skin-tone-1:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♂️", + [":man_gesturing_ok::skin-tone-2:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♂️", + [":man_gesturing_ok::skin-tone-3:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♂️", + [":man_gesturing_ok::skin-tone-4:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♂️", + [":man_gesturing_ok::skin-tone-5:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♂️", + [":man_gesturing_ok_dark_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♂️", + [":man_gesturing_ok_light_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♂️", + [":man_gesturing_ok_medium_dark_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♂️", + [":man_gesturing_ok_medium_light_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♂️", + [":man_gesturing_ok_medium_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♂️", + [":man_gesturing_ok_tone1:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♂️", + [":man_gesturing_ok_tone2:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♂️", + [":man_gesturing_ok_tone3:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♂️", + [":man_gesturing_ok_tone4:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♂️", + [":man_gesturing_ok_tone5:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♂️", + [":man_getting_face_massage:"] = "\uD83D\uDC86\u200D♂️", + [":man_getting_face_massage::skin-tone-1:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♂️", + [":man_getting_face_massage::skin-tone-2:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♂️", + [":man_getting_face_massage::skin-tone-3:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♂️", + [":man_getting_face_massage::skin-tone-4:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♂️", + [":man_getting_face_massage::skin-tone-5:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♂️", + [":man_getting_face_massage_dark_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♂️", + [":man_getting_face_massage_light_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♂️", + [":man_getting_face_massage_medium_dark_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♂️", + [":man_getting_face_massage_medium_light_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♂️", + [":man_getting_face_massage_medium_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♂️", + [":man_getting_face_massage_tone1:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♂️", + [":man_getting_face_massage_tone2:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♂️", + [":man_getting_face_massage_tone3:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♂️", + [":man_getting_face_massage_tone4:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♂️", + [":man_getting_face_massage_tone5:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♂️", + [":man_getting_haircut:"] = "\uD83D\uDC87\u200D♂️", + [":man_getting_haircut::skin-tone-1:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♂️", + [":man_getting_haircut::skin-tone-2:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♂️", + [":man_getting_haircut::skin-tone-3:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♂️", + [":man_getting_haircut::skin-tone-4:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♂️", + [":man_getting_haircut::skin-tone-5:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♂️", + [":man_getting_haircut_dark_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♂️", + [":man_getting_haircut_light_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♂️", + [":man_getting_haircut_medium_dark_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♂️", + [":man_getting_haircut_medium_light_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♂️", + [":man_getting_haircut_medium_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♂️", + [":man_getting_haircut_tone1:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♂️", + [":man_getting_haircut_tone2:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♂️", + [":man_getting_haircut_tone3:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♂️", + [":man_getting_haircut_tone4:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♂️", + [":man_getting_haircut_tone5:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♂️", + [":man_golfing:"] = "\uD83C\uDFCC️\u200D♂️", + [":man_golfing::skin-tone-1:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♂️", + [":man_golfing::skin-tone-2:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♂️", + [":man_golfing::skin-tone-3:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♂️", + [":man_golfing::skin-tone-4:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♂️", + [":man_golfing::skin-tone-5:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♂️", + [":man_golfing_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♂️", + [":man_golfing_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♂️", + [":man_golfing_medium_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♂️", + [":man_golfing_medium_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♂️", + [":man_golfing_medium_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♂️", + [":man_golfing_tone1:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♂️", + [":man_golfing_tone2:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♂️", + [":man_golfing_tone3:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♂️", + [":man_golfing_tone4:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♂️", + [":man_golfing_tone5:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♂️", + [":man_guard:"] = "\uD83D\uDC82\u200D♂️", + [":man_guard::skin-tone-1:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♂️", + [":man_guard::skin-tone-2:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♂️", + [":man_guard::skin-tone-3:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♂️", + [":man_guard::skin-tone-4:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♂️", + [":man_guard::skin-tone-5:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♂️", + [":man_guard_dark_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♂️", + [":man_guard_light_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♂️", + [":man_guard_medium_dark_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♂️", + [":man_guard_medium_light_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♂️", + [":man_guard_medium_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♂️", + [":man_guard_tone1:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♂️", + [":man_guard_tone2:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♂️", + [":man_guard_tone3:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♂️", + [":man_guard_tone4:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♂️", + [":man_guard_tone5:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♂️", + [":man_health_worker:"] = "\uD83D\uDC68\u200D⚕️", + [":man_health_worker::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚕️", + [":man_health_worker::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚕️", + [":man_health_worker::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚕️", + [":man_health_worker::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚕️", + [":man_health_worker::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚕️", + [":man_health_worker_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚕️", + [":man_health_worker_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚕️", + [":man_health_worker_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚕️", + [":man_health_worker_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚕️", + [":man_health_worker_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚕️", + [":man_health_worker_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚕️", + [":man_health_worker_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚕️", + [":man_health_worker_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚕️", + [":man_health_worker_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚕️", + [":man_health_worker_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚕️", + [":man_in_business_suit_levitating:"] = "\uD83D\uDD74️", + [":man_in_business_suit_levitating::skin-tone-1:"] = "\uD83D\uDD74\uD83C\uDFFB", + [":man_in_business_suit_levitating::skin-tone-2:"] = "\uD83D\uDD74\uD83C\uDFFC", + [":man_in_business_suit_levitating::skin-tone-3:"] = "\uD83D\uDD74\uD83C\uDFFD", + [":man_in_business_suit_levitating::skin-tone-4:"] = "\uD83D\uDD74\uD83C\uDFFE", + [":man_in_business_suit_levitating::skin-tone-5:"] = "\uD83D\uDD74\uD83C\uDFFF", + [":man_in_business_suit_levitating_dark_skin_tone:"] = "\uD83D\uDD74\uD83C\uDFFF", + [":man_in_business_suit_levitating_light_skin_tone:"] = "\uD83D\uDD74\uD83C\uDFFB", + [":man_in_business_suit_levitating_medium_dark_skin_tone:"] = "\uD83D\uDD74\uD83C\uDFFE", + [":man_in_business_suit_levitating_medium_light_skin_tone:"] = "\uD83D\uDD74\uD83C\uDFFC", + [":man_in_business_suit_levitating_medium_skin_tone:"] = "\uD83D\uDD74\uD83C\uDFFD", + [":man_in_business_suit_levitating_tone1:"] = "\uD83D\uDD74\uD83C\uDFFB", + [":man_in_business_suit_levitating_tone2:"] = "\uD83D\uDD74\uD83C\uDFFC", + [":man_in_business_suit_levitating_tone3:"] = "\uD83D\uDD74\uD83C\uDFFD", + [":man_in_business_suit_levitating_tone4:"] = "\uD83D\uDD74\uD83C\uDFFE", + [":man_in_business_suit_levitating_tone5:"] = "\uD83D\uDD74\uD83C\uDFFF", + [":man_in_lotus_position:"] = "\uD83E\uDDD8\u200D♂️", + [":man_in_lotus_position::skin-tone-1:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♂️", + [":man_in_lotus_position::skin-tone-2:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♂️", + [":man_in_lotus_position::skin-tone-3:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♂️", + [":man_in_lotus_position::skin-tone-4:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♂️", + [":man_in_lotus_position::skin-tone-5:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♂️", + [":man_in_lotus_position_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♂️", + [":man_in_lotus_position_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♂️", + [":man_in_lotus_position_medium_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♂️", + [":man_in_lotus_position_medium_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♂️", + [":man_in_lotus_position_medium_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♂️", + [":man_in_lotus_position_tone1:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♂️", + [":man_in_lotus_position_tone2:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♂️", + [":man_in_lotus_position_tone3:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♂️", + [":man_in_lotus_position_tone4:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♂️", + [":man_in_lotus_position_tone5:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♂️", + [":man_in_manual_wheelchair:"] = "\uD83D\uDC68\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":man_in_manual_wheelchair_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":man_in_motorized_wheelchair:"] = "\uD83D\uDC68\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":man_in_motorized_wheelchair_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":man_in_steamy_room:"] = "\uD83E\uDDD6\u200D♂️", + [":man_in_steamy_room::skin-tone-1:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♂️", + [":man_in_steamy_room::skin-tone-2:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♂️", + [":man_in_steamy_room::skin-tone-3:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♂️", + [":man_in_steamy_room::skin-tone-4:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♂️", + [":man_in_steamy_room::skin-tone-5:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♂️", + [":man_in_steamy_room_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♂️", + [":man_in_steamy_room_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♂️", + [":man_in_steamy_room_medium_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♂️", + [":man_in_steamy_room_medium_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♂️", + [":man_in_steamy_room_medium_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♂️", + [":man_in_steamy_room_tone1:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♂️", + [":man_in_steamy_room_tone2:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♂️", + [":man_in_steamy_room_tone3:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♂️", + [":man_in_steamy_room_tone4:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♂️", + [":man_in_steamy_room_tone5:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♂️", + [":man_in_tuxedo:"] = "\uD83E\uDD35", + [":man_in_tuxedo::skin-tone-1:"] = "\uD83E\uDD35\uD83C\uDFFB", + [":man_in_tuxedo::skin-tone-2:"] = "\uD83E\uDD35\uD83C\uDFFC", + [":man_in_tuxedo::skin-tone-3:"] = "\uD83E\uDD35\uD83C\uDFFD", + [":man_in_tuxedo::skin-tone-4:"] = "\uD83E\uDD35\uD83C\uDFFE", + [":man_in_tuxedo::skin-tone-5:"] = "\uD83E\uDD35\uD83C\uDFFF", + [":man_in_tuxedo_tone1:"] = "\uD83E\uDD35\uD83C\uDFFB", + [":man_in_tuxedo_tone2:"] = "\uD83E\uDD35\uD83C\uDFFC", + [":man_in_tuxedo_tone3:"] = "\uD83E\uDD35\uD83C\uDFFD", + [":man_in_tuxedo_tone4:"] = "\uD83E\uDD35\uD83C\uDFFE", + [":man_in_tuxedo_tone5:"] = "\uD83E\uDD35\uD83C\uDFFF", + [":man_judge:"] = "\uD83D\uDC68\u200D⚖️", + [":man_judge::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚖️", + [":man_judge::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚖️", + [":man_judge::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚖️", + [":man_judge::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚖️", + [":man_judge::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚖️", + [":man_judge_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚖️", + [":man_judge_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚖️", + [":man_judge_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚖️", + [":man_judge_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚖️", + [":man_judge_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚖️", + [":man_judge_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D⚖️", + [":man_judge_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D⚖️", + [":man_judge_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D⚖️", + [":man_judge_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D⚖️", + [":man_judge_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D⚖️", + [":man_juggling:"] = "\uD83E\uDD39\u200D♂️", + [":man_juggling::skin-tone-1:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♂️", + [":man_juggling::skin-tone-2:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♂️", + [":man_juggling::skin-tone-3:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♂️", + [":man_juggling::skin-tone-4:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♂️", + [":man_juggling::skin-tone-5:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♂️", + [":man_juggling_dark_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♂️", + [":man_juggling_light_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♂️", + [":man_juggling_medium_dark_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♂️", + [":man_juggling_medium_light_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♂️", + [":man_juggling_medium_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♂️", + [":man_juggling_tone1:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♂️", + [":man_juggling_tone2:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♂️", + [":man_juggling_tone3:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♂️", + [":man_juggling_tone4:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♂️", + [":man_juggling_tone5:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♂️", + [":man_kneeling:"] = "\uD83E\uDDCE\u200D♂️", + [":man_kneeling::skin-tone-1:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♂️", + [":man_kneeling::skin-tone-2:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♂️", + [":man_kneeling::skin-tone-3:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♂️", + [":man_kneeling::skin-tone-4:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♂️", + [":man_kneeling::skin-tone-5:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♂️", + [":man_kneeling_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♂️", + [":man_kneeling_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♂️", + [":man_kneeling_medium_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♂️", + [":man_kneeling_medium_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♂️", + [":man_kneeling_medium_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♂️", + [":man_kneeling_tone1:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♂️", + [":man_kneeling_tone2:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♂️", + [":man_kneeling_tone3:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♂️", + [":man_kneeling_tone4:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♂️", + [":man_kneeling_tone5:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♂️", + [":man_lifting_weights:"] = "\uD83C\uDFCB️\u200D♂️", + [":man_lifting_weights::skin-tone-1:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♂️", + [":man_lifting_weights::skin-tone-2:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♂️", + [":man_lifting_weights::skin-tone-3:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♂️", + [":man_lifting_weights::skin-tone-4:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♂️", + [":man_lifting_weights::skin-tone-5:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♂️", + [":man_lifting_weights_dark_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♂️", + [":man_lifting_weights_light_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♂️", + [":man_lifting_weights_medium_dark_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♂️", + [":man_lifting_weights_medium_light_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♂️", + [":man_lifting_weights_medium_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♂️", + [":man_lifting_weights_tone1:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♂️", + [":man_lifting_weights_tone2:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♂️", + [":man_lifting_weights_tone3:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♂️", + [":man_lifting_weights_tone4:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♂️", + [":man_lifting_weights_tone5:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♂️", + [":man_mage:"] = "\uD83E\uDDD9\u200D♂️", + [":man_mage::skin-tone-1:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♂️", + [":man_mage::skin-tone-2:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♂️", + [":man_mage::skin-tone-3:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♂️", + [":man_mage::skin-tone-4:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♂️", + [":man_mage::skin-tone-5:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♂️", + [":man_mage_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♂️", + [":man_mage_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♂️", + [":man_mage_medium_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♂️", + [":man_mage_medium_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♂️", + [":man_mage_medium_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♂️", + [":man_mage_tone1:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♂️", + [":man_mage_tone2:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♂️", + [":man_mage_tone3:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♂️", + [":man_mage_tone4:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♂️", + [":man_mage_tone5:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♂️", + [":man_mechanic:"] = "\uD83D\uDC68\u200D\uD83D\uDD27", + [":man_mechanic::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD27", + [":man_mechanic::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD27", + [":man_mechanic::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD27", + [":man_mechanic::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD27", + [":man_mechanic::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD27", + [":man_mechanic_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD27", + [":man_mechanic_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD27", + [":man_mechanic_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD27", + [":man_mechanic_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD27", + [":man_mechanic_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD27", + [":man_mechanic_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD27", + [":man_mechanic_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD27", + [":man_mechanic_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD27", + [":man_mechanic_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD27", + [":man_mechanic_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD27", + [":man_mountain_biking:"] = "\uD83D\uDEB5\u200D♂️", + [":man_mountain_biking::skin-tone-1:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♂️", + [":man_mountain_biking::skin-tone-2:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♂️", + [":man_mountain_biking::skin-tone-3:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♂️", + [":man_mountain_biking::skin-tone-4:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♂️", + [":man_mountain_biking::skin-tone-5:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♂️", + [":man_mountain_biking_dark_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♂️", + [":man_mountain_biking_light_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♂️", + [":man_mountain_biking_medium_dark_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♂️", + [":man_mountain_biking_medium_light_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♂️", + [":man_mountain_biking_medium_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♂️", + [":man_mountain_biking_tone1:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♂️", + [":man_mountain_biking_tone2:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♂️", + [":man_mountain_biking_tone3:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♂️", + [":man_mountain_biking_tone4:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♂️", + [":man_mountain_biking_tone5:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♂️", + [":man_office_worker:"] = "\uD83D\uDC68\u200D\uD83D\uDCBC", + [":man_office_worker::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":man_office_worker::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":man_office_worker::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":man_office_worker::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":man_office_worker::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":man_office_worker_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":man_office_worker_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":man_office_worker_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":man_office_worker_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":man_office_worker_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":man_office_worker_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":man_office_worker_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":man_office_worker_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":man_office_worker_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":man_office_worker_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":man_pilot:"] = "\uD83D\uDC68\u200D✈️", + [":man_pilot::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D✈️", + [":man_pilot::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D✈️", + [":man_pilot::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D✈️", + [":man_pilot::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D✈️", + [":man_pilot::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D✈️", + [":man_pilot_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D✈️", + [":man_pilot_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D✈️", + [":man_pilot_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D✈️", + [":man_pilot_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D✈️", + [":man_pilot_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D✈️", + [":man_pilot_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D✈️", + [":man_pilot_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D✈️", + [":man_pilot_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D✈️", + [":man_pilot_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D✈️", + [":man_pilot_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D✈️", + [":man_playing_handball:"] = "\uD83E\uDD3E\u200D♂️", + [":man_playing_handball::skin-tone-1:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♂️", + [":man_playing_handball::skin-tone-2:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♂️", + [":man_playing_handball::skin-tone-3:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♂️", + [":man_playing_handball::skin-tone-4:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♂️", + [":man_playing_handball::skin-tone-5:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♂️", + [":man_playing_handball_dark_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♂️", + [":man_playing_handball_light_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♂️", + [":man_playing_handball_medium_dark_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♂️", + [":man_playing_handball_medium_light_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♂️", + [":man_playing_handball_medium_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♂️", + [":man_playing_handball_tone1:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♂️", + [":man_playing_handball_tone2:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♂️", + [":man_playing_handball_tone3:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♂️", + [":man_playing_handball_tone4:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♂️", + [":man_playing_handball_tone5:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♂️", + [":man_playing_water_polo:"] = "\uD83E\uDD3D\u200D♂️", + [":man_playing_water_polo::skin-tone-1:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♂️", + [":man_playing_water_polo::skin-tone-2:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♂️", + [":man_playing_water_polo::skin-tone-3:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♂️", + [":man_playing_water_polo::skin-tone-4:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♂️", + [":man_playing_water_polo::skin-tone-5:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♂️", + [":man_playing_water_polo_dark_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♂️", + [":man_playing_water_polo_light_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♂️", + [":man_playing_water_polo_medium_dark_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♂️", + [":man_playing_water_polo_medium_light_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♂️", + [":man_playing_water_polo_medium_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♂️", + [":man_playing_water_polo_tone1:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♂️", + [":man_playing_water_polo_tone2:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♂️", + [":man_playing_water_polo_tone3:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♂️", + [":man_playing_water_polo_tone4:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♂️", + [":man_playing_water_polo_tone5:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♂️", + [":man_police_officer:"] = "\uD83D\uDC6E\u200D♂️", + [":man_police_officer::skin-tone-1:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♂️", + [":man_police_officer::skin-tone-2:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♂️", + [":man_police_officer::skin-tone-3:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♂️", + [":man_police_officer::skin-tone-4:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♂️", + [":man_police_officer::skin-tone-5:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♂️", + [":man_police_officer_dark_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♂️", + [":man_police_officer_light_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♂️", + [":man_police_officer_medium_dark_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♂️", + [":man_police_officer_medium_light_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♂️", + [":man_police_officer_medium_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♂️", + [":man_police_officer_tone1:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♂️", + [":man_police_officer_tone2:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♂️", + [":man_police_officer_tone3:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♂️", + [":man_police_officer_tone4:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♂️", + [":man_police_officer_tone5:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♂️", + [":man_pouting:"] = "\uD83D\uDE4E\u200D♂️", + [":man_pouting::skin-tone-1:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♂️", + [":man_pouting::skin-tone-2:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♂️", + [":man_pouting::skin-tone-3:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♂️", + [":man_pouting::skin-tone-4:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♂️", + [":man_pouting::skin-tone-5:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♂️", + [":man_pouting_dark_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♂️", + [":man_pouting_light_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♂️", + [":man_pouting_medium_dark_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♂️", + [":man_pouting_medium_light_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♂️", + [":man_pouting_medium_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♂️", + [":man_pouting_tone1:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♂️", + [":man_pouting_tone2:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♂️", + [":man_pouting_tone3:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♂️", + [":man_pouting_tone4:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♂️", + [":man_pouting_tone5:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♂️", + [":man_raising_hand:"] = "\uD83D\uDE4B\u200D♂️", + [":man_raising_hand::skin-tone-1:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♂️", + [":man_raising_hand::skin-tone-2:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♂️", + [":man_raising_hand::skin-tone-3:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♂️", + [":man_raising_hand::skin-tone-4:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♂️", + [":man_raising_hand::skin-tone-5:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♂️", + [":man_raising_hand_dark_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♂️", + [":man_raising_hand_light_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♂️", + [":man_raising_hand_medium_dark_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♂️", + [":man_raising_hand_medium_light_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♂️", + [":man_raising_hand_medium_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♂️", + [":man_raising_hand_tone1:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♂️", + [":man_raising_hand_tone2:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♂️", + [":man_raising_hand_tone3:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♂️", + [":man_raising_hand_tone4:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♂️", + [":man_raising_hand_tone5:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♂️", + [":man_red_haired:"] = "\uD83D\uDC68\u200D\uD83E\uDDB0", + [":man_red_haired::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":man_red_haired::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":man_red_haired::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":man_red_haired::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":man_red_haired::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":man_red_haired_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":man_red_haired_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":man_red_haired_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":man_red_haired_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":man_red_haired_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":man_red_haired_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":man_red_haired_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":man_red_haired_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":man_red_haired_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":man_red_haired_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":man_rowing_boat:"] = "\uD83D\uDEA3\u200D♂️", + [":man_rowing_boat::skin-tone-1:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♂️", + [":man_rowing_boat::skin-tone-2:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♂️", + [":man_rowing_boat::skin-tone-3:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♂️", + [":man_rowing_boat::skin-tone-4:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♂️", + [":man_rowing_boat::skin-tone-5:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♂️", + [":man_rowing_boat_dark_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♂️", + [":man_rowing_boat_light_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♂️", + [":man_rowing_boat_medium_dark_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♂️", + [":man_rowing_boat_medium_light_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♂️", + [":man_rowing_boat_medium_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♂️", + [":man_rowing_boat_tone1:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♂️", + [":man_rowing_boat_tone2:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♂️", + [":man_rowing_boat_tone3:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♂️", + [":man_rowing_boat_tone4:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♂️", + [":man_rowing_boat_tone5:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♂️", + [":man_running:"] = "\uD83C\uDFC3\u200D♂️", + [":man_running::skin-tone-1:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♂️", + [":man_running::skin-tone-2:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♂️", + [":man_running::skin-tone-3:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♂️", + [":man_running::skin-tone-4:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♂️", + [":man_running::skin-tone-5:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♂️", + [":man_running_dark_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♂️", + [":man_running_light_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♂️", + [":man_running_medium_dark_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♂️", + [":man_running_medium_light_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♂️", + [":man_running_medium_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♂️", + [":man_running_tone1:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♂️", + [":man_running_tone2:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♂️", + [":man_running_tone3:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♂️", + [":man_running_tone4:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♂️", + [":man_running_tone5:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♂️", + [":man_scientist:"] = "\uD83D\uDC68\u200D\uD83D\uDD2C", + [":man_scientist::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":man_scientist::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":man_scientist::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":man_scientist::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":man_scientist::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":man_scientist_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":man_scientist_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":man_scientist_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":man_scientist_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":man_scientist_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":man_scientist_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":man_scientist_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":man_scientist_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":man_scientist_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":man_scientist_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":man_shrugging:"] = "\uD83E\uDD37\u200D♂️", + [":man_shrugging::skin-tone-1:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♂️", + [":man_shrugging::skin-tone-2:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♂️", + [":man_shrugging::skin-tone-3:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♂️", + [":man_shrugging::skin-tone-4:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♂️", + [":man_shrugging::skin-tone-5:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♂️", + [":man_shrugging_dark_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♂️", + [":man_shrugging_light_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♂️", + [":man_shrugging_medium_dark_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♂️", + [":man_shrugging_medium_light_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♂️", + [":man_shrugging_medium_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♂️", + [":man_shrugging_tone1:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♂️", + [":man_shrugging_tone2:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♂️", + [":man_shrugging_tone3:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♂️", + [":man_shrugging_tone4:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♂️", + [":man_shrugging_tone5:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♂️", + [":man_singer:"] = "\uD83D\uDC68\u200D\uD83C\uDFA4", + [":man_singer::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":man_singer::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":man_singer::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":man_singer::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":man_singer::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":man_singer_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":man_singer_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":man_singer_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":man_singer_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":man_singer_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":man_singer_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":man_singer_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":man_singer_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":man_singer_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":man_singer_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":man_standing:"] = "\uD83E\uDDCD\u200D♂️", + [":man_standing::skin-tone-1:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♂️", + [":man_standing::skin-tone-2:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♂️", + [":man_standing::skin-tone-3:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♂️", + [":man_standing::skin-tone-4:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♂️", + [":man_standing::skin-tone-5:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♂️", + [":man_standing_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♂️", + [":man_standing_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♂️", + [":man_standing_medium_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♂️", + [":man_standing_medium_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♂️", + [":man_standing_medium_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♂️", + [":man_standing_tone1:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♂️", + [":man_standing_tone2:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♂️", + [":man_standing_tone3:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♂️", + [":man_standing_tone4:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♂️", + [":man_standing_tone5:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♂️", + [":man_student:"] = "\uD83D\uDC68\u200D\uD83C\uDF93", + [":man_student::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF93", + [":man_student::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF93", + [":man_student::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF93", + [":man_student::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF93", + [":man_student::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF93", + [":man_student_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF93", + [":man_student_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF93", + [":man_student_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF93", + [":man_student_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF93", + [":man_student_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF93", + [":man_student_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDF93", + [":man_student_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDF93", + [":man_student_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDF93", + [":man_student_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDF93", + [":man_student_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDF93", + [":man_superhero:"] = "\uD83E\uDDB8\u200D♂️", + [":man_superhero::skin-tone-1:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♂️", + [":man_superhero::skin-tone-2:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♂️", + [":man_superhero::skin-tone-3:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♂️", + [":man_superhero::skin-tone-4:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♂️", + [":man_superhero::skin-tone-5:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♂️", + [":man_superhero_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♂️", + [":man_superhero_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♂️", + [":man_superhero_medium_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♂️", + [":man_superhero_medium_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♂️", + [":man_superhero_medium_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♂️", + [":man_superhero_tone1:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♂️", + [":man_superhero_tone2:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♂️", + [":man_superhero_tone3:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♂️", + [":man_superhero_tone4:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♂️", + [":man_superhero_tone5:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♂️", + [":man_supervillain:"] = "\uD83E\uDDB9\u200D♂️", + [":man_supervillain::skin-tone-1:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♂️", + [":man_supervillain::skin-tone-2:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♂️", + [":man_supervillain::skin-tone-3:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♂️", + [":man_supervillain::skin-tone-4:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♂️", + [":man_supervillain::skin-tone-5:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♂️", + [":man_supervillain_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♂️", + [":man_supervillain_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♂️", + [":man_supervillain_medium_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♂️", + [":man_supervillain_medium_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♂️", + [":man_supervillain_medium_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♂️", + [":man_supervillain_tone1:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♂️", + [":man_supervillain_tone2:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♂️", + [":man_supervillain_tone3:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♂️", + [":man_supervillain_tone4:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♂️", + [":man_supervillain_tone5:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♂️", + [":man_surfing:"] = "\uD83C\uDFC4\u200D♂️", + [":man_surfing::skin-tone-1:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♂️", + [":man_surfing::skin-tone-2:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♂️", + [":man_surfing::skin-tone-3:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♂️", + [":man_surfing::skin-tone-4:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♂️", + [":man_surfing::skin-tone-5:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♂️", + [":man_surfing_dark_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♂️", + [":man_surfing_light_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♂️", + [":man_surfing_medium_dark_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♂️", + [":man_surfing_medium_light_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♂️", + [":man_surfing_medium_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♂️", + [":man_surfing_tone1:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♂️", + [":man_surfing_tone2:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♂️", + [":man_surfing_tone3:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♂️", + [":man_surfing_tone4:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♂️", + [":man_surfing_tone5:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♂️", + [":man_swimming:"] = "\uD83C\uDFCA\u200D♂️", + [":man_swimming::skin-tone-1:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♂️", + [":man_swimming::skin-tone-2:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♂️", + [":man_swimming::skin-tone-3:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♂️", + [":man_swimming::skin-tone-4:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♂️", + [":man_swimming::skin-tone-5:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♂️", + [":man_swimming_dark_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♂️", + [":man_swimming_light_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♂️", + [":man_swimming_medium_dark_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♂️", + [":man_swimming_medium_light_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♂️", + [":man_swimming_medium_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♂️", + [":man_swimming_tone1:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♂️", + [":man_swimming_tone2:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♂️", + [":man_swimming_tone3:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♂️", + [":man_swimming_tone4:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♂️", + [":man_swimming_tone5:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♂️", + [":man_teacher:"] = "\uD83D\uDC68\u200D\uD83C\uDFEB", + [":man_teacher::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":man_teacher::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":man_teacher::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":man_teacher::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":man_teacher::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":man_teacher_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":man_teacher_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":man_teacher_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":man_teacher_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":man_teacher_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":man_teacher_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":man_teacher_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":man_teacher_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":man_teacher_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":man_teacher_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":man_technologist:"] = "\uD83D\uDC68\u200D\uD83D\uDCBB", + [":man_technologist::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":man_technologist::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":man_technologist::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":man_technologist::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":man_technologist::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":man_technologist_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":man_technologist_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":man_technologist_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":man_technologist_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":man_technologist_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":man_technologist_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":man_technologist_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":man_technologist_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":man_technologist_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":man_technologist_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":man_tipping_hand:"] = "\uD83D\uDC81\u200D♂️", + [":man_tipping_hand::skin-tone-1:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♂️", + [":man_tipping_hand::skin-tone-2:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♂️", + [":man_tipping_hand::skin-tone-3:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♂️", + [":man_tipping_hand::skin-tone-4:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♂️", + [":man_tipping_hand::skin-tone-5:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♂️", + [":man_tipping_hand_dark_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♂️", + [":man_tipping_hand_light_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♂️", + [":man_tipping_hand_medium_dark_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♂️", + [":man_tipping_hand_medium_light_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♂️", + [":man_tipping_hand_medium_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♂️", + [":man_tipping_hand_tone1:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♂️", + [":man_tipping_hand_tone2:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♂️", + [":man_tipping_hand_tone3:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♂️", + [":man_tipping_hand_tone4:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♂️", + [":man_tipping_hand_tone5:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♂️", + [":man_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB", + [":man_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC", + [":man_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD", + [":man_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE", + [":man_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF", + [":man_vampire:"] = "\uD83E\uDDDB\u200D♂️", + [":man_vampire::skin-tone-1:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♂️", + [":man_vampire::skin-tone-2:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♂️", + [":man_vampire::skin-tone-3:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♂️", + [":man_vampire::skin-tone-4:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♂️", + [":man_vampire::skin-tone-5:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♂️", + [":man_vampire_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♂️", + [":man_vampire_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♂️", + [":man_vampire_medium_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♂️", + [":man_vampire_medium_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♂️", + [":man_vampire_medium_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♂️", + [":man_vampire_tone1:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♂️", + [":man_vampire_tone2:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♂️", + [":man_vampire_tone3:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♂️", + [":man_vampire_tone4:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♂️", + [":man_vampire_tone5:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♂️", + [":man_walking:"] = "\uD83D\uDEB6\u200D♂️", + [":man_walking::skin-tone-1:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♂️", + [":man_walking::skin-tone-2:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♂️", + [":man_walking::skin-tone-3:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♂️", + [":man_walking::skin-tone-4:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♂️", + [":man_walking::skin-tone-5:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♂️", + [":man_walking_dark_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♂️", + [":man_walking_light_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♂️", + [":man_walking_medium_dark_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♂️", + [":man_walking_medium_light_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♂️", + [":man_walking_medium_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♂️", + [":man_walking_tone1:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♂️", + [":man_walking_tone2:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♂️", + [":man_walking_tone3:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♂️", + [":man_walking_tone4:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♂️", + [":man_walking_tone5:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♂️", + [":man_wearing_turban:"] = "\uD83D\uDC73\u200D♂️", + [":man_wearing_turban::skin-tone-1:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♂️", + [":man_wearing_turban::skin-tone-2:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♂️", + [":man_wearing_turban::skin-tone-3:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♂️", + [":man_wearing_turban::skin-tone-4:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♂️", + [":man_wearing_turban::skin-tone-5:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♂️", + [":man_wearing_turban_dark_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♂️", + [":man_wearing_turban_light_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♂️", + [":man_wearing_turban_medium_dark_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♂️", + [":man_wearing_turban_medium_light_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♂️", + [":man_wearing_turban_medium_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♂️", + [":man_wearing_turban_tone1:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♂️", + [":man_wearing_turban_tone2:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♂️", + [":man_wearing_turban_tone3:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♂️", + [":man_wearing_turban_tone4:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♂️", + [":man_wearing_turban_tone5:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♂️", + [":man_white_haired:"] = "\uD83D\uDC68\u200D\uD83E\uDDB3", + [":man_white_haired::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":man_white_haired::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":man_white_haired::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":man_white_haired::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":man_white_haired::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":man_white_haired_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":man_white_haired_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":man_white_haired_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":man_white_haired_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":man_white_haired_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":man_white_haired_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":man_white_haired_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":man_white_haired_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":man_white_haired_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":man_white_haired_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":man_with_chinese_cap:"] = "\uD83D\uDC72", + [":man_with_chinese_cap::skin-tone-1:"] = "\uD83D\uDC72\uD83C\uDFFB", + [":man_with_chinese_cap::skin-tone-2:"] = "\uD83D\uDC72\uD83C\uDFFC", + [":man_with_chinese_cap::skin-tone-3:"] = "\uD83D\uDC72\uD83C\uDFFD", + [":man_with_chinese_cap::skin-tone-4:"] = "\uD83D\uDC72\uD83C\uDFFE", + [":man_with_chinese_cap::skin-tone-5:"] = "\uD83D\uDC72\uD83C\uDFFF", + [":man_with_chinese_cap_tone1:"] = "\uD83D\uDC72\uD83C\uDFFB", + [":man_with_chinese_cap_tone2:"] = "\uD83D\uDC72\uD83C\uDFFC", + [":man_with_chinese_cap_tone3:"] = "\uD83D\uDC72\uD83C\uDFFD", + [":man_with_chinese_cap_tone4:"] = "\uD83D\uDC72\uD83C\uDFFE", + [":man_with_chinese_cap_tone5:"] = "\uD83D\uDC72\uD83C\uDFFF", + [":man_with_gua_pi_mao:"] = "\uD83D\uDC72", + [":man_with_gua_pi_mao::skin-tone-1:"] = "\uD83D\uDC72\uD83C\uDFFB", + [":man_with_gua_pi_mao::skin-tone-2:"] = "\uD83D\uDC72\uD83C\uDFFC", + [":man_with_gua_pi_mao::skin-tone-3:"] = "\uD83D\uDC72\uD83C\uDFFD", + [":man_with_gua_pi_mao::skin-tone-4:"] = "\uD83D\uDC72\uD83C\uDFFE", + [":man_with_gua_pi_mao::skin-tone-5:"] = "\uD83D\uDC72\uD83C\uDFFF", + [":man_with_gua_pi_mao_tone1:"] = "\uD83D\uDC72\uD83C\uDFFB", + [":man_with_gua_pi_mao_tone2:"] = "\uD83D\uDC72\uD83C\uDFFC", + [":man_with_gua_pi_mao_tone3:"] = "\uD83D\uDC72\uD83C\uDFFD", + [":man_with_gua_pi_mao_tone4:"] = "\uD83D\uDC72\uD83C\uDFFE", + [":man_with_gua_pi_mao_tone5:"] = "\uD83D\uDC72\uD83C\uDFFF", + [":man_with_probing_cane:"] = "\uD83D\uDC68\u200D\uD83E\uDDAF", + [":man_with_probing_cane::skin-tone-1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":man_with_probing_cane::skin-tone-2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":man_with_probing_cane::skin-tone-3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":man_with_probing_cane::skin-tone-4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":man_with_probing_cane::skin-tone-5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":man_with_probing_cane_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":man_with_probing_cane_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":man_with_probing_cane_medium_dark_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":man_with_probing_cane_medium_light_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":man_with_probing_cane_medium_skin_tone:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":man_with_probing_cane_tone1:"] = "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":man_with_probing_cane_tone2:"] = "\uD83D\uDC68\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":man_with_probing_cane_tone3:"] = "\uD83D\uDC68\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":man_with_probing_cane_tone4:"] = "\uD83D\uDC68\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":man_with_probing_cane_tone5:"] = "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":man_with_turban:"] = "\uD83D\uDC73", + [":man_with_turban::skin-tone-1:"] = "\uD83D\uDC73\uD83C\uDFFB", + [":man_with_turban::skin-tone-2:"] = "\uD83D\uDC73\uD83C\uDFFC", + [":man_with_turban::skin-tone-3:"] = "\uD83D\uDC73\uD83C\uDFFD", + [":man_with_turban::skin-tone-4:"] = "\uD83D\uDC73\uD83C\uDFFE", + [":man_with_turban::skin-tone-5:"] = "\uD83D\uDC73\uD83C\uDFFF", + [":man_with_turban_tone1:"] = "\uD83D\uDC73\uD83C\uDFFB", + [":man_with_turban_tone2:"] = "\uD83D\uDC73\uD83C\uDFFC", + [":man_with_turban_tone3:"] = "\uD83D\uDC73\uD83C\uDFFD", + [":man_with_turban_tone4:"] = "\uD83D\uDC73\uD83C\uDFFE", + [":man_with_turban_tone5:"] = "\uD83D\uDC73\uD83C\uDFFF", + [":man_zombie:"] = "\uD83E\uDDDF\u200D♂️", + [":mango:"] = "\uD83E\uDD6D", + [":mans_shoe:"] = "\uD83D\uDC5E", + [":mantlepiece_clock:"] = "\uD83D\uDD70️", + [":manual_wheelchair:"] = "\uD83E\uDDBD", + [":map:"] = "\uD83D\uDDFA️", + [":maple_leaf:"] = "\uD83C\uDF41", + [":martial_arts_uniform:"] = "\uD83E\uDD4B", + [":mask:"] = "\uD83D\uDE37", + [":massage:"] = "\uD83D\uDC86", + [":massage::skin-tone-1:"] = "\uD83D\uDC86\uD83C\uDFFB", + [":massage::skin-tone-2:"] = "\uD83D\uDC86\uD83C\uDFFC", + [":massage::skin-tone-3:"] = "\uD83D\uDC86\uD83C\uDFFD", + [":massage::skin-tone-4:"] = "\uD83D\uDC86\uD83C\uDFFE", + [":massage::skin-tone-5:"] = "\uD83D\uDC86\uD83C\uDFFF", + [":massage_tone1:"] = "\uD83D\uDC86\uD83C\uDFFB", + [":massage_tone2:"] = "\uD83D\uDC86\uD83C\uDFFC", + [":massage_tone3:"] = "\uD83D\uDC86\uD83C\uDFFD", + [":massage_tone4:"] = "\uD83D\uDC86\uD83C\uDFFE", + [":massage_tone5:"] = "\uD83D\uDC86\uD83C\uDFFF", + [":mate:"] = "\uD83E\uDDC9", + [":meat_on_bone:"] = "\uD83C\uDF56", + [":mechanical_arm:"] = "\uD83E\uDDBE", + [":mechanical_leg:"] = "\uD83E\uDDBF", + [":medal:"] = "\uD83C\uDFC5", + [":medical_symbol:"] = "⚕️", + [":mega:"] = "\uD83D\uDCE3", + [":melon:"] = "\uD83C\uDF48", + [":memo:"] = "\uD83D\uDCDD", + [":men_with_bunny_ears_partying:"] = "\uD83D\uDC6F\u200D♂️", + [":men_wrestling:"] = "\uD83E\uDD3C\u200D♂️", + [":menorah:"] = "\uD83D\uDD4E", + [":mens:"] = "\uD83D\uDEB9", + [":mermaid:"] = "\uD83E\uDDDC\u200D♀️", + [":mermaid::skin-tone-1:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♀️", + [":mermaid::skin-tone-2:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♀️", + [":mermaid::skin-tone-3:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♀️", + [":mermaid::skin-tone-4:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♀️", + [":mermaid::skin-tone-5:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♀️", + [":mermaid_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♀️", + [":mermaid_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♀️", + [":mermaid_medium_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♀️", + [":mermaid_medium_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♀️", + [":mermaid_medium_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♀️", + [":mermaid_tone1:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♀️", + [":mermaid_tone2:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♀️", + [":mermaid_tone3:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♀️", + [":mermaid_tone4:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♀️", + [":mermaid_tone5:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♀️", + [":merman:"] = "\uD83E\uDDDC\u200D♂️", + [":merman::skin-tone-1:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♂️", + [":merman::skin-tone-2:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♂️", + [":merman::skin-tone-3:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♂️", + [":merman::skin-tone-4:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♂️", + [":merman::skin-tone-5:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♂️", + [":merman_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♂️", + [":merman_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♂️", + [":merman_medium_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♂️", + [":merman_medium_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♂️", + [":merman_medium_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♂️", + [":merman_tone1:"] = "\uD83E\uDDDC\uD83C\uDFFB\u200D♂️", + [":merman_tone2:"] = "\uD83E\uDDDC\uD83C\uDFFC\u200D♂️", + [":merman_tone3:"] = "\uD83E\uDDDC\uD83C\uDFFD\u200D♂️", + [":merman_tone4:"] = "\uD83E\uDDDC\uD83C\uDFFE\u200D♂️", + [":merman_tone5:"] = "\uD83E\uDDDC\uD83C\uDFFF\u200D♂️", + [":merperson:"] = "\uD83E\uDDDC", + [":merperson::skin-tone-1:"] = "\uD83E\uDDDC\uD83C\uDFFB", + [":merperson::skin-tone-2:"] = "\uD83E\uDDDC\uD83C\uDFFC", + [":merperson::skin-tone-3:"] = "\uD83E\uDDDC\uD83C\uDFFD", + [":merperson::skin-tone-4:"] = "\uD83E\uDDDC\uD83C\uDFFE", + [":merperson::skin-tone-5:"] = "\uD83E\uDDDC\uD83C\uDFFF", + [":merperson_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFF", + [":merperson_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFB", + [":merperson_medium_dark_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFE", + [":merperson_medium_light_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFC", + [":merperson_medium_skin_tone:"] = "\uD83E\uDDDC\uD83C\uDFFD", + [":merperson_tone1:"] = "\uD83E\uDDDC\uD83C\uDFFB", + [":merperson_tone2:"] = "\uD83E\uDDDC\uD83C\uDFFC", + [":merperson_tone3:"] = "\uD83E\uDDDC\uD83C\uDFFD", + [":merperson_tone4:"] = "\uD83E\uDDDC\uD83C\uDFFE", + [":merperson_tone5:"] = "\uD83E\uDDDC\uD83C\uDFFF", + [":metal:"] = "\uD83E\uDD18", + [":metal::skin-tone-1:"] = "\uD83E\uDD18\uD83C\uDFFB", + [":metal::skin-tone-2:"] = "\uD83E\uDD18\uD83C\uDFFC", + [":metal::skin-tone-3:"] = "\uD83E\uDD18\uD83C\uDFFD", + [":metal::skin-tone-4:"] = "\uD83E\uDD18\uD83C\uDFFE", + [":metal::skin-tone-5:"] = "\uD83E\uDD18\uD83C\uDFFF", + [":metal_tone1:"] = "\uD83E\uDD18\uD83C\uDFFB", + [":metal_tone2:"] = "\uD83E\uDD18\uD83C\uDFFC", + [":metal_tone3:"] = "\uD83E\uDD18\uD83C\uDFFD", + [":metal_tone4:"] = "\uD83E\uDD18\uD83C\uDFFE", + [":metal_tone5:"] = "\uD83E\uDD18\uD83C\uDFFF", + [":metro:"] = "\uD83D\uDE87", + [":microbe:"] = "\uD83E\uDDA0", + [":microphone2:"] = "\uD83C\uDF99️", + [":microphone:"] = "\uD83C\uDFA4", + [":microscope:"] = "\uD83D\uDD2C", + [":middle_finger:"] = "\uD83D\uDD95", + [":middle_finger::skin-tone-1:"] = "\uD83D\uDD95\uD83C\uDFFB", + [":middle_finger::skin-tone-2:"] = "\uD83D\uDD95\uD83C\uDFFC", + [":middle_finger::skin-tone-3:"] = "\uD83D\uDD95\uD83C\uDFFD", + [":middle_finger::skin-tone-4:"] = "\uD83D\uDD95\uD83C\uDFFE", + [":middle_finger::skin-tone-5:"] = "\uD83D\uDD95\uD83C\uDFFF", + [":middle_finger_tone1:"] = "\uD83D\uDD95\uD83C\uDFFB", + [":middle_finger_tone2:"] = "\uD83D\uDD95\uD83C\uDFFC", + [":middle_finger_tone3:"] = "\uD83D\uDD95\uD83C\uDFFD", + [":middle_finger_tone4:"] = "\uD83D\uDD95\uD83C\uDFFE", + [":middle_finger_tone5:"] = "\uD83D\uDD95\uD83C\uDFFF", + [":military_medal:"] = "\uD83C\uDF96️", + [":milk:"] = "\uD83E\uDD5B", + [":milky_way:"] = "\uD83C\uDF0C", + [":minibus:"] = "\uD83D\uDE90", + [":minidisc:"] = "\uD83D\uDCBD", + [":mobile_phone_off:"] = "\uD83D\uDCF4", + [":money_mouth:"] = "\uD83E\uDD11", + [":money_mouth_face:"] = "\uD83E\uDD11", + [":money_with_wings:"] = "\uD83D\uDCB8", + [":moneybag:"] = "\uD83D\uDCB0", + [":monkey:"] = "\uD83D\uDC12", + [":monkey_face:"] = "\uD83D\uDC35", + [":monorail:"] = "\uD83D\uDE9D", + [":moon_cake:"] = "\uD83E\uDD6E", + [":mortar_board:"] = "\uD83C\uDF93", + [":mosque:"] = "\uD83D\uDD4C", + [":mosquito:"] = "\uD83E\uDD9F", + [":mother_christmas:"] = "\uD83E\uDD36", + [":mother_christmas::skin-tone-1:"] = "\uD83E\uDD36\uD83C\uDFFB", + [":mother_christmas::skin-tone-2:"] = "\uD83E\uDD36\uD83C\uDFFC", + [":mother_christmas::skin-tone-3:"] = "\uD83E\uDD36\uD83C\uDFFD", + [":mother_christmas::skin-tone-4:"] = "\uD83E\uDD36\uD83C\uDFFE", + [":mother_christmas::skin-tone-5:"] = "\uD83E\uDD36\uD83C\uDFFF", + [":mother_christmas_tone1:"] = "\uD83E\uDD36\uD83C\uDFFB", + [":mother_christmas_tone2:"] = "\uD83E\uDD36\uD83C\uDFFC", + [":mother_christmas_tone3:"] = "\uD83E\uDD36\uD83C\uDFFD", + [":mother_christmas_tone4:"] = "\uD83E\uDD36\uD83C\uDFFE", + [":mother_christmas_tone5:"] = "\uD83E\uDD36\uD83C\uDFFF", + [":motor_scooter:"] = "\uD83D\uDEF5", + [":motorbike:"] = "\uD83D\uDEF5", + [":motorboat:"] = "\uD83D\uDEE5️", + [":motorcycle:"] = "\uD83C\uDFCD️", + [":motorized_wheelchair:"] = "\uD83E\uDDBC", + [":motorway:"] = "\uD83D\uDEE3️", + [":mount_fuji:"] = "\uD83D\uDDFB", + [":mountain:"] = "⛰️", + [":mountain_bicyclist:"] = "\uD83D\uDEB5", + [":mountain_bicyclist::skin-tone-1:"] = "\uD83D\uDEB5\uD83C\uDFFB", + [":mountain_bicyclist::skin-tone-2:"] = "\uD83D\uDEB5\uD83C\uDFFC", + [":mountain_bicyclist::skin-tone-3:"] = "\uD83D\uDEB5\uD83C\uDFFD", + [":mountain_bicyclist::skin-tone-4:"] = "\uD83D\uDEB5\uD83C\uDFFE", + [":mountain_bicyclist::skin-tone-5:"] = "\uD83D\uDEB5\uD83C\uDFFF", + [":mountain_bicyclist_tone1:"] = "\uD83D\uDEB5\uD83C\uDFFB", + [":mountain_bicyclist_tone2:"] = "\uD83D\uDEB5\uD83C\uDFFC", + [":mountain_bicyclist_tone3:"] = "\uD83D\uDEB5\uD83C\uDFFD", + [":mountain_bicyclist_tone4:"] = "\uD83D\uDEB5\uD83C\uDFFE", + [":mountain_bicyclist_tone5:"] = "\uD83D\uDEB5\uD83C\uDFFF", + [":mountain_cableway:"] = "\uD83D\uDEA0", + [":mountain_railway:"] = "\uD83D\uDE9E", + [":mountain_snow:"] = "\uD83C\uDFD4️", + [":mouse2:"] = "\uD83D\uDC01", + [":mouse:"] = "\uD83D\uDC2D", + [":mouse_three_button:"] = "\uD83D\uDDB1️", + [":movie_camera:"] = "\uD83C\uDFA5", + [":moyai:"] = "\uD83D\uDDFF", + [":mrs_claus:"] = "\uD83E\uDD36", + [":mrs_claus::skin-tone-1:"] = "\uD83E\uDD36\uD83C\uDFFB", + [":mrs_claus::skin-tone-2:"] = "\uD83E\uDD36\uD83C\uDFFC", + [":mrs_claus::skin-tone-3:"] = "\uD83E\uDD36\uD83C\uDFFD", + [":mrs_claus::skin-tone-4:"] = "\uD83E\uDD36\uD83C\uDFFE", + [":mrs_claus::skin-tone-5:"] = "\uD83E\uDD36\uD83C\uDFFF", + [":mrs_claus_tone1:"] = "\uD83E\uDD36\uD83C\uDFFB", + [":mrs_claus_tone2:"] = "\uD83E\uDD36\uD83C\uDFFC", + [":mrs_claus_tone3:"] = "\uD83E\uDD36\uD83C\uDFFD", + [":mrs_claus_tone4:"] = "\uD83E\uDD36\uD83C\uDFFE", + [":mrs_claus_tone5:"] = "\uD83E\uDD36\uD83C\uDFFF", + [":muscle:"] = "\uD83D\uDCAA", + [":muscle::skin-tone-1:"] = "\uD83D\uDCAA\uD83C\uDFFB", + [":muscle::skin-tone-2:"] = "\uD83D\uDCAA\uD83C\uDFFC", + [":muscle::skin-tone-3:"] = "\uD83D\uDCAA\uD83C\uDFFD", + [":muscle::skin-tone-4:"] = "\uD83D\uDCAA\uD83C\uDFFE", + [":muscle::skin-tone-5:"] = "\uD83D\uDCAA\uD83C\uDFFF", + [":muscle_tone1:"] = "\uD83D\uDCAA\uD83C\uDFFB", + [":muscle_tone2:"] = "\uD83D\uDCAA\uD83C\uDFFC", + [":muscle_tone3:"] = "\uD83D\uDCAA\uD83C\uDFFD", + [":muscle_tone4:"] = "\uD83D\uDCAA\uD83C\uDFFE", + [":muscle_tone5:"] = "\uD83D\uDCAA\uD83C\uDFFF", + [":mushroom:"] = "\uD83C\uDF44", + [":musical_keyboard:"] = "\uD83C\uDFB9", + [":musical_note:"] = "\uD83C\uDFB5", + [":musical_score:"] = "\uD83C\uDFBC", + [":mute:"] = "\uD83D\uDD07", + [":nail_care:"] = "\uD83D\uDC85", + [":nail_care::skin-tone-1:"] = "\uD83D\uDC85\uD83C\uDFFB", + [":nail_care::skin-tone-2:"] = "\uD83D\uDC85\uD83C\uDFFC", + [":nail_care::skin-tone-3:"] = "\uD83D\uDC85\uD83C\uDFFD", + [":nail_care::skin-tone-4:"] = "\uD83D\uDC85\uD83C\uDFFE", + [":nail_care::skin-tone-5:"] = "\uD83D\uDC85\uD83C\uDFFF", + [":nail_care_tone1:"] = "\uD83D\uDC85\uD83C\uDFFB", + [":nail_care_tone2:"] = "\uD83D\uDC85\uD83C\uDFFC", + [":nail_care_tone3:"] = "\uD83D\uDC85\uD83C\uDFFD", + [":nail_care_tone4:"] = "\uD83D\uDC85\uD83C\uDFFE", + [":nail_care_tone5:"] = "\uD83D\uDC85\uD83C\uDFFF", + [":name_badge:"] = "\uD83D\uDCDB", + [":national_park:"] = "\uD83C\uDFDE️", + [":nauseated_face:"] = "\uD83E\uDD22", + [":nazar_amulet:"] = "\uD83E\uDDFF", + [":necktie:"] = "\uD83D\uDC54", + [":negative_squared_cross_mark:"] = "❎", + [":nerd:"] = "\uD83E\uDD13", + [":nerd_face:"] = "\uD83E\uDD13", + [":neutral_face:"] = "\uD83D\uDE10", + [":new:"] = "\uD83C\uDD95", + [":new_moon:"] = "\uD83C\uDF11", + [":new_moon_with_face:"] = "\uD83C\uDF1A", + [":newspaper2:"] = "\uD83D\uDDDE️", + [":newspaper:"] = "\uD83D\uDCF0", + [":next_track:"] = "⏭️", + [":ng:"] = "\uD83C\uDD96", + [":night_with_stars:"] = "\uD83C\uDF03", + [":nine:"] = "9️⃣", + [":no_bell:"] = "\uD83D\uDD15", + [":no_bicycles:"] = "\uD83D\uDEB3", + [":no_entry:"] = "⛔", + [":no_entry_sign:"] = "\uD83D\uDEAB", + [":no_good:"] = "\uD83D\uDE45", + [":no_good::skin-tone-1:"] = "\uD83D\uDE45\uD83C\uDFFB", + [":no_good::skin-tone-2:"] = "\uD83D\uDE45\uD83C\uDFFC", + [":no_good::skin-tone-3:"] = "\uD83D\uDE45\uD83C\uDFFD", + [":no_good::skin-tone-4:"] = "\uD83D\uDE45\uD83C\uDFFE", + [":no_good::skin-tone-5:"] = "\uD83D\uDE45\uD83C\uDFFF", + [":no_good_tone1:"] = "\uD83D\uDE45\uD83C\uDFFB", + [":no_good_tone2:"] = "\uD83D\uDE45\uD83C\uDFFC", + [":no_good_tone3:"] = "\uD83D\uDE45\uD83C\uDFFD", + [":no_good_tone4:"] = "\uD83D\uDE45\uD83C\uDFFE", + [":no_good_tone5:"] = "\uD83D\uDE45\uD83C\uDFFF", + [":no_mobile_phones:"] = "\uD83D\uDCF5", + [":no_mouth:"] = "\uD83D\uDE36", + [":no_pedestrians:"] = "\uD83D\uDEB7", + [":no_smoking:"] = "\uD83D\uDEAD", + [":non_potable_water:"] = "\uD83D\uDEB1", + [":nose:"] = "\uD83D\uDC43", + [":nose::skin-tone-1:"] = "\uD83D\uDC43\uD83C\uDFFB", + [":nose::skin-tone-2:"] = "\uD83D\uDC43\uD83C\uDFFC", + [":nose::skin-tone-3:"] = "\uD83D\uDC43\uD83C\uDFFD", + [":nose::skin-tone-4:"] = "\uD83D\uDC43\uD83C\uDFFE", + [":nose::skin-tone-5:"] = "\uD83D\uDC43\uD83C\uDFFF", + [":nose_tone1:"] = "\uD83D\uDC43\uD83C\uDFFB", + [":nose_tone2:"] = "\uD83D\uDC43\uD83C\uDFFC", + [":nose_tone3:"] = "\uD83D\uDC43\uD83C\uDFFD", + [":nose_tone4:"] = "\uD83D\uDC43\uD83C\uDFFE", + [":nose_tone5:"] = "\uD83D\uDC43\uD83C\uDFFF", + [":notebook:"] = "\uD83D\uDCD3", + [":notebook_with_decorative_cover:"] = "\uD83D\uDCD4", + [":notepad_spiral:"] = "\uD83D\uDDD2️", + [":notes:"] = "\uD83C\uDFB6", + [":nut_and_bolt:"] = "\uD83D\uDD29", + [":o"] = "\uD83D\uDE2E", + [":o2:"] = "\uD83C\uDD7E️", + [":o:"] = "⭕", + [":ocean:"] = "\uD83C\uDF0A", + [":octagonal_sign:"] = "\uD83D\uDED1", + [":octopus:"] = "\uD83D\uDC19", + [":oden:"] = "\uD83C\uDF62", + [":office:"] = "\uD83C\uDFE2", + [":oil:"] = "\uD83D\uDEE2️", + [":oil_drum:"] = "\uD83D\uDEE2️", + [":ok:"] = "\uD83C\uDD97", + [":ok_hand:"] = "\uD83D\uDC4C", + [":ok_hand::skin-tone-1:"] = "\uD83D\uDC4C\uD83C\uDFFB", + [":ok_hand::skin-tone-2:"] = "\uD83D\uDC4C\uD83C\uDFFC", + [":ok_hand::skin-tone-3:"] = "\uD83D\uDC4C\uD83C\uDFFD", + [":ok_hand::skin-tone-4:"] = "\uD83D\uDC4C\uD83C\uDFFE", + [":ok_hand::skin-tone-5:"] = "\uD83D\uDC4C\uD83C\uDFFF", + [":ok_hand_tone1:"] = "\uD83D\uDC4C\uD83C\uDFFB", + [":ok_hand_tone2:"] = "\uD83D\uDC4C\uD83C\uDFFC", + [":ok_hand_tone3:"] = "\uD83D\uDC4C\uD83C\uDFFD", + [":ok_hand_tone4:"] = "\uD83D\uDC4C\uD83C\uDFFE", + [":ok_hand_tone5:"] = "\uD83D\uDC4C\uD83C\uDFFF", + [":ok_woman:"] = "\uD83D\uDE46", + [":ok_woman::skin-tone-1:"] = "\uD83D\uDE46\uD83C\uDFFB", + [":ok_woman::skin-tone-2:"] = "\uD83D\uDE46\uD83C\uDFFC", + [":ok_woman::skin-tone-3:"] = "\uD83D\uDE46\uD83C\uDFFD", + [":ok_woman::skin-tone-4:"] = "\uD83D\uDE46\uD83C\uDFFE", + [":ok_woman::skin-tone-5:"] = "\uD83D\uDE46\uD83C\uDFFF", + [":ok_woman_tone1:"] = "\uD83D\uDE46\uD83C\uDFFB", + [":ok_woman_tone2:"] = "\uD83D\uDE46\uD83C\uDFFC", + [":ok_woman_tone3:"] = "\uD83D\uDE46\uD83C\uDFFD", + [":ok_woman_tone4:"] = "\uD83D\uDE46\uD83C\uDFFE", + [":ok_woman_tone5:"] = "\uD83D\uDE46\uD83C\uDFFF", + [":old_key:"] = "\uD83D\uDDDD️", + [":older_adult:"] = "\uD83E\uDDD3", + [":older_adult::skin-tone-1:"] = "\uD83E\uDDD3\uD83C\uDFFB", + [":older_adult::skin-tone-2:"] = "\uD83E\uDDD3\uD83C\uDFFC", + [":older_adult::skin-tone-3:"] = "\uD83E\uDDD3\uD83C\uDFFD", + [":older_adult::skin-tone-4:"] = "\uD83E\uDDD3\uD83C\uDFFE", + [":older_adult::skin-tone-5:"] = "\uD83E\uDDD3\uD83C\uDFFF", + [":older_adult_dark_skin_tone:"] = "\uD83E\uDDD3\uD83C\uDFFF", + [":older_adult_light_skin_tone:"] = "\uD83E\uDDD3\uD83C\uDFFB", + [":older_adult_medium_dark_skin_tone:"] = "\uD83E\uDDD3\uD83C\uDFFE", + [":older_adult_medium_light_skin_tone:"] = "\uD83E\uDDD3\uD83C\uDFFC", + [":older_adult_medium_skin_tone:"] = "\uD83E\uDDD3\uD83C\uDFFD", + [":older_adult_tone1:"] = "\uD83E\uDDD3\uD83C\uDFFB", + [":older_adult_tone2:"] = "\uD83E\uDDD3\uD83C\uDFFC", + [":older_adult_tone3:"] = "\uD83E\uDDD3\uD83C\uDFFD", + [":older_adult_tone4:"] = "\uD83E\uDDD3\uD83C\uDFFE", + [":older_adult_tone5:"] = "\uD83E\uDDD3\uD83C\uDFFF", + [":older_man:"] = "\uD83D\uDC74", + [":older_man::skin-tone-1:"] = "\uD83D\uDC74\uD83C\uDFFB", + [":older_man::skin-tone-2:"] = "\uD83D\uDC74\uD83C\uDFFC", + [":older_man::skin-tone-3:"] = "\uD83D\uDC74\uD83C\uDFFD", + [":older_man::skin-tone-4:"] = "\uD83D\uDC74\uD83C\uDFFE", + [":older_man::skin-tone-5:"] = "\uD83D\uDC74\uD83C\uDFFF", + [":older_man_tone1:"] = "\uD83D\uDC74\uD83C\uDFFB", + [":older_man_tone2:"] = "\uD83D\uDC74\uD83C\uDFFC", + [":older_man_tone3:"] = "\uD83D\uDC74\uD83C\uDFFD", + [":older_man_tone4:"] = "\uD83D\uDC74\uD83C\uDFFE", + [":older_man_tone5:"] = "\uD83D\uDC74\uD83C\uDFFF", + [":older_woman:"] = "\uD83D\uDC75", + [":older_woman::skin-tone-1:"] = "\uD83D\uDC75\uD83C\uDFFB", + [":older_woman::skin-tone-2:"] = "\uD83D\uDC75\uD83C\uDFFC", + [":older_woman::skin-tone-3:"] = "\uD83D\uDC75\uD83C\uDFFD", + [":older_woman::skin-tone-4:"] = "\uD83D\uDC75\uD83C\uDFFE", + [":older_woman::skin-tone-5:"] = "\uD83D\uDC75\uD83C\uDFFF", + [":older_woman_tone1:"] = "\uD83D\uDC75\uD83C\uDFFB", + [":older_woman_tone2:"] = "\uD83D\uDC75\uD83C\uDFFC", + [":older_woman_tone3:"] = "\uD83D\uDC75\uD83C\uDFFD", + [":older_woman_tone4:"] = "\uD83D\uDC75\uD83C\uDFFE", + [":older_woman_tone5:"] = "\uD83D\uDC75\uD83C\uDFFF", + [":om_symbol:"] = "\uD83D\uDD49️", + [":on:"] = "\uD83D\uDD1B", + [":oncoming_automobile:"] = "\uD83D\uDE98", + [":oncoming_bus:"] = "\uD83D\uDE8D", + [":oncoming_police_car:"] = "\uD83D\uDE94", + [":oncoming_taxi:"] = "\uD83D\uDE96", + [":one:"] = "1️⃣", + [":one_piece_swimsuit:"] = "\uD83E\uDE71", + [":onion:"] = "\uD83E\uDDC5", + [":open_file_folder:"] = "\uD83D\uDCC2", + [":open_hands:"] = "\uD83D\uDC50", + [":open_hands::skin-tone-1:"] = "\uD83D\uDC50\uD83C\uDFFB", + [":open_hands::skin-tone-2:"] = "\uD83D\uDC50\uD83C\uDFFC", + [":open_hands::skin-tone-3:"] = "\uD83D\uDC50\uD83C\uDFFD", + [":open_hands::skin-tone-4:"] = "\uD83D\uDC50\uD83C\uDFFE", + [":open_hands::skin-tone-5:"] = "\uD83D\uDC50\uD83C\uDFFF", + [":open_hands_tone1:"] = "\uD83D\uDC50\uD83C\uDFFB", + [":open_hands_tone2:"] = "\uD83D\uDC50\uD83C\uDFFC", + [":open_hands_tone3:"] = "\uD83D\uDC50\uD83C\uDFFD", + [":open_hands_tone4:"] = "\uD83D\uDC50\uD83C\uDFFE", + [":open_hands_tone5:"] = "\uD83D\uDC50\uD83C\uDFFF", + [":open_mouth:"] = "\uD83D\uDE2E", + [":ophiuchus:"] = "⛎", + [":orange_book:"] = "\uD83D\uDCD9", + [":orange_circle:"] = "\uD83D\uDFE0", + [":orange_heart:"] = "\uD83E\uDDE1", + [":orange_square:"] = "\uD83D\uDFE7", + [":orangutan:"] = "\uD83E\uDDA7", + [":orthodox_cross:"] = "☦️", + [":otter:"] = "\uD83E\uDDA6", + [":outbox_tray:"] = "\uD83D\uDCE4", + [":owl:"] = "\uD83E\uDD89", + [":ox:"] = "\uD83D\uDC02", + [":oyster:"] = "\uD83E\uDDAA", + [":package:"] = "\uD83D\uDCE6", + [":paella:"] = "\uD83E\uDD58", + [":page_facing_up:"] = "\uD83D\uDCC4", + [":page_with_curl:"] = "\uD83D\uDCC3", + [":pager:"] = "\uD83D\uDCDF", + [":paintbrush:"] = "\uD83D\uDD8C️", + [":palm_tree:"] = "\uD83C\uDF34", + [":palms_up_together:"] = "\uD83E\uDD32", + [":palms_up_together::skin-tone-1:"] = "\uD83E\uDD32\uD83C\uDFFB", + [":palms_up_together::skin-tone-2:"] = "\uD83E\uDD32\uD83C\uDFFC", + [":palms_up_together::skin-tone-3:"] = "\uD83E\uDD32\uD83C\uDFFD", + [":palms_up_together::skin-tone-4:"] = "\uD83E\uDD32\uD83C\uDFFE", + [":palms_up_together::skin-tone-5:"] = "\uD83E\uDD32\uD83C\uDFFF", + [":palms_up_together_dark_skin_tone:"] = "\uD83E\uDD32\uD83C\uDFFF", + [":palms_up_together_light_skin_tone:"] = "\uD83E\uDD32\uD83C\uDFFB", + [":palms_up_together_medium_dark_skin_tone:"] = "\uD83E\uDD32\uD83C\uDFFE", + [":palms_up_together_medium_light_skin_tone:"] = "\uD83E\uDD32\uD83C\uDFFC", + [":palms_up_together_medium_skin_tone:"] = "\uD83E\uDD32\uD83C\uDFFD", + [":palms_up_together_tone1:"] = "\uD83E\uDD32\uD83C\uDFFB", + [":palms_up_together_tone2:"] = "\uD83E\uDD32\uD83C\uDFFC", + [":palms_up_together_tone3:"] = "\uD83E\uDD32\uD83C\uDFFD", + [":palms_up_together_tone4:"] = "\uD83E\uDD32\uD83C\uDFFE", + [":palms_up_together_tone5:"] = "\uD83E\uDD32\uD83C\uDFFF", + [":pancakes:"] = "\uD83E\uDD5E", + [":panda_face:"] = "\uD83D\uDC3C", + [":paperclip:"] = "\uD83D\uDCCE", + [":paperclips:"] = "\uD83D\uDD87️", + [":parachute:"] = "\uD83E\uDE82", + [":park:"] = "\uD83C\uDFDE️", + [":parking:"] = "\uD83C\uDD7F️", + [":parrot:"] = "\uD83E\uDD9C", + [":part_alternation_mark:"] = "〽️", + [":partly_sunny:"] = "⛅", + [":partying_face:"] = "\uD83E\uDD73", + [":passenger_ship:"] = "\uD83D\uDEF3️", + [":passport_control:"] = "\uD83D\uDEC2", + [":pause_button:"] = "⏸️", + [":paw_prints:"] = "\uD83D\uDC3E", + [":peace:"] = "☮️", + [":peace_symbol:"] = "☮️", + [":peach:"] = "\uD83C\uDF51", + [":peacock:"] = "\uD83E\uDD9A", + [":peanuts:"] = "\uD83E\uDD5C", + [":pear:"] = "\uD83C\uDF50", + [":pen_ballpoint:"] = "\uD83D\uDD8A️", + [":pen_fountain:"] = "\uD83D\uDD8B️", + [":pencil2:"] = "✏️", + [":pencil:"] = "\uD83D\uDCDD", + [":penguin:"] = "\uD83D\uDC27", + [":pensive:"] = "\uD83D\uDE14", + [":people_holding_hands:"] = "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", + [":people_with_bunny_ears_partying:"] = "\uD83D\uDC6F", + [":people_wrestling:"] = "\uD83E\uDD3C", + [":performing_arts:"] = "\uD83C\uDFAD", + [":persevere:"] = "\uD83D\uDE23", + [":person_biking:"] = "\uD83D\uDEB4", + [":person_biking::skin-tone-1:"] = "\uD83D\uDEB4\uD83C\uDFFB", + [":person_biking::skin-tone-2:"] = "\uD83D\uDEB4\uD83C\uDFFC", + [":person_biking::skin-tone-3:"] = "\uD83D\uDEB4\uD83C\uDFFD", + [":person_biking::skin-tone-4:"] = "\uD83D\uDEB4\uD83C\uDFFE", + [":person_biking::skin-tone-5:"] = "\uD83D\uDEB4\uD83C\uDFFF", + [":person_biking_tone1:"] = "\uD83D\uDEB4\uD83C\uDFFB", + [":person_biking_tone2:"] = "\uD83D\uDEB4\uD83C\uDFFC", + [":person_biking_tone3:"] = "\uD83D\uDEB4\uD83C\uDFFD", + [":person_biking_tone4:"] = "\uD83D\uDEB4\uD83C\uDFFE", + [":person_biking_tone5:"] = "\uD83D\uDEB4\uD83C\uDFFF", + [":person_bouncing_ball:"] = "⛹️", + [":person_bouncing_ball::skin-tone-1:"] = "⛹\uD83C\uDFFB", + [":person_bouncing_ball::skin-tone-2:"] = "⛹\uD83C\uDFFC", + [":person_bouncing_ball::skin-tone-3:"] = "⛹\uD83C\uDFFD", + [":person_bouncing_ball::skin-tone-4:"] = "⛹\uD83C\uDFFE", + [":person_bouncing_ball::skin-tone-5:"] = "⛹\uD83C\uDFFF", + [":person_bouncing_ball_tone1:"] = "⛹\uD83C\uDFFB", + [":person_bouncing_ball_tone2:"] = "⛹\uD83C\uDFFC", + [":person_bouncing_ball_tone3:"] = "⛹\uD83C\uDFFD", + [":person_bouncing_ball_tone4:"] = "⛹\uD83C\uDFFE", + [":person_bouncing_ball_tone5:"] = "⛹\uD83C\uDFFF", + [":person_bowing:"] = "\uD83D\uDE47", + [":person_bowing::skin-tone-1:"] = "\uD83D\uDE47\uD83C\uDFFB", + [":person_bowing::skin-tone-2:"] = "\uD83D\uDE47\uD83C\uDFFC", + [":person_bowing::skin-tone-3:"] = "\uD83D\uDE47\uD83C\uDFFD", + [":person_bowing::skin-tone-4:"] = "\uD83D\uDE47\uD83C\uDFFE", + [":person_bowing::skin-tone-5:"] = "\uD83D\uDE47\uD83C\uDFFF", + [":person_bowing_tone1:"] = "\uD83D\uDE47\uD83C\uDFFB", + [":person_bowing_tone2:"] = "\uD83D\uDE47\uD83C\uDFFC", + [":person_bowing_tone3:"] = "\uD83D\uDE47\uD83C\uDFFD", + [":person_bowing_tone4:"] = "\uD83D\uDE47\uD83C\uDFFE", + [":person_bowing_tone5:"] = "\uD83D\uDE47\uD83C\uDFFF", + [":person_climbing:"] = "\uD83E\uDDD7", + [":person_climbing::skin-tone-1:"] = "\uD83E\uDDD7\uD83C\uDFFB", + [":person_climbing::skin-tone-2:"] = "\uD83E\uDDD7\uD83C\uDFFC", + [":person_climbing::skin-tone-3:"] = "\uD83E\uDDD7\uD83C\uDFFD", + [":person_climbing::skin-tone-4:"] = "\uD83E\uDDD7\uD83C\uDFFE", + [":person_climbing::skin-tone-5:"] = "\uD83E\uDDD7\uD83C\uDFFF", + [":person_climbing_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFF", + [":person_climbing_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFB", + [":person_climbing_medium_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFE", + [":person_climbing_medium_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFC", + [":person_climbing_medium_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFD", + [":person_climbing_tone1:"] = "\uD83E\uDDD7\uD83C\uDFFB", + [":person_climbing_tone2:"] = "\uD83E\uDDD7\uD83C\uDFFC", + [":person_climbing_tone3:"] = "\uD83E\uDDD7\uD83C\uDFFD", + [":person_climbing_tone4:"] = "\uD83E\uDDD7\uD83C\uDFFE", + [":person_climbing_tone5:"] = "\uD83E\uDDD7\uD83C\uDFFF", + [":person_doing_cartwheel:"] = "\uD83E\uDD38", + [":person_doing_cartwheel::skin-tone-1:"] = "\uD83E\uDD38\uD83C\uDFFB", + [":person_doing_cartwheel::skin-tone-2:"] = "\uD83E\uDD38\uD83C\uDFFC", + [":person_doing_cartwheel::skin-tone-3:"] = "\uD83E\uDD38\uD83C\uDFFD", + [":person_doing_cartwheel::skin-tone-4:"] = "\uD83E\uDD38\uD83C\uDFFE", + [":person_doing_cartwheel::skin-tone-5:"] = "\uD83E\uDD38\uD83C\uDFFF", + [":person_doing_cartwheel_tone1:"] = "\uD83E\uDD38\uD83C\uDFFB", + [":person_doing_cartwheel_tone2:"] = "\uD83E\uDD38\uD83C\uDFFC", + [":person_doing_cartwheel_tone3:"] = "\uD83E\uDD38\uD83C\uDFFD", + [":person_doing_cartwheel_tone4:"] = "\uD83E\uDD38\uD83C\uDFFE", + [":person_doing_cartwheel_tone5:"] = "\uD83E\uDD38\uD83C\uDFFF", + [":person_facepalming:"] = "\uD83E\uDD26", + [":person_facepalming::skin-tone-1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":person_facepalming::skin-tone-2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":person_facepalming::skin-tone-3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":person_facepalming::skin-tone-4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":person_facepalming::skin-tone-5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":person_facepalming_tone1:"] = "\uD83E\uDD26\uD83C\uDFFB", + [":person_facepalming_tone2:"] = "\uD83E\uDD26\uD83C\uDFFC", + [":person_facepalming_tone3:"] = "\uD83E\uDD26\uD83C\uDFFD", + [":person_facepalming_tone4:"] = "\uD83E\uDD26\uD83C\uDFFE", + [":person_facepalming_tone5:"] = "\uD83E\uDD26\uD83C\uDFFF", + [":person_fencing:"] = "\uD83E\uDD3A", + [":person_frowning:"] = "\uD83D\uDE4D", + [":person_frowning::skin-tone-1:"] = "\uD83D\uDE4D\uD83C\uDFFB", + [":person_frowning::skin-tone-2:"] = "\uD83D\uDE4D\uD83C\uDFFC", + [":person_frowning::skin-tone-3:"] = "\uD83D\uDE4D\uD83C\uDFFD", + [":person_frowning::skin-tone-4:"] = "\uD83D\uDE4D\uD83C\uDFFE", + [":person_frowning::skin-tone-5:"] = "\uD83D\uDE4D\uD83C\uDFFF", + [":person_frowning_tone1:"] = "\uD83D\uDE4D\uD83C\uDFFB", + [":person_frowning_tone2:"] = "\uD83D\uDE4D\uD83C\uDFFC", + [":person_frowning_tone3:"] = "\uD83D\uDE4D\uD83C\uDFFD", + [":person_frowning_tone4:"] = "\uD83D\uDE4D\uD83C\uDFFE", + [":person_frowning_tone5:"] = "\uD83D\uDE4D\uD83C\uDFFF", + [":person_gesturing_no:"] = "\uD83D\uDE45", + [":person_gesturing_no::skin-tone-1:"] = "\uD83D\uDE45\uD83C\uDFFB", + [":person_gesturing_no::skin-tone-2:"] = "\uD83D\uDE45\uD83C\uDFFC", + [":person_gesturing_no::skin-tone-3:"] = "\uD83D\uDE45\uD83C\uDFFD", + [":person_gesturing_no::skin-tone-4:"] = "\uD83D\uDE45\uD83C\uDFFE", + [":person_gesturing_no::skin-tone-5:"] = "\uD83D\uDE45\uD83C\uDFFF", + [":person_gesturing_no_tone1:"] = "\uD83D\uDE45\uD83C\uDFFB", + [":person_gesturing_no_tone2:"] = "\uD83D\uDE45\uD83C\uDFFC", + [":person_gesturing_no_tone3:"] = "\uD83D\uDE45\uD83C\uDFFD", + [":person_gesturing_no_tone4:"] = "\uD83D\uDE45\uD83C\uDFFE", + [":person_gesturing_no_tone5:"] = "\uD83D\uDE45\uD83C\uDFFF", + [":person_gesturing_ok:"] = "\uD83D\uDE46", + [":person_gesturing_ok::skin-tone-1:"] = "\uD83D\uDE46\uD83C\uDFFB", + [":person_gesturing_ok::skin-tone-2:"] = "\uD83D\uDE46\uD83C\uDFFC", + [":person_gesturing_ok::skin-tone-3:"] = "\uD83D\uDE46\uD83C\uDFFD", + [":person_gesturing_ok::skin-tone-4:"] = "\uD83D\uDE46\uD83C\uDFFE", + [":person_gesturing_ok::skin-tone-5:"] = "\uD83D\uDE46\uD83C\uDFFF", + [":person_gesturing_ok_tone1:"] = "\uD83D\uDE46\uD83C\uDFFB", + [":person_gesturing_ok_tone2:"] = "\uD83D\uDE46\uD83C\uDFFC", + [":person_gesturing_ok_tone3:"] = "\uD83D\uDE46\uD83C\uDFFD", + [":person_gesturing_ok_tone4:"] = "\uD83D\uDE46\uD83C\uDFFE", + [":person_gesturing_ok_tone5:"] = "\uD83D\uDE46\uD83C\uDFFF", + [":person_getting_haircut:"] = "\uD83D\uDC87", + [":person_getting_haircut::skin-tone-1:"] = "\uD83D\uDC87\uD83C\uDFFB", + [":person_getting_haircut::skin-tone-2:"] = "\uD83D\uDC87\uD83C\uDFFC", + [":person_getting_haircut::skin-tone-3:"] = "\uD83D\uDC87\uD83C\uDFFD", + [":person_getting_haircut::skin-tone-4:"] = "\uD83D\uDC87\uD83C\uDFFE", + [":person_getting_haircut::skin-tone-5:"] = "\uD83D\uDC87\uD83C\uDFFF", + [":person_getting_haircut_tone1:"] = "\uD83D\uDC87\uD83C\uDFFB", + [":person_getting_haircut_tone2:"] = "\uD83D\uDC87\uD83C\uDFFC", + [":person_getting_haircut_tone3:"] = "\uD83D\uDC87\uD83C\uDFFD", + [":person_getting_haircut_tone4:"] = "\uD83D\uDC87\uD83C\uDFFE", + [":person_getting_haircut_tone5:"] = "\uD83D\uDC87\uD83C\uDFFF", + [":person_getting_massage:"] = "\uD83D\uDC86", + [":person_getting_massage::skin-tone-1:"] = "\uD83D\uDC86\uD83C\uDFFB", + [":person_getting_massage::skin-tone-2:"] = "\uD83D\uDC86\uD83C\uDFFC", + [":person_getting_massage::skin-tone-3:"] = "\uD83D\uDC86\uD83C\uDFFD", + [":person_getting_massage::skin-tone-4:"] = "\uD83D\uDC86\uD83C\uDFFE", + [":person_getting_massage::skin-tone-5:"] = "\uD83D\uDC86\uD83C\uDFFF", + [":person_getting_massage_tone1:"] = "\uD83D\uDC86\uD83C\uDFFB", + [":person_getting_massage_tone2:"] = "\uD83D\uDC86\uD83C\uDFFC", + [":person_getting_massage_tone3:"] = "\uD83D\uDC86\uD83C\uDFFD", + [":person_getting_massage_tone4:"] = "\uD83D\uDC86\uD83C\uDFFE", + [":person_getting_massage_tone5:"] = "\uD83D\uDC86\uD83C\uDFFF", + [":person_golfing:"] = "\uD83C\uDFCC️", + [":person_golfing::skin-tone-1:"] = "\uD83C\uDFCC\uD83C\uDFFB", + [":person_golfing::skin-tone-2:"] = "\uD83C\uDFCC\uD83C\uDFFC", + [":person_golfing::skin-tone-3:"] = "\uD83C\uDFCC\uD83C\uDFFD", + [":person_golfing::skin-tone-4:"] = "\uD83C\uDFCC\uD83C\uDFFE", + [":person_golfing::skin-tone-5:"] = "\uD83C\uDFCC\uD83C\uDFFF", + [":person_golfing_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFF", + [":person_golfing_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFB", + [":person_golfing_medium_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFE", + [":person_golfing_medium_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFC", + [":person_golfing_medium_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFD", + [":person_golfing_tone1:"] = "\uD83C\uDFCC\uD83C\uDFFB", + [":person_golfing_tone2:"] = "\uD83C\uDFCC\uD83C\uDFFC", + [":person_golfing_tone3:"] = "\uD83C\uDFCC\uD83C\uDFFD", + [":person_golfing_tone4:"] = "\uD83C\uDFCC\uD83C\uDFFE", + [":person_golfing_tone5:"] = "\uD83C\uDFCC\uD83C\uDFFF", + [":person_in_bed_dark_skin_tone:"] = "\uD83D\uDECC\uD83C\uDFFF", + [":person_in_bed_light_skin_tone:"] = "\uD83D\uDECC\uD83C\uDFFB", + [":person_in_bed_medium_dark_skin_tone:"] = "\uD83D\uDECC\uD83C\uDFFE", + [":person_in_bed_medium_light_skin_tone:"] = "\uD83D\uDECC\uD83C\uDFFC", + [":person_in_bed_medium_skin_tone:"] = "\uD83D\uDECC\uD83C\uDFFD", + [":person_in_bed_tone1:"] = "\uD83D\uDECC\uD83C\uDFFB", + [":person_in_bed_tone2:"] = "\uD83D\uDECC\uD83C\uDFFC", + [":person_in_bed_tone3:"] = "\uD83D\uDECC\uD83C\uDFFD", + [":person_in_bed_tone4:"] = "\uD83D\uDECC\uD83C\uDFFE", + [":person_in_bed_tone5:"] = "\uD83D\uDECC\uD83C\uDFFF", + [":person_in_lotus_position:"] = "\uD83E\uDDD8", + [":person_in_lotus_position::skin-tone-1:"] = "\uD83E\uDDD8\uD83C\uDFFB", + [":person_in_lotus_position::skin-tone-2:"] = "\uD83E\uDDD8\uD83C\uDFFC", + [":person_in_lotus_position::skin-tone-3:"] = "\uD83E\uDDD8\uD83C\uDFFD", + [":person_in_lotus_position::skin-tone-4:"] = "\uD83E\uDDD8\uD83C\uDFFE", + [":person_in_lotus_position::skin-tone-5:"] = "\uD83E\uDDD8\uD83C\uDFFF", + [":person_in_lotus_position_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFF", + [":person_in_lotus_position_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFB", + [":person_in_lotus_position_medium_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFE", + [":person_in_lotus_position_medium_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFC", + [":person_in_lotus_position_medium_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFD", + [":person_in_lotus_position_tone1:"] = "\uD83E\uDDD8\uD83C\uDFFB", + [":person_in_lotus_position_tone2:"] = "\uD83E\uDDD8\uD83C\uDFFC", + [":person_in_lotus_position_tone3:"] = "\uD83E\uDDD8\uD83C\uDFFD", + [":person_in_lotus_position_tone4:"] = "\uD83E\uDDD8\uD83C\uDFFE", + [":person_in_lotus_position_tone5:"] = "\uD83E\uDDD8\uD83C\uDFFF", + [":person_in_steamy_room:"] = "\uD83E\uDDD6", + [":person_in_steamy_room::skin-tone-1:"] = "\uD83E\uDDD6\uD83C\uDFFB", + [":person_in_steamy_room::skin-tone-2:"] = "\uD83E\uDDD6\uD83C\uDFFC", + [":person_in_steamy_room::skin-tone-3:"] = "\uD83E\uDDD6\uD83C\uDFFD", + [":person_in_steamy_room::skin-tone-4:"] = "\uD83E\uDDD6\uD83C\uDFFE", + [":person_in_steamy_room::skin-tone-5:"] = "\uD83E\uDDD6\uD83C\uDFFF", + [":person_in_steamy_room_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFF", + [":person_in_steamy_room_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFB", + [":person_in_steamy_room_medium_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFE", + [":person_in_steamy_room_medium_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFC", + [":person_in_steamy_room_medium_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFD", + [":person_in_steamy_room_tone1:"] = "\uD83E\uDDD6\uD83C\uDFFB", + [":person_in_steamy_room_tone2:"] = "\uD83E\uDDD6\uD83C\uDFFC", + [":person_in_steamy_room_tone3:"] = "\uD83E\uDDD6\uD83C\uDFFD", + [":person_in_steamy_room_tone4:"] = "\uD83E\uDDD6\uD83C\uDFFE", + [":person_in_steamy_room_tone5:"] = "\uD83E\uDDD6\uD83C\uDFFF", + [":person_juggling:"] = "\uD83E\uDD39", + [":person_juggling::skin-tone-1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":person_juggling::skin-tone-2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":person_juggling::skin-tone-3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":person_juggling::skin-tone-4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":person_juggling::skin-tone-5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":person_juggling_tone1:"] = "\uD83E\uDD39\uD83C\uDFFB", + [":person_juggling_tone2:"] = "\uD83E\uDD39\uD83C\uDFFC", + [":person_juggling_tone3:"] = "\uD83E\uDD39\uD83C\uDFFD", + [":person_juggling_tone4:"] = "\uD83E\uDD39\uD83C\uDFFE", + [":person_juggling_tone5:"] = "\uD83E\uDD39\uD83C\uDFFF", + [":person_kneeling:"] = "\uD83E\uDDCE", + [":person_kneeling::skin-tone-1:"] = "\uD83E\uDDCE\uD83C\uDFFB", + [":person_kneeling::skin-tone-2:"] = "\uD83E\uDDCE\uD83C\uDFFC", + [":person_kneeling::skin-tone-3:"] = "\uD83E\uDDCE\uD83C\uDFFD", + [":person_kneeling::skin-tone-4:"] = "\uD83E\uDDCE\uD83C\uDFFE", + [":person_kneeling::skin-tone-5:"] = "\uD83E\uDDCE\uD83C\uDFFF", + [":person_kneeling_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFF", + [":person_kneeling_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFB", + [":person_kneeling_medium_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFE", + [":person_kneeling_medium_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFC", + [":person_kneeling_medium_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFD", + [":person_kneeling_tone1:"] = "\uD83E\uDDCE\uD83C\uDFFB", + [":person_kneeling_tone2:"] = "\uD83E\uDDCE\uD83C\uDFFC", + [":person_kneeling_tone3:"] = "\uD83E\uDDCE\uD83C\uDFFD", + [":person_kneeling_tone4:"] = "\uD83E\uDDCE\uD83C\uDFFE", + [":person_kneeling_tone5:"] = "\uD83E\uDDCE\uD83C\uDFFF", + [":person_lifting_weights:"] = "\uD83C\uDFCB️", + [":person_lifting_weights::skin-tone-1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":person_lifting_weights::skin-tone-2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":person_lifting_weights::skin-tone-3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":person_lifting_weights::skin-tone-4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":person_lifting_weights::skin-tone-5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":person_lifting_weights_tone1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":person_lifting_weights_tone2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":person_lifting_weights_tone3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":person_lifting_weights_tone4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":person_lifting_weights_tone5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":person_mountain_biking:"] = "\uD83D\uDEB5", + [":person_mountain_biking::skin-tone-1:"] = "\uD83D\uDEB5\uD83C\uDFFB", + [":person_mountain_biking::skin-tone-2:"] = "\uD83D\uDEB5\uD83C\uDFFC", + [":person_mountain_biking::skin-tone-3:"] = "\uD83D\uDEB5\uD83C\uDFFD", + [":person_mountain_biking::skin-tone-4:"] = "\uD83D\uDEB5\uD83C\uDFFE", + [":person_mountain_biking::skin-tone-5:"] = "\uD83D\uDEB5\uD83C\uDFFF", + [":person_mountain_biking_tone1:"] = "\uD83D\uDEB5\uD83C\uDFFB", + [":person_mountain_biking_tone2:"] = "\uD83D\uDEB5\uD83C\uDFFC", + [":person_mountain_biking_tone3:"] = "\uD83D\uDEB5\uD83C\uDFFD", + [":person_mountain_biking_tone4:"] = "\uD83D\uDEB5\uD83C\uDFFE", + [":person_mountain_biking_tone5:"] = "\uD83D\uDEB5\uD83C\uDFFF", + [":person_playing_handball:"] = "\uD83E\uDD3E", + [":person_playing_handball::skin-tone-1:"] = "\uD83E\uDD3E\uD83C\uDFFB", + [":person_playing_handball::skin-tone-2:"] = "\uD83E\uDD3E\uD83C\uDFFC", + [":person_playing_handball::skin-tone-3:"] = "\uD83E\uDD3E\uD83C\uDFFD", + [":person_playing_handball::skin-tone-4:"] = "\uD83E\uDD3E\uD83C\uDFFE", + [":person_playing_handball::skin-tone-5:"] = "\uD83E\uDD3E\uD83C\uDFFF", + [":person_playing_handball_tone1:"] = "\uD83E\uDD3E\uD83C\uDFFB", + [":person_playing_handball_tone2:"] = "\uD83E\uDD3E\uD83C\uDFFC", + [":person_playing_handball_tone3:"] = "\uD83E\uDD3E\uD83C\uDFFD", + [":person_playing_handball_tone4:"] = "\uD83E\uDD3E\uD83C\uDFFE", + [":person_playing_handball_tone5:"] = "\uD83E\uDD3E\uD83C\uDFFF", + [":person_playing_water_polo:"] = "\uD83E\uDD3D", + [":person_playing_water_polo::skin-tone-1:"] = "\uD83E\uDD3D\uD83C\uDFFB", + [":person_playing_water_polo::skin-tone-2:"] = "\uD83E\uDD3D\uD83C\uDFFC", + [":person_playing_water_polo::skin-tone-3:"] = "\uD83E\uDD3D\uD83C\uDFFD", + [":person_playing_water_polo::skin-tone-4:"] = "\uD83E\uDD3D\uD83C\uDFFE", + [":person_playing_water_polo::skin-tone-5:"] = "\uD83E\uDD3D\uD83C\uDFFF", + [":person_playing_water_polo_tone1:"] = "\uD83E\uDD3D\uD83C\uDFFB", + [":person_playing_water_polo_tone2:"] = "\uD83E\uDD3D\uD83C\uDFFC", + [":person_playing_water_polo_tone3:"] = "\uD83E\uDD3D\uD83C\uDFFD", + [":person_playing_water_polo_tone4:"] = "\uD83E\uDD3D\uD83C\uDFFE", + [":person_playing_water_polo_tone5:"] = "\uD83E\uDD3D\uD83C\uDFFF", + [":person_pouting:"] = "\uD83D\uDE4E", + [":person_pouting::skin-tone-1:"] = "\uD83D\uDE4E\uD83C\uDFFB", + [":person_pouting::skin-tone-2:"] = "\uD83D\uDE4E\uD83C\uDFFC", + [":person_pouting::skin-tone-3:"] = "\uD83D\uDE4E\uD83C\uDFFD", + [":person_pouting::skin-tone-4:"] = "\uD83D\uDE4E\uD83C\uDFFE", + [":person_pouting::skin-tone-5:"] = "\uD83D\uDE4E\uD83C\uDFFF", + [":person_pouting_tone1:"] = "\uD83D\uDE4E\uD83C\uDFFB", + [":person_pouting_tone2:"] = "\uD83D\uDE4E\uD83C\uDFFC", + [":person_pouting_tone3:"] = "\uD83D\uDE4E\uD83C\uDFFD", + [":person_pouting_tone4:"] = "\uD83D\uDE4E\uD83C\uDFFE", + [":person_pouting_tone5:"] = "\uD83D\uDE4E\uD83C\uDFFF", + [":person_raising_hand:"] = "\uD83D\uDE4B", + [":person_raising_hand::skin-tone-1:"] = "\uD83D\uDE4B\uD83C\uDFFB", + [":person_raising_hand::skin-tone-2:"] = "\uD83D\uDE4B\uD83C\uDFFC", + [":person_raising_hand::skin-tone-3:"] = "\uD83D\uDE4B\uD83C\uDFFD", + [":person_raising_hand::skin-tone-4:"] = "\uD83D\uDE4B\uD83C\uDFFE", + [":person_raising_hand::skin-tone-5:"] = "\uD83D\uDE4B\uD83C\uDFFF", + [":person_raising_hand_tone1:"] = "\uD83D\uDE4B\uD83C\uDFFB", + [":person_raising_hand_tone2:"] = "\uD83D\uDE4B\uD83C\uDFFC", + [":person_raising_hand_tone3:"] = "\uD83D\uDE4B\uD83C\uDFFD", + [":person_raising_hand_tone4:"] = "\uD83D\uDE4B\uD83C\uDFFE", + [":person_raising_hand_tone5:"] = "\uD83D\uDE4B\uD83C\uDFFF", + [":person_rowing_boat:"] = "\uD83D\uDEA3", + [":person_rowing_boat::skin-tone-1:"] = "\uD83D\uDEA3\uD83C\uDFFB", + [":person_rowing_boat::skin-tone-2:"] = "\uD83D\uDEA3\uD83C\uDFFC", + [":person_rowing_boat::skin-tone-3:"] = "\uD83D\uDEA3\uD83C\uDFFD", + [":person_rowing_boat::skin-tone-4:"] = "\uD83D\uDEA3\uD83C\uDFFE", + [":person_rowing_boat::skin-tone-5:"] = "\uD83D\uDEA3\uD83C\uDFFF", + [":person_rowing_boat_tone1:"] = "\uD83D\uDEA3\uD83C\uDFFB", + [":person_rowing_boat_tone2:"] = "\uD83D\uDEA3\uD83C\uDFFC", + [":person_rowing_boat_tone3:"] = "\uD83D\uDEA3\uD83C\uDFFD", + [":person_rowing_boat_tone4:"] = "\uD83D\uDEA3\uD83C\uDFFE", + [":person_rowing_boat_tone5:"] = "\uD83D\uDEA3\uD83C\uDFFF", + [":person_running:"] = "\uD83C\uDFC3", + [":person_running::skin-tone-1:"] = "\uD83C\uDFC3\uD83C\uDFFB", + [":person_running::skin-tone-2:"] = "\uD83C\uDFC3\uD83C\uDFFC", + [":person_running::skin-tone-3:"] = "\uD83C\uDFC3\uD83C\uDFFD", + [":person_running::skin-tone-4:"] = "\uD83C\uDFC3\uD83C\uDFFE", + [":person_running::skin-tone-5:"] = "\uD83C\uDFC3\uD83C\uDFFF", + [":person_running_tone1:"] = "\uD83C\uDFC3\uD83C\uDFFB", + [":person_running_tone2:"] = "\uD83C\uDFC3\uD83C\uDFFC", + [":person_running_tone3:"] = "\uD83C\uDFC3\uD83C\uDFFD", + [":person_running_tone4:"] = "\uD83C\uDFC3\uD83C\uDFFE", + [":person_running_tone5:"] = "\uD83C\uDFC3\uD83C\uDFFF", + [":person_shrugging:"] = "\uD83E\uDD37", + [":person_shrugging::skin-tone-1:"] = "\uD83E\uDD37\uD83C\uDFFB", + [":person_shrugging::skin-tone-2:"] = "\uD83E\uDD37\uD83C\uDFFC", + [":person_shrugging::skin-tone-3:"] = "\uD83E\uDD37\uD83C\uDFFD", + [":person_shrugging::skin-tone-4:"] = "\uD83E\uDD37\uD83C\uDFFE", + [":person_shrugging::skin-tone-5:"] = "\uD83E\uDD37\uD83C\uDFFF", + [":person_shrugging_tone1:"] = "\uD83E\uDD37\uD83C\uDFFB", + [":person_shrugging_tone2:"] = "\uD83E\uDD37\uD83C\uDFFC", + [":person_shrugging_tone3:"] = "\uD83E\uDD37\uD83C\uDFFD", + [":person_shrugging_tone4:"] = "\uD83E\uDD37\uD83C\uDFFE", + [":person_shrugging_tone5:"] = "\uD83E\uDD37\uD83C\uDFFF", + [":person_standing:"] = "\uD83E\uDDCD", + [":person_standing::skin-tone-1:"] = "\uD83E\uDDCD\uD83C\uDFFB", + [":person_standing::skin-tone-2:"] = "\uD83E\uDDCD\uD83C\uDFFC", + [":person_standing::skin-tone-3:"] = "\uD83E\uDDCD\uD83C\uDFFD", + [":person_standing::skin-tone-4:"] = "\uD83E\uDDCD\uD83C\uDFFE", + [":person_standing::skin-tone-5:"] = "\uD83E\uDDCD\uD83C\uDFFF", + [":person_standing_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFF", + [":person_standing_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFB", + [":person_standing_medium_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFE", + [":person_standing_medium_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFC", + [":person_standing_medium_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFD", + [":person_standing_tone1:"] = "\uD83E\uDDCD\uD83C\uDFFB", + [":person_standing_tone2:"] = "\uD83E\uDDCD\uD83C\uDFFC", + [":person_standing_tone3:"] = "\uD83E\uDDCD\uD83C\uDFFD", + [":person_standing_tone4:"] = "\uD83E\uDDCD\uD83C\uDFFE", + [":person_standing_tone5:"] = "\uD83E\uDDCD\uD83C\uDFFF", + [":person_surfing:"] = "\uD83C\uDFC4", + [":person_surfing::skin-tone-1:"] = "\uD83C\uDFC4\uD83C\uDFFB", + [":person_surfing::skin-tone-2:"] = "\uD83C\uDFC4\uD83C\uDFFC", + [":person_surfing::skin-tone-3:"] = "\uD83C\uDFC4\uD83C\uDFFD", + [":person_surfing::skin-tone-4:"] = "\uD83C\uDFC4\uD83C\uDFFE", + [":person_surfing::skin-tone-5:"] = "\uD83C\uDFC4\uD83C\uDFFF", + [":person_surfing_tone1:"] = "\uD83C\uDFC4\uD83C\uDFFB", + [":person_surfing_tone2:"] = "\uD83C\uDFC4\uD83C\uDFFC", + [":person_surfing_tone3:"] = "\uD83C\uDFC4\uD83C\uDFFD", + [":person_surfing_tone4:"] = "\uD83C\uDFC4\uD83C\uDFFE", + [":person_surfing_tone5:"] = "\uD83C\uDFC4\uD83C\uDFFF", + [":person_swimming:"] = "\uD83C\uDFCA", + [":person_swimming::skin-tone-1:"] = "\uD83C\uDFCA\uD83C\uDFFB", + [":person_swimming::skin-tone-2:"] = "\uD83C\uDFCA\uD83C\uDFFC", + [":person_swimming::skin-tone-3:"] = "\uD83C\uDFCA\uD83C\uDFFD", + [":person_swimming::skin-tone-4:"] = "\uD83C\uDFCA\uD83C\uDFFE", + [":person_swimming::skin-tone-5:"] = "\uD83C\uDFCA\uD83C\uDFFF", + [":person_swimming_tone1:"] = "\uD83C\uDFCA\uD83C\uDFFB", + [":person_swimming_tone2:"] = "\uD83C\uDFCA\uD83C\uDFFC", + [":person_swimming_tone3:"] = "\uD83C\uDFCA\uD83C\uDFFD", + [":person_swimming_tone4:"] = "\uD83C\uDFCA\uD83C\uDFFE", + [":person_swimming_tone5:"] = "\uD83C\uDFCA\uD83C\uDFFF", + [":person_tipping_hand:"] = "\uD83D\uDC81", + [":person_tipping_hand::skin-tone-1:"] = "\uD83D\uDC81\uD83C\uDFFB", + [":person_tipping_hand::skin-tone-2:"] = "\uD83D\uDC81\uD83C\uDFFC", + [":person_tipping_hand::skin-tone-3:"] = "\uD83D\uDC81\uD83C\uDFFD", + [":person_tipping_hand::skin-tone-4:"] = "\uD83D\uDC81\uD83C\uDFFE", + [":person_tipping_hand::skin-tone-5:"] = "\uD83D\uDC81\uD83C\uDFFF", + [":person_tipping_hand_tone1:"] = "\uD83D\uDC81\uD83C\uDFFB", + [":person_tipping_hand_tone2:"] = "\uD83D\uDC81\uD83C\uDFFC", + [":person_tipping_hand_tone3:"] = "\uD83D\uDC81\uD83C\uDFFD", + [":person_tipping_hand_tone4:"] = "\uD83D\uDC81\uD83C\uDFFE", + [":person_tipping_hand_tone5:"] = "\uD83D\uDC81\uD83C\uDFFF", + [":person_walking:"] = "\uD83D\uDEB6", + [":person_walking::skin-tone-1:"] = "\uD83D\uDEB6\uD83C\uDFFB", + [":person_walking::skin-tone-2:"] = "\uD83D\uDEB6\uD83C\uDFFC", + [":person_walking::skin-tone-3:"] = "\uD83D\uDEB6\uD83C\uDFFD", + [":person_walking::skin-tone-4:"] = "\uD83D\uDEB6\uD83C\uDFFE", + [":person_walking::skin-tone-5:"] = "\uD83D\uDEB6\uD83C\uDFFF", + [":person_walking_tone1:"] = "\uD83D\uDEB6\uD83C\uDFFB", + [":person_walking_tone2:"] = "\uD83D\uDEB6\uD83C\uDFFC", + [":person_walking_tone3:"] = "\uD83D\uDEB6\uD83C\uDFFD", + [":person_walking_tone4:"] = "\uD83D\uDEB6\uD83C\uDFFE", + [":person_walking_tone5:"] = "\uD83D\uDEB6\uD83C\uDFFF", + [":person_wearing_turban:"] = "\uD83D\uDC73", + [":person_wearing_turban::skin-tone-1:"] = "\uD83D\uDC73\uD83C\uDFFB", + [":person_wearing_turban::skin-tone-2:"] = "\uD83D\uDC73\uD83C\uDFFC", + [":person_wearing_turban::skin-tone-3:"] = "\uD83D\uDC73\uD83C\uDFFD", + [":person_wearing_turban::skin-tone-4:"] = "\uD83D\uDC73\uD83C\uDFFE", + [":person_wearing_turban::skin-tone-5:"] = "\uD83D\uDC73\uD83C\uDFFF", + [":person_wearing_turban_tone1:"] = "\uD83D\uDC73\uD83C\uDFFB", + [":person_wearing_turban_tone2:"] = "\uD83D\uDC73\uD83C\uDFFC", + [":person_wearing_turban_tone3:"] = "\uD83D\uDC73\uD83C\uDFFD", + [":person_wearing_turban_tone4:"] = "\uD83D\uDC73\uD83C\uDFFE", + [":person_wearing_turban_tone5:"] = "\uD83D\uDC73\uD83C\uDFFF", + [":person_with_ball:"] = "⛹️", + [":person_with_ball::skin-tone-1:"] = "⛹\uD83C\uDFFB", + [":person_with_ball::skin-tone-2:"] = "⛹\uD83C\uDFFC", + [":person_with_ball::skin-tone-3:"] = "⛹\uD83C\uDFFD", + [":person_with_ball::skin-tone-4:"] = "⛹\uD83C\uDFFE", + [":person_with_ball::skin-tone-5:"] = "⛹\uD83C\uDFFF", + [":person_with_ball_tone1:"] = "⛹\uD83C\uDFFB", + [":person_with_ball_tone2:"] = "⛹\uD83C\uDFFC", + [":person_with_ball_tone3:"] = "⛹\uD83C\uDFFD", + [":person_with_ball_tone4:"] = "⛹\uD83C\uDFFE", + [":person_with_ball_tone5:"] = "⛹\uD83C\uDFFF", + [":person_with_blond_hair:"] = "\uD83D\uDC71", + [":person_with_blond_hair::skin-tone-1:"] = "\uD83D\uDC71\uD83C\uDFFB", + [":person_with_blond_hair::skin-tone-2:"] = "\uD83D\uDC71\uD83C\uDFFC", + [":person_with_blond_hair::skin-tone-3:"] = "\uD83D\uDC71\uD83C\uDFFD", + [":person_with_blond_hair::skin-tone-4:"] = "\uD83D\uDC71\uD83C\uDFFE", + [":person_with_blond_hair::skin-tone-5:"] = "\uD83D\uDC71\uD83C\uDFFF", + [":person_with_blond_hair_tone1:"] = "\uD83D\uDC71\uD83C\uDFFB", + [":person_with_blond_hair_tone2:"] = "\uD83D\uDC71\uD83C\uDFFC", + [":person_with_blond_hair_tone3:"] = "\uD83D\uDC71\uD83C\uDFFD", + [":person_with_blond_hair_tone4:"] = "\uD83D\uDC71\uD83C\uDFFE", + [":person_with_blond_hair_tone5:"] = "\uD83D\uDC71\uD83C\uDFFF", + [":person_with_pouting_face:"] = "\uD83D\uDE4E", + [":person_with_pouting_face::skin-tone-1:"] = "\uD83D\uDE4E\uD83C\uDFFB", + [":person_with_pouting_face::skin-tone-2:"] = "\uD83D\uDE4E\uD83C\uDFFC", + [":person_with_pouting_face::skin-tone-3:"] = "\uD83D\uDE4E\uD83C\uDFFD", + [":person_with_pouting_face::skin-tone-4:"] = "\uD83D\uDE4E\uD83C\uDFFE", + [":person_with_pouting_face::skin-tone-5:"] = "\uD83D\uDE4E\uD83C\uDFFF", + [":person_with_pouting_face_tone1:"] = "\uD83D\uDE4E\uD83C\uDFFB", + [":person_with_pouting_face_tone2:"] = "\uD83D\uDE4E\uD83C\uDFFC", + [":person_with_pouting_face_tone3:"] = "\uD83D\uDE4E\uD83C\uDFFD", + [":person_with_pouting_face_tone4:"] = "\uD83D\uDE4E\uD83C\uDFFE", + [":person_with_pouting_face_tone5:"] = "\uD83D\uDE4E\uD83C\uDFFF", + [":petri_dish:"] = "\uD83E\uDDEB", + [":pick:"] = "⛏️", + [":pie:"] = "\uD83E\uDD67", + [":pig2:"] = "\uD83D\uDC16", + [":pig:"] = "\uD83D\uDC37", + [":pig_nose:"] = "\uD83D\uDC3D", + [":pill:"] = "\uD83D\uDC8A", + [":pinching_hand:"] = "\uD83E\uDD0F", + [":pinching_hand::skin-tone-1:"] = "\uD83E\uDD0F\uD83C\uDFFB", + [":pinching_hand::skin-tone-2:"] = "\uD83E\uDD0F\uD83C\uDFFC", + [":pinching_hand::skin-tone-3:"] = "\uD83E\uDD0F\uD83C\uDFFD", + [":pinching_hand::skin-tone-4:"] = "\uD83E\uDD0F\uD83C\uDFFE", + [":pinching_hand::skin-tone-5:"] = "\uD83E\uDD0F\uD83C\uDFFF", + [":pinching_hand_dark_skin_tone:"] = "\uD83E\uDD0F\uD83C\uDFFF", + [":pinching_hand_light_skin_tone:"] = "\uD83E\uDD0F\uD83C\uDFFB", + [":pinching_hand_medium_dark_skin_tone:"] = "\uD83E\uDD0F\uD83C\uDFFE", + [":pinching_hand_medium_light_skin_tone:"] = "\uD83E\uDD0F\uD83C\uDFFC", + [":pinching_hand_medium_skin_tone:"] = "\uD83E\uDD0F\uD83C\uDFFD", + [":pinching_hand_tone1:"] = "\uD83E\uDD0F\uD83C\uDFFB", + [":pinching_hand_tone2:"] = "\uD83E\uDD0F\uD83C\uDFFC", + [":pinching_hand_tone3:"] = "\uD83E\uDD0F\uD83C\uDFFD", + [":pinching_hand_tone4:"] = "\uD83E\uDD0F\uD83C\uDFFE", + [":pinching_hand_tone5:"] = "\uD83E\uDD0F\uD83C\uDFFF", + [":pineapple:"] = "\uD83C\uDF4D", + [":ping_pong:"] = "\uD83C\uDFD3", + [":pirate_flag:"] = "\uD83C\uDFF4\u200D☠️", + [":pisces:"] = "♓", + [":pizza:"] = "\uD83C\uDF55", + [":place_of_worship:"] = "\uD83D\uDED0", + [":play_pause:"] = "⏯️", + [":pleading_face:"] = "\uD83E\uDD7A", + [":point_down:"] = "\uD83D\uDC47", + [":point_down::skin-tone-1:"] = "\uD83D\uDC47\uD83C\uDFFB", + [":point_down::skin-tone-2:"] = "\uD83D\uDC47\uD83C\uDFFC", + [":point_down::skin-tone-3:"] = "\uD83D\uDC47\uD83C\uDFFD", + [":point_down::skin-tone-4:"] = "\uD83D\uDC47\uD83C\uDFFE", + [":point_down::skin-tone-5:"] = "\uD83D\uDC47\uD83C\uDFFF", + [":point_down_tone1:"] = "\uD83D\uDC47\uD83C\uDFFB", + [":point_down_tone2:"] = "\uD83D\uDC47\uD83C\uDFFC", + [":point_down_tone3:"] = "\uD83D\uDC47\uD83C\uDFFD", + [":point_down_tone4:"] = "\uD83D\uDC47\uD83C\uDFFE", + [":point_down_tone5:"] = "\uD83D\uDC47\uD83C\uDFFF", + [":point_left:"] = "\uD83D\uDC48", + [":point_left::skin-tone-1:"] = "\uD83D\uDC48\uD83C\uDFFB", + [":point_left::skin-tone-2:"] = "\uD83D\uDC48\uD83C\uDFFC", + [":point_left::skin-tone-3:"] = "\uD83D\uDC48\uD83C\uDFFD", + [":point_left::skin-tone-4:"] = "\uD83D\uDC48\uD83C\uDFFE", + [":point_left::skin-tone-5:"] = "\uD83D\uDC48\uD83C\uDFFF", + [":point_left_tone1:"] = "\uD83D\uDC48\uD83C\uDFFB", + [":point_left_tone2:"] = "\uD83D\uDC48\uD83C\uDFFC", + [":point_left_tone3:"] = "\uD83D\uDC48\uD83C\uDFFD", + [":point_left_tone4:"] = "\uD83D\uDC48\uD83C\uDFFE", + [":point_left_tone5:"] = "\uD83D\uDC48\uD83C\uDFFF", + [":point_right:"] = "\uD83D\uDC49", + [":point_right::skin-tone-1:"] = "\uD83D\uDC49\uD83C\uDFFB", + [":point_right::skin-tone-2:"] = "\uD83D\uDC49\uD83C\uDFFC", + [":point_right::skin-tone-3:"] = "\uD83D\uDC49\uD83C\uDFFD", + [":point_right::skin-tone-4:"] = "\uD83D\uDC49\uD83C\uDFFE", + [":point_right::skin-tone-5:"] = "\uD83D\uDC49\uD83C\uDFFF", + [":point_right_tone1:"] = "\uD83D\uDC49\uD83C\uDFFB", + [":point_right_tone2:"] = "\uD83D\uDC49\uD83C\uDFFC", + [":point_right_tone3:"] = "\uD83D\uDC49\uD83C\uDFFD", + [":point_right_tone4:"] = "\uD83D\uDC49\uD83C\uDFFE", + [":point_right_tone5:"] = "\uD83D\uDC49\uD83C\uDFFF", + [":point_up:"] = "☝️", + [":point_up::skin-tone-1:"] = "☝\uD83C\uDFFB", + [":point_up::skin-tone-2:"] = "☝\uD83C\uDFFC", + [":point_up::skin-tone-3:"] = "☝\uD83C\uDFFD", + [":point_up::skin-tone-4:"] = "☝\uD83C\uDFFE", + [":point_up::skin-tone-5:"] = "☝\uD83C\uDFFF", + [":point_up_2:"] = "\uD83D\uDC46", + [":point_up_2::skin-tone-1:"] = "\uD83D\uDC46\uD83C\uDFFB", + [":point_up_2::skin-tone-2:"] = "\uD83D\uDC46\uD83C\uDFFC", + [":point_up_2::skin-tone-3:"] = "\uD83D\uDC46\uD83C\uDFFD", + [":point_up_2::skin-tone-4:"] = "\uD83D\uDC46\uD83C\uDFFE", + [":point_up_2::skin-tone-5:"] = "\uD83D\uDC46\uD83C\uDFFF", + [":point_up_2_tone1:"] = "\uD83D\uDC46\uD83C\uDFFB", + [":point_up_2_tone2:"] = "\uD83D\uDC46\uD83C\uDFFC", + [":point_up_2_tone3:"] = "\uD83D\uDC46\uD83C\uDFFD", + [":point_up_2_tone4:"] = "\uD83D\uDC46\uD83C\uDFFE", + [":point_up_2_tone5:"] = "\uD83D\uDC46\uD83C\uDFFF", + [":point_up_tone1:"] = "☝\uD83C\uDFFB", + [":point_up_tone2:"] = "☝\uD83C\uDFFC", + [":point_up_tone3:"] = "☝\uD83C\uDFFD", + [":point_up_tone4:"] = "☝\uD83C\uDFFE", + [":point_up_tone5:"] = "☝\uD83C\uDFFF", + [":police_car:"] = "\uD83D\uDE93", + [":police_officer:"] = "\uD83D\uDC6E", + [":police_officer::skin-tone-1:"] = "\uD83D\uDC6E\uD83C\uDFFB", + [":police_officer::skin-tone-2:"] = "\uD83D\uDC6E\uD83C\uDFFC", + [":police_officer::skin-tone-3:"] = "\uD83D\uDC6E\uD83C\uDFFD", + [":police_officer::skin-tone-4:"] = "\uD83D\uDC6E\uD83C\uDFFE", + [":police_officer::skin-tone-5:"] = "\uD83D\uDC6E\uD83C\uDFFF", + [":police_officer_tone1:"] = "\uD83D\uDC6E\uD83C\uDFFB", + [":police_officer_tone2:"] = "\uD83D\uDC6E\uD83C\uDFFC", + [":police_officer_tone3:"] = "\uD83D\uDC6E\uD83C\uDFFD", + [":police_officer_tone4:"] = "\uD83D\uDC6E\uD83C\uDFFE", + [":police_officer_tone5:"] = "\uD83D\uDC6E\uD83C\uDFFF", + [":poo:"] = "\uD83D\uDCA9", + [":poodle:"] = "\uD83D\uDC29", + [":poop:"] = "\uD83D\uDCA9", + [":popcorn:"] = "\uD83C\uDF7F", + [":post_office:"] = "\uD83C\uDFE3", + [":postal_horn:"] = "\uD83D\uDCEF", + [":postbox:"] = "\uD83D\uDCEE", + [":potable_water:"] = "\uD83D\uDEB0", + [":potato:"] = "\uD83E\uDD54", + [":pouch:"] = "\uD83D\uDC5D", + [":poultry_leg:"] = "\uD83C\uDF57", + [":pound:"] = "\uD83D\uDCB7", + [":pouting_cat:"] = "\uD83D\uDE3E", + [":pray:"] = "\uD83D\uDE4F", + [":pray::skin-tone-1:"] = "\uD83D\uDE4F\uD83C\uDFFB", + [":pray::skin-tone-2:"] = "\uD83D\uDE4F\uD83C\uDFFC", + [":pray::skin-tone-3:"] = "\uD83D\uDE4F\uD83C\uDFFD", + [":pray::skin-tone-4:"] = "\uD83D\uDE4F\uD83C\uDFFE", + [":pray::skin-tone-5:"] = "\uD83D\uDE4F\uD83C\uDFFF", + [":pray_tone1:"] = "\uD83D\uDE4F\uD83C\uDFFB", + [":pray_tone2:"] = "\uD83D\uDE4F\uD83C\uDFFC", + [":pray_tone3:"] = "\uD83D\uDE4F\uD83C\uDFFD", + [":pray_tone4:"] = "\uD83D\uDE4F\uD83C\uDFFE", + [":pray_tone5:"] = "\uD83D\uDE4F\uD83C\uDFFF", + [":prayer_beads:"] = "\uD83D\uDCFF", + [":pregnant_woman:"] = "\uD83E\uDD30", + [":pregnant_woman::skin-tone-1:"] = "\uD83E\uDD30\uD83C\uDFFB", + [":pregnant_woman::skin-tone-2:"] = "\uD83E\uDD30\uD83C\uDFFC", + [":pregnant_woman::skin-tone-3:"] = "\uD83E\uDD30\uD83C\uDFFD", + [":pregnant_woman::skin-tone-4:"] = "\uD83E\uDD30\uD83C\uDFFE", + [":pregnant_woman::skin-tone-5:"] = "\uD83E\uDD30\uD83C\uDFFF", + [":pregnant_woman_tone1:"] = "\uD83E\uDD30\uD83C\uDFFB", + [":pregnant_woman_tone2:"] = "\uD83E\uDD30\uD83C\uDFFC", + [":pregnant_woman_tone3:"] = "\uD83E\uDD30\uD83C\uDFFD", + [":pregnant_woman_tone4:"] = "\uD83E\uDD30\uD83C\uDFFE", + [":pregnant_woman_tone5:"] = "\uD83E\uDD30\uD83C\uDFFF", + [":pretzel:"] = "\uD83E\uDD68", + [":previous_track:"] = "⏮️", + [":prince:"] = "\uD83E\uDD34", + [":prince::skin-tone-1:"] = "\uD83E\uDD34\uD83C\uDFFB", + [":prince::skin-tone-2:"] = "\uD83E\uDD34\uD83C\uDFFC", + [":prince::skin-tone-3:"] = "\uD83E\uDD34\uD83C\uDFFD", + [":prince::skin-tone-4:"] = "\uD83E\uDD34\uD83C\uDFFE", + [":prince::skin-tone-5:"] = "\uD83E\uDD34\uD83C\uDFFF", + [":prince_tone1:"] = "\uD83E\uDD34\uD83C\uDFFB", + [":prince_tone2:"] = "\uD83E\uDD34\uD83C\uDFFC", + [":prince_tone3:"] = "\uD83E\uDD34\uD83C\uDFFD", + [":prince_tone4:"] = "\uD83E\uDD34\uD83C\uDFFE", + [":prince_tone5:"] = "\uD83E\uDD34\uD83C\uDFFF", + [":princess:"] = "\uD83D\uDC78", + [":princess::skin-tone-1:"] = "\uD83D\uDC78\uD83C\uDFFB", + [":princess::skin-tone-2:"] = "\uD83D\uDC78\uD83C\uDFFC", + [":princess::skin-tone-3:"] = "\uD83D\uDC78\uD83C\uDFFD", + [":princess::skin-tone-4:"] = "\uD83D\uDC78\uD83C\uDFFE", + [":princess::skin-tone-5:"] = "\uD83D\uDC78\uD83C\uDFFF", + [":princess_tone1:"] = "\uD83D\uDC78\uD83C\uDFFB", + [":princess_tone2:"] = "\uD83D\uDC78\uD83C\uDFFC", + [":princess_tone3:"] = "\uD83D\uDC78\uD83C\uDFFD", + [":princess_tone4:"] = "\uD83D\uDC78\uD83C\uDFFE", + [":princess_tone5:"] = "\uD83D\uDC78\uD83C\uDFFF", + [":printer:"] = "\uD83D\uDDA8️", + [":probing_cane:"] = "\uD83E\uDDAF", + [":projector:"] = "\uD83D\uDCFD️", + [":pudding:"] = "\uD83C\uDF6E", + [":punch:"] = "\uD83D\uDC4A", + [":punch::skin-tone-1:"] = "\uD83D\uDC4A\uD83C\uDFFB", + [":punch::skin-tone-2:"] = "\uD83D\uDC4A\uD83C\uDFFC", + [":punch::skin-tone-3:"] = "\uD83D\uDC4A\uD83C\uDFFD", + [":punch::skin-tone-4:"] = "\uD83D\uDC4A\uD83C\uDFFE", + [":punch::skin-tone-5:"] = "\uD83D\uDC4A\uD83C\uDFFF", + [":punch_tone1:"] = "\uD83D\uDC4A\uD83C\uDFFB", + [":punch_tone2:"] = "\uD83D\uDC4A\uD83C\uDFFC", + [":punch_tone3:"] = "\uD83D\uDC4A\uD83C\uDFFD", + [":punch_tone4:"] = "\uD83D\uDC4A\uD83C\uDFFE", + [":punch_tone5:"] = "\uD83D\uDC4A\uD83C\uDFFF", + [":purple_circle:"] = "\uD83D\uDFE3", + [":purple_heart:"] = "\uD83D\uDC9C", + [":purple_square:"] = "\uD83D\uDFEA", + [":purse:"] = "\uD83D\uDC5B", + [":pushpin:"] = "\uD83D\uDCCC", + [":put_litter_in_its_place:"] = "\uD83D\uDEAE", + [":question:"] = "❓", + [":rabbit2:"] = "\uD83D\uDC07", + [":rabbit:"] = "\uD83D\uDC30", + [":raccoon:"] = "\uD83E\uDD9D", + [":race_car:"] = "\uD83C\uDFCE️", + [":racehorse:"] = "\uD83D\uDC0E", + [":racing_car:"] = "\uD83C\uDFCE️", + [":racing_motorcycle:"] = "\uD83C\uDFCD️", + [":radio:"] = "\uD83D\uDCFB", + [":radio_button:"] = "\uD83D\uDD18", + [":radioactive:"] = "☢️", + [":radioactive_sign:"] = "☢️", + [":rage:"] = "\uD83D\uDE21", + [":railroad_track:"] = "\uD83D\uDEE4️", + [":railway_car:"] = "\uD83D\uDE83", + [":railway_track:"] = "\uD83D\uDEE4️", + [":rainbow:"] = "\uD83C\uDF08", + [":rainbow_flag:"] = "\uD83C\uDFF3️\u200D\uD83C\uDF08", + [":raised_back_of_hand:"] = "\uD83E\uDD1A", + [":raised_back_of_hand::skin-tone-1:"] = "\uD83E\uDD1A\uD83C\uDFFB", + [":raised_back_of_hand::skin-tone-2:"] = "\uD83E\uDD1A\uD83C\uDFFC", + [":raised_back_of_hand::skin-tone-3:"] = "\uD83E\uDD1A\uD83C\uDFFD", + [":raised_back_of_hand::skin-tone-4:"] = "\uD83E\uDD1A\uD83C\uDFFE", + [":raised_back_of_hand::skin-tone-5:"] = "\uD83E\uDD1A\uD83C\uDFFF", + [":raised_back_of_hand_tone1:"] = "\uD83E\uDD1A\uD83C\uDFFB", + [":raised_back_of_hand_tone2:"] = "\uD83E\uDD1A\uD83C\uDFFC", + [":raised_back_of_hand_tone3:"] = "\uD83E\uDD1A\uD83C\uDFFD", + [":raised_back_of_hand_tone4:"] = "\uD83E\uDD1A\uD83C\uDFFE", + [":raised_back_of_hand_tone5:"] = "\uD83E\uDD1A\uD83C\uDFFF", + [":raised_hand:"] = "✋", + [":raised_hand::skin-tone-1:"] = "✋\uD83C\uDFFB", + [":raised_hand::skin-tone-2:"] = "✋\uD83C\uDFFC", + [":raised_hand::skin-tone-3:"] = "✋\uD83C\uDFFD", + [":raised_hand::skin-tone-4:"] = "✋\uD83C\uDFFE", + [":raised_hand::skin-tone-5:"] = "✋\uD83C\uDFFF", + [":raised_hand_tone1:"] = "✋\uD83C\uDFFB", + [":raised_hand_tone2:"] = "✋\uD83C\uDFFC", + [":raised_hand_tone3:"] = "✋\uD83C\uDFFD", + [":raised_hand_tone4:"] = "✋\uD83C\uDFFE", + [":raised_hand_tone5:"] = "✋\uD83C\uDFFF", + [":raised_hand_with_fingers_splayed:"] = "\uD83D\uDD90️", + [":raised_hand_with_fingers_splayed::skin-tone-1:"] = "\uD83D\uDD90\uD83C\uDFFB", + [":raised_hand_with_fingers_splayed::skin-tone-2:"] = "\uD83D\uDD90\uD83C\uDFFC", + [":raised_hand_with_fingers_splayed::skin-tone-3:"] = "\uD83D\uDD90\uD83C\uDFFD", + [":raised_hand_with_fingers_splayed::skin-tone-4:"] = "\uD83D\uDD90\uD83C\uDFFE", + [":raised_hand_with_fingers_splayed::skin-tone-5:"] = "\uD83D\uDD90\uD83C\uDFFF", + [":raised_hand_with_fingers_splayed_tone1:"] = "\uD83D\uDD90\uD83C\uDFFB", + [":raised_hand_with_fingers_splayed_tone2:"] = "\uD83D\uDD90\uD83C\uDFFC", + [":raised_hand_with_fingers_splayed_tone3:"] = "\uD83D\uDD90\uD83C\uDFFD", + [":raised_hand_with_fingers_splayed_tone4:"] = "\uD83D\uDD90\uD83C\uDFFE", + [":raised_hand_with_fingers_splayed_tone5:"] = "\uD83D\uDD90\uD83C\uDFFF", + [":raised_hand_with_part_between_middle_and_ring_fingers:"] = "\uD83D\uDD96", + [":raised_hand_with_part_between_middle_and_ring_fingers::skin-tone-1:"] = "\uD83D\uDD96\uD83C\uDFFB", + [":raised_hand_with_part_between_middle_and_ring_fingers::skin-tone-2:"] = "\uD83D\uDD96\uD83C\uDFFC", + [":raised_hand_with_part_between_middle_and_ring_fingers::skin-tone-3:"] = "\uD83D\uDD96\uD83C\uDFFD", + [":raised_hand_with_part_between_middle_and_ring_fingers::skin-tone-4:"] = "\uD83D\uDD96\uD83C\uDFFE", + [":raised_hand_with_part_between_middle_and_ring_fingers::skin-tone-5:"] = "\uD83D\uDD96\uD83C\uDFFF", + [":raised_hand_with_part_between_middle_and_ring_fingers_tone1:"] = "\uD83D\uDD96\uD83C\uDFFB", + [":raised_hand_with_part_between_middle_and_ring_fingers_tone2:"] = "\uD83D\uDD96\uD83C\uDFFC", + [":raised_hand_with_part_between_middle_and_ring_fingers_tone3:"] = "\uD83D\uDD96\uD83C\uDFFD", + [":raised_hand_with_part_between_middle_and_ring_fingers_tone4:"] = "\uD83D\uDD96\uD83C\uDFFE", + [":raised_hand_with_part_between_middle_and_ring_fingers_tone5:"] = "\uD83D\uDD96\uD83C\uDFFF", + [":raised_hands:"] = "\uD83D\uDE4C", + [":raised_hands::skin-tone-1:"] = "\uD83D\uDE4C\uD83C\uDFFB", + [":raised_hands::skin-tone-2:"] = "\uD83D\uDE4C\uD83C\uDFFC", + [":raised_hands::skin-tone-3:"] = "\uD83D\uDE4C\uD83C\uDFFD", + [":raised_hands::skin-tone-4:"] = "\uD83D\uDE4C\uD83C\uDFFE", + [":raised_hands::skin-tone-5:"] = "\uD83D\uDE4C\uD83C\uDFFF", + [":raised_hands_tone1:"] = "\uD83D\uDE4C\uD83C\uDFFB", + [":raised_hands_tone2:"] = "\uD83D\uDE4C\uD83C\uDFFC", + [":raised_hands_tone3:"] = "\uD83D\uDE4C\uD83C\uDFFD", + [":raised_hands_tone4:"] = "\uD83D\uDE4C\uD83C\uDFFE", + [":raised_hands_tone5:"] = "\uD83D\uDE4C\uD83C\uDFFF", + [":raising_hand:"] = "\uD83D\uDE4B", + [":raising_hand::skin-tone-1:"] = "\uD83D\uDE4B\uD83C\uDFFB", + [":raising_hand::skin-tone-2:"] = "\uD83D\uDE4B\uD83C\uDFFC", + [":raising_hand::skin-tone-3:"] = "\uD83D\uDE4B\uD83C\uDFFD", + [":raising_hand::skin-tone-4:"] = "\uD83D\uDE4B\uD83C\uDFFE", + [":raising_hand::skin-tone-5:"] = "\uD83D\uDE4B\uD83C\uDFFF", + [":raising_hand_tone1:"] = "\uD83D\uDE4B\uD83C\uDFFB", + [":raising_hand_tone2:"] = "\uD83D\uDE4B\uD83C\uDFFC", + [":raising_hand_tone3:"] = "\uD83D\uDE4B\uD83C\uDFFD", + [":raising_hand_tone4:"] = "\uD83D\uDE4B\uD83C\uDFFE", + [":raising_hand_tone5:"] = "\uD83D\uDE4B\uD83C\uDFFF", + [":ram:"] = "\uD83D\uDC0F", + [":ramen:"] = "\uD83C\uDF5C", + [":rat:"] = "\uD83D\uDC00", + [":razor:"] = "\uD83E\uDE92", + [":receipt:"] = "\uD83E\uDDFE", + [":record_button:"] = "⏺️", + [":recycle:"] = "♻️", + [":red_car:"] = "\uD83D\uDE97", + [":red_circle:"] = "\uD83D\uDD34", + [":red_envelope:"] = "\uD83E\uDDE7", + [":red_square:"] = "\uD83D\uDFE5", + [":regional_indicator_a:"] = "\uD83C\uDDE6", + [":regional_indicator_b:"] = "\uD83C\uDDE7", + [":regional_indicator_c:"] = "\uD83C\uDDE8", + [":regional_indicator_d:"] = "\uD83C\uDDE9", + [":regional_indicator_e:"] = "\uD83C\uDDEA", + [":regional_indicator_f:"] = "\uD83C\uDDEB", + [":regional_indicator_g:"] = "\uD83C\uDDEC", + [":regional_indicator_h:"] = "\uD83C\uDDED", + [":regional_indicator_i:"] = "\uD83C\uDDEE", + [":regional_indicator_j:"] = "\uD83C\uDDEF", + [":regional_indicator_k:"] = "\uD83C\uDDF0", + [":regional_indicator_l:"] = "\uD83C\uDDF1", + [":regional_indicator_m:"] = "\uD83C\uDDF2", + [":regional_indicator_n:"] = "\uD83C\uDDF3", + [":regional_indicator_o:"] = "\uD83C\uDDF4", + [":regional_indicator_p:"] = "\uD83C\uDDF5", + [":regional_indicator_q:"] = "\uD83C\uDDF6", + [":regional_indicator_r:"] = "\uD83C\uDDF7", + [":regional_indicator_s:"] = "\uD83C\uDDF8", + [":regional_indicator_t:"] = "\uD83C\uDDF9", + [":regional_indicator_u:"] = "\uD83C\uDDFA", + [":regional_indicator_v:"] = "\uD83C\uDDFB", + [":regional_indicator_w:"] = "\uD83C\uDDFC", + [":regional_indicator_x:"] = "\uD83C\uDDFD", + [":regional_indicator_y:"] = "\uD83C\uDDFE", + [":regional_indicator_z:"] = "\uD83C\uDDFF", + [":registered:"] = "®️", + [":relaxed:"] = "☺️", + [":relieved:"] = "\uD83D\uDE0C", + [":reminder_ribbon:"] = "\uD83C\uDF97️", + [":repeat:"] = "\uD83D\uDD01", + [":repeat_one:"] = "\uD83D\uDD02", + [":restroom:"] = "\uD83D\uDEBB", + [":reversed_hand_with_middle_finger_extended:"] = "\uD83D\uDD95", + [":reversed_hand_with_middle_finger_extended::skin-tone-1:"] = "\uD83D\uDD95\uD83C\uDFFB", + [":reversed_hand_with_middle_finger_extended::skin-tone-2:"] = "\uD83D\uDD95\uD83C\uDFFC", + [":reversed_hand_with_middle_finger_extended::skin-tone-3:"] = "\uD83D\uDD95\uD83C\uDFFD", + [":reversed_hand_with_middle_finger_extended::skin-tone-4:"] = "\uD83D\uDD95\uD83C\uDFFE", + [":reversed_hand_with_middle_finger_extended::skin-tone-5:"] = "\uD83D\uDD95\uD83C\uDFFF", + [":reversed_hand_with_middle_finger_extended_tone1:"] = "\uD83D\uDD95\uD83C\uDFFB", + [":reversed_hand_with_middle_finger_extended_tone2:"] = "\uD83D\uDD95\uD83C\uDFFC", + [":reversed_hand_with_middle_finger_extended_tone3:"] = "\uD83D\uDD95\uD83C\uDFFD", + [":reversed_hand_with_middle_finger_extended_tone4:"] = "\uD83D\uDD95\uD83C\uDFFE", + [":reversed_hand_with_middle_finger_extended_tone5:"] = "\uD83D\uDD95\uD83C\uDFFF", + [":revolving_hearts:"] = "\uD83D\uDC9E", + [":rewind:"] = "⏪", + [":rhino:"] = "\uD83E\uDD8F", + [":rhinoceros:"] = "\uD83E\uDD8F", + [":ribbon:"] = "\uD83C\uDF80", + [":rice:"] = "\uD83C\uDF5A", + [":rice_ball:"] = "\uD83C\uDF59", + [":rice_cracker:"] = "\uD83C\uDF58", + [":rice_scene:"] = "\uD83C\uDF91", + [":right_anger_bubble:"] = "\uD83D\uDDEF️", + [":right_facing_fist:"] = "\uD83E\uDD1C", + [":right_facing_fist::skin-tone-1:"] = "\uD83E\uDD1C\uD83C\uDFFB", + [":right_facing_fist::skin-tone-2:"] = "\uD83E\uDD1C\uD83C\uDFFC", + [":right_facing_fist::skin-tone-3:"] = "\uD83E\uDD1C\uD83C\uDFFD", + [":right_facing_fist::skin-tone-4:"] = "\uD83E\uDD1C\uD83C\uDFFE", + [":right_facing_fist::skin-tone-5:"] = "\uD83E\uDD1C\uD83C\uDFFF", + [":right_facing_fist_tone1:"] = "\uD83E\uDD1C\uD83C\uDFFB", + [":right_facing_fist_tone2:"] = "\uD83E\uDD1C\uD83C\uDFFC", + [":right_facing_fist_tone3:"] = "\uD83E\uDD1C\uD83C\uDFFD", + [":right_facing_fist_tone4:"] = "\uD83E\uDD1C\uD83C\uDFFE", + [":right_facing_fist_tone5:"] = "\uD83E\uDD1C\uD83C\uDFFF", + [":right_fist:"] = "\uD83E\uDD1C", + [":right_fist::skin-tone-1:"] = "\uD83E\uDD1C\uD83C\uDFFB", + [":right_fist::skin-tone-2:"] = "\uD83E\uDD1C\uD83C\uDFFC", + [":right_fist::skin-tone-3:"] = "\uD83E\uDD1C\uD83C\uDFFD", + [":right_fist::skin-tone-4:"] = "\uD83E\uDD1C\uD83C\uDFFE", + [":right_fist::skin-tone-5:"] = "\uD83E\uDD1C\uD83C\uDFFF", + [":right_fist_tone1:"] = "\uD83E\uDD1C\uD83C\uDFFB", + [":right_fist_tone2:"] = "\uD83E\uDD1C\uD83C\uDFFC", + [":right_fist_tone3:"] = "\uD83E\uDD1C\uD83C\uDFFD", + [":right_fist_tone4:"] = "\uD83E\uDD1C\uD83C\uDFFE", + [":right_fist_tone5:"] = "\uD83E\uDD1C\uD83C\uDFFF", + [":ring:"] = "\uD83D\uDC8D", + [":ringed_planet:"] = "\uD83E\uDE90", + [":robot:"] = "\uD83E\uDD16", + [":robot_face:"] = "\uD83E\uDD16", + [":rocket:"] = "\uD83D\uDE80", + [":rofl:"] = "\uD83E\uDD23", + [":roll_of_paper:"] = "\uD83E\uDDFB", + [":rolled_up_newspaper:"] = "\uD83D\uDDDE️", + [":roller_coaster:"] = "\uD83C\uDFA2", + [":rolling_eyes:"] = "\uD83D\uDE44", + [":rolling_on_the_floor_laughing:"] = "\uD83E\uDD23", + [":rooster:"] = "\uD83D\uDC13", + [":rose:"] = "\uD83C\uDF39", + [":rosette:"] = "\uD83C\uDFF5️", + [":rotating_light:"] = "\uD83D\uDEA8", + [":round_pushpin:"] = "\uD83D\uDCCD", + [":rowboat:"] = "\uD83D\uDEA3", + [":rowboat::skin-tone-1:"] = "\uD83D\uDEA3\uD83C\uDFFB", + [":rowboat::skin-tone-2:"] = "\uD83D\uDEA3\uD83C\uDFFC", + [":rowboat::skin-tone-3:"] = "\uD83D\uDEA3\uD83C\uDFFD", + [":rowboat::skin-tone-4:"] = "\uD83D\uDEA3\uD83C\uDFFE", + [":rowboat::skin-tone-5:"] = "\uD83D\uDEA3\uD83C\uDFFF", + [":rowboat_tone1:"] = "\uD83D\uDEA3\uD83C\uDFFB", + [":rowboat_tone2:"] = "\uD83D\uDEA3\uD83C\uDFFC", + [":rowboat_tone3:"] = "\uD83D\uDEA3\uD83C\uDFFD", + [":rowboat_tone4:"] = "\uD83D\uDEA3\uD83C\uDFFE", + [":rowboat_tone5:"] = "\uD83D\uDEA3\uD83C\uDFFF", + [":rugby_football:"] = "\uD83C\uDFC9", + [":runner:"] = "\uD83C\uDFC3", + [":runner::skin-tone-1:"] = "\uD83C\uDFC3\uD83C\uDFFB", + [":runner::skin-tone-2:"] = "\uD83C\uDFC3\uD83C\uDFFC", + [":runner::skin-tone-3:"] = "\uD83C\uDFC3\uD83C\uDFFD", + [":runner::skin-tone-4:"] = "\uD83C\uDFC3\uD83C\uDFFE", + [":runner::skin-tone-5:"] = "\uD83C\uDFC3\uD83C\uDFFF", + [":runner_tone1:"] = "\uD83C\uDFC3\uD83C\uDFFB", + [":runner_tone2:"] = "\uD83C\uDFC3\uD83C\uDFFC", + [":runner_tone3:"] = "\uD83C\uDFC3\uD83C\uDFFD", + [":runner_tone4:"] = "\uD83C\uDFC3\uD83C\uDFFE", + [":runner_tone5:"] = "\uD83C\uDFC3\uD83C\uDFFF", + [":running_shirt_with_sash:"] = "\uD83C\uDFBD", + [":s"] = "\uD83D\uDE12", + [":sa:"] = "\uD83C\uDE02️", + [":safety_pin:"] = "\uD83E\uDDF7", + [":safety_vest:"] = "\uD83E\uDDBA", + [":sagittarius:"] = "♐", + [":sailboat:"] = "⛵", + [":sake:"] = "\uD83C\uDF76", + [":salad:"] = "\uD83E\uDD57", + [":salt:"] = "\uD83E\uDDC2", + [":sandal:"] = "\uD83D\uDC61", + [":sandwich:"] = "\uD83E\uDD6A", + [":santa:"] = "\uD83C\uDF85", + [":santa::skin-tone-1:"] = "\uD83C\uDF85\uD83C\uDFFB", + [":santa::skin-tone-2:"] = "\uD83C\uDF85\uD83C\uDFFC", + [":santa::skin-tone-3:"] = "\uD83C\uDF85\uD83C\uDFFD", + [":santa::skin-tone-4:"] = "\uD83C\uDF85\uD83C\uDFFE", + [":santa::skin-tone-5:"] = "\uD83C\uDF85\uD83C\uDFFF", + [":santa_tone1:"] = "\uD83C\uDF85\uD83C\uDFFB", + [":santa_tone2:"] = "\uD83C\uDF85\uD83C\uDFFC", + [":santa_tone3:"] = "\uD83C\uDF85\uD83C\uDFFD", + [":santa_tone4:"] = "\uD83C\uDF85\uD83C\uDFFE", + [":santa_tone5:"] = "\uD83C\uDF85\uD83C\uDFFF", + [":sari:"] = "\uD83E\uDD7B", + [":satellite:"] = "\uD83D\uDCE1", + [":satellite_orbital:"] = "\uD83D\uDEF0️", + [":satisfied:"] = "\uD83D\uDE06", + [":sauropod:"] = "\uD83E\uDD95", + [":saxophone:"] = "\uD83C\uDFB7", + [":scales:"] = "⚖️", + [":scarf:"] = "\uD83E\uDDE3", + [":school:"] = "\uD83C\uDFEB", + [":school_satchel:"] = "\uD83C\uDF92", + [":scissors:"] = "✂️", + [":scooter:"] = "\uD83D\uDEF4", + [":scorpion:"] = "\uD83E\uDD82", + [":scorpius:"] = "♏", + [":scotland:"] = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F", + [":scream:"] = "\uD83D\uDE31", + [":scream_cat:"] = "\uD83D\uDE40", + [":scroll:"] = "\uD83D\uDCDC", + [":seat:"] = "\uD83D\uDCBA", + [":second_place:"] = "\uD83E\uDD48", + [":second_place_medal:"] = "\uD83E\uDD48", + [":secret:"] = "㊙️", + [":see_no_evil:"] = "\uD83D\uDE48", + [":seedling:"] = "\uD83C\uDF31", + [":selfie:"] = "\uD83E\uDD33", + [":selfie::skin-tone-1:"] = "\uD83E\uDD33\uD83C\uDFFB", + [":selfie::skin-tone-2:"] = "\uD83E\uDD33\uD83C\uDFFC", + [":selfie::skin-tone-3:"] = "\uD83E\uDD33\uD83C\uDFFD", + [":selfie::skin-tone-4:"] = "\uD83E\uDD33\uD83C\uDFFE", + [":selfie::skin-tone-5:"] = "\uD83E\uDD33\uD83C\uDFFF", + [":selfie_tone1:"] = "\uD83E\uDD33\uD83C\uDFFB", + [":selfie_tone2:"] = "\uD83E\uDD33\uD83C\uDFFC", + [":selfie_tone3:"] = "\uD83E\uDD33\uD83C\uDFFD", + [":selfie_tone4:"] = "\uD83E\uDD33\uD83C\uDFFE", + [":selfie_tone5:"] = "\uD83E\uDD33\uD83C\uDFFF", + [":service_dog:"] = "\uD83D\uDC15\u200D\uD83E\uDDBA", + [":seven:"] = "7️⃣", + [":shaking_hands:"] = "\uD83E\uDD1D", + [":shallow_pan_of_food:"] = "\uD83E\uDD58", + [":shamrock:"] = "☘️", + [":shark:"] = "\uD83E\uDD88", + [":shaved_ice:"] = "\uD83C\uDF67", + [":sheep:"] = "\uD83D\uDC11", + [":shell:"] = "\uD83D\uDC1A", + [":shelled_peanut:"] = "\uD83E\uDD5C", + [":shield:"] = "\uD83D\uDEE1️", + [":shinto_shrine:"] = "⛩️", + [":ship:"] = "\uD83D\uDEA2", + [":shirt:"] = "\uD83D\uDC55", + [":shit:"] = "\uD83D\uDCA9", + [":shopping_bags:"] = "\uD83D\uDECD️", + [":shopping_cart:"] = "\uD83D\uDED2", + [":shopping_trolley:"] = "\uD83D\uDED2", + [":shorts:"] = "\uD83E\uDE73", + [":shower:"] = "\uD83D\uDEBF", + [":shrimp:"] = "\uD83E\uDD90", + [":shrug:"] = "\uD83E\uDD37", + [":shrug::skin-tone-1:"] = "\uD83E\uDD37\uD83C\uDFFB", + [":shrug::skin-tone-2:"] = "\uD83E\uDD37\uD83C\uDFFC", + [":shrug::skin-tone-3:"] = "\uD83E\uDD37\uD83C\uDFFD", + [":shrug::skin-tone-4:"] = "\uD83E\uDD37\uD83C\uDFFE", + [":shrug::skin-tone-5:"] = "\uD83E\uDD37\uD83C\uDFFF", + [":shrug_tone1:"] = "\uD83E\uDD37\uD83C\uDFFB", + [":shrug_tone2:"] = "\uD83E\uDD37\uD83C\uDFFC", + [":shrug_tone3:"] = "\uD83E\uDD37\uD83C\uDFFD", + [":shrug_tone4:"] = "\uD83E\uDD37\uD83C\uDFFE", + [":shrug_tone5:"] = "\uD83E\uDD37\uD83C\uDFFF", + [":shushing_face:"] = "\uD83E\uDD2B", + [":sick:"] = "\uD83E\uDD22", + [":sign_of_the_horns:"] = "\uD83E\uDD18", + [":sign_of_the_horns::skin-tone-1:"] = "\uD83E\uDD18\uD83C\uDFFB", + [":sign_of_the_horns::skin-tone-2:"] = "\uD83E\uDD18\uD83C\uDFFC", + [":sign_of_the_horns::skin-tone-3:"] = "\uD83E\uDD18\uD83C\uDFFD", + [":sign_of_the_horns::skin-tone-4:"] = "\uD83E\uDD18\uD83C\uDFFE", + [":sign_of_the_horns::skin-tone-5:"] = "\uD83E\uDD18\uD83C\uDFFF", + [":sign_of_the_horns_tone1:"] = "\uD83E\uDD18\uD83C\uDFFB", + [":sign_of_the_horns_tone2:"] = "\uD83E\uDD18\uD83C\uDFFC", + [":sign_of_the_horns_tone3:"] = "\uD83E\uDD18\uD83C\uDFFD", + [":sign_of_the_horns_tone4:"] = "\uD83E\uDD18\uD83C\uDFFE", + [":sign_of_the_horns_tone5:"] = "\uD83E\uDD18\uD83C\uDFFF", + [":signal_strength:"] = "\uD83D\uDCF6", + [":six:"] = "6️⃣", + [":six_pointed_star:"] = "\uD83D\uDD2F", + [":skateboard:"] = "\uD83D\uDEF9", + [":skeleton:"] = "\uD83D\uDC80", + [":ski:"] = "\uD83C\uDFBF", + [":skier:"] = "⛷️", + [":skull:"] = "\uD83D\uDC80", + [":skull_and_crossbones:"] = "☠️", + [":skull_crossbones:"] = "☠️", + [":skunk:"] = "\uD83E\uDDA8", + [":sled:"] = "\uD83D\uDEF7", + [":sleeping:"] = "\uD83D\uDE34", + [":sleeping_accommodation:"] = "\uD83D\uDECC", + [":sleeping_accommodation::skin-tone-1:"] = "\uD83D\uDECC\uD83C\uDFFB", + [":sleeping_accommodation::skin-tone-2:"] = "\uD83D\uDECC\uD83C\uDFFC", + [":sleeping_accommodation::skin-tone-3:"] = "\uD83D\uDECC\uD83C\uDFFD", + [":sleeping_accommodation::skin-tone-4:"] = "\uD83D\uDECC\uD83C\uDFFE", + [":sleeping_accommodation::skin-tone-5:"] = "\uD83D\uDECC\uD83C\uDFFF", + [":sleepy:"] = "\uD83D\uDE2A", + [":sleuth_or_spy:"] = "\uD83D\uDD75️", + [":sleuth_or_spy::skin-tone-1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":sleuth_or_spy::skin-tone-2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":sleuth_or_spy::skin-tone-3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":sleuth_or_spy::skin-tone-4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":sleuth_or_spy::skin-tone-5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":sleuth_or_spy_tone1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":sleuth_or_spy_tone2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":sleuth_or_spy_tone3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":sleuth_or_spy_tone4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":sleuth_or_spy_tone5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":slight_frown:"] = "\uD83D\uDE41", + [":slight_smile:"] = "\uD83D\uDE42", + [":slightly_frowning_face:"] = "\uD83D\uDE41", + [":slightly_smiling_face:"] = "\uD83D\uDE42", + [":slot_machine:"] = "\uD83C\uDFB0", + [":sloth:"] = "\uD83E\uDDA5", + [":small_airplane:"] = "\uD83D\uDEE9️", + [":small_blue_diamond:"] = "\uD83D\uDD39", + [":small_orange_diamond:"] = "\uD83D\uDD38", + [":small_red_triangle:"] = "\uD83D\uDD3A", + [":small_red_triangle_down:"] = "\uD83D\uDD3B", + [":smile:"] = "\uD83D\uDE04", + [":smile_cat:"] = "\uD83D\uDE38", + [":smiley:"] = "\uD83D\uDE03", + [":smiley_cat:"] = "\uD83D\uDE3A", + [":smiling_face_with_3_hearts:"] = "\uD83E\uDD70", + [":smiling_imp:"] = "\uD83D\uDE08", + [":smirk:"] = "\uD83D\uDE0F", + [":smirk_cat:"] = "\uD83D\uDE3C", + [":smoking:"] = "\uD83D\uDEAC", + [":snail:"] = "\uD83D\uDC0C", + [":snake:"] = "\uD83D\uDC0D", + [":sneeze:"] = "\uD83E\uDD27", + [":sneezing_face:"] = "\uD83E\uDD27", + [":snow_capped_mountain:"] = "\uD83C\uDFD4️", + [":snowboarder:"] = "\uD83C\uDFC2", + [":snowboarder::skin-tone-1:"] = "\uD83C\uDFC2\uD83C\uDFFB", + [":snowboarder::skin-tone-2:"] = "\uD83C\uDFC2\uD83C\uDFFC", + [":snowboarder::skin-tone-3:"] = "\uD83C\uDFC2\uD83C\uDFFD", + [":snowboarder::skin-tone-4:"] = "\uD83C\uDFC2\uD83C\uDFFE", + [":snowboarder::skin-tone-5:"] = "\uD83C\uDFC2\uD83C\uDFFF", + [":snowboarder_dark_skin_tone:"] = "\uD83C\uDFC2\uD83C\uDFFF", + [":snowboarder_light_skin_tone:"] = "\uD83C\uDFC2\uD83C\uDFFB", + [":snowboarder_medium_dark_skin_tone:"] = "\uD83C\uDFC2\uD83C\uDFFE", + [":snowboarder_medium_light_skin_tone:"] = "\uD83C\uDFC2\uD83C\uDFFC", + [":snowboarder_medium_skin_tone:"] = "\uD83C\uDFC2\uD83C\uDFFD", + [":snowboarder_tone1:"] = "\uD83C\uDFC2\uD83C\uDFFB", + [":snowboarder_tone2:"] = "\uD83C\uDFC2\uD83C\uDFFC", + [":snowboarder_tone3:"] = "\uD83C\uDFC2\uD83C\uDFFD", + [":snowboarder_tone4:"] = "\uD83C\uDFC2\uD83C\uDFFE", + [":snowboarder_tone5:"] = "\uD83C\uDFC2\uD83C\uDFFF", + [":snowflake:"] = "❄️", + [":snowman2:"] = "☃️", + [":snowman:"] = "⛄", + [":soap:"] = "\uD83E\uDDFC", + [":sob:"] = "\uD83D\uDE2D", + [":soccer:"] = "⚽", + [":socks:"] = "\uD83E\uDDE6", + [":softball:"] = "\uD83E\uDD4E", + [":soon:"] = "\uD83D\uDD1C", + [":sos:"] = "\uD83C\uDD98", + [":sound:"] = "\uD83D\uDD09", + [":space_invader:"] = "\uD83D\uDC7E", + [":spades:"] = "♠️", + [":spaghetti:"] = "\uD83C\uDF5D", + [":sparkle:"] = "❇️", + [":sparkler:"] = "\uD83C\uDF87", + [":sparkles:"] = "✨", + [":sparkling_heart:"] = "\uD83D\uDC96", + [":speak_no_evil:"] = "\uD83D\uDE4A", + [":speaker:"] = "\uD83D\uDD08", + [":speaking_head:"] = "\uD83D\uDDE3️", + [":speaking_head_in_silhouette:"] = "\uD83D\uDDE3️", + [":speech_balloon:"] = "\uD83D\uDCAC", + [":speech_left:"] = "\uD83D\uDDE8️", + [":speedboat:"] = "\uD83D\uDEA4", + [":spider:"] = "\uD83D\uDD77️", + [":spider_web:"] = "\uD83D\uDD78️", + [":spiral_calendar_pad:"] = "\uD83D\uDDD3️", + [":spiral_note_pad:"] = "\uD83D\uDDD2️", + [":sponge:"] = "\uD83E\uDDFD", + [":spoon:"] = "\uD83E\uDD44", + [":sports_medal:"] = "\uD83C\uDFC5", + [":spy:"] = "\uD83D\uDD75️", + [":spy::skin-tone-1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":spy::skin-tone-2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":spy::skin-tone-3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":spy::skin-tone-4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":spy::skin-tone-5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":spy_tone1:"] = "\uD83D\uDD75\uD83C\uDFFB", + [":spy_tone2:"] = "\uD83D\uDD75\uD83C\uDFFC", + [":spy_tone3:"] = "\uD83D\uDD75\uD83C\uDFFD", + [":spy_tone4:"] = "\uD83D\uDD75\uD83C\uDFFE", + [":spy_tone5:"] = "\uD83D\uDD75\uD83C\uDFFF", + [":squeeze_bottle:"] = "\uD83E\uDDF4", + [":squid:"] = "\uD83E\uDD91", + [":stadium:"] = "\uD83C\uDFDF️", + [":star2:"] = "\uD83C\uDF1F", + [":star:"] = "⭐", + [":star_and_crescent:"] = "☪️", + [":star_of_david:"] = "✡️", + [":star_struck:"] = "\uD83E\uDD29", + [":stars:"] = "\uD83C\uDF20", + [":station:"] = "\uD83D\uDE89", + [":statue_of_liberty:"] = "\uD83D\uDDFD", + [":steam_locomotive:"] = "\uD83D\uDE82", + [":stethoscope:"] = "\uD83E\uDE7A", + [":stew:"] = "\uD83C\uDF72", + [":stop_button:"] = "⏹️", + [":stop_sign:"] = "\uD83D\uDED1", + [":stopwatch:"] = "⏱️", + [":straight_ruler:"] = "\uD83D\uDCCF", + [":strawberry:"] = "\uD83C\uDF53", + [":stuck_out_tongue:"] = "\uD83D\uDE1B", + [":stuck_out_tongue_closed_eyes:"] = "\uD83D\uDE1D", + [":stuck_out_tongue_winking_eye:"] = "\uD83D\uDE1C", + [":studio_microphone:"] = "\uD83C\uDF99️", + [":stuffed_flatbread:"] = "\uD83E\uDD59", + [":stuffed_pita:"] = "\uD83E\uDD59", + [":sun_with_face:"] = "\uD83C\uDF1E", + [":sunflower:"] = "\uD83C\uDF3B", + [":sunglasses:"] = "\uD83D\uDE0E", + [":sunny:"] = "☀️", + [":sunrise:"] = "\uD83C\uDF05", + [":sunrise_over_mountains:"] = "\uD83C\uDF04", + [":superhero:"] = "\uD83E\uDDB8", + [":superhero::skin-tone-1:"] = "\uD83E\uDDB8\uD83C\uDFFB", + [":superhero::skin-tone-2:"] = "\uD83E\uDDB8\uD83C\uDFFC", + [":superhero::skin-tone-3:"] = "\uD83E\uDDB8\uD83C\uDFFD", + [":superhero::skin-tone-4:"] = "\uD83E\uDDB8\uD83C\uDFFE", + [":superhero::skin-tone-5:"] = "\uD83E\uDDB8\uD83C\uDFFF", + [":superhero_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFF", + [":superhero_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFB", + [":superhero_medium_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFE", + [":superhero_medium_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFC", + [":superhero_medium_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFD", + [":superhero_tone1:"] = "\uD83E\uDDB8\uD83C\uDFFB", + [":superhero_tone2:"] = "\uD83E\uDDB8\uD83C\uDFFC", + [":superhero_tone3:"] = "\uD83E\uDDB8\uD83C\uDFFD", + [":superhero_tone4:"] = "\uD83E\uDDB8\uD83C\uDFFE", + [":superhero_tone5:"] = "\uD83E\uDDB8\uD83C\uDFFF", + [":supervillain:"] = "\uD83E\uDDB9", + [":supervillain::skin-tone-1:"] = "\uD83E\uDDB9\uD83C\uDFFB", + [":supervillain::skin-tone-2:"] = "\uD83E\uDDB9\uD83C\uDFFC", + [":supervillain::skin-tone-3:"] = "\uD83E\uDDB9\uD83C\uDFFD", + [":supervillain::skin-tone-4:"] = "\uD83E\uDDB9\uD83C\uDFFE", + [":supervillain::skin-tone-5:"] = "\uD83E\uDDB9\uD83C\uDFFF", + [":supervillain_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFF", + [":supervillain_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFB", + [":supervillain_medium_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFE", + [":supervillain_medium_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFC", + [":supervillain_medium_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFD", + [":supervillain_tone1:"] = "\uD83E\uDDB9\uD83C\uDFFB", + [":supervillain_tone2:"] = "\uD83E\uDDB9\uD83C\uDFFC", + [":supervillain_tone3:"] = "\uD83E\uDDB9\uD83C\uDFFD", + [":supervillain_tone4:"] = "\uD83E\uDDB9\uD83C\uDFFE", + [":supervillain_tone5:"] = "\uD83E\uDDB9\uD83C\uDFFF", + [":surfer:"] = "\uD83C\uDFC4", + [":surfer::skin-tone-1:"] = "\uD83C\uDFC4\uD83C\uDFFB", + [":surfer::skin-tone-2:"] = "\uD83C\uDFC4\uD83C\uDFFC", + [":surfer::skin-tone-3:"] = "\uD83C\uDFC4\uD83C\uDFFD", + [":surfer::skin-tone-4:"] = "\uD83C\uDFC4\uD83C\uDFFE", + [":surfer::skin-tone-5:"] = "\uD83C\uDFC4\uD83C\uDFFF", + [":surfer_tone1:"] = "\uD83C\uDFC4\uD83C\uDFFB", + [":surfer_tone2:"] = "\uD83C\uDFC4\uD83C\uDFFC", + [":surfer_tone3:"] = "\uD83C\uDFC4\uD83C\uDFFD", + [":surfer_tone4:"] = "\uD83C\uDFC4\uD83C\uDFFE", + [":surfer_tone5:"] = "\uD83C\uDFC4\uD83C\uDFFF", + [":sushi:"] = "\uD83C\uDF63", + [":suspension_railway:"] = "\uD83D\uDE9F", + [":swan:"] = "\uD83E\uDDA2", + [":sweat:"] = "\uD83D\uDE13", + [":sweat_drops:"] = "\uD83D\uDCA6", + [":sweat_smile:"] = "\uD83D\uDE05", + [":sweet_potato:"] = "\uD83C\uDF60", + [":swimmer:"] = "\uD83C\uDFCA", + [":swimmer::skin-tone-1:"] = "\uD83C\uDFCA\uD83C\uDFFB", + [":swimmer::skin-tone-2:"] = "\uD83C\uDFCA\uD83C\uDFFC", + [":swimmer::skin-tone-3:"] = "\uD83C\uDFCA\uD83C\uDFFD", + [":swimmer::skin-tone-4:"] = "\uD83C\uDFCA\uD83C\uDFFE", + [":swimmer::skin-tone-5:"] = "\uD83C\uDFCA\uD83C\uDFFF", + [":swimmer_tone1:"] = "\uD83C\uDFCA\uD83C\uDFFB", + [":swimmer_tone2:"] = "\uD83C\uDFCA\uD83C\uDFFC", + [":swimmer_tone3:"] = "\uD83C\uDFCA\uD83C\uDFFD", + [":swimmer_tone4:"] = "\uD83C\uDFCA\uD83C\uDFFE", + [":swimmer_tone5:"] = "\uD83C\uDFCA\uD83C\uDFFF", + [":symbols:"] = "\uD83D\uDD23", + [":synagogue:"] = "\uD83D\uDD4D", + [":syringe:"] = "\uD83D\uDC89", + [":t_rex:"] = "\uD83E\uDD96", + [":table_tennis:"] = "\uD83C\uDFD3", + [":taco:"] = "\uD83C\uDF2E", + [":tada:"] = "\uD83C\uDF89", + [":takeout_box:"] = "\uD83E\uDD61", + [":tanabata_tree:"] = "\uD83C\uDF8B", + [":tangerine:"] = "\uD83C\uDF4A", + [":taurus:"] = "♉", + [":taxi:"] = "\uD83D\uDE95", + [":tea:"] = "\uD83C\uDF75", + [":teddy_bear:"] = "\uD83E\uDDF8", + [":telephone:"] = "☎️", + [":telephone_receiver:"] = "\uD83D\uDCDE", + [":telescope:"] = "\uD83D\uDD2D", + [":tennis:"] = "\uD83C\uDFBE", + [":tent:"] = "⛺", + [":test_tube:"] = "\uD83E\uDDEA", + [":thermometer:"] = "\uD83C\uDF21️", + [":thermometer_face:"] = "\uD83E\uDD12", + [":thinking:"] = "\uD83E\uDD14", + [":thinking_face:"] = "\uD83E\uDD14", + [":third_place:"] = "\uD83E\uDD49", + [":third_place_medal:"] = "\uD83E\uDD49", + [":thought_balloon:"] = "\uD83D\uDCAD", + [":thread:"] = "\uD83E\uDDF5", + [":three:"] = "3️⃣", + [":three_button_mouse:"] = "\uD83D\uDDB1️", + [":thumbdown:"] = "\uD83D\uDC4E", + [":thumbdown::skin-tone-1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":thumbdown::skin-tone-2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":thumbdown::skin-tone-3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":thumbdown::skin-tone-4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":thumbdown::skin-tone-5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":thumbdown_tone1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":thumbdown_tone2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":thumbdown_tone3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":thumbdown_tone4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":thumbdown_tone5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":thumbsdown:"] = "\uD83D\uDC4E", + [":thumbsdown::skin-tone-1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":thumbsdown::skin-tone-2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":thumbsdown::skin-tone-3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":thumbsdown::skin-tone-4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":thumbsdown::skin-tone-5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":thumbsdown_tone1:"] = "\uD83D\uDC4E\uD83C\uDFFB", + [":thumbsdown_tone2:"] = "\uD83D\uDC4E\uD83C\uDFFC", + [":thumbsdown_tone3:"] = "\uD83D\uDC4E\uD83C\uDFFD", + [":thumbsdown_tone4:"] = "\uD83D\uDC4E\uD83C\uDFFE", + [":thumbsdown_tone5:"] = "\uD83D\uDC4E\uD83C\uDFFF", + [":thumbsup:"] = "\uD83D\uDC4D", + [":thumbsup::skin-tone-1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":thumbsup::skin-tone-2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":thumbsup::skin-tone-3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":thumbsup::skin-tone-4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":thumbsup::skin-tone-5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":thumbsup_tone1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":thumbsup_tone2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":thumbsup_tone3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":thumbsup_tone4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":thumbsup_tone5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":thumbup:"] = "\uD83D\uDC4D", + [":thumbup::skin-tone-1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":thumbup::skin-tone-2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":thumbup::skin-tone-3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":thumbup::skin-tone-4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":thumbup::skin-tone-5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":thumbup_tone1:"] = "\uD83D\uDC4D\uD83C\uDFFB", + [":thumbup_tone2:"] = "\uD83D\uDC4D\uD83C\uDFFC", + [":thumbup_tone3:"] = "\uD83D\uDC4D\uD83C\uDFFD", + [":thumbup_tone4:"] = "\uD83D\uDC4D\uD83C\uDFFE", + [":thumbup_tone5:"] = "\uD83D\uDC4D\uD83C\uDFFF", + [":thunder_cloud_and_rain:"] = "⛈️", + [":thunder_cloud_rain:"] = "⛈️", + [":ticket:"] = "\uD83C\uDFAB", + [":tickets:"] = "\uD83C\uDF9F️", + [":tiger2:"] = "\uD83D\uDC05", + [":tiger:"] = "\uD83D\uDC2F", + [":timer:"] = "⏲️", + [":timer_clock:"] = "⏲️", + [":tired_face:"] = "\uD83D\uDE2B", + [":tm:"] = "™️", + [":toilet:"] = "\uD83D\uDEBD", + [":tokyo_tower:"] = "\uD83D\uDDFC", + [":tomato:"] = "\uD83C\uDF45", + [":tongue:"] = "\uD83D\uDC45", + [":toolbox:"] = "\uD83E\uDDF0", + [":tools:"] = "\uD83D\uDEE0️", + [":tooth:"] = "\uD83E\uDDB7", + [":top:"] = "\uD83D\uDD1D", + [":tophat:"] = "\uD83C\uDFA9", + [":track_next:"] = "⏭️", + [":track_previous:"] = "⏮️", + [":trackball:"] = "\uD83D\uDDB2️", + [":tractor:"] = "\uD83D\uDE9C", + [":traffic_light:"] = "\uD83D\uDEA5", + [":train2:"] = "\uD83D\uDE86", + [":train:"] = "\uD83D\uDE8B", + [":tram:"] = "\uD83D\uDE8A", + [":triangular_flag_on_post:"] = "\uD83D\uDEA9", + [":triangular_ruler:"] = "\uD83D\uDCD0", + [":trident:"] = "\uD83D\uDD31", + [":triumph:"] = "\uD83D\uDE24", + [":trolleybus:"] = "\uD83D\uDE8E", + [":trophy:"] = "\uD83C\uDFC6", + [":tropical_drink:"] = "\uD83C\uDF79", + [":tropical_fish:"] = "\uD83D\uDC20", + [":truck:"] = "\uD83D\uDE9A", + [":trumpet:"] = "\uD83C\uDFBA", + [":tulip:"] = "\uD83C\uDF37", + [":tumbler_glass:"] = "\uD83E\uDD43", + [":turkey:"] = "\uD83E\uDD83", + [":turtle:"] = "\uD83D\uDC22", + [":tuxedo_tone1:"] = "\uD83E\uDD35\uD83C\uDFFB", + [":tuxedo_tone2:"] = "\uD83E\uDD35\uD83C\uDFFC", + [":tuxedo_tone3:"] = "\uD83E\uDD35\uD83C\uDFFD", + [":tuxedo_tone4:"] = "\uD83E\uDD35\uD83C\uDFFE", + [":tuxedo_tone5:"] = "\uD83E\uDD35\uD83C\uDFFF", + [":tv:"] = "\uD83D\uDCFA", + [":twisted_rightwards_arrows:"] = "\uD83D\uDD00", + [":two:"] = "2️⃣", + [":two_hearts:"] = "\uD83D\uDC95", + [":two_men_holding_hands:"] = "\uD83D\uDC6C", + [":two_women_holding_hands:"] = "\uD83D\uDC6D", + [":u5272:"] = "\uD83C\uDE39", + [":u5408:"] = "\uD83C\uDE34", + [":u55b6:"] = "\uD83C\uDE3A", + [":u6307:"] = "\uD83C\uDE2F", + [":u6708:"] = "\uD83C\uDE37️", + [":u6709:"] = "\uD83C\uDE36", + [":u6e80:"] = "\uD83C\uDE35", + [":u7121:"] = "\uD83C\uDE1A", + [":u7533:"] = "\uD83C\uDE38", + [":u7981:"] = "\uD83C\uDE32", + [":u7a7a:"] = "\uD83C\uDE33", + [":umbrella2:"] = "☂️", + [":umbrella:"] = "☔", + [":umbrella_on_ground:"] = "⛱️", + [":unamused:"] = "\uD83D\uDE12", + [":underage:"] = "\uD83D\uDD1E", + [":unicorn:"] = "\uD83E\uDD84", + [":unicorn_face:"] = "\uD83E\uDD84", + [":united_nations:"] = "\uD83C\uDDFA\uD83C\uDDF3", + [":unlock:"] = "\uD83D\uDD13", + [":up:"] = "\uD83C\uDD99", + [":upside_down:"] = "\uD83D\uDE43", + [":upside_down_face:"] = "\uD83D\uDE43", + [":urn:"] = "⚱️", + [":v:"] = "✌️", + [":v::skin-tone-1:"] = "✌\uD83C\uDFFB", + [":v::skin-tone-2:"] = "✌\uD83C\uDFFC", + [":v::skin-tone-3:"] = "✌\uD83C\uDFFD", + [":v::skin-tone-4:"] = "✌\uD83C\uDFFE", + [":v::skin-tone-5:"] = "✌\uD83C\uDFFF", + [":v_tone1:"] = "✌\uD83C\uDFFB", + [":v_tone2:"] = "✌\uD83C\uDFFC", + [":v_tone3:"] = "✌\uD83C\uDFFD", + [":v_tone4:"] = "✌\uD83C\uDFFE", + [":v_tone5:"] = "✌\uD83C\uDFFF", + [":vampire:"] = "\uD83E\uDDDB", + [":vampire::skin-tone-1:"] = "\uD83E\uDDDB\uD83C\uDFFB", + [":vampire::skin-tone-2:"] = "\uD83E\uDDDB\uD83C\uDFFC", + [":vampire::skin-tone-3:"] = "\uD83E\uDDDB\uD83C\uDFFD", + [":vampire::skin-tone-4:"] = "\uD83E\uDDDB\uD83C\uDFFE", + [":vampire::skin-tone-5:"] = "\uD83E\uDDDB\uD83C\uDFFF", + [":vampire_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFF", + [":vampire_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFB", + [":vampire_medium_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFE", + [":vampire_medium_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFC", + [":vampire_medium_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFD", + [":vampire_tone1:"] = "\uD83E\uDDDB\uD83C\uDFFB", + [":vampire_tone2:"] = "\uD83E\uDDDB\uD83C\uDFFC", + [":vampire_tone3:"] = "\uD83E\uDDDB\uD83C\uDFFD", + [":vampire_tone4:"] = "\uD83E\uDDDB\uD83C\uDFFE", + [":vampire_tone5:"] = "\uD83E\uDDDB\uD83C\uDFFF", + [":vertical_traffic_light:"] = "\uD83D\uDEA6", + [":vhs:"] = "\uD83D\uDCFC", + [":vibration_mode:"] = "\uD83D\uDCF3", + [":video_camera:"] = "\uD83D\uDCF9", + [":video_game:"] = "\uD83C\uDFAE", + [":violin:"] = "\uD83C\uDFBB", + [":virgo:"] = "♍", + [":volcano:"] = "\uD83C\uDF0B", + [":volleyball:"] = "\uD83C\uDFD0", + [":vs:"] = "\uD83C\uDD9A", + [":vulcan:"] = "\uD83D\uDD96", + [":vulcan::skin-tone-1:"] = "\uD83D\uDD96\uD83C\uDFFB", + [":vulcan::skin-tone-2:"] = "\uD83D\uDD96\uD83C\uDFFC", + [":vulcan::skin-tone-3:"] = "\uD83D\uDD96\uD83C\uDFFD", + [":vulcan::skin-tone-4:"] = "\uD83D\uDD96\uD83C\uDFFE", + [":vulcan::skin-tone-5:"] = "\uD83D\uDD96\uD83C\uDFFF", + [":vulcan_tone1:"] = "\uD83D\uDD96\uD83C\uDFFB", + [":vulcan_tone2:"] = "\uD83D\uDD96\uD83C\uDFFC", + [":vulcan_tone3:"] = "\uD83D\uDD96\uD83C\uDFFD", + [":vulcan_tone4:"] = "\uD83D\uDD96\uD83C\uDFFE", + [":vulcan_tone5:"] = "\uD83D\uDD96\uD83C\uDFFF", + [":waffle:"] = "\uD83E\uDDC7", + [":wales:"] = "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73\uDB40\uDC7F", + [":walking:"] = "\uD83D\uDEB6", + [":walking::skin-tone-1:"] = "\uD83D\uDEB6\uD83C\uDFFB", + [":walking::skin-tone-2:"] = "\uD83D\uDEB6\uD83C\uDFFC", + [":walking::skin-tone-3:"] = "\uD83D\uDEB6\uD83C\uDFFD", + [":walking::skin-tone-4:"] = "\uD83D\uDEB6\uD83C\uDFFE", + [":walking::skin-tone-5:"] = "\uD83D\uDEB6\uD83C\uDFFF", + [":walking_tone1:"] = "\uD83D\uDEB6\uD83C\uDFFB", + [":walking_tone2:"] = "\uD83D\uDEB6\uD83C\uDFFC", + [":walking_tone3:"] = "\uD83D\uDEB6\uD83C\uDFFD", + [":walking_tone4:"] = "\uD83D\uDEB6\uD83C\uDFFE", + [":walking_tone5:"] = "\uD83D\uDEB6\uD83C\uDFFF", + [":waning_crescent_moon:"] = "\uD83C\uDF18", + [":waning_gibbous_moon:"] = "\uD83C\uDF16", + [":warning:"] = "⚠️", + [":wastebasket:"] = "\uD83D\uDDD1️", + [":watch:"] = "⌚", + [":water_buffalo:"] = "\uD83D\uDC03", + [":water_polo:"] = "\uD83E\uDD3D", + [":water_polo::skin-tone-1:"] = "\uD83E\uDD3D\uD83C\uDFFB", + [":water_polo::skin-tone-2:"] = "\uD83E\uDD3D\uD83C\uDFFC", + [":water_polo::skin-tone-3:"] = "\uD83E\uDD3D\uD83C\uDFFD", + [":water_polo::skin-tone-4:"] = "\uD83E\uDD3D\uD83C\uDFFE", + [":water_polo::skin-tone-5:"] = "\uD83E\uDD3D\uD83C\uDFFF", + [":water_polo_tone1:"] = "\uD83E\uDD3D\uD83C\uDFFB", + [":water_polo_tone2:"] = "\uD83E\uDD3D\uD83C\uDFFC", + [":water_polo_tone3:"] = "\uD83E\uDD3D\uD83C\uDFFD", + [":water_polo_tone4:"] = "\uD83E\uDD3D\uD83C\uDFFE", + [":water_polo_tone5:"] = "\uD83E\uDD3D\uD83C\uDFFF", + [":watermelon:"] = "\uD83C\uDF49", + [":wave:"] = "\uD83D\uDC4B", + [":wave::skin-tone-1:"] = "\uD83D\uDC4B\uD83C\uDFFB", + [":wave::skin-tone-2:"] = "\uD83D\uDC4B\uD83C\uDFFC", + [":wave::skin-tone-3:"] = "\uD83D\uDC4B\uD83C\uDFFD", + [":wave::skin-tone-4:"] = "\uD83D\uDC4B\uD83C\uDFFE", + [":wave::skin-tone-5:"] = "\uD83D\uDC4B\uD83C\uDFFF", + [":wave_tone1:"] = "\uD83D\uDC4B\uD83C\uDFFB", + [":wave_tone2:"] = "\uD83D\uDC4B\uD83C\uDFFC", + [":wave_tone3:"] = "\uD83D\uDC4B\uD83C\uDFFD", + [":wave_tone4:"] = "\uD83D\uDC4B\uD83C\uDFFE", + [":wave_tone5:"] = "\uD83D\uDC4B\uD83C\uDFFF", + [":wavy_dash:"] = "〰️", + [":waxing_crescent_moon:"] = "\uD83C\uDF12", + [":waxing_gibbous_moon:"] = "\uD83C\uDF14", + [":wc:"] = "\uD83D\uDEBE", + [":weary:"] = "\uD83D\uDE29", + [":wedding:"] = "\uD83D\uDC92", + [":weight_lifter:"] = "\uD83C\uDFCB️", + [":weight_lifter::skin-tone-1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":weight_lifter::skin-tone-2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":weight_lifter::skin-tone-3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":weight_lifter::skin-tone-4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":weight_lifter::skin-tone-5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":weight_lifter_tone1:"] = "\uD83C\uDFCB\uD83C\uDFFB", + [":weight_lifter_tone2:"] = "\uD83C\uDFCB\uD83C\uDFFC", + [":weight_lifter_tone3:"] = "\uD83C\uDFCB\uD83C\uDFFD", + [":weight_lifter_tone4:"] = "\uD83C\uDFCB\uD83C\uDFFE", + [":weight_lifter_tone5:"] = "\uD83C\uDFCB\uD83C\uDFFF", + [":whale2:"] = "\uD83D\uDC0B", + [":whale:"] = "\uD83D\uDC33", + [":wheel_of_dharma:"] = "☸️", + [":wheelchair:"] = "♿", + [":whisky:"] = "\uD83E\uDD43", + [":white_check_mark:"] = "✅", + [":white_circle:"] = "⚪", + [":white_flower:"] = "\uD83D\uDCAE", + [":white_frowning_face:"] = "☹️", + [":white_heart:"] = "\uD83E\uDD0D", + [":white_large_square:"] = "⬜", + [":white_medium_small_square:"] = "◽", + [":white_medium_square:"] = "◻️", + [":white_small_square:"] = "▫️", + [":white_square_button:"] = "\uD83D\uDD33", + [":white_sun_behind_cloud:"] = "\uD83C\uDF25️", + [":white_sun_behind_cloud_with_rain:"] = "\uD83C\uDF26️", + [":white_sun_cloud:"] = "\uD83C\uDF25️", + [":white_sun_rain_cloud:"] = "\uD83C\uDF26️", + [":white_sun_small_cloud:"] = "\uD83C\uDF24️", + [":white_sun_with_small_cloud:"] = "\uD83C\uDF24️", + [":wilted_flower:"] = "\uD83E\uDD40", + [":wilted_rose:"] = "\uD83E\uDD40", + [":wind_blowing_face:"] = "\uD83C\uDF2C️", + [":wind_chime:"] = "\uD83C\uDF90", + [":wine_glass:"] = "\uD83C\uDF77", + [":wink:"] = "\uD83D\uDE09", + [":wolf:"] = "\uD83D\uDC3A", + [":woman:"] = "\uD83D\uDC69", + [":woman::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB", + [":woman::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC", + [":woman::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD", + [":woman::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE", + [":woman::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF", + [":woman_artist:"] = "\uD83D\uDC69\u200D\uD83C\uDFA8", + [":woman_artist::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":woman_artist::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":woman_artist::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":woman_artist::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":woman_artist::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":woman_artist_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":woman_artist_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":woman_artist_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":woman_artist_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":woman_artist_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":woman_artist_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA8", + [":woman_artist_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA8", + [":woman_artist_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA8", + [":woman_artist_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA8", + [":woman_artist_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA8", + [":woman_astronaut:"] = "\uD83D\uDC69\u200D\uD83D\uDE80", + [":woman_astronaut::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE80", + [":woman_astronaut::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE80", + [":woman_astronaut::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE80", + [":woman_astronaut::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE80", + [":woman_astronaut::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE80", + [":woman_astronaut_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE80", + [":woman_astronaut_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE80", + [":woman_astronaut_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE80", + [":woman_astronaut_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE80", + [":woman_astronaut_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE80", + [":woman_astronaut_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE80", + [":woman_astronaut_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE80", + [":woman_astronaut_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE80", + [":woman_astronaut_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE80", + [":woman_astronaut_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE80", + [":woman_bald:"] = "\uD83D\uDC69\u200D\uD83E\uDDB2", + [":woman_bald::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":woman_bald::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":woman_bald::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":woman_bald::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":woman_bald::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":woman_bald_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":woman_bald_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":woman_bald_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":woman_bald_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":woman_bald_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":woman_bald_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB2", + [":woman_bald_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB2", + [":woman_bald_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB2", + [":woman_bald_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB2", + [":woman_bald_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB2", + [":woman_biking:"] = "\uD83D\uDEB4\u200D♀️", + [":woman_biking::skin-tone-1:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♀️", + [":woman_biking::skin-tone-2:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♀️", + [":woman_biking::skin-tone-3:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♀️", + [":woman_biking::skin-tone-4:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♀️", + [":woman_biking::skin-tone-5:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♀️", + [":woman_biking_dark_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♀️", + [":woman_biking_light_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♀️", + [":woman_biking_medium_dark_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♀️", + [":woman_biking_medium_light_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♀️", + [":woman_biking_medium_skin_tone:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♀️", + [":woman_biking_tone1:"] = "\uD83D\uDEB4\uD83C\uDFFB\u200D♀️", + [":woman_biking_tone2:"] = "\uD83D\uDEB4\uD83C\uDFFC\u200D♀️", + [":woman_biking_tone3:"] = "\uD83D\uDEB4\uD83C\uDFFD\u200D♀️", + [":woman_biking_tone4:"] = "\uD83D\uDEB4\uD83C\uDFFE\u200D♀️", + [":woman_biking_tone5:"] = "\uD83D\uDEB4\uD83C\uDFFF\u200D♀️", + [":woman_bouncing_ball:"] = "⛹️\u200D♀️", + [":woman_bouncing_ball::skin-tone-1:"] = "⛹\uD83C\uDFFB\u200D♀️", + [":woman_bouncing_ball::skin-tone-2:"] = "⛹\uD83C\uDFFC\u200D♀️", + [":woman_bouncing_ball::skin-tone-3:"] = "⛹\uD83C\uDFFD\u200D♀️", + [":woman_bouncing_ball::skin-tone-4:"] = "⛹\uD83C\uDFFE\u200D♀️", + [":woman_bouncing_ball::skin-tone-5:"] = "⛹\uD83C\uDFFF\u200D♀️", + [":woman_bouncing_ball_dark_skin_tone:"] = "⛹\uD83C\uDFFF\u200D♀️", + [":woman_bouncing_ball_light_skin_tone:"] = "⛹\uD83C\uDFFB\u200D♀️", + [":woman_bouncing_ball_medium_dark_skin_tone:"] = "⛹\uD83C\uDFFE\u200D♀️", + [":woman_bouncing_ball_medium_light_skin_tone:"] = "⛹\uD83C\uDFFC\u200D♀️", + [":woman_bouncing_ball_medium_skin_tone:"] = "⛹\uD83C\uDFFD\u200D♀️", + [":woman_bouncing_ball_tone1:"] = "⛹\uD83C\uDFFB\u200D♀️", + [":woman_bouncing_ball_tone2:"] = "⛹\uD83C\uDFFC\u200D♀️", + [":woman_bouncing_ball_tone3:"] = "⛹\uD83C\uDFFD\u200D♀️", + [":woman_bouncing_ball_tone4:"] = "⛹\uD83C\uDFFE\u200D♀️", + [":woman_bouncing_ball_tone5:"] = "⛹\uD83C\uDFFF\u200D♀️", + [":woman_bowing:"] = "\uD83D\uDE47\u200D♀️", + [":woman_bowing::skin-tone-1:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♀️", + [":woman_bowing::skin-tone-2:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♀️", + [":woman_bowing::skin-tone-3:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♀️", + [":woman_bowing::skin-tone-4:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♀️", + [":woman_bowing::skin-tone-5:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♀️", + [":woman_bowing_dark_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♀️", + [":woman_bowing_light_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♀️", + [":woman_bowing_medium_dark_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♀️", + [":woman_bowing_medium_light_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♀️", + [":woman_bowing_medium_skin_tone:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♀️", + [":woman_bowing_tone1:"] = "\uD83D\uDE47\uD83C\uDFFB\u200D♀️", + [":woman_bowing_tone2:"] = "\uD83D\uDE47\uD83C\uDFFC\u200D♀️", + [":woman_bowing_tone3:"] = "\uD83D\uDE47\uD83C\uDFFD\u200D♀️", + [":woman_bowing_tone4:"] = "\uD83D\uDE47\uD83C\uDFFE\u200D♀️", + [":woman_bowing_tone5:"] = "\uD83D\uDE47\uD83C\uDFFF\u200D♀️", + [":woman_cartwheeling:"] = "\uD83E\uDD38\u200D♀️", + [":woman_cartwheeling::skin-tone-1:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♀️", + [":woman_cartwheeling::skin-tone-2:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♀️", + [":woman_cartwheeling::skin-tone-3:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♀️", + [":woman_cartwheeling::skin-tone-4:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♀️", + [":woman_cartwheeling::skin-tone-5:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♀️", + [":woman_cartwheeling_dark_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♀️", + [":woman_cartwheeling_light_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♀️", + [":woman_cartwheeling_medium_dark_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♀️", + [":woman_cartwheeling_medium_light_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♀️", + [":woman_cartwheeling_medium_skin_tone:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♀️", + [":woman_cartwheeling_tone1:"] = "\uD83E\uDD38\uD83C\uDFFB\u200D♀️", + [":woman_cartwheeling_tone2:"] = "\uD83E\uDD38\uD83C\uDFFC\u200D♀️", + [":woman_cartwheeling_tone3:"] = "\uD83E\uDD38\uD83C\uDFFD\u200D♀️", + [":woman_cartwheeling_tone4:"] = "\uD83E\uDD38\uD83C\uDFFE\u200D♀️", + [":woman_cartwheeling_tone5:"] = "\uD83E\uDD38\uD83C\uDFFF\u200D♀️", + [":woman_climbing:"] = "\uD83E\uDDD7\u200D♀️", + [":woman_climbing::skin-tone-1:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♀️", + [":woman_climbing::skin-tone-2:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♀️", + [":woman_climbing::skin-tone-3:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♀️", + [":woman_climbing::skin-tone-4:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♀️", + [":woman_climbing::skin-tone-5:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♀️", + [":woman_climbing_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♀️", + [":woman_climbing_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♀️", + [":woman_climbing_medium_dark_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♀️", + [":woman_climbing_medium_light_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♀️", + [":woman_climbing_medium_skin_tone:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♀️", + [":woman_climbing_tone1:"] = "\uD83E\uDDD7\uD83C\uDFFB\u200D♀️", + [":woman_climbing_tone2:"] = "\uD83E\uDDD7\uD83C\uDFFC\u200D♀️", + [":woman_climbing_tone3:"] = "\uD83E\uDDD7\uD83C\uDFFD\u200D♀️", + [":woman_climbing_tone4:"] = "\uD83E\uDDD7\uD83C\uDFFE\u200D♀️", + [":woman_climbing_tone5:"] = "\uD83E\uDDD7\uD83C\uDFFF\u200D♀️", + [":woman_construction_worker:"] = "\uD83D\uDC77\u200D♀️", + [":woman_construction_worker::skin-tone-1:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♀️", + [":woman_construction_worker::skin-tone-2:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♀️", + [":woman_construction_worker::skin-tone-3:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♀️", + [":woman_construction_worker::skin-tone-4:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♀️", + [":woman_construction_worker::skin-tone-5:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♀️", + [":woman_construction_worker_dark_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♀️", + [":woman_construction_worker_light_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♀️", + [":woman_construction_worker_medium_dark_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♀️", + [":woman_construction_worker_medium_light_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♀️", + [":woman_construction_worker_medium_skin_tone:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♀️", + [":woman_construction_worker_tone1:"] = "\uD83D\uDC77\uD83C\uDFFB\u200D♀️", + [":woman_construction_worker_tone2:"] = "\uD83D\uDC77\uD83C\uDFFC\u200D♀️", + [":woman_construction_worker_tone3:"] = "\uD83D\uDC77\uD83C\uDFFD\u200D♀️", + [":woman_construction_worker_tone4:"] = "\uD83D\uDC77\uD83C\uDFFE\u200D♀️", + [":woman_construction_worker_tone5:"] = "\uD83D\uDC77\uD83C\uDFFF\u200D♀️", + [":woman_cook:"] = "\uD83D\uDC69\u200D\uD83C\uDF73", + [":woman_cook::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF73", + [":woman_cook::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF73", + [":woman_cook::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF73", + [":woman_cook::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF73", + [":woman_cook::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF73", + [":woman_cook_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF73", + [":woman_cook_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF73", + [":woman_cook_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF73", + [":woman_cook_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF73", + [":woman_cook_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF73", + [":woman_cook_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF73", + [":woman_cook_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF73", + [":woman_cook_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF73", + [":woman_cook_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF73", + [":woman_cook_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF73", + [":woman_curly_haired:"] = "\uD83D\uDC69\u200D\uD83E\uDDB1", + [":woman_curly_haired::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":woman_curly_haired::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":woman_curly_haired::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":woman_curly_haired::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":woman_curly_haired::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":woman_curly_haired_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":woman_curly_haired_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":woman_curly_haired_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":woman_curly_haired_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":woman_curly_haired_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":woman_curly_haired_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB1", + [":woman_curly_haired_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB1", + [":woman_curly_haired_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB1", + [":woman_curly_haired_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB1", + [":woman_curly_haired_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB1", + [":woman_detective:"] = "\uD83D\uDD75️\u200D♀️", + [":woman_detective::skin-tone-1:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♀️", + [":woman_detective::skin-tone-2:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♀️", + [":woman_detective::skin-tone-3:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♀️", + [":woman_detective::skin-tone-4:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♀️", + [":woman_detective::skin-tone-5:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♀️", + [":woman_detective_dark_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♀️", + [":woman_detective_light_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♀️", + [":woman_detective_medium_dark_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♀️", + [":woman_detective_medium_light_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♀️", + [":woman_detective_medium_skin_tone:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♀️", + [":woman_detective_tone1:"] = "\uD83D\uDD75\uD83C\uDFFB\u200D♀️", + [":woman_detective_tone2:"] = "\uD83D\uDD75\uD83C\uDFFC\u200D♀️", + [":woman_detective_tone3:"] = "\uD83D\uDD75\uD83C\uDFFD\u200D♀️", + [":woman_detective_tone4:"] = "\uD83D\uDD75\uD83C\uDFFE\u200D♀️", + [":woman_detective_tone5:"] = "\uD83D\uDD75\uD83C\uDFFF\u200D♀️", + [":woman_elf:"] = "\uD83E\uDDDD\u200D♀️", + [":woman_elf::skin-tone-1:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♀️", + [":woman_elf::skin-tone-2:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♀️", + [":woman_elf::skin-tone-3:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♀️", + [":woman_elf::skin-tone-4:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♀️", + [":woman_elf::skin-tone-5:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♀️", + [":woman_elf_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♀️", + [":woman_elf_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♀️", + [":woman_elf_medium_dark_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♀️", + [":woman_elf_medium_light_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♀️", + [":woman_elf_medium_skin_tone:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♀️", + [":woman_elf_tone1:"] = "\uD83E\uDDDD\uD83C\uDFFB\u200D♀️", + [":woman_elf_tone2:"] = "\uD83E\uDDDD\uD83C\uDFFC\u200D♀️", + [":woman_elf_tone3:"] = "\uD83E\uDDDD\uD83C\uDFFD\u200D♀️", + [":woman_elf_tone4:"] = "\uD83E\uDDDD\uD83C\uDFFE\u200D♀️", + [":woman_elf_tone5:"] = "\uD83E\uDDDD\uD83C\uDFFF\u200D♀️", + [":woman_facepalming:"] = "\uD83E\uDD26\u200D♀️", + [":woman_facepalming::skin-tone-1:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♀️", + [":woman_facepalming::skin-tone-2:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♀️", + [":woman_facepalming::skin-tone-3:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♀️", + [":woman_facepalming::skin-tone-4:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♀️", + [":woman_facepalming::skin-tone-5:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♀️", + [":woman_facepalming_dark_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♀️", + [":woman_facepalming_light_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♀️", + [":woman_facepalming_medium_dark_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♀️", + [":woman_facepalming_medium_light_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♀️", + [":woman_facepalming_medium_skin_tone:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♀️", + [":woman_facepalming_tone1:"] = "\uD83E\uDD26\uD83C\uDFFB\u200D♀️", + [":woman_facepalming_tone2:"] = "\uD83E\uDD26\uD83C\uDFFC\u200D♀️", + [":woman_facepalming_tone3:"] = "\uD83E\uDD26\uD83C\uDFFD\u200D♀️", + [":woman_facepalming_tone4:"] = "\uD83E\uDD26\uD83C\uDFFE\u200D♀️", + [":woman_facepalming_tone5:"] = "\uD83E\uDD26\uD83C\uDFFF\u200D♀️", + [":woman_factory_worker:"] = "\uD83D\uDC69\u200D\uD83C\uDFED", + [":woman_factory_worker::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFED", + [":woman_factory_worker::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFED", + [":woman_factory_worker::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFED", + [":woman_factory_worker::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFED", + [":woman_factory_worker::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFED", + [":woman_factory_worker_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFED", + [":woman_factory_worker_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFED", + [":woman_factory_worker_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFED", + [":woman_factory_worker_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFED", + [":woman_factory_worker_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFED", + [":woman_factory_worker_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFED", + [":woman_factory_worker_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFED", + [":woman_factory_worker_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFED", + [":woman_factory_worker_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFED", + [":woman_factory_worker_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFED", + [":woman_fairy:"] = "\uD83E\uDDDA\u200D♀️", + [":woman_fairy::skin-tone-1:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♀️", + [":woman_fairy::skin-tone-2:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♀️", + [":woman_fairy::skin-tone-3:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♀️", + [":woman_fairy::skin-tone-4:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♀️", + [":woman_fairy::skin-tone-5:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♀️", + [":woman_fairy_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♀️", + [":woman_fairy_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♀️", + [":woman_fairy_medium_dark_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♀️", + [":woman_fairy_medium_light_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♀️", + [":woman_fairy_medium_skin_tone:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♀️", + [":woman_fairy_tone1:"] = "\uD83E\uDDDA\uD83C\uDFFB\u200D♀️", + [":woman_fairy_tone2:"] = "\uD83E\uDDDA\uD83C\uDFFC\u200D♀️", + [":woman_fairy_tone3:"] = "\uD83E\uDDDA\uD83C\uDFFD\u200D♀️", + [":woman_fairy_tone4:"] = "\uD83E\uDDDA\uD83C\uDFFE\u200D♀️", + [":woman_fairy_tone5:"] = "\uD83E\uDDDA\uD83C\uDFFF\u200D♀️", + [":woman_farmer:"] = "\uD83D\uDC69\u200D\uD83C\uDF3E", + [":woman_farmer::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":woman_farmer::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":woman_farmer::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":woman_farmer::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":woman_farmer::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":woman_farmer_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":woman_farmer_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":woman_farmer_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":woman_farmer_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":woman_farmer_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":woman_farmer_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF3E", + [":woman_farmer_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF3E", + [":woman_farmer_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF3E", + [":woman_farmer_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF3E", + [":woman_farmer_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF3E", + [":woman_firefighter:"] = "\uD83D\uDC69\u200D\uD83D\uDE92", + [":woman_firefighter::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE92", + [":woman_firefighter::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE92", + [":woman_firefighter::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE92", + [":woman_firefighter::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE92", + [":woman_firefighter::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE92", + [":woman_firefighter_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE92", + [":woman_firefighter_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE92", + [":woman_firefighter_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE92", + [":woman_firefighter_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE92", + [":woman_firefighter_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE92", + [":woman_firefighter_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDE92", + [":woman_firefighter_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDE92", + [":woman_firefighter_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDE92", + [":woman_firefighter_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDE92", + [":woman_firefighter_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDE92", + [":woman_frowning:"] = "\uD83D\uDE4D\u200D♀️", + [":woman_frowning::skin-tone-1:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♀️", + [":woman_frowning::skin-tone-2:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♀️", + [":woman_frowning::skin-tone-3:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♀️", + [":woman_frowning::skin-tone-4:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♀️", + [":woman_frowning::skin-tone-5:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♀️", + [":woman_frowning_dark_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♀️", + [":woman_frowning_light_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♀️", + [":woman_frowning_medium_dark_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♀️", + [":woman_frowning_medium_light_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♀️", + [":woman_frowning_medium_skin_tone:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♀️", + [":woman_frowning_tone1:"] = "\uD83D\uDE4D\uD83C\uDFFB\u200D♀️", + [":woman_frowning_tone2:"] = "\uD83D\uDE4D\uD83C\uDFFC\u200D♀️", + [":woman_frowning_tone3:"] = "\uD83D\uDE4D\uD83C\uDFFD\u200D♀️", + [":woman_frowning_tone4:"] = "\uD83D\uDE4D\uD83C\uDFFE\u200D♀️", + [":woman_frowning_tone5:"] = "\uD83D\uDE4D\uD83C\uDFFF\u200D♀️", + [":woman_genie:"] = "\uD83E\uDDDE\u200D♀️", + [":woman_gesturing_no:"] = "\uD83D\uDE45\u200D♀️", + [":woman_gesturing_no::skin-tone-1:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_no::skin-tone-2:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_no::skin-tone-3:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_no::skin-tone-4:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_no::skin-tone-5:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♀️", + [":woman_gesturing_no_dark_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♀️", + [":woman_gesturing_no_light_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_no_medium_dark_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_no_medium_light_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_no_medium_skin_tone:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_no_tone1:"] = "\uD83D\uDE45\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_no_tone2:"] = "\uD83D\uDE45\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_no_tone3:"] = "\uD83D\uDE45\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_no_tone4:"] = "\uD83D\uDE45\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_no_tone5:"] = "\uD83D\uDE45\uD83C\uDFFF\u200D♀️", + [":woman_gesturing_ok:"] = "\uD83D\uDE46\u200D♀️", + [":woman_gesturing_ok::skin-tone-1:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_ok::skin-tone-2:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_ok::skin-tone-3:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_ok::skin-tone-4:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_ok::skin-tone-5:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♀️", + [":woman_gesturing_ok_dark_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♀️", + [":woman_gesturing_ok_light_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_ok_medium_dark_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_ok_medium_light_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_ok_medium_skin_tone:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_ok_tone1:"] = "\uD83D\uDE46\uD83C\uDFFB\u200D♀️", + [":woman_gesturing_ok_tone2:"] = "\uD83D\uDE46\uD83C\uDFFC\u200D♀️", + [":woman_gesturing_ok_tone3:"] = "\uD83D\uDE46\uD83C\uDFFD\u200D♀️", + [":woman_gesturing_ok_tone4:"] = "\uD83D\uDE46\uD83C\uDFFE\u200D♀️", + [":woman_gesturing_ok_tone5:"] = "\uD83D\uDE46\uD83C\uDFFF\u200D♀️", + [":woman_getting_face_massage:"] = "\uD83D\uDC86\u200D♀️", + [":woman_getting_face_massage::skin-tone-1:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♀️", + [":woman_getting_face_massage::skin-tone-2:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♀️", + [":woman_getting_face_massage::skin-tone-3:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♀️", + [":woman_getting_face_massage::skin-tone-4:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♀️", + [":woman_getting_face_massage::skin-tone-5:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♀️", + [":woman_getting_face_massage_dark_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♀️", + [":woman_getting_face_massage_light_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♀️", + [":woman_getting_face_massage_medium_dark_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♀️", + [":woman_getting_face_massage_medium_light_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♀️", + [":woman_getting_face_massage_medium_skin_tone:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♀️", + [":woman_getting_face_massage_tone1:"] = "\uD83D\uDC86\uD83C\uDFFB\u200D♀️", + [":woman_getting_face_massage_tone2:"] = "\uD83D\uDC86\uD83C\uDFFC\u200D♀️", + [":woman_getting_face_massage_tone3:"] = "\uD83D\uDC86\uD83C\uDFFD\u200D♀️", + [":woman_getting_face_massage_tone4:"] = "\uD83D\uDC86\uD83C\uDFFE\u200D♀️", + [":woman_getting_face_massage_tone5:"] = "\uD83D\uDC86\uD83C\uDFFF\u200D♀️", + [":woman_getting_haircut:"] = "\uD83D\uDC87\u200D♀️", + [":woman_getting_haircut::skin-tone-1:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♀️", + [":woman_getting_haircut::skin-tone-2:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♀️", + [":woman_getting_haircut::skin-tone-3:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♀️", + [":woman_getting_haircut::skin-tone-4:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♀️", + [":woman_getting_haircut::skin-tone-5:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♀️", + [":woman_getting_haircut_dark_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♀️", + [":woman_getting_haircut_light_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♀️", + [":woman_getting_haircut_medium_dark_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♀️", + [":woman_getting_haircut_medium_light_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♀️", + [":woman_getting_haircut_medium_skin_tone:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♀️", + [":woman_getting_haircut_tone1:"] = "\uD83D\uDC87\uD83C\uDFFB\u200D♀️", + [":woman_getting_haircut_tone2:"] = "\uD83D\uDC87\uD83C\uDFFC\u200D♀️", + [":woman_getting_haircut_tone3:"] = "\uD83D\uDC87\uD83C\uDFFD\u200D♀️", + [":woman_getting_haircut_tone4:"] = "\uD83D\uDC87\uD83C\uDFFE\u200D♀️", + [":woman_getting_haircut_tone5:"] = "\uD83D\uDC87\uD83C\uDFFF\u200D♀️", + [":woman_golfing:"] = "\uD83C\uDFCC️\u200D♀️", + [":woman_golfing::skin-tone-1:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♀️", + [":woman_golfing::skin-tone-2:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♀️", + [":woman_golfing::skin-tone-3:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♀️", + [":woman_golfing::skin-tone-4:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♀️", + [":woman_golfing::skin-tone-5:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♀️", + [":woman_golfing_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♀️", + [":woman_golfing_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♀️", + [":woman_golfing_medium_dark_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♀️", + [":woman_golfing_medium_light_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♀️", + [":woman_golfing_medium_skin_tone:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♀️", + [":woman_golfing_tone1:"] = "\uD83C\uDFCC\uD83C\uDFFB\u200D♀️", + [":woman_golfing_tone2:"] = "\uD83C\uDFCC\uD83C\uDFFC\u200D♀️", + [":woman_golfing_tone3:"] = "\uD83C\uDFCC\uD83C\uDFFD\u200D♀️", + [":woman_golfing_tone4:"] = "\uD83C\uDFCC\uD83C\uDFFE\u200D♀️", + [":woman_golfing_tone5:"] = "\uD83C\uDFCC\uD83C\uDFFF\u200D♀️", + [":woman_guard:"] = "\uD83D\uDC82\u200D♀️", + [":woman_guard::skin-tone-1:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♀️", + [":woman_guard::skin-tone-2:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♀️", + [":woman_guard::skin-tone-3:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♀️", + [":woman_guard::skin-tone-4:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♀️", + [":woman_guard::skin-tone-5:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♀️", + [":woman_guard_dark_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♀️", + [":woman_guard_light_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♀️", + [":woman_guard_medium_dark_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♀️", + [":woman_guard_medium_light_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♀️", + [":woman_guard_medium_skin_tone:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♀️", + [":woman_guard_tone1:"] = "\uD83D\uDC82\uD83C\uDFFB\u200D♀️", + [":woman_guard_tone2:"] = "\uD83D\uDC82\uD83C\uDFFC\u200D♀️", + [":woman_guard_tone3:"] = "\uD83D\uDC82\uD83C\uDFFD\u200D♀️", + [":woman_guard_tone4:"] = "\uD83D\uDC82\uD83C\uDFFE\u200D♀️", + [":woman_guard_tone5:"] = "\uD83D\uDC82\uD83C\uDFFF\u200D♀️", + [":woman_health_worker:"] = "\uD83D\uDC69\u200D⚕️", + [":woman_health_worker::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚕️", + [":woman_health_worker::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚕️", + [":woman_health_worker::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚕️", + [":woman_health_worker::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚕️", + [":woman_health_worker::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚕️", + [":woman_health_worker_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚕️", + [":woman_health_worker_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚕️", + [":woman_health_worker_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚕️", + [":woman_health_worker_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚕️", + [":woman_health_worker_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚕️", + [":woman_health_worker_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚕️", + [":woman_health_worker_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚕️", + [":woman_health_worker_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚕️", + [":woman_health_worker_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚕️", + [":woman_health_worker_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚕️", + [":woman_in_lotus_position:"] = "\uD83E\uDDD8\u200D♀️", + [":woman_in_lotus_position::skin-tone-1:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♀️", + [":woman_in_lotus_position::skin-tone-2:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♀️", + [":woman_in_lotus_position::skin-tone-3:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♀️", + [":woman_in_lotus_position::skin-tone-4:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♀️", + [":woman_in_lotus_position::skin-tone-5:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♀️", + [":woman_in_lotus_position_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♀️", + [":woman_in_lotus_position_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♀️", + [":woman_in_lotus_position_medium_dark_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♀️", + [":woman_in_lotus_position_medium_light_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♀️", + [":woman_in_lotus_position_medium_skin_tone:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♀️", + [":woman_in_lotus_position_tone1:"] = "\uD83E\uDDD8\uD83C\uDFFB\u200D♀️", + [":woman_in_lotus_position_tone2:"] = "\uD83E\uDDD8\uD83C\uDFFC\u200D♀️", + [":woman_in_lotus_position_tone3:"] = "\uD83E\uDDD8\uD83C\uDFFD\u200D♀️", + [":woman_in_lotus_position_tone4:"] = "\uD83E\uDDD8\uD83C\uDFFE\u200D♀️", + [":woman_in_lotus_position_tone5:"] = "\uD83E\uDDD8\uD83C\uDFFF\u200D♀️", + [":woman_in_manual_wheelchair:"] = "\uD83D\uDC69\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBD", + [":woman_in_manual_wheelchair_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBD", + [":woman_in_motorized_wheelchair:"] = "\uD83D\uDC69\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDBC", + [":woman_in_motorized_wheelchair_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDBC", + [":woman_in_steamy_room:"] = "\uD83E\uDDD6\u200D♀️", + [":woman_in_steamy_room::skin-tone-1:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♀️", + [":woman_in_steamy_room::skin-tone-2:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♀️", + [":woman_in_steamy_room::skin-tone-3:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♀️", + [":woman_in_steamy_room::skin-tone-4:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♀️", + [":woman_in_steamy_room::skin-tone-5:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♀️", + [":woman_in_steamy_room_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♀️", + [":woman_in_steamy_room_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♀️", + [":woman_in_steamy_room_medium_dark_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♀️", + [":woman_in_steamy_room_medium_light_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♀️", + [":woman_in_steamy_room_medium_skin_tone:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♀️", + [":woman_in_steamy_room_tone1:"] = "\uD83E\uDDD6\uD83C\uDFFB\u200D♀️", + [":woman_in_steamy_room_tone2:"] = "\uD83E\uDDD6\uD83C\uDFFC\u200D♀️", + [":woman_in_steamy_room_tone3:"] = "\uD83E\uDDD6\uD83C\uDFFD\u200D♀️", + [":woman_in_steamy_room_tone4:"] = "\uD83E\uDDD6\uD83C\uDFFE\u200D♀️", + [":woman_in_steamy_room_tone5:"] = "\uD83E\uDDD6\uD83C\uDFFF\u200D♀️", + [":woman_judge:"] = "\uD83D\uDC69\u200D⚖️", + [":woman_judge::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚖️", + [":woman_judge::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚖️", + [":woman_judge::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚖️", + [":woman_judge::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚖️", + [":woman_judge::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚖️", + [":woman_judge_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚖️", + [":woman_judge_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚖️", + [":woman_judge_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚖️", + [":woman_judge_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚖️", + [":woman_judge_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚖️", + [":woman_judge_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D⚖️", + [":woman_judge_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D⚖️", + [":woman_judge_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D⚖️", + [":woman_judge_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D⚖️", + [":woman_judge_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D⚖️", + [":woman_juggling:"] = "\uD83E\uDD39\u200D♀️", + [":woman_juggling::skin-tone-1:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♀️", + [":woman_juggling::skin-tone-2:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♀️", + [":woman_juggling::skin-tone-3:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♀️", + [":woman_juggling::skin-tone-4:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♀️", + [":woman_juggling::skin-tone-5:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♀️", + [":woman_juggling_dark_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♀️", + [":woman_juggling_light_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♀️", + [":woman_juggling_medium_dark_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♀️", + [":woman_juggling_medium_light_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♀️", + [":woman_juggling_medium_skin_tone:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♀️", + [":woman_juggling_tone1:"] = "\uD83E\uDD39\uD83C\uDFFB\u200D♀️", + [":woman_juggling_tone2:"] = "\uD83E\uDD39\uD83C\uDFFC\u200D♀️", + [":woman_juggling_tone3:"] = "\uD83E\uDD39\uD83C\uDFFD\u200D♀️", + [":woman_juggling_tone4:"] = "\uD83E\uDD39\uD83C\uDFFE\u200D♀️", + [":woman_juggling_tone5:"] = "\uD83E\uDD39\uD83C\uDFFF\u200D♀️", + [":woman_kneeling:"] = "\uD83E\uDDCE\u200D♀️", + [":woman_kneeling::skin-tone-1:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♀️", + [":woman_kneeling::skin-tone-2:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♀️", + [":woman_kneeling::skin-tone-3:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♀️", + [":woman_kneeling::skin-tone-4:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♀️", + [":woman_kneeling::skin-tone-5:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♀️", + [":woman_kneeling_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♀️", + [":woman_kneeling_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♀️", + [":woman_kneeling_medium_dark_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♀️", + [":woman_kneeling_medium_light_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♀️", + [":woman_kneeling_medium_skin_tone:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♀️", + [":woman_kneeling_tone1:"] = "\uD83E\uDDCE\uD83C\uDFFB\u200D♀️", + [":woman_kneeling_tone2:"] = "\uD83E\uDDCE\uD83C\uDFFC\u200D♀️", + [":woman_kneeling_tone3:"] = "\uD83E\uDDCE\uD83C\uDFFD\u200D♀️", + [":woman_kneeling_tone4:"] = "\uD83E\uDDCE\uD83C\uDFFE\u200D♀️", + [":woman_kneeling_tone5:"] = "\uD83E\uDDCE\uD83C\uDFFF\u200D♀️", + [":woman_lifting_weights:"] = "\uD83C\uDFCB️\u200D♀️", + [":woman_lifting_weights::skin-tone-1:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♀️", + [":woman_lifting_weights::skin-tone-2:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♀️", + [":woman_lifting_weights::skin-tone-3:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♀️", + [":woman_lifting_weights::skin-tone-4:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♀️", + [":woman_lifting_weights::skin-tone-5:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♀️", + [":woman_lifting_weights_dark_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♀️", + [":woman_lifting_weights_light_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♀️", + [":woman_lifting_weights_medium_dark_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♀️", + [":woman_lifting_weights_medium_light_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♀️", + [":woman_lifting_weights_medium_skin_tone:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♀️", + [":woman_lifting_weights_tone1:"] = "\uD83C\uDFCB\uD83C\uDFFB\u200D♀️", + [":woman_lifting_weights_tone2:"] = "\uD83C\uDFCB\uD83C\uDFFC\u200D♀️", + [":woman_lifting_weights_tone3:"] = "\uD83C\uDFCB\uD83C\uDFFD\u200D♀️", + [":woman_lifting_weights_tone4:"] = "\uD83C\uDFCB\uD83C\uDFFE\u200D♀️", + [":woman_lifting_weights_tone5:"] = "\uD83C\uDFCB\uD83C\uDFFF\u200D♀️", + [":woman_mage:"] = "\uD83E\uDDD9\u200D♀️", + [":woman_mage::skin-tone-1:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♀️", + [":woman_mage::skin-tone-2:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♀️", + [":woman_mage::skin-tone-3:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♀️", + [":woman_mage::skin-tone-4:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♀️", + [":woman_mage::skin-tone-5:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♀️", + [":woman_mage_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♀️", + [":woman_mage_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♀️", + [":woman_mage_medium_dark_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♀️", + [":woman_mage_medium_light_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♀️", + [":woman_mage_medium_skin_tone:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♀️", + [":woman_mage_tone1:"] = "\uD83E\uDDD9\uD83C\uDFFB\u200D♀️", + [":woman_mage_tone2:"] = "\uD83E\uDDD9\uD83C\uDFFC\u200D♀️", + [":woman_mage_tone3:"] = "\uD83E\uDDD9\uD83C\uDFFD\u200D♀️", + [":woman_mage_tone4:"] = "\uD83E\uDDD9\uD83C\uDFFE\u200D♀️", + [":woman_mage_tone5:"] = "\uD83E\uDDD9\uD83C\uDFFF\u200D♀️", + [":woman_mechanic:"] = "\uD83D\uDC69\u200D\uD83D\uDD27", + [":woman_mechanic::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD27", + [":woman_mechanic::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD27", + [":woman_mechanic::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD27", + [":woman_mechanic::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD27", + [":woman_mechanic::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD27", + [":woman_mechanic_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD27", + [":woman_mechanic_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD27", + [":woman_mechanic_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD27", + [":woman_mechanic_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD27", + [":woman_mechanic_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD27", + [":woman_mechanic_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD27", + [":woman_mechanic_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD27", + [":woman_mechanic_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD27", + [":woman_mechanic_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD27", + [":woman_mechanic_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD27", + [":woman_mountain_biking:"] = "\uD83D\uDEB5\u200D♀️", + [":woman_mountain_biking::skin-tone-1:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♀️", + [":woman_mountain_biking::skin-tone-2:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♀️", + [":woman_mountain_biking::skin-tone-3:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♀️", + [":woman_mountain_biking::skin-tone-4:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♀️", + [":woman_mountain_biking::skin-tone-5:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♀️", + [":woman_mountain_biking_dark_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♀️", + [":woman_mountain_biking_light_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♀️", + [":woman_mountain_biking_medium_dark_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♀️", + [":woman_mountain_biking_medium_light_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♀️", + [":woman_mountain_biking_medium_skin_tone:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♀️", + [":woman_mountain_biking_tone1:"] = "\uD83D\uDEB5\uD83C\uDFFB\u200D♀️", + [":woman_mountain_biking_tone2:"] = "\uD83D\uDEB5\uD83C\uDFFC\u200D♀️", + [":woman_mountain_biking_tone3:"] = "\uD83D\uDEB5\uD83C\uDFFD\u200D♀️", + [":woman_mountain_biking_tone4:"] = "\uD83D\uDEB5\uD83C\uDFFE\u200D♀️", + [":woman_mountain_biking_tone5:"] = "\uD83D\uDEB5\uD83C\uDFFF\u200D♀️", + [":woman_office_worker:"] = "\uD83D\uDC69\u200D\uD83D\uDCBC", + [":woman_office_worker::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":woman_office_worker::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":woman_office_worker::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":woman_office_worker::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":woman_office_worker::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":woman_office_worker_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":woman_office_worker_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":woman_office_worker_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":woman_office_worker_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":woman_office_worker_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":woman_office_worker_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBC", + [":woman_office_worker_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBC", + [":woman_office_worker_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBC", + [":woman_office_worker_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBC", + [":woman_office_worker_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBC", + [":woman_pilot:"] = "\uD83D\uDC69\u200D✈️", + [":woman_pilot::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D✈️", + [":woman_pilot::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D✈️", + [":woman_pilot::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D✈️", + [":woman_pilot::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D✈️", + [":woman_pilot::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D✈️", + [":woman_pilot_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D✈️", + [":woman_pilot_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D✈️", + [":woman_pilot_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D✈️", + [":woman_pilot_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D✈️", + [":woman_pilot_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D✈️", + [":woman_pilot_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D✈️", + [":woman_pilot_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D✈️", + [":woman_pilot_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D✈️", + [":woman_pilot_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D✈️", + [":woman_pilot_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D✈️", + [":woman_playing_handball:"] = "\uD83E\uDD3E\u200D♀️", + [":woman_playing_handball::skin-tone-1:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♀️", + [":woman_playing_handball::skin-tone-2:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♀️", + [":woman_playing_handball::skin-tone-3:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♀️", + [":woman_playing_handball::skin-tone-4:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♀️", + [":woman_playing_handball::skin-tone-5:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♀️", + [":woman_playing_handball_dark_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♀️", + [":woman_playing_handball_light_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♀️", + [":woman_playing_handball_medium_dark_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♀️", + [":woman_playing_handball_medium_light_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♀️", + [":woman_playing_handball_medium_skin_tone:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♀️", + [":woman_playing_handball_tone1:"] = "\uD83E\uDD3E\uD83C\uDFFB\u200D♀️", + [":woman_playing_handball_tone2:"] = "\uD83E\uDD3E\uD83C\uDFFC\u200D♀️", + [":woman_playing_handball_tone3:"] = "\uD83E\uDD3E\uD83C\uDFFD\u200D♀️", + [":woman_playing_handball_tone4:"] = "\uD83E\uDD3E\uD83C\uDFFE\u200D♀️", + [":woman_playing_handball_tone5:"] = "\uD83E\uDD3E\uD83C\uDFFF\u200D♀️", + [":woman_playing_water_polo:"] = "\uD83E\uDD3D\u200D♀️", + [":woman_playing_water_polo::skin-tone-1:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♀️", + [":woman_playing_water_polo::skin-tone-2:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♀️", + [":woman_playing_water_polo::skin-tone-3:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♀️", + [":woman_playing_water_polo::skin-tone-4:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♀️", + [":woman_playing_water_polo::skin-tone-5:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♀️", + [":woman_playing_water_polo_dark_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♀️", + [":woman_playing_water_polo_light_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♀️", + [":woman_playing_water_polo_medium_dark_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♀️", + [":woman_playing_water_polo_medium_light_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♀️", + [":woman_playing_water_polo_medium_skin_tone:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♀️", + [":woman_playing_water_polo_tone1:"] = "\uD83E\uDD3D\uD83C\uDFFB\u200D♀️", + [":woman_playing_water_polo_tone2:"] = "\uD83E\uDD3D\uD83C\uDFFC\u200D♀️", + [":woman_playing_water_polo_tone3:"] = "\uD83E\uDD3D\uD83C\uDFFD\u200D♀️", + [":woman_playing_water_polo_tone4:"] = "\uD83E\uDD3D\uD83C\uDFFE\u200D♀️", + [":woman_playing_water_polo_tone5:"] = "\uD83E\uDD3D\uD83C\uDFFF\u200D♀️", + [":woman_police_officer:"] = "\uD83D\uDC6E\u200D♀️", + [":woman_police_officer::skin-tone-1:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♀️", + [":woman_police_officer::skin-tone-2:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♀️", + [":woman_police_officer::skin-tone-3:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♀️", + [":woman_police_officer::skin-tone-4:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♀️", + [":woman_police_officer::skin-tone-5:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♀️", + [":woman_police_officer_dark_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♀️", + [":woman_police_officer_light_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♀️", + [":woman_police_officer_medium_dark_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♀️", + [":woman_police_officer_medium_light_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♀️", + [":woman_police_officer_medium_skin_tone:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♀️", + [":woman_police_officer_tone1:"] = "\uD83D\uDC6E\uD83C\uDFFB\u200D♀️", + [":woman_police_officer_tone2:"] = "\uD83D\uDC6E\uD83C\uDFFC\u200D♀️", + [":woman_police_officer_tone3:"] = "\uD83D\uDC6E\uD83C\uDFFD\u200D♀️", + [":woman_police_officer_tone4:"] = "\uD83D\uDC6E\uD83C\uDFFE\u200D♀️", + [":woman_police_officer_tone5:"] = "\uD83D\uDC6E\uD83C\uDFFF\u200D♀️", + [":woman_pouting:"] = "\uD83D\uDE4E\u200D♀️", + [":woman_pouting::skin-tone-1:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♀️", + [":woman_pouting::skin-tone-2:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♀️", + [":woman_pouting::skin-tone-3:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♀️", + [":woman_pouting::skin-tone-4:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♀️", + [":woman_pouting::skin-tone-5:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♀️", + [":woman_pouting_dark_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♀️", + [":woman_pouting_light_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♀️", + [":woman_pouting_medium_dark_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♀️", + [":woman_pouting_medium_light_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♀️", + [":woman_pouting_medium_skin_tone:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♀️", + [":woman_pouting_tone1:"] = "\uD83D\uDE4E\uD83C\uDFFB\u200D♀️", + [":woman_pouting_tone2:"] = "\uD83D\uDE4E\uD83C\uDFFC\u200D♀️", + [":woman_pouting_tone3:"] = "\uD83D\uDE4E\uD83C\uDFFD\u200D♀️", + [":woman_pouting_tone4:"] = "\uD83D\uDE4E\uD83C\uDFFE\u200D♀️", + [":woman_pouting_tone5:"] = "\uD83D\uDE4E\uD83C\uDFFF\u200D♀️", + [":woman_raising_hand:"] = "\uD83D\uDE4B\u200D♀️", + [":woman_raising_hand::skin-tone-1:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♀️", + [":woman_raising_hand::skin-tone-2:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♀️", + [":woman_raising_hand::skin-tone-3:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♀️", + [":woman_raising_hand::skin-tone-4:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♀️", + [":woman_raising_hand::skin-tone-5:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♀️", + [":woman_raising_hand_dark_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♀️", + [":woman_raising_hand_light_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♀️", + [":woman_raising_hand_medium_dark_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♀️", + [":woman_raising_hand_medium_light_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♀️", + [":woman_raising_hand_medium_skin_tone:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♀️", + [":woman_raising_hand_tone1:"] = "\uD83D\uDE4B\uD83C\uDFFB\u200D♀️", + [":woman_raising_hand_tone2:"] = "\uD83D\uDE4B\uD83C\uDFFC\u200D♀️", + [":woman_raising_hand_tone3:"] = "\uD83D\uDE4B\uD83C\uDFFD\u200D♀️", + [":woman_raising_hand_tone4:"] = "\uD83D\uDE4B\uD83C\uDFFE\u200D♀️", + [":woman_raising_hand_tone5:"] = "\uD83D\uDE4B\uD83C\uDFFF\u200D♀️", + [":woman_red_haired:"] = "\uD83D\uDC69\u200D\uD83E\uDDB0", + [":woman_red_haired::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":woman_red_haired::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":woman_red_haired::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":woman_red_haired::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":woman_red_haired::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":woman_red_haired_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":woman_red_haired_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":woman_red_haired_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":woman_red_haired_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":woman_red_haired_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":woman_red_haired_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB0", + [":woman_red_haired_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB0", + [":woman_red_haired_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB0", + [":woman_red_haired_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB0", + [":woman_red_haired_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB0", + [":woman_rowing_boat:"] = "\uD83D\uDEA3\u200D♀️", + [":woman_rowing_boat::skin-tone-1:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♀️", + [":woman_rowing_boat::skin-tone-2:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♀️", + [":woman_rowing_boat::skin-tone-3:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♀️", + [":woman_rowing_boat::skin-tone-4:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♀️", + [":woman_rowing_boat::skin-tone-5:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♀️", + [":woman_rowing_boat_dark_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♀️", + [":woman_rowing_boat_light_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♀️", + [":woman_rowing_boat_medium_dark_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♀️", + [":woman_rowing_boat_medium_light_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♀️", + [":woman_rowing_boat_medium_skin_tone:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♀️", + [":woman_rowing_boat_tone1:"] = "\uD83D\uDEA3\uD83C\uDFFB\u200D♀️", + [":woman_rowing_boat_tone2:"] = "\uD83D\uDEA3\uD83C\uDFFC\u200D♀️", + [":woman_rowing_boat_tone3:"] = "\uD83D\uDEA3\uD83C\uDFFD\u200D♀️", + [":woman_rowing_boat_tone4:"] = "\uD83D\uDEA3\uD83C\uDFFE\u200D♀️", + [":woman_rowing_boat_tone5:"] = "\uD83D\uDEA3\uD83C\uDFFF\u200D♀️", + [":woman_running:"] = "\uD83C\uDFC3\u200D♀️", + [":woman_running::skin-tone-1:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♀️", + [":woman_running::skin-tone-2:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♀️", + [":woman_running::skin-tone-3:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♀️", + [":woman_running::skin-tone-4:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♀️", + [":woman_running::skin-tone-5:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♀️", + [":woman_running_dark_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♀️", + [":woman_running_light_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♀️", + [":woman_running_medium_dark_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♀️", + [":woman_running_medium_light_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♀️", + [":woman_running_medium_skin_tone:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♀️", + [":woman_running_tone1:"] = "\uD83C\uDFC3\uD83C\uDFFB\u200D♀️", + [":woman_running_tone2:"] = "\uD83C\uDFC3\uD83C\uDFFC\u200D♀️", + [":woman_running_tone3:"] = "\uD83C\uDFC3\uD83C\uDFFD\u200D♀️", + [":woman_running_tone4:"] = "\uD83C\uDFC3\uD83C\uDFFE\u200D♀️", + [":woman_running_tone5:"] = "\uD83C\uDFC3\uD83C\uDFFF\u200D♀️", + [":woman_scientist:"] = "\uD83D\uDC69\u200D\uD83D\uDD2C", + [":woman_scientist::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":woman_scientist::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":woman_scientist::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":woman_scientist::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":woman_scientist::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":woman_scientist_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":woman_scientist_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":woman_scientist_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":woman_scientist_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":woman_scientist_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":woman_scientist_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDD2C", + [":woman_scientist_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDD2C", + [":woman_scientist_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDD2C", + [":woman_scientist_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDD2C", + [":woman_scientist_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDD2C", + [":woman_shrugging:"] = "\uD83E\uDD37\u200D♀️", + [":woman_shrugging::skin-tone-1:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♀️", + [":woman_shrugging::skin-tone-2:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♀️", + [":woman_shrugging::skin-tone-3:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♀️", + [":woman_shrugging::skin-tone-4:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♀️", + [":woman_shrugging::skin-tone-5:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♀️", + [":woman_shrugging_dark_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♀️", + [":woman_shrugging_light_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♀️", + [":woman_shrugging_medium_dark_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♀️", + [":woman_shrugging_medium_light_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♀️", + [":woman_shrugging_medium_skin_tone:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♀️", + [":woman_shrugging_tone1:"] = "\uD83E\uDD37\uD83C\uDFFB\u200D♀️", + [":woman_shrugging_tone2:"] = "\uD83E\uDD37\uD83C\uDFFC\u200D♀️", + [":woman_shrugging_tone3:"] = "\uD83E\uDD37\uD83C\uDFFD\u200D♀️", + [":woman_shrugging_tone4:"] = "\uD83E\uDD37\uD83C\uDFFE\u200D♀️", + [":woman_shrugging_tone5:"] = "\uD83E\uDD37\uD83C\uDFFF\u200D♀️", + [":woman_singer:"] = "\uD83D\uDC69\u200D\uD83C\uDFA4", + [":woman_singer::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":woman_singer::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":woman_singer::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":woman_singer::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":woman_singer::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":woman_singer_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":woman_singer_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":woman_singer_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":woman_singer_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":woman_singer_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":woman_singer_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFA4", + [":woman_singer_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFA4", + [":woman_singer_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFA4", + [":woman_singer_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFA4", + [":woman_singer_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFA4", + [":woman_standing:"] = "\uD83E\uDDCD\u200D♀️", + [":woman_standing::skin-tone-1:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♀️", + [":woman_standing::skin-tone-2:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♀️", + [":woman_standing::skin-tone-3:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♀️", + [":woman_standing::skin-tone-4:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♀️", + [":woman_standing::skin-tone-5:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♀️", + [":woman_standing_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♀️", + [":woman_standing_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♀️", + [":woman_standing_medium_dark_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♀️", + [":woman_standing_medium_light_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♀️", + [":woman_standing_medium_skin_tone:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♀️", + [":woman_standing_tone1:"] = "\uD83E\uDDCD\uD83C\uDFFB\u200D♀️", + [":woman_standing_tone2:"] = "\uD83E\uDDCD\uD83C\uDFFC\u200D♀️", + [":woman_standing_tone3:"] = "\uD83E\uDDCD\uD83C\uDFFD\u200D♀️", + [":woman_standing_tone4:"] = "\uD83E\uDDCD\uD83C\uDFFE\u200D♀️", + [":woman_standing_tone5:"] = "\uD83E\uDDCD\uD83C\uDFFF\u200D♀️", + [":woman_student:"] = "\uD83D\uDC69\u200D\uD83C\uDF93", + [":woman_student::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF93", + [":woman_student::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF93", + [":woman_student::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF93", + [":woman_student::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF93", + [":woman_student::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF93", + [":woman_student_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF93", + [":woman_student_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF93", + [":woman_student_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF93", + [":woman_student_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF93", + [":woman_student_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF93", + [":woman_student_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDF93", + [":woman_student_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDF93", + [":woman_student_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDF93", + [":woman_student_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF93", + [":woman_student_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDF93", + [":woman_superhero:"] = "\uD83E\uDDB8\u200D♀️", + [":woman_superhero::skin-tone-1:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♀️", + [":woman_superhero::skin-tone-2:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♀️", + [":woman_superhero::skin-tone-3:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♀️", + [":woman_superhero::skin-tone-4:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♀️", + [":woman_superhero::skin-tone-5:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♀️", + [":woman_superhero_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♀️", + [":woman_superhero_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♀️", + [":woman_superhero_medium_dark_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♀️", + [":woman_superhero_medium_light_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♀️", + [":woman_superhero_medium_skin_tone:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♀️", + [":woman_superhero_tone1:"] = "\uD83E\uDDB8\uD83C\uDFFB\u200D♀️", + [":woman_superhero_tone2:"] = "\uD83E\uDDB8\uD83C\uDFFC\u200D♀️", + [":woman_superhero_tone3:"] = "\uD83E\uDDB8\uD83C\uDFFD\u200D♀️", + [":woman_superhero_tone4:"] = "\uD83E\uDDB8\uD83C\uDFFE\u200D♀️", + [":woman_superhero_tone5:"] = "\uD83E\uDDB8\uD83C\uDFFF\u200D♀️", + [":woman_supervillain:"] = "\uD83E\uDDB9\u200D♀️", + [":woman_supervillain::skin-tone-1:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♀️", + [":woman_supervillain::skin-tone-2:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♀️", + [":woman_supervillain::skin-tone-3:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♀️", + [":woman_supervillain::skin-tone-4:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♀️", + [":woman_supervillain::skin-tone-5:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♀️", + [":woman_supervillain_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♀️", + [":woman_supervillain_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♀️", + [":woman_supervillain_medium_dark_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♀️", + [":woman_supervillain_medium_light_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♀️", + [":woman_supervillain_medium_skin_tone:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♀️", + [":woman_supervillain_tone1:"] = "\uD83E\uDDB9\uD83C\uDFFB\u200D♀️", + [":woman_supervillain_tone2:"] = "\uD83E\uDDB9\uD83C\uDFFC\u200D♀️", + [":woman_supervillain_tone3:"] = "\uD83E\uDDB9\uD83C\uDFFD\u200D♀️", + [":woman_supervillain_tone4:"] = "\uD83E\uDDB9\uD83C\uDFFE\u200D♀️", + [":woman_supervillain_tone5:"] = "\uD83E\uDDB9\uD83C\uDFFF\u200D♀️", + [":woman_surfing:"] = "\uD83C\uDFC4\u200D♀️", + [":woman_surfing::skin-tone-1:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♀️", + [":woman_surfing::skin-tone-2:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♀️", + [":woman_surfing::skin-tone-3:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♀️", + [":woman_surfing::skin-tone-4:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♀️", + [":woman_surfing::skin-tone-5:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♀️", + [":woman_surfing_dark_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♀️", + [":woman_surfing_light_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♀️", + [":woman_surfing_medium_dark_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♀️", + [":woman_surfing_medium_light_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♀️", + [":woman_surfing_medium_skin_tone:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♀️", + [":woman_surfing_tone1:"] = "\uD83C\uDFC4\uD83C\uDFFB\u200D♀️", + [":woman_surfing_tone2:"] = "\uD83C\uDFC4\uD83C\uDFFC\u200D♀️", + [":woman_surfing_tone3:"] = "\uD83C\uDFC4\uD83C\uDFFD\u200D♀️", + [":woman_surfing_tone4:"] = "\uD83C\uDFC4\uD83C\uDFFE\u200D♀️", + [":woman_surfing_tone5:"] = "\uD83C\uDFC4\uD83C\uDFFF\u200D♀️", + [":woman_swimming:"] = "\uD83C\uDFCA\u200D♀️", + [":woman_swimming::skin-tone-1:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♀️", + [":woman_swimming::skin-tone-2:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♀️", + [":woman_swimming::skin-tone-3:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♀️", + [":woman_swimming::skin-tone-4:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♀️", + [":woman_swimming::skin-tone-5:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♀️", + [":woman_swimming_dark_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♀️", + [":woman_swimming_light_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♀️", + [":woman_swimming_medium_dark_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♀️", + [":woman_swimming_medium_light_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♀️", + [":woman_swimming_medium_skin_tone:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♀️", + [":woman_swimming_tone1:"] = "\uD83C\uDFCA\uD83C\uDFFB\u200D♀️", + [":woman_swimming_tone2:"] = "\uD83C\uDFCA\uD83C\uDFFC\u200D♀️", + [":woman_swimming_tone3:"] = "\uD83C\uDFCA\uD83C\uDFFD\u200D♀️", + [":woman_swimming_tone4:"] = "\uD83C\uDFCA\uD83C\uDFFE\u200D♀️", + [":woman_swimming_tone5:"] = "\uD83C\uDFCA\uD83C\uDFFF\u200D♀️", + [":woman_teacher:"] = "\uD83D\uDC69\u200D\uD83C\uDFEB", + [":woman_teacher::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":woman_teacher::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":woman_teacher::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":woman_teacher::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":woman_teacher::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":woman_teacher_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":woman_teacher_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":woman_teacher_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":woman_teacher_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":woman_teacher_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":woman_teacher_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83C\uDFEB", + [":woman_teacher_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83C\uDFEB", + [":woman_teacher_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83C\uDFEB", + [":woman_teacher_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDFEB", + [":woman_teacher_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83C\uDFEB", + [":woman_technologist:"] = "\uD83D\uDC69\u200D\uD83D\uDCBB", + [":woman_technologist::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":woman_technologist::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":woman_technologist::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":woman_technologist::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":woman_technologist::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":woman_technologist_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":woman_technologist_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":woman_technologist_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":woman_technologist_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":woman_technologist_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":woman_technologist_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDCBB", + [":woman_technologist_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83D\uDCBB", + [":woman_technologist_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83D\uDCBB", + [":woman_technologist_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83D\uDCBB", + [":woman_technologist_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83D\uDCBB", + [":woman_tipping_hand:"] = "\uD83D\uDC81\u200D♀️", + [":woman_tipping_hand::skin-tone-1:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♀️", + [":woman_tipping_hand::skin-tone-2:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♀️", + [":woman_tipping_hand::skin-tone-3:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♀️", + [":woman_tipping_hand::skin-tone-4:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♀️", + [":woman_tipping_hand::skin-tone-5:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♀️", + [":woman_tipping_hand_dark_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♀️", + [":woman_tipping_hand_light_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♀️", + [":woman_tipping_hand_medium_dark_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♀️", + [":woman_tipping_hand_medium_light_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♀️", + [":woman_tipping_hand_medium_skin_tone:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♀️", + [":woman_tipping_hand_tone1:"] = "\uD83D\uDC81\uD83C\uDFFB\u200D♀️", + [":woman_tipping_hand_tone2:"] = "\uD83D\uDC81\uD83C\uDFFC\u200D♀️", + [":woman_tipping_hand_tone3:"] = "\uD83D\uDC81\uD83C\uDFFD\u200D♀️", + [":woman_tipping_hand_tone4:"] = "\uD83D\uDC81\uD83C\uDFFE\u200D♀️", + [":woman_tipping_hand_tone5:"] = "\uD83D\uDC81\uD83C\uDFFF\u200D♀️", + [":woman_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB", + [":woman_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC", + [":woman_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD", + [":woman_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE", + [":woman_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF", + [":woman_vampire:"] = "\uD83E\uDDDB\u200D♀️", + [":woman_vampire::skin-tone-1:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♀️", + [":woman_vampire::skin-tone-2:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♀️", + [":woman_vampire::skin-tone-3:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♀️", + [":woman_vampire::skin-tone-4:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♀️", + [":woman_vampire::skin-tone-5:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♀️", + [":woman_vampire_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♀️", + [":woman_vampire_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♀️", + [":woman_vampire_medium_dark_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♀️", + [":woman_vampire_medium_light_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♀️", + [":woman_vampire_medium_skin_tone:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♀️", + [":woman_vampire_tone1:"] = "\uD83E\uDDDB\uD83C\uDFFB\u200D♀️", + [":woman_vampire_tone2:"] = "\uD83E\uDDDB\uD83C\uDFFC\u200D♀️", + [":woman_vampire_tone3:"] = "\uD83E\uDDDB\uD83C\uDFFD\u200D♀️", + [":woman_vampire_tone4:"] = "\uD83E\uDDDB\uD83C\uDFFE\u200D♀️", + [":woman_vampire_tone5:"] = "\uD83E\uDDDB\uD83C\uDFFF\u200D♀️", + [":woman_walking:"] = "\uD83D\uDEB6\u200D♀️", + [":woman_walking::skin-tone-1:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♀️", + [":woman_walking::skin-tone-2:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♀️", + [":woman_walking::skin-tone-3:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♀️", + [":woman_walking::skin-tone-4:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♀️", + [":woman_walking::skin-tone-5:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♀️", + [":woman_walking_dark_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♀️", + [":woman_walking_light_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♀️", + [":woman_walking_medium_dark_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♀️", + [":woman_walking_medium_light_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♀️", + [":woman_walking_medium_skin_tone:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♀️", + [":woman_walking_tone1:"] = "\uD83D\uDEB6\uD83C\uDFFB\u200D♀️", + [":woman_walking_tone2:"] = "\uD83D\uDEB6\uD83C\uDFFC\u200D♀️", + [":woman_walking_tone3:"] = "\uD83D\uDEB6\uD83C\uDFFD\u200D♀️", + [":woman_walking_tone4:"] = "\uD83D\uDEB6\uD83C\uDFFE\u200D♀️", + [":woman_walking_tone5:"] = "\uD83D\uDEB6\uD83C\uDFFF\u200D♀️", + [":woman_wearing_turban:"] = "\uD83D\uDC73\u200D♀️", + [":woman_wearing_turban::skin-tone-1:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♀️", + [":woman_wearing_turban::skin-tone-2:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♀️", + [":woman_wearing_turban::skin-tone-3:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♀️", + [":woman_wearing_turban::skin-tone-4:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♀️", + [":woman_wearing_turban::skin-tone-5:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♀️", + [":woman_wearing_turban_dark_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♀️", + [":woman_wearing_turban_light_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♀️", + [":woman_wearing_turban_medium_dark_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♀️", + [":woman_wearing_turban_medium_light_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♀️", + [":woman_wearing_turban_medium_skin_tone:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♀️", + [":woman_wearing_turban_tone1:"] = "\uD83D\uDC73\uD83C\uDFFB\u200D♀️", + [":woman_wearing_turban_tone2:"] = "\uD83D\uDC73\uD83C\uDFFC\u200D♀️", + [":woman_wearing_turban_tone3:"] = "\uD83D\uDC73\uD83C\uDFFD\u200D♀️", + [":woman_wearing_turban_tone4:"] = "\uD83D\uDC73\uD83C\uDFFE\u200D♀️", + [":woman_wearing_turban_tone5:"] = "\uD83D\uDC73\uD83C\uDFFF\u200D♀️", + [":woman_white_haired:"] = "\uD83D\uDC69\u200D\uD83E\uDDB3", + [":woman_white_haired::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":woman_white_haired::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":woman_white_haired::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":woman_white_haired::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":woman_white_haired::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":woman_white_haired_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":woman_white_haired_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":woman_white_haired_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":woman_white_haired_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":woman_white_haired_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":woman_white_haired_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB3", + [":woman_white_haired_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDB3", + [":woman_white_haired_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDB3", + [":woman_white_haired_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDB3", + [":woman_white_haired_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDB3", + [":woman_with_headscarf:"] = "\uD83E\uDDD5", + [":woman_with_headscarf::skin-tone-1:"] = "\uD83E\uDDD5\uD83C\uDFFB", + [":woman_with_headscarf::skin-tone-2:"] = "\uD83E\uDDD5\uD83C\uDFFC", + [":woman_with_headscarf::skin-tone-3:"] = "\uD83E\uDDD5\uD83C\uDFFD", + [":woman_with_headscarf::skin-tone-4:"] = "\uD83E\uDDD5\uD83C\uDFFE", + [":woman_with_headscarf::skin-tone-5:"] = "\uD83E\uDDD5\uD83C\uDFFF", + [":woman_with_headscarf_dark_skin_tone:"] = "\uD83E\uDDD5\uD83C\uDFFF", + [":woman_with_headscarf_light_skin_tone:"] = "\uD83E\uDDD5\uD83C\uDFFB", + [":woman_with_headscarf_medium_dark_skin_tone:"] = "\uD83E\uDDD5\uD83C\uDFFE", + [":woman_with_headscarf_medium_light_skin_tone:"] = "\uD83E\uDDD5\uD83C\uDFFC", + [":woman_with_headscarf_medium_skin_tone:"] = "\uD83E\uDDD5\uD83C\uDFFD", + [":woman_with_headscarf_tone1:"] = "\uD83E\uDDD5\uD83C\uDFFB", + [":woman_with_headscarf_tone2:"] = "\uD83E\uDDD5\uD83C\uDFFC", + [":woman_with_headscarf_tone3:"] = "\uD83E\uDDD5\uD83C\uDFFD", + [":woman_with_headscarf_tone4:"] = "\uD83E\uDDD5\uD83C\uDFFE", + [":woman_with_headscarf_tone5:"] = "\uD83E\uDDD5\uD83C\uDFFF", + [":woman_with_probing_cane:"] = "\uD83D\uDC69\u200D\uD83E\uDDAF", + [":woman_with_probing_cane::skin-tone-1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":woman_with_probing_cane::skin-tone-2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":woman_with_probing_cane::skin-tone-3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":woman_with_probing_cane::skin-tone-4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":woman_with_probing_cane::skin-tone-5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_medium_dark_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_medium_light_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_medium_skin_tone:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_tone1:"] = "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_tone2:"] = "\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_tone3:"] = "\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_tone4:"] = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDDAF", + [":woman_with_probing_cane_tone5:"] = "\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDDAF", + [":woman_zombie:"] = "\uD83E\uDDDF\u200D♀️", + [":womans_clothes:"] = "\uD83D\uDC5A", + [":womans_flat_shoe:"] = "\uD83E\uDD7F", + [":womans_hat:"] = "\uD83D\uDC52", + [":women_with_bunny_ears_partying:"] = "\uD83D\uDC6F\u200D♀️", + [":women_wrestling:"] = "\uD83E\uDD3C\u200D♀️", + [":womens:"] = "\uD83D\uDEBA", + [":woozy_face:"] = "\uD83E\uDD74", + [":world_map:"] = "\uD83D\uDDFA️", + [":worried:"] = "\uD83D\uDE1F", + [":worship_symbol:"] = "\uD83D\uDED0", + [":wrench:"] = "\uD83D\uDD27", + [":wrestlers:"] = "\uD83E\uDD3C", + [":wrestling:"] = "\uD83E\uDD3C", + [":writing_hand:"] = "✍️", + [":writing_hand::skin-tone-1:"] = "✍\uD83C\uDFFB", + [":writing_hand::skin-tone-2:"] = "✍\uD83C\uDFFC", + [":writing_hand::skin-tone-3:"] = "✍\uD83C\uDFFD", + [":writing_hand::skin-tone-4:"] = "✍\uD83C\uDFFE", + [":writing_hand::skin-tone-5:"] = "✍\uD83C\uDFFF", + [":writing_hand_tone1:"] = "✍\uD83C\uDFFB", + [":writing_hand_tone2:"] = "✍\uD83C\uDFFC", + [":writing_hand_tone3:"] = "✍\uD83C\uDFFD", + [":writing_hand_tone4:"] = "✍\uD83C\uDFFE", + [":writing_hand_tone5:"] = "✍\uD83C\uDFFF", + [":x:"] = "❌", + [":yarn:"] = "\uD83E\uDDF6", + [":yawning_face:"] = "\uD83E\uDD71", + [":yellow_circle:"] = "\uD83D\uDFE1", + [":yellow_heart:"] = "\uD83D\uDC9B", + [":yellow_square:"] = "\uD83D\uDFE8", + [":yen:"] = "\uD83D\uDCB4", + [":yin_yang:"] = "☯️", + [":yo_yo:"] = "\uD83E\uDE80", + [":yum:"] = "\uD83D\uDE0B", + [":z"] = "\uD83D\uDE12", + [":zany_face:"] = "\uD83E\uDD2A", + [":zap:"] = "⚡", + [":zebra:"] = "\uD83E\uDD93", + [":zero:"] = "0️⃣", + [":zipper_mouth:"] = "\uD83E\uDD10", + [":zipper_mouth_face:"] = "\uD83E\uDD10", + [":zombie:"] = "\uD83E\uDDDF", + [":zzz:"] = "\uD83D\uDCA4", + [":|"] = "\uD83D\uDE10", + [";("] = "\uD83D\uDE2D", + [";)"] = "\uD83D\uDE09", + [";-("] = "\uD83D\uDE2D", + [";-)"] = "\uD83D\uDE09", + [":("] = "\uD83D\uDE20", + [">:-("] = "\uD83D\uDE20", + [">=("] = "\uD83D\uDE20", + [">=-("] = "\uD83D\uDE20", + ["B-)"] = "\uD83D\uDE0E", + ["O:)"] = "\uD83D\uDE07", + ["O:-)"] = "\uD83D\uDE07", + ["O=)"] = "\uD83D\uDE07", + ["O=-)"] = "\uD83D\uDE07", + ["X-)"] = "\uD83D\uDE06", + ["]:("] = "\uD83D\uDC7F", + ["]:)"] = "\uD83D\uDE08", + ["]:-("] = "\uD83D\uDC7F", + ["]:-)"] = "\uD83D\uDE08", + ["]=("] = "\uD83D\uDC7F", + ["]=)"] = "\uD83D\uDE08", + ["]=-("] = "\uD83D\uDC7F", + ["]=-)"] = "\uD83D\uDE08", + ["o:)"] = "\uD83D\uDE07", + ["o:-)"] = "\uD83D\uDE07", + ["o=)"] = "\uD83D\uDE07", + ["o=-)"] = "\uD83D\uDE07", + ["x-)"] = "\uD83D\uDE06", + ["♡"] = "❤️" + }; + + private static IReadOnlyCollection _unicodes; + private static IReadOnlyCollection Unicodes + { + get + { + _unicodes ??= NamesAndUnicodes.Select(kvp => kvp.Value).ToImmutableHashSet(); + return _unicodes; + } + } + + private static IReadOnlyDictionary> _unicodesAndNames; + private static IReadOnlyDictionary> UnicodesAndNames + { + get + { + _unicodesAndNames ??= + NamesAndUnicodes + .GroupBy(kvp => kvp.Value) + .ToImmutableDictionary( + grouping => grouping.Key, + grouping => grouping.Select(kvp => kvp.Key) + .ToList() + .AsReadOnly() + ); + return _unicodesAndNames; + } + } + + public static implicit operator Emoji(string s) => Parse(s); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index a71a73327..3a8cd7457 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -1,26 +1,34 @@ using System; using System.Globalization; +using System.Diagnostics; namespace Discord { /// - /// A custom image-based emote + /// A custom image-based emote. /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Emote : IEmote, ISnowflakeEntity { - /// - /// The display name (tooltip) of this emote - /// + /// public string Name { get; } - /// - /// The ID of this emote - /// + /// public ulong Id { get; } /// - /// Is this emote animated? + /// Gets whether this emote is animated. /// + /// + /// A boolean that determines whether or not this emote is an animated one. + /// public bool Animated { get; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets the image URL of this emote. + /// + /// + /// A string that points to the URL of this emote. + /// public string Url => CDN.GetEmojiUrl(Id, Animated); internal Emote(ulong id, string name, bool animated) @@ -30,6 +38,10 @@ namespace Discord Animated = animated; } + /// + /// Determines whether the specified emote is equal to the current emote. + /// + /// The object to compare with the current object. public override bool Equals(object other) { if (other == null) return false; @@ -38,32 +50,34 @@ namespace Discord var otherEmote = other as Emote; if (otherEmote == null) return false; - return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id; + return Id == otherEmote.Id; } + /// public override int GetHashCode() - { - unchecked - { - return (Name.GetHashCode() * 397) ^ Id.GetHashCode(); - } - } + => Id.GetHashCode(); - /// - /// Parse an Emote from its raw format - /// - /// The raw encoding of an emote; for example, <:dab:277855270321782784> - /// An emote + /// Parses an from its raw format. + /// The raw encoding of an emote (e.g. <:dab:277855270321782784>). + /// An emote. + /// Invalid emote format. public static Emote Parse(string text) { if (TryParse(text, out Emote result)) return result; - throw new ArgumentException(message: "Invalid emote format", paramName: nameof(text)); + throw new ArgumentException(message: "Invalid emote format.", paramName: nameof(text)); } + /// Tries to parse an from its raw format. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// An emote. public static bool TryParse(string text, out Emote result) { result = null; + + if (text == null) + return false; + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') { bool animated = text[1] == 'a'; @@ -85,6 +99,14 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Returns the raw representation of the emote. + /// + /// + /// A string representing the raw presentation of the emote (e.g. <:thonkang:282745590985523200>). + /// public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; + + public static implicit operator Emote(string s) => Parse(s); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs index be24d306c..41679d2af 100644 --- a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -1,10 +1,20 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { + /// + /// Provides properties that are used to modify an with the specified changes. + /// + /// public class EmoteProperties { + /// + /// Gets or sets the name of the . + /// public Optional Name { get; set; } + /// + /// Gets or sets the roles that can access this . + /// public Optional> Roles { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 95b062bd2..4bd0845c8 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -1,26 +1,59 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord { /// - /// An image-based emote that is attached to a guild + /// An image-based emote that is attached to a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class GuildEmote : Emote { + /// + /// Gets whether this emoji is managed by an integration. + /// + /// + /// A boolean that determines whether or not this emote is managed by a Twitch integration. + /// public bool IsManaged { get; } + /// + /// Gets whether this emoji must be wrapped in colons. + /// + /// + /// A boolean that determines whether or not this emote requires the use of colons in chat to be used. + /// public bool RequireColons { get; } + /// + /// Gets the roles that are allowed to use this emoji. + /// + /// + /// A read-only list containing snowflake identifiers for roles that are allowed to use this emoji. + /// public IReadOnlyList RoleIds { get; } + /// + /// Gets the user ID associated with the creation of this emoji. + /// + /// + /// An snowflake identifier representing the user who created this emoji; + /// null if unknown. + /// + public ulong? CreatorId { get; } - internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds, ulong? userId) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; RoleIds = roleIds; + CreatorId = userId; } private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Gets the raw representation of the emote. + /// + /// + /// A string representing the raw presentation of the emote (e.g. <:thonkang:282745590985523200>). + /// public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs index fac61402a..9141e8537 100644 --- a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs @@ -1,13 +1,16 @@ -namespace Discord +namespace Discord { /// - /// A general container for any type of emote in a message. + /// Represents a general container for any type of emote in a message. /// public interface IEmote { /// - /// The display name or unicode representation of this emote + /// Gets the display name or Unicode representation of this emote. /// + /// + /// A string representing the display name or the Unicode representation (e.g. 🤔) of this emote. + /// string Name { get; } } } diff --git a/src/Discord.Net.Core/Entities/Gateway/BotGateway.cs b/src/Discord.Net.Core/Entities/Gateway/BotGateway.cs new file mode 100644 index 000000000..c9be0ac1f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Gateway/BotGateway.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + /// + /// Stores the gateway information related to the current bot. + /// + public class BotGateway + { + /// + /// Gets the WSS URL that can be used for connecting to the gateway. + /// + public string Url { get; internal set; } + /// + /// Gets the recommended number of shards to use when connecting. + /// + public int Shards { get; internal set; } + /// + /// Gets the that contains the information + /// about the current session start limit. + /// + public SessionStartLimit SessionStartLimit { get; internal set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Gateway/SessionStartLimit.cs b/src/Discord.Net.Core/Entities/Gateway/SessionStartLimit.cs new file mode 100644 index 000000000..74ae96af1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Gateway/SessionStartLimit.cs @@ -0,0 +1,38 @@ +namespace Discord +{ + /// + /// Stores the information related to the gateway identify request. + /// + public class SessionStartLimit + { + /// + /// Gets the total number of session starts the current user is allowed. + /// + /// + /// The maximum amount of session starts the current user is allowed. + /// + public int Total { get; internal set; } + /// + /// Gets the remaining number of session starts the current user is allowed. + /// + /// + /// The remaining amount of session starts the current user is allowed. + /// + public int Remaining { get; internal set; } + /// + /// Gets the number of milliseconds after which the limit resets. + /// + /// + /// The milliseconds until the limit resets back to the . + /// + public int ResetAfter { get; internal set; } + /// + /// Gets the maximum concurrent identify requests in a time window. + /// + /// + /// The maximum concurrent identify requests in a time window, + /// limited to the same rate limit key. + /// + public int MaxConcurrency { get; internal set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs index a5cabc117..ffcd28cee 100644 --- a/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs +++ b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the default message notification behavior the guild uses. + /// public enum DefaultMessageNotifications { - /// By default, all messages will trigger notifications. + /// + /// By default, all messages will trigger notifications. + /// AllMessages = 0, - /// By default, only mentions will trigger notifications. + /// + /// By default, only mentions will trigger notifications. + /// MentionsOnly = 1 } } diff --git a/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs b/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs new file mode 100644 index 000000000..54c0bdafe --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public enum ExplicitContentFilterLevel + { + /// No messages will be scanned. + Disabled = 0, + /// Scans messages from all guild members that do not have a role. + /// Recommented option for servers that use roles for trusted membership. + MembersWithoutRoles = 1, + /// Scan messages sent by all guild members. + AllMembers = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs deleted file mode 100644 index a2b2ec4fc..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Discord -{ - /// - /// Modify the widget of an IGuild with the specified parameters - /// - public class GuildEmbedProperties - { - /// - /// Should the widget be enabled? - /// - public Optional Enabled { get; set; } - /// - /// What channel should the invite place users in, if not null. - /// - public Optional Channel { get; set; } - /// - /// What channel should the invite place users in, if not null. - /// - public Optional ChannelId { get; set; } - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs new file mode 100644 index 000000000..e3c325227 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + [Flags] + public enum GuildFeature + { + /// + /// The guild has no features. + /// + None = 0, + /// + /// The guild has access to set an animated guild icon. + /// + AnimatedIcon = 1 << 0, + /// + /// The guild has access to set a guild banner image. + /// + Banner = 1 << 1, + /// + /// The guild has access to use commerce features (i.e. create store channels). + /// + Commerce = 1 << 2, + /// + /// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates. + /// + Community = 1 << 3, + /// + /// The guild is able to be discovered in the directory. + /// + Discoverable = 1 << 4, + /// + /// The guild is able to be featured in the directory. + /// + Featureable = 1 << 5, + /// + /// The guild has access to set an invite splash background. + /// + InviteSplash = 1 << 6, + /// + /// The guild has enabled Membership Screening. + /// + MemberVerificationGateEnabled = 1 << 7, + /// + /// The guild has enabled monetization. + /// + MonetizationEnabled = 1 << 8, + /// + /// The guild has increased custom sticker slots. + /// + MoreStickers = 1 << 9, + /// + /// The guild has access to create news channels. + /// + News = 1 << 10, + /// + /// The guild is partnered. + /// + Partnered = 1 << 11, + /// + /// The guild can be previewed before joining via Membership Screening or the directory. + /// + PreviewEnabled = 1 << 12, + /// + /// The guild has access to create private threads. + /// + PrivateThreads = 1 << 13, + /// + /// The guild is able to set role icons. + /// + RoleIcons = 1 << 14, + /// + /// The guild has access to the seven day archive time for threads. + /// + SevenDayThreadArchive = 1 << 15, + /// + /// The guild has access to the three day archive time for threads. + /// + ThreeDayThreadArchive = 1 << 16, + /// + /// The guild has enabled ticketed events. + /// + TicketedEventsEnabled = 1 << 17, + /// + /// The guild has access to set a vanity URL. + /// + VanityUrl = 1 << 18, + /// + /// The guild is verified. + /// + Verified = 1 << 19, + /// + /// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers). + /// + VIPRegions = 1 << 20, + /// + /// The guild has enabled the welcome screen. + /// + WelcomeScreenEnabled = 1 << 21, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs new file mode 100644 index 000000000..699e47cf3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeatures.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public class GuildFeatures + { + /// + /// Gets the flags of recognized features for this guild. + /// + public GuildFeature Value { get; } + + /// + /// Gets a collection of experimental features for this guild. + /// + public IReadOnlyCollection Experimental { get; } + + + internal GuildFeatures(GuildFeature value, string[] experimental) + { + Value = value; + Experimental = experimental.ToImmutableArray(); + } + + public bool HasFeature(GuildFeature feature) + => Value.HasFlag(feature); + public bool HasFeature(string feature) + => Experimental.Contains(feature); + + internal void EnsureFeature(GuildFeature feature) + { + if (!HasFeature(feature)) + { + var vals = Enum.GetValues(typeof(GuildFeature)).Cast(); + + var missingValues = vals.Where(x => feature.HasFlag(x) && !Value.HasFlag(x)); + + throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs index f329e78e6..2ca19b50a 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs @@ -1,9 +1,21 @@ -namespace Discord +namespace Discord { + /// + /// Provides properties used to modify an with the specified changes. + /// public class GuildIntegrationProperties { + /// + /// Gets or sets the behavior when an integration subscription lapses. + /// public Optional ExpireBehavior { get; set; } + /// + /// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions. + /// public Optional ExpireGracePeriod { get; set; } + /// + /// Gets or sets whether emoticons should be synced for this integration. + /// public Optional EnableEmoticons { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 1b406ef7f..d50b2ac38 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -1,79 +1,117 @@ -namespace Discord +using System.Globalization; + +namespace Discord { /// - /// Modify an IGuild with the specified changes + /// Provides properties that are used to modify an with the specified changes. /// - /// - /// - /// await Context.Guild.ModifyAsync(async x => - /// { - /// x.Name = "aaaaaah"; - /// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id; - /// }); - /// - /// - /// + /// public class GuildProperties { - public Optional Username { get; set; } /// - /// The name of the Guild + /// Gets or sets the name of the guild. Must be within 100 characters. /// public Optional Name { get; set; } /// - /// The region for the Guild's voice connections + /// Gets or sets the region for the guild's voice connections. /// public Optional Region { get; set; } /// - /// The ID of the region for the Guild's voice connections + /// Gets or sets the ID of the region for the guild's voice connections. /// public Optional RegionId { get; set; } /// - /// What verification level new users need to achieve before speaking + /// Gets or sets the verification level new users need to achieve before speaking. /// public Optional VerificationLevel { get; set; } /// - /// The default message notification state for the guild + /// Gets or sets the default message notification state for the guild. /// public Optional DefaultMessageNotifications { get; set; } /// - /// How many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, 1800, 3600). + /// Gets or sets how many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, + /// 1800, 3600). /// public Optional AfkTimeout { get; set; } /// - /// The icon of the guild + /// Gets or sets the icon of the guild. /// public Optional Icon { get; set; } /// - /// The guild's splash image + /// Gets or sets the banner of the guild. + /// + public Optional Banner { get; set; } + /// + /// Gets or sets the guild's splash image. /// /// - /// The guild must be partnered for this value to have any effect. + /// The guild must be partnered for this value to have any effect. /// public Optional Splash { get; set; } /// - /// The IVoiceChannel where AFK users should be sent. + /// Gets or sets the where AFK users should be sent. /// public Optional AfkChannel { get; set; } /// - /// The ID of the IVoiceChannel where AFK users should be sent. + /// Gets or sets the ID of the where AFK users should be sent. /// public Optional AfkChannelId { get; set; } /// - /// The ITextChannel where System messages should be sent. + /// Gets or sets the where system messages should be sent. /// public Optional SystemChannel { get; set; } /// - /// The ID of the ITextChannel where System messages should be sent. + /// Gets or sets the ID of the where system messages should be sent. /// public Optional SystemChannelId { get; set; } /// - /// The owner of this guild. + /// Gets or sets the owner of this guild. /// public Optional Owner { get; set; } /// - /// The ID of the owner of this guild. + /// Gets or sets the ID of the owner of this guild. /// public Optional OwnerId { get; set; } + /// + /// Gets or sets the explicit content filter level of this guild. + /// + public Optional ExplicitContentFilter { get; set; } + /// + /// Gets or sets the flags that DISABLE types of system channel messages. + /// + /// + /// These flags are inverted. Setting a flag will disable that system channel message from being sent. + /// A value of will allow all system channel message types to be sent, + /// given that the has also been set. + /// A value of will deny guild boost messages from being sent, and allow all + /// other types of messages. + /// Refer to the extension methods , + /// , , + /// and to check if these system channel message types + /// are enabled, without the need to manipulate the logic of the flag. + /// + public Optional SystemChannelFlags { get; set; } + /// + /// Gets or sets the preferred locale of the guild in IETF BCP 47 language tag format. + /// + /// + /// This property takes precedence over . + /// When it is set, the value of + /// will not be used. + /// + public Optional PreferredLocale { get; set; } + /// + /// Gets or sets the preferred locale of the guild. + /// + /// + /// The property takes precedence + /// over this property. When is set, + /// the value of will be unused. + /// + public Optional PreferredCulture { get; set; } + /// + /// Gets or sets if the boost progress bar is enabled. + /// + public Optional IsBoostProgressBarEnabled { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs new file mode 100644 index 000000000..87881104c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventPrivacyLevel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the privacy level of a guild scheduled event. + /// + public enum GuildScheduledEventPrivacyLevel + { + /// + /// The scheduled event is public and available in discovery. + /// + [Obsolete("This event type isn't supported yet! check back later.", true)] + Public = 1, + + /// + /// The scheduled event is only accessible to guild members. + /// + Private = 2, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs new file mode 100644 index 000000000..6e3aa1ab3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventStatus.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the status of a guild event. + /// + public enum GuildScheduledEventStatus + { + /// + /// The event is scheduled for a set time. + /// + Scheduled = 1, + + /// + /// The event has started. + /// + Active = 2, + + /// + /// The event was completed. + /// + Completed = 3, + + /// + /// The event was canceled. + /// + Cancelled = 4, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs new file mode 100644 index 000000000..ad741eee1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventType.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the type of a guild scheduled event. + /// + public enum GuildScheduledEventType + { + /// + /// The event doesn't have a set type. + /// + None = 0, + + /// + /// The event is set in a stage channel. + /// + Stage = 1, + + /// + /// The event is set in a voice channel. + /// + Voice = 2, + + /// + /// The event is set for somewhere externally from discord. + /// + External = 3, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs new file mode 100644 index 000000000..a3fd729e5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + public class GuildScheduledEventsProperties + { + /// + /// Gets or sets the channel id of the event. + /// + public Optional ChannelId { get; set; } + + /// + /// Gets or sets the location of this event. + /// + public Optional Location { get; set; } + + /// + /// Gets or sets the name of the event. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the privacy level of the event. + /// + public Optional PrivacyLevel { get; set; } + + /// + /// Gets or sets the start time of the event. + /// + public Optional StartTime { get; set; } + /// + /// Gets or sets the end time of the event. + /// + public Optional EndTime { get; set; } + + /// + /// Gets or sets the description of the event. + /// + public Optional Description { get; set; } + + /// + /// Gets or sets the type of the event. + /// + public Optional Type { get; set; } + + /// + /// Gets or sets the status of the event. + /// + public Optional Status { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildWidgetProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildWidgetProperties.cs new file mode 100644 index 000000000..842bb7f3e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildWidgetProperties.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Provides properties that are used to modify the widget of an with the specified changes. + /// + public class GuildWidgetProperties + { + /// + /// Sets whether the widget should be enabled. + /// + public Optional Enabled { get; set; } + /// + /// Sets the channel that the invite should place its users in, if not . + /// + public Optional Channel { get; set; } + /// + /// Sets the channel that the invite should place its users in, if not . + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/Discord.Net.Core/Entities/Guilds/IBan.cs index 05ab0c00f..617f2fe04 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IBan.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -1,8 +1,23 @@ -namespace Discord +namespace Discord { + /// + /// Represents a generic ban object. + /// public interface IBan { + /// + /// Gets the banned user. + /// + /// + /// A user that was banned. + /// IUser User { get; } + /// + /// Gets the reason why the user is banned if specified. + /// + /// + /// A string containing the reason behind the ban; null if none is specified. + /// string Reason { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index bbe7051cb..c2db435cf 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,169 +1,1162 @@ using Discord.Audio; using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic guild/server. + /// public interface IGuild : IDeletable, ISnowflakeEntity { - /// Gets the name of this guild. + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// string Name { get; } - /// 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. + /// + /// 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. + /// + /// + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. + /// int AFKTimeout { get; } - /// Returns true if this guild is embeddable (e.g. widget) - bool IsEmbeddable { get; } - /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// + /// Gets a value that indicates whether this guild has the widget enabled. + /// + /// + /// if this guild has a widget enabled; otherwise . + /// + bool IsWidgetEnabled { get; } + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// DefaultMessageNotifications DefaultMessageNotifications { get; } - /// Gets the level of mfa requirements a user must fulfill before being allowed to perform administrative actions in this guild. + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// + /// + /// The level of MFA requirement. + /// MfaLevel MfaLevel { get; } - /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// VerificationLevel VerificationLevel { get; } - /// Returns the id of this guild's icon, or null if one is not set. + /// + /// Gets the level of content filtering applied to user's content in a Guild. + /// + /// + /// The level of explicit content filtering. + /// + ExplicitContentFilterLevel ExplicitContentFilter { get; } + /// + /// Gets the ID of this guild's icon. + /// + /// + /// An identifier for the splash image; if none is set. + /// string IconId { get; } - /// Returns the url to this guild's icon, or null if one is not set. + /// + /// Gets the URL of this guild's icon. + /// + /// + /// A URL pointing to the guild's icon; if none is set. + /// string IconUrl { get; } - /// Returns the id of this guild's splash image, or null if one is not set. + /// + /// Gets the ID of this guild's splash image. + /// + /// + /// An identifier for the splash image; if none is set. + /// string SplashId { get; } - /// Returns the url to this guild's splash image, or null if one is not set. + /// + /// Gets the URL of this guild's splash image. + /// + /// + /// A URL pointing to the guild's splash image; if none is set. + /// string SplashUrl { get; } - /// Returns true if this guild is currently connected and ready to be used. Only applies to the WebSocket client. + /// + /// Gets the ID of this guild's discovery splash image. + /// + /// + /// An identifier for the discovery splash image; if none is set. + /// + string DiscoverySplashId { get; } + /// + /// Gets the URL of this guild's discovery splash image. + /// + /// + /// A URL pointing to the guild's discovery splash image; if none is set. + /// + string DiscoverySplashUrl { get; } + /// + /// Determines if this guild is currently connected and ready to be used. + /// + /// + /// + /// This property only applies to a WebSocket-based client. + /// + /// This boolean is used to determine if the guild is currently connected to the WebSocket and is ready to be used/accessed. + /// + /// + /// true if this guild is currently connected and ready to be used; otherwise . + /// bool Available { get; } - /// Gets the id of the AFK voice channel for this guild if set, or null if not. + /// + /// Gets the ID of the AFK voice channel for this guild. + /// + /// + /// A representing the snowflake identifier of the AFK voice channel; if + /// none is set. + /// ulong? AFKChannelId { get; } - /// Gets the id of the the default channel for this guild. - ulong DefaultChannelId { get; } - /// Gets the id of the embed channel for this guild if set, or null if not. - ulong? EmbedChannelId { get; } - /// Gets the id of the channel where randomized welcome messages are sent, or null if not. + /// + /// Gets the ID of the channel assigned to the widget of this guild. + /// + /// + /// A representing the snowflake identifier of the channel assigned to the widget found + /// within the widget settings of this guild; if none is set. + /// + ulong? WidgetChannelId { get; } + /// + /// Gets the ID of the channel where randomized welcome messages are sent. + /// + /// + /// A representing the snowflake identifier of the system channel where randomized + /// welcome messages are sent; if none is set. + /// ulong? SystemChannelId { get; } - /// Gets the id of the user that created this guild. + /// + /// Gets the ID of the channel with the rules. + /// + /// + /// A representing the snowflake identifier of the channel that contains the rules; + /// if none is set. + /// + ulong? RulesChannelId { get; } + /// + /// Gets the ID of the channel where admins and moderators of Community guilds receive notices from Discord. + /// + /// + /// A representing the snowflake identifier of the channel where admins and moderators + /// of Community guilds receive notices from Discord; if none is set. + /// + ulong? PublicUpdatesChannelId { get; } + /// + /// Gets the ID of the user that owns this guild. + /// + /// + /// A representing the snowflake identifier of the user that owns this guild. + /// ulong OwnerId { get; } - /// Gets the id of the region hosting this guild's voice channels. + /// + /// Gets the application ID of the guild creator if it is bot-created. + /// + /// + /// A representing the snowflake identifier of the application ID that created this guild, or if it was not bot-created. + /// + ulong? ApplicationId { get; } + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// + /// + /// A string containing the identifier for the voice region that this guild uses (e.g. eu-central). + /// string VoiceRegionId { get; } - /// Gets the IAudioClient currently associated with this guild. + /// + /// Gets the currently associated with this guild. + /// + /// + /// An currently associated with this guild. + /// IAudioClient AudioClient { get; } - /// Gets the built-in role containing all users in this guild. + /// + /// Gets the built-in role containing all users in this guild. + /// + /// + /// A role object that represents an @everyone role in this guild. + /// IRole EveryoneRole { get; } - /// Gets a collection of all custom emojis for this guild. + /// + /// Gets a collection of all custom emotes for this guild. + /// + /// + /// A read-only collection of all custom emotes for this guild. + /// IReadOnlyCollection Emotes { get; } - /// Gets a collection of all extra features added to this guild. - IReadOnlyCollection Features { get; } - /// Gets a collection of all roles in this guild. + /// + /// Gets a collection of all custom stickers for this guild. + /// + /// + /// A read-only collection of all custom stickers for this guild. + /// + IReadOnlyCollection Stickers { get; } + /// + /// Gets the features for this guild. + /// + /// + /// A flags enum containing all the features for the guild. + /// + GuildFeatures Features { get; } + /// + /// Gets a collection of all roles in this guild. + /// + /// + /// A read-only collection of roles found within this guild. + /// IReadOnlyCollection Roles { get; } + /// + /// Gets the tier of guild boosting in this guild. + /// + /// + /// The tier of guild boosting in this guild. + /// + PremiumTier PremiumTier { get; } + /// + /// Gets the identifier for this guilds banner image. + /// + /// + /// An identifier for the banner image; if none is set. + /// + string BannerId { get; } + /// + /// Gets the URL of this guild's banner image. + /// + /// + /// A URL pointing to the guild's banner image; if none is set. + /// + string BannerUrl { get; } + /// + /// Gets the code for this guild's vanity invite URL. + /// + /// + /// A string containing the vanity invite code for this guild; if none is set. + /// + string VanityURLCode { get; } + /// + /// Gets the flags for the types of system channel messages that are disabled. + /// + /// + /// The flags for the types of system channel messages that are disabled. + /// + SystemChannelMessageDeny SystemChannelFlags { get; } + /// + /// Gets the description for the guild. + /// + /// + /// The description for the guild; if none is set. + /// + string Description { get; } + /// + /// Gets the number of premium subscribers of this guild. + /// + /// + /// This is the number of users who have boosted this guild. + /// + /// + /// The number of premium subscribers of this guild; if not available. + /// + int PremiumSubscriptionCount { get; } + /// + /// Gets the maximum number of presences for the guild. + /// + /// + /// The maximum number of presences for the guild. + /// + int? MaxPresences { get; } + /// + /// Gets the maximum number of members for the guild. + /// + /// + /// The maximum number of members for the guild. + /// + int? MaxMembers { get; } + /// + /// Gets the maximum amount of users in a video channel. + /// + /// + /// The maximum amount of users in a video channel. + /// + int? MaxVideoChannelUsers { get; } + /// + /// Gets the approximate number of members in this guild. + /// + /// + /// Only available when getting a guild via REST when `with_counts` is true. + /// + /// + /// The approximate number of members in this guild. + /// + int? ApproximateMemberCount { get; } + /// + /// Gets the approximate number of non-offline members in this guild. + /// + /// + /// Only available when getting a guild via REST when `with_counts` is true. + /// + /// + /// The approximate number of non-offline members in this guild. + /// + int? ApproximatePresenceCount { get; } + /// + /// Gets the max bitrate for voice channels in this guild. + /// + /// + /// A representing the maximum bitrate value allowed by Discord in this guild. + /// + int MaxBitrate { get; } + + /// + /// Gets the preferred locale of this guild in IETF BCP 47 + /// language tag format. + /// + /// + /// The preferred locale of the guild in IETF BCP 47 + /// language tag format. + /// + string PreferredLocale { get; } + + /// + /// Gets the NSFW level of this guild. + /// + /// + /// The NSFW level of this guild. + /// + NsfwLevel NsfwLevel { get; } + + /// + /// Gets the preferred culture of this guild. + /// + /// + /// The preferred culture information of this guild. + /// + CultureInfo PreferredCulture { get; } + /// + /// Gets whether the guild has the boost progress bar enabled. + /// + /// + /// if the boost progress bar is enabled; otherwise . + /// + bool IsBoostProgressBarEnabled { get; } - /// Modifies this guild. + /// + /// Modifies this guild. + /// + /// The delegate containing the properties to modify the guild with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Modifies this guild's embed. - Task ModifyEmbedAsync(Action func, RequestOptions options = null); - /// Bulk modifies the channels of this guild. + /// + /// Modifies this guild's widget. + /// + /// The delegate containing the properties to modify the guild widget with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyWidgetAsync(Action func, RequestOptions options = null); + /// + /// Bulk-modifies the order of channels in this guild. + /// + /// The properties used to modify the channel positions with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous reorder operation. + /// Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); - /// Bulk modifies the roles of this guild. + /// + /// Bulk-modifies the order of roles in this guild. + /// + /// The properties used to modify the role positions with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous reorder operation. + /// Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); - /// Leaves this guild. If you are the owner, use Delete instead. + /// + /// Leaves this guild. + /// + /// + /// This method will make the currently logged-in user leave the guild. + /// + /// If the user is the owner of this guild, use instead. + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// Task LeaveAsync(RequestOptions options = null); - /// Gets a collection of all users banned on this guild. + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// Task> GetBansAsync(RequestOptions options = null); /// /// Gets a ban object for a banned user. /// /// The banned user. + /// The options to be used when sending the request. /// - /// An awaitable containing the ban object, which contains the user information and the - /// reason for the ban; if the ban entry cannot be found. + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. /// Task GetBanAsync(IUser user, RequestOptions options = null); /// /// Gets a ban object for a banned user. /// /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. /// - /// An awaitable containing the ban object, which contains the user information and the - /// reason for the ban; if the ban entry cannot be found. + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. /// Task GetBanAsync(ulong userId, RequestOptions options = null); - /// Bans the provided user from this guild and optionally prunes their recent messages. - /// The number of days to remove messages from this user for - must be between [0, 7] + /// + /// Bans the user from this guild and optionally prunes their recent messages. + /// + /// The user to ban. + /// The number of days to remove messages from this user for, and this number must be between [0, 7]. + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous add operation for the ban. + /// Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); - /// Bans the provided user id from this guild and optionally prunes their recent messages. - /// The number of days to remove messages from this user for - must be between [0, 7] + /// + /// Bans the user from this guild and optionally prunes their recent messages. + /// + /// The snowflake ID of the user to ban. + /// The number of days to remove messages from this user for, and this number must be between [0, 7]. + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous add operation for the ban. + /// Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); - /// Unbans the provided user if it is currently banned. + /// + /// Unbans the user if they are currently banned. + /// + /// The user to be unbanned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation for the ban. + /// Task RemoveBanAsync(IUser user, RequestOptions options = null); - /// Unbans the provided user id if it is currently banned. + /// + /// Unbans the user if they are currently banned. + /// + /// The snowflake identifier of the user to be unbanned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation for the ban. + /// Task RemoveBanAsync(ulong userId, RequestOptions options = null); - /// Gets a collection of all channels in this guild. + /// + /// Gets a collection of all channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// generic channels found within this guild. + /// Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the channel in this guild with the provided id, or null if not found. - Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Creates a new text channel. - Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null); - /// Creates a new voice channel. - Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null); - /// Creates a new channel category. - Task CreateCategoryAsync(string name, RequestOptions options = null); - - Task> GetIntegrationsAsync(RequestOptions options = null); - Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); - - /// Gets a collection of all invites to this guild. - Task> GetInvitesAsync(RequestOptions options = null); /// - /// Gets the vanity invite URL of this guild. + /// Gets a channel in this guild. /// + /// The snowflake identifier for the channel. + /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// - /// An awaitable containing the partial metadata of the vanity invite found within - /// this guild. + /// A task that represents the asynchronous get operation. The task result contains the generic channel + /// associated with the specified ; if none is found. /// - Task GetVanityInviteAsync(RequestOptions options = null); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all text channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// message channels found within this guild. + /// + Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// associated with the specified ; if none is found. + /// + Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all voice channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice channels found within this guild. + /// + Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all category channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// category channels found within this guild. + /// + Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel associated + /// with the specified ; if none is found. + /// + Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a stage channel in this guild. + /// + /// The snowflake identifier for the stage channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the stage channel associated + /// with the specified ; if none is found. + /// + Task GetStageChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all stage channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// stage channels found within this guild. + /// + Task> GetStageChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the AFK voice channel in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel that the + /// AFK users will be moved to after they have idled for too long; if none is set. + /// + Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the system channel where randomized welcome messages are sent in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel where + /// randomized welcome messages will be sent to; if none is set. + /// + Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the first viewable text channel in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the first viewable text + /// channel in this guild; if none is found. + /// + Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the widget channel set + /// within the server's widget settings; if none is set. + /// + Task GetWidgetChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the text channel where Community guilds can display rules and/or guidelines. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// where Community guilds can display rules and/or guidelines; if none is set. + /// + Task GetRulesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the text channel where admins and moderators of Community guilds receive notices from Discord. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel where + /// admins and moderators of Community guilds receive notices from Discord; if none is set. + /// + Task GetPublicUpdatesChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a thread channel within this guild. + /// + /// The id of the thread channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the thread channel. + /// + Task GetThreadChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all thread channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// thread channels found within this guild. + /// + Task> GetThreadChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// + Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new voice channel in this guild. + /// + /// The new name for the voice channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// voice channel. + /// + Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new stage channel in this guild. + /// + /// The new name for the stage channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// stage channel. + /// + Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new channel category in this guild. + /// + /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// category channel. + /// + Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null); + + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + Task> GetVoiceRegionsAsync(RequestOptions options = null); + + Task> GetIntegrationsAsync(RequestOptions options = null); + Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); - /// Gets the role in this guild with the provided id, or null if not found. + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// + Task> GetInvitesAsync(RequestOptions options = null); + /// + /// Gets the vanity invite URL of this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the partial metadata of + /// the vanity invite found within this guild; if none is found. + /// + Task GetVanityInviteAsync(RequestOptions options = null); + + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; if none is found. + /// IRole GetRole(ulong id); - /// Creates a new role. + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + // TODO remove CreateRoleAsync overload that does not have isMentionable when breaking change is acceptable + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null); - /// Gets a collection of all users in this guild. - Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); //TODO: shouldnt this be paged? - /// Gets the user in this guild with the provided id, or null if not found. + /// + /// Adds a user to this guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + /// A guild user associated with the specified ; if the user is already in the guild. + Task AddGuildUserAsync(ulong userId, string accessToken, Action func = null, RequestOptions options = null); + /// + /// Disconnects the user from its current voice channel. + /// + /// The user to disconnect. + /// A task that represents the asynchronous operation for disconnecting a user. + Task DisconnectAsync(IGuildUser user); + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild. + /// + /// This may return an incomplete collection in the WebSocket implementation due to how Discord does not + /// send a complete user list for large guilds. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// This may return in the WebSocket implementation due to incomplete user collection in + /// large guilds. + /// + /// + /// The snowflake identifier of the user. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild user + /// associated with the specified ; if none is found. + /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the current user for this guild. + /// + /// Gets the current user for this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the currently logged-in + /// user within this guild. + /// Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the owner of this guild. + /// + /// Gets the owner of this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. + /// Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Downloads all users for this guild if the current list is incomplete. + /// + /// Downloads all users for this guild if the current list is incomplete. + /// + /// + /// This method downloads all users found within this guild through the Gateway and caches them. + /// + /// + /// A task that represents the asynchronous download operation. + /// Task DownloadUsersAsync(); - /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. - Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + /// + /// Prunes inactive users. + /// + /// + /// + /// This method removes all users that have not logged on in the provided number of . + /// + /// + /// If is true, this method will only return the number of users that + /// would be removed without kicking the users. + /// + /// + /// The number of days required for the users to be kicked. + /// Whether this prune action is a simulation. + /// The options to be used when sending the request. + /// An array of role IDs to be included in the prune of users who do not have any additional roles. + /// + /// A task that represents the asynchronous prune operation. The task result contains the number of users to + /// be or has been removed from this guild. + /// + Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null); + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users that the name or nickname starts with the provided at . + /// + Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the specified number of audit log entries for this guild. + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// The audit log entry ID to get entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// Task> GetAuditLogsAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch, - CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, + ActionType? actionType = null); - /// Gets the webhook in this guild with the provided id, or null if not found. + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; if none is found. + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); - /// Gets a collection of all webhooks for this guild. + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// Task> GetWebhooksAsync(RequestOptions options = null); - - /// Gets a specific emote from this guild. + + /// + /// Gets a collection of emotes from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of emotes found within the guild. + /// + Task> GetEmotesAsync(RequestOptions options = null); + /// + /// Gets a specific emote from this guild. + /// + /// The snowflake identifier for the guild emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the emote found with the + /// specified ; if none is found. + /// Task GetEmoteAsync(ulong id, RequestOptions options = null); - /// Creates a new emote in this guild. + /// + /// Creates a new in this guild. + /// + /// The name of the guild emote. + /// The image of the new emote. + /// The roles to limit the emote usage to. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created emote. + /// Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); - /// Modifies an existing emote in this guild. + + /// + /// Modifies an existing in this guild. + /// + /// The emote to be modified. + /// The delegate containing the properties to modify the emote with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. The task result contains the modified + /// emote. + /// Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); - /// Deletes an existing emote from this guild. + + /// + /// Moves the user to the voice channel. + /// + /// The user to move. + /// the channel where the user gets moved to. + /// A task that represents the asynchronous operation for moving a user. + Task MoveAsync(IGuildUser user, IVoiceChannel targetChannel); + + /// + /// Deletes an existing from this guild. + /// + /// The emote to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); + + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image of the new emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options = null); + + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The path of the file to upload. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + Task CreateStickerAsync(string name, string description, IEnumerable tags, string path, RequestOptions options = null); + + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The stream containing the file data. + /// The name of the file with the extension, ex: image.png. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + Task CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, string filename, RequestOptions options = null); + + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the sticker found with the + /// specified ; if none is found. + /// + Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Gets a collection of all stickers within this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of stickers found within the guild. + /// + Task> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Deletes a sticker within this guild. + /// + /// The sticker to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); + + /// + /// Gets a event within this guild. + /// + /// The id of the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + Task GetEventAsync(ulong id, RequestOptions options = null); + + /// + /// Gets a collection of events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + Task> GetEventsAsync(RequestOptions options = null); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null); + + /// + /// Gets this guilds application commands. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of application commands found within the guild. + /// + Task> GetApplicationCommandsAsync(RequestOptions options = null); + + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + Task GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, + RequestOptions options = null); + + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null); + + /// + /// Overwrites the application commands within this guild. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + /// + Task> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs index 225ce05d6..6fe3f7b55 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs @@ -1,17 +1,69 @@ -using System; +using System; namespace Discord { + /// + /// Holds information for a guild integration feature. + /// public interface IGuildIntegration { + /// + /// Gets the integration ID. + /// + /// + /// An representing the unique identifier value of this integration. + /// ulong Id { get; } + /// + /// Gets the integration name. + /// + /// + /// A string containing the name of this integration. + /// string Name { get; } + /// + /// Gets the integration type (Twitch, YouTube, etc). + /// + /// + /// A string containing the name of the type of integration. + /// string Type { get; } + /// + /// Gets a value that indicates whether this integration is enabled or not. + /// + /// + /// true if this integration is enabled; otherwise false. + /// bool IsEnabled { get; } + /// + /// Gets a value that indicates whether this integration is syncing or not. + /// + /// + /// An integration with syncing enabled will update its "subscribers" on an interval, while one with syncing + /// disabled will not. A user must manually choose when sync the integration if syncing is disabled. + /// + /// + /// true if this integration is syncing; otherwise false. + /// bool IsSyncing { get; } + /// + /// Gets the ID that this integration uses for "subscribers". + /// ulong ExpireBehavior { get; } + /// + /// Gets the grace period before expiring "subscribers". + /// ulong ExpireGracePeriod { get; } + /// + /// Gets when this integration was last synced. + /// + /// + /// A containing a date and time of day when the integration was last synced. + /// DateTimeOffset SyncedAt { get; } + /// + /// Gets integration account information. + /// IntegrationAccount Account { get; } IGuild Guild { get; } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs new file mode 100644 index 000000000..e50f4cc2b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic guild scheduled event. + /// + public interface IGuildScheduledEvent : IEntity + { + /// + /// Gets the guild this event is scheduled in. + /// + IGuild Guild { get; } + + /// + /// Gets the optional channel id where this event will be hosted. + /// + ulong? ChannelId { get; } + + /// + /// Gets the user who created the event. + /// + IUser Creator { get; } + + /// + /// Gets the name of the event. + /// + string Name { get; } + + /// + /// Gets the description of the event. + /// + /// + /// This field is when the event doesn't have a discription. + /// + string Description { get; } + + /// + /// Gets the start time of the event. + /// + DateTimeOffset StartTime { get; } + + /// + /// Gets the optional end time of the event. + /// + DateTimeOffset? EndTime { get; } + + /// + /// Gets the privacy level of the event. + /// + GuildScheduledEventPrivacyLevel PrivacyLevel { get; } + + /// + /// Gets the status of the event. + /// + GuildScheduledEventStatus Status { get; } + + /// + /// Gets the type of the event. + /// + GuildScheduledEventType Type { get; } + + /// + /// Gets the optional entity id of the event. The "entity" of the event + /// can be a stage instance event as is seperate from . + /// + ulong? EntityId { get; } + + /// + /// Gets the location of the event if the is external. + /// + string Location { get; } + + /// + /// Gets the user count of the event. + /// + int? UserCount { get; } + + /// + /// Starts the event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous start operation. + /// + Task StartAsync(RequestOptions options = null); + /// + /// Ends or canceles the event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous end operation. + /// + Task EndAsync(RequestOptions options = null); + + /// + /// Modifies the guild event. + /// + /// The delegate containing the properties to modify the event with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Deletes the current event. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous delete operation. + /// + Task DeleteAsync(RequestOptions options = null); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetUsersAsync(RequestOptions options = null); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs index b27db9377..b6685edf6 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs @@ -1,14 +1,22 @@ -namespace Discord +namespace Discord { public interface IUserGuild : IDeletable, ISnowflakeEntity { - /// Gets the name of this guild. + /// + /// Gets the name of this guild. + /// string Name { get; } - /// Returns the url to this guild's icon, or null if one is not set. + /// + /// Gets the icon URL associated with this guild, or null if one is not set. + /// string IconUrl { get; } - /// Returns true if the current user owns this guild. + /// + /// Returns true if the current user owns this guild. + /// bool IsOwner { get; } - /// Returns the current user's permissions for this guild. + /// + /// Returns the current user's permissions for this guild. + /// GuildPermissions Permissions { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs index 67bedbc3d..9cef84914 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs @@ -1,18 +1,51 @@ namespace Discord { + /// + /// Represents a region of which the user connects to when using voice. + /// public interface IVoiceRegion { - /// Gets the unique identifier for this voice region. + /// + /// Gets the unique identifier for this voice region. + /// + /// + /// A string that represents the identifier for this voice region (e.g. eu-central). + /// string Id { get; } - /// Gets the name of this voice region. + /// + /// Gets the name of this voice region. + /// + /// + /// A string that represents the human-readable name of this voice region (e.g. Central Europe). + /// string Name { get; } - /// Returns true if this voice region is exclusive to VIP accounts. + /// + /// Gets a value that indicates whether or not this voice region is exclusive to partnered servers. + /// + /// + /// true if this voice region is exclusive to VIP accounts; otherwise false. + /// bool IsVip { get; } - /// Returns true if this voice region is the closest to your machine. + /// + /// Gets a value that indicates whether this voice region is optimal for your client in terms of latency. + /// + /// + /// true if this voice region is the closest to your machine; otherwise false . + /// bool IsOptimal { get; } - /// Returns true if this is a deprecated voice region (avoid switching to these). + /// + /// Gets a value that indicates whether this voice region is no longer being maintained. + /// + /// + /// true if this is a deprecated voice region; otherwise false. + /// bool IsDeprecated { get; } - /// Returns true if this is a custom voice region (used for events/etc) + /// + /// Gets a value that indicates whether this voice region is custom-made for events. + /// + /// + /// true if this is a custom voice region (used for events/etc); otherwise false/ + /// bool IsCustom { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs index 71bcf10ed..340115fde 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs @@ -1,11 +1,15 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct IntegrationAccount { + /// Gets the ID of the account. + /// A unique identifier of this integration account. public string Id { get; } + /// Gets the name of the account. + /// A string containing the name of this integration account. public string Name { get; private set; } public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs index 1dfef17d5..57edac2b0 100644 --- a/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the guild's Multi-Factor Authentication (MFA) level requirement. + /// public enum MfaLevel { - /// Users have no additional MFA restriction on this guild. + /// + /// Users have no additional MFA restriction on this guild. + /// Disabled = 0, - /// Users must have MFA enabled on their account to perform administrative actions. + /// + /// Users must have MFA enabled on their account to perform administrative actions. + /// Enabled = 1 } } diff --git a/src/Discord.Net.Core/Entities/Guilds/NsfwLevel.cs b/src/Discord.Net.Core/Entities/Guilds/NsfwLevel.cs new file mode 100644 index 000000000..e3ac345d9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/NsfwLevel.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + public enum NsfwLevel + { + /// + /// Default or unset. + /// + Default = 0, + /// + /// Guild has extremely suggestive or mature content that would only be suitable for users 18 or over. + /// + Explicit = 1, + /// + /// Guild has no content that could be deemed NSFW; in other words, SFW. + /// + Safe = 2, + /// + /// Guild has mildly NSFW content that may not be suitable for users under 18. + /// + AgeRestricted = 3 + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs index 96595fb69..fb759e4c5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs +++ b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the target of the permission. + /// public enum PermissionTarget { - Role, - User + /// + /// The target of the permission is a role. + /// + Role = 0, + /// + /// The target of the permission is a user. + /// + User = 1, } } diff --git a/src/Discord.Net.Core/Entities/Guilds/PremiumTier.cs b/src/Discord.Net.Core/Entities/Guilds/PremiumTier.cs new file mode 100644 index 000000000..b7e4c9323 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/PremiumTier.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + public enum PremiumTier + { + /// + /// Used for guilds that have no guild boosts. + /// + None = 0, + /// + /// Used for guilds that have Tier 1 guild boosts. + /// + Tier1 = 1, + /// + /// Used for guilds that have Tier 2 guild boosts. + /// + Tier2 = 2, + /// + /// Used for guilds that have Tier 3 guild boosts. + /// + Tier3 = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs b/src/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs new file mode 100644 index 000000000..06de7b812 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord +{ + [Flags] + public enum SystemChannelMessageDeny + { + /// + /// Deny none of the system channel messages. + /// This will enable all of the system channel messages. + /// + None = 0, + /// + /// Deny the messages that are sent when a user joins the guild. + /// + WelcomeMessage = 0b1, + /// + /// Deny the messages that are sent when a user boosts the guild. + /// + GuildBoost = 0b10, + /// + /// Deny the messages that are related to guild setup. + /// + GuildSetupTip = 0b100, + /// + /// Deny the reply with sticker button on welcome messages. + /// + WelcomeMessageReply = 0b1000 + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs index ac51fe927..3a5ae0468 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -1,16 +1,29 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the verification level the guild uses. + /// public enum VerificationLevel { - /// Users have no additional restrictions on sending messages to this guild. + /// + /// Users have no additional restrictions on sending messages to this guild. + /// None = 0, - /// Users must have a verified email on their account. + /// + /// Users must have a verified email on their account. + /// Low = 1, - /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. + /// + /// Users must fulfill the requirements of Low and be registered on Discord for at least 5 minutes. + /// Medium = 2, - /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. + /// + /// Users must fulfill the requirements of Medium and be a member of this guild for at least 10 minutes. + /// High = 3, - /// Users must fulfill the requirements of High, and must have a verified phone on their Discord account. + /// + /// Users must fulfill the requirements of High and must have a verified phone on their Discord account. + /// Extreme = 4 } } diff --git a/src/Discord.Net.Core/Entities/IApplication.cs b/src/Discord.Net.Core/Entities/IApplication.cs index 4fb1e4b91..9f9881340 100644 --- a/src/Discord.Net.Core/Entities/IApplication.cs +++ b/src/Discord.Net.Core/Entities/IApplication.cs @@ -1,13 +1,53 @@ -namespace Discord +using System.Collections.Generic; + +namespace Discord { + /// + /// Represents a Discord application created via the developer portal. + /// public interface IApplication : ISnowflakeEntity { + /// + /// Gets the name of the application. + /// string Name { get; } + /// + /// Gets the description of the application. + /// string Description { get; } - string[] RPCOrigins { get; } - ulong Flags { get; } + /// + /// Gets the RPC origins of the application. + /// + IReadOnlyCollection RPCOrigins { get; } + ApplicationFlags Flags { get; } + /// + /// Gets a collection of install parameters for this application. + /// + ApplicationInstallParams InstallParams { get; } + /// + /// Gets a collection of tags related to the application. + /// + IReadOnlyCollection Tags { get; } + /// + /// Gets the icon URL of the application. + /// string IconUrl { get; } + /// + /// Gets if the bot is public. + /// + bool IsBotPublic { get; } + /// + /// Gets if the bot requires code grant. + /// + bool BotRequiresCodeGrant { get; } + /// + /// Gets the team associated with this application if there is one. + /// + ITeam Team { get; } + /// + /// Gets the partial user object containing info on the owner of the application. + /// IUser Owner { get; } } } diff --git a/src/Discord.Net.Core/Entities/IDeletable.cs b/src/Discord.Net.Core/Entities/IDeletable.cs index ba22a537a..9696eb838 100644 --- a/src/Discord.Net.Core/Entities/IDeletable.cs +++ b/src/Discord.Net.Core/Entities/IDeletable.cs @@ -1,10 +1,16 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Determines whether the object is deletable or not. + /// public interface IDeletable { - /// Deletes this object and all its children. + /// + /// Deletes this object and all its children. + /// + /// The options to be used when sending the request. Task DeleteAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IEntity.cs b/src/Discord.Net.Core/Entities/IEntity.cs index 711fd0555..0cd692a41 100644 --- a/src/Discord.Net.Core/Entities/IEntity.cs +++ b/src/Discord.Net.Core/Entities/IEntity.cs @@ -8,7 +8,9 @@ namespace Discord ///// Gets the IDiscordClient that created this object. //IDiscordClient Discord { get; } - /// Gets the unique identifier for this object. + /// + /// Gets the unique identifier for this object. + /// TId Id { get; } } diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index abccc4480..225806723 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -1,8 +1,16 @@ -namespace Discord +namespace Discord { + /// + /// Determines whether the object is mentionable or not. + /// public interface IMentionable { - /// Returns a special string used to mention this object. + /// + /// Returns a special string used to mention this object. + /// + /// + /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). + /// string Mention { get; } } } diff --git a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs index 5b099b5ac..6f2c7512b 100644 --- a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs @@ -2,8 +2,15 @@ using System; namespace Discord { + /// Represents a Discord snowflake entity. public interface ISnowflakeEntity : IEntity { + /// + /// Gets when the snowflake was created. + /// + /// + /// A representing when the entity was first created. + /// DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net.Core/Entities/IUpdateable.cs b/src/Discord.Net.Core/Entities/IUpdateable.cs index b0f51aee7..d561e57a6 100644 --- a/src/Discord.Net.Core/Entities/IUpdateable.cs +++ b/src/Discord.Net.Core/Entities/IUpdateable.cs @@ -1,10 +1,16 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Defines whether the object is updateable or not. + /// public interface IUpdateable { - /// Updates this object's properties with its current state. + /// + /// Updates this object's properties with its current state. + /// + /// The options to be used when sending the request. Task UpdateAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 3b946ce80..3f5a01f6a 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -1,32 +1,79 @@ +using System; using System.IO; + namespace Discord { /// - /// An image that will be uploaded to Discord. + /// An image that will be uploaded to Discord. /// - public struct Image + public struct Image : IDisposable { + private bool _isDisposed; + + /// + /// Gets the stream to be uploaded to Discord. + /// +#pragma warning disable IDISP008 public Stream Stream { get; } +#pragma warning restore IDISP008 /// - /// Create the image with a Stream. + /// Create the image with a . /// - /// This must be some type of stream with the contents of a file in it. + /// + /// The to create the image with. Note that this must be some type of stream + /// with the contents of a file in it. + /// public Image(Stream stream) { + _isDisposed = false; Stream = stream; } /// - /// Create the image from a file path. + /// Create the image from a file path. /// /// - /// This file path is NOT validated, and is passed directly into a + /// This file path is NOT validated and is passed directly into a + /// . /// /// The path to the file. + /// + /// is a zero-length string, contains only white space, or contains one or more invalid + /// characters as defined by . + /// + /// is null. + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// is in an invalid format. + /// + /// The specified is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// The file specified in was not found. + /// + /// An I/O error occurred while opening the file. public Image(string path) { + _isDisposed = false; Stream = File.OpenRead(path); } + /// + public void Dispose() + { + if (!_isDisposed) + { +#pragma warning disable IDISP007 + Stream?.Dispose(); +#pragma warning restore IDISP007 + + _isDisposed = true; + } + } } } diff --git a/src/Discord.Net.Core/Entities/ImageFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs index 302da79c8..9c04328f4 100644 --- a/src/Discord.Net.Core/Entities/ImageFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,11 +1,29 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the type of format the image should return in. + /// public enum ImageFormat { + /// + /// Use automatically detected format. + /// Auto, + /// + /// Use Google's WebP image format. + /// WebP, + /// + /// Use PNG. + /// Png, + /// + /// Use JPEG. + /// Jpeg, + /// + /// Use GIF. + /// Gif, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs new file mode 100644 index 000000000..9a69d9d18 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Discord +{ + /// + /// Represents a for making slash commands. + /// + public class ApplicationCommandOptionProperties + { + private string _name; + private string _description; + + /// + /// Gets or sets the name of this option. + /// + public string Name + { + get => _name; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); + + if (value.Length > 32) + throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32."); + + if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) + throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$"); + + _name = value; + } + } + + /// + /// Gets or sets the description of this option. + /// + public string Description + { + get => _description; + set => _description = value?.Length switch + { + > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the type of this option. + /// + public ApplicationCommandOptionType Type { get; set; } + + /// + /// Gets or sets whether or not this options is the first required option for the user to complete. only one option can be default. + /// + public bool? IsDefault { get; set; } + + /// + /// Gets or sets if the option is required. + /// + public bool? IsRequired { get; set; } + + /// + /// Gets or sets whether or not this option supports autocomplete. + /// + public bool IsAutocomplete { get; set; } + + /// + /// Gets or sets the smallest number value the user can input. + /// + public double? MinValue { get; set; } + + /// + /// Gets or sets the largest number value the user can input. + /// + public double? MaxValue { get; set; } + + /// + /// Gets or sets the choices for string and int types for the user to pick from. + /// + public List Choices { get; set; } + + /// + /// Gets or sets if this option is a subcommand or subcommand group type, these nested options will be the parameters. + /// + public List Options { get; set; } + + /// + /// Gets or sets the allowed channel types for this option. + /// + public List ChannelTypes { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..6a908b075 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -0,0 +1,44 @@ +using System; + +namespace Discord +{ + /// + /// Represents a choice for a . This class is used when making new commands. + /// + public class ApplicationCommandOptionChoiceProperties + { + private string _name; + private object _value; + + /// + /// Gets or sets the name of this choice. + /// + public string Name + { + get => _name; + set => _name = value?.Length switch + { + > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 100."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."), + _ => value + }; + } + + /// + /// Gets the value of this choice. + /// + /// Discord only accepts int, double/floats, and string as the input. + /// + /// + public object Value + { + get => _value; + set + { + if (value != null && value is not string && !value.IsNumericType()) + throw new ArgumentException("The value of a choice must be a string or a numeric type!"); + _value = value; + } + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs new file mode 100644 index 000000000..0f919f1f6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -0,0 +1,58 @@ +namespace Discord +{ + /// + /// The option type of the Slash command parameter, See the discord docs. + /// + public enum ApplicationCommandOptionType : byte + { + /// + /// A sub command. + /// + SubCommand = 1, + + /// + /// A group of sub commands. + /// + SubCommandGroup = 2, + + /// + /// A of text. + /// + String = 3, + + /// + /// An . + /// + Integer = 4, + + /// + /// A . + /// + Boolean = 5, + + /// + /// A . + /// + User = 6, + + /// + /// A . + /// + Channel = 7, + + /// + /// A . + /// + Role = 8, + + /// + /// A or . + /// + Mentionable = 9, + + /// + /// A . + /// + Number = 10 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs new file mode 100644 index 000000000..501a0e905 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + /// + /// Represents the base class to create/modify application commands. + /// + public abstract class ApplicationCommandProperties + { + internal abstract ApplicationCommandType Type { get; } + + /// + /// Gets or sets the name of this command. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets whether the command is enabled by default when the app is added to a guild. Default is + /// + public Optional IsDefaultPermission { get; set; } + + internal ApplicationCommandProperties() { } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs new file mode 100644 index 000000000..8cd31a420 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Represents the types of application commands. + /// + public enum ApplicationCommandType : byte + { + /// + /// A Slash command type + /// + Slash = 1, + + /// + /// A Context Menu User command type + /// + User = 2, + + /// + /// A Context Menu Message command type + /// + Message = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/AutocompleteOption.cs b/src/Discord.Net.Core/Entities/Interactions/AutocompleteOption.cs new file mode 100644 index 000000000..eb22a9d27 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/AutocompleteOption.cs @@ -0,0 +1,36 @@ +namespace Discord +{ + /// + /// Represents an autocomplete option. + /// + public class AutocompleteOption + { + /// + /// Gets the type of this option. + /// + public ApplicationCommandOptionType Type { get; } + + /// + /// Gets the name of the option. + /// + public string Name { get; } + + /// + /// Gets the value of the option. + /// + public object Value { get; } + + /// + /// Gets whether or not this option is focused by the executing user. + /// + public bool Focused { get; } + + internal AutocompleteOption(ApplicationCommandOptionType type, string name, object value, bool focused) + { + Type = type; + Name = name; + Value = value; + Focused = focused; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/AutocompleteResult.cs b/src/Discord.Net.Core/Entities/Interactions/AutocompleteResult.cs new file mode 100644 index 000000000..0603a5a50 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/AutocompleteResult.cs @@ -0,0 +1,73 @@ +using System; + +namespace Discord +{ + /// + /// Represents a result to an autocomplete interaction. + /// + public class AutocompleteResult + { + private object _value; + private string _name; + + /// + /// Gets or sets the name of the result. + /// + /// + /// Name cannot be null and has to be between 1-100 characters in length. + /// + /// + /// + public string Name + { + get => _name; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); + _name = value.Length switch + { + > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 100."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Name length must be at least 1."), + _ => value + }; + } + } + + /// + /// Gets or sets the value of the result. + /// + /// + /// Only , , and are allowed for a value. + /// + /// + /// + public object Value + { + get => _value; + set + { + if (value is not string && !value.IsNumericType()) + throw new ArgumentException($"{nameof(value)} must be a numeric type or a string!"); + + _value = value; + } + } + + /// + /// Creates a new . + /// + public AutocompleteResult() { } + + /// + /// Creates a new with the passed in and . + /// + /// + /// + public AutocompleteResult(string name, object value) + { + Name = name; + Value = value; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteraction.cs new file mode 100644 index 000000000..b1b331e8b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteraction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents a Message Command interaction. + /// + public interface IMessageCommandInteraction : IDiscordInteraction + { + /// + /// Gets the data associated with this interaction. + /// + new IMessageCommandInteractionData Data { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteractionData.cs new file mode 100644 index 000000000..311eef2d6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IMessageCommandInteractionData.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents the data tied with the interaction. + /// + public interface IMessageCommandInteractionData : IApplicationCommandInteractionData + { + /// + /// Gets the message associated with this message command. + /// + IMessage Message { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteraction.cs new file mode 100644 index 000000000..f7cfd67f0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteraction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents a User Command interaction. + /// + public interface IUserCommandInteraction : IDiscordInteraction + { + /// + /// Gets the data associated with this interaction. + /// + new IUserCommandInteractionData Data { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteractionData.cs new file mode 100644 index 000000000..36e482ec9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/IUserCommandInteractionData.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents the data tied with the interaction. + /// + public interface IUserCommandInteractionData : IApplicationCommandInteractionData + { + /// + /// Gets the user who this command targets. + /// + IUser User { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs new file mode 100644 index 000000000..c7a7cf741 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -0,0 +1,77 @@ +namespace Discord +{ + /// + /// A class used to build Message commands. + /// + public class MessageCommandBuilder + { + /// + /// Returns the maximum length a commands name allowed by Discord + /// + public const int MaxNameLength = 32; + + /// + /// Gets or sets the name of this Message command. + /// + public string Name + { + get => _name; + set + { + Preconditions.NotNullOrEmpty(value, nameof(Name)); + Preconditions.AtLeast(value.Length, 1, nameof(Name)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name)); + + _name = value; + } + } + + /// + /// Gets or sets whether the command is enabled by default when the app is added to a guild + /// + public bool IsDefaultPermission { get; set; } = true; + + private string _name; + + /// + /// Build the current builder into a class. + /// + /// + /// A that can be used to create message commands. + /// + public MessageCommandProperties Build() + { + var props = new MessageCommandProperties + { + Name = Name, + IsDefaultPermission = IsDefaultPermission + }; + + return props; + } + + /// + /// Sets the field name. + /// + /// The value to set the field name to. + /// + /// The current builder. + /// + public MessageCommandBuilder WithName(string name) + { + Name = name; + return this; + } + + /// + /// Sets the default permission of the current command. + /// + /// The default permission value to set. + /// The current builder. + public MessageCommandBuilder WithDefaultPermission(bool isDefaultPermission) + { + IsDefaultPermission = isDefaultPermission; + return this; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandProperties.cs new file mode 100644 index 000000000..356ed23d6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandProperties.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + /// + /// A class used to create message commands. + /// + public class MessageCommandProperties : ApplicationCommandProperties + { + internal override ApplicationCommandType Type => ApplicationCommandType.Message; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs new file mode 100644 index 000000000..bd1078be3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -0,0 +1,75 @@ +namespace Discord +{ + /// + /// A class used to build user commands. + /// + public class UserCommandBuilder + { + /// + /// Returns the maximum length a commands name allowed by Discord. + /// + public const int MaxNameLength = 32; + + /// + /// Gets or sets the name of this User command. + /// + public string Name + { + get => _name; + set + { + Preconditions.NotNullOrEmpty(value, nameof(Name)); + Preconditions.AtLeast(value.Length, 1, nameof(Name)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name)); + + _name = value; + } + } + + /// + /// Gets or sets whether the command is enabled by default when the app is added to a guild. + /// + public bool IsDefaultPermission { get; set; } = true; + + private string _name; + + /// + /// Build the current builder into a class. + /// + /// A that can be used to create user commands. + public UserCommandProperties Build() + { + var props = new UserCommandProperties + { + Name = Name, + IsDefaultPermission = IsDefaultPermission + }; + + return props; + } + + /// + /// Sets the field name. + /// + /// The value to set the field name to. + /// + /// The current builder. + /// + public UserCommandBuilder WithName(string name) + { + Name = name; + return this; + } + + /// + /// Sets the default permission of the current command. + /// + /// The default permission value to set. + /// The current builder. + public UserCommandBuilder WithDefaultPermission(bool isDefaultPermission) + { + IsDefaultPermission = isDefaultPermission; + return this; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandProperties.cs new file mode 100644 index 000000000..c42e916d9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandProperties.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + /// + /// A class used to create User commands. + /// + public class UserCommandProperties : ApplicationCommandProperties + { + internal override ApplicationCommandType Type => ApplicationCommandType.User; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs new file mode 100644 index 000000000..72045a52a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// The base command model that belongs to an application. + /// + public interface IApplicationCommand : ISnowflakeEntity, IDeletable + { + /// + /// Gets the unique id of the parent application. + /// + ulong ApplicationId { get; } + + /// + /// Gets the type of the command. + /// + ApplicationCommandType Type { get; } + + /// + /// Gets the name of the command. + /// + string Name { get; } + + /// + /// Gets the description of the command. + /// + string Description { get; } + + /// + /// Gets whether the command is enabled by default when the app is added to a guild. + /// + bool IsDefaultPermission { get; } + + /// + /// Gets a collection of options for this application command. + /// + IReadOnlyCollection Options { get; } + + /// + /// Modifies the current application command. + /// + /// The new properties to use when modifying the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Modifies the current application command. + /// + /// The new properties to use when modifying the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// Thrown when you pass in an invalid type. + Task ModifyAsync(Action func, RequestOptions options = null) + where TArg : ApplicationCommandProperties; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs new file mode 100644 index 000000000..428f20fb6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents data of an Interaction Command, see . + /// + public interface IApplicationCommandInteractionData : IDiscordInteractionData + { + /// + /// Gets the snowflake id of this command. + /// + ulong Id { get; } + + /// + /// Gets the name of this command. + /// + string Name { get; } + + /// + /// Gets the options that the user has provided. + /// + IReadOnlyCollection Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs new file mode 100644 index 000000000..072d2b32b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a option group for a command. + /// + public interface IApplicationCommandInteractionDataOption + { + /// + /// Gets the name of the parameter. + /// + string Name { get; } + + /// + /// Gets the value of the pair. + /// + /// This objects type can be any one of the option types in . + /// + /// + object Value { get; } + + /// + /// Gets the type of this data's option. + /// + ApplicationCommandOptionType Type { get; } + + /// + /// Gets the nested options of this option. + /// + IReadOnlyCollection Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs new file mode 100644 index 000000000..440c4bd6b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Options for the . + /// + public interface IApplicationCommandOption + { + /// + /// Gets the type of this . + /// + ApplicationCommandOptionType Type { get; } + + /// + /// Gets the name of this command option. + /// + string Name { get; } + + /// + /// Gets the description of this command option. + /// + string Description { get; } + + /// + /// Gets whether or not this is the first required option for the user to complete. + /// + bool? IsDefault { get; } + + /// + /// Gets whether or not the parameter is required or optional. + /// + bool? IsRequired { get; } + + /// + /// Gets the smallest number value the user can input. + /// + double? MinValue { get; } + + /// + /// Gets the largest number value the user can input. + /// + double? MaxValue { get; } + + /// + /// Gets the choices for string and int types for the user to pick from. + /// + IReadOnlyCollection Choices { get; } + + /// + /// Gets the sub-options for this command option. + /// + IReadOnlyCollection Options { get; } + + /// + /// Gets the allowed channel types for this option. + /// + IReadOnlyCollection ChannelTypes { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..631706c6f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Specifies choices for command group. + /// + public interface IApplicationCommandOptionChoice + { + /// + /// Gets the choice name. + /// + string Name { get; } + + /// + /// Gets the value of the choice. + /// + object Value { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs new file mode 100644 index 000000000..d9e250118 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a discord interaction. + /// + public interface IDiscordInteraction : ISnowflakeEntity + { + /// + /// Gets the id of the interaction. + /// + new ulong Id { get; } + + /// + /// Gets the type of this . + /// + InteractionType Type { get; } + + /// + /// Gets the data sent within this interaction. + /// + IDiscordInteractionData Data { get; } + + /// + /// Gets the continuation token for responding to the interaction. + /// + string Token { get; } + + /// + /// Gets the version of the interaction, always 1. + /// + int Version { get; } + + /// + /// Responds to an Interaction with type . + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, + bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Gets the original response for this interaction. + /// + /// The request options for this request. + /// A that represents the initial response. + Task GetOriginalResponseAsync(RequestOptions options = null); + + /// + /// Edits original response for this interaction. + /// + /// A delegate containing the properties to modify the message with. + /// The request options for this request. + /// A that represents the initial response. + Task ModifyOriginalResponseAsync(Action func, RequestOptions options = null); + + /// + /// Acknowledges this interaction. + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + Task DeferAsync(bool ephemeral = false, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs new file mode 100644 index 000000000..42b95738e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteractionData.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + /// + /// Represents an interface used to specify classes that they are a valid data type of a class. + /// + public interface IDiscordInteractionData { } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs new file mode 100644 index 000000000..ebdf29781 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -0,0 +1,46 @@ +using System; + +namespace Discord +{ + /// + /// The response type for an . + /// + /// + /// After receiving an interaction, you must respond to acknowledge it. You can choose to respond with a message immediately using + /// or you can choose to send a deferred response with . If choosing a deferred response, the user will see a loading state for the interaction, + /// and you'll have up to 15 minutes to edit the original deferred response using Edit Original Interaction Response. + /// You can read more about Response types Here. + /// + public enum InteractionResponseType : byte + { + /// + /// ACK a Ping. + /// + Pong = 1, + + /// + /// Respond to an interaction with a message. + /// + ChannelMessageWithSource = 4, + + /// + /// ACK an interaction and edit a response later, the user sees a loading state. + /// + DeferredChannelMessageWithSource = 5, + + /// + /// For components: ACK an interaction and edit the original message later; the user does not see a loading state. + /// + DeferredUpdateMessage = 6, + + /// + /// For components: edit the message the component was attached to. + /// + UpdateMessage = 7, + + /// + /// Respond with a set of choices to a autocomplete interaction. + /// + ApplicationCommandAutocompleteResult = 8 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs new file mode 100644 index 000000000..e09c906b5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionType.cs @@ -0,0 +1,28 @@ +namespace Discord +{ + /// + /// Represents a type of Interaction from discord. + /// + public enum InteractionType : byte + { + /// + /// A ping from discord. + /// + Ping = 1, + + /// + /// A sent from discord. + /// + ApplicationCommand = 2, + + /// + /// A sent from discord. + /// + MessageComponent = 3, + + /// + /// An autocomplete request sent from discord. + /// + ApplicationCommandAutocomplete = 4 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs new file mode 100644 index 000000000..202a5687f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a Row for child components to live in. + /// + public class ActionRowComponent : IMessageComponent + { + /// + public ComponentType Type => ComponentType.ActionRow; + + /// + /// Gets the child components in this row. + /// + public IReadOnlyCollection Components { get; internal set; } + + internal ActionRowComponent() { } + + internal ActionRowComponent(List components) + { + Components = components; + } + + string IMessageComponent.CustomId => null; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs new file mode 100644 index 000000000..4b9fa2753 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs @@ -0,0 +1,61 @@ +namespace Discord +{ + /// + /// Represents a Button. + /// + public class ButtonComponent : IMessageComponent + { + /// + public ComponentType Type => ComponentType.Button; + + /// + /// Gets the of this button, example buttons with each style can be found Here. + /// + public ButtonStyle Style { get; } + + /// + /// Gets the label of the button, this is the text that is shown. + /// + public string Label { get; } + + /// + /// Gets the displayed with this button. + /// + public IEmote Emote { get; } + + /// + public string CustomId { get; } + + /// + /// Gets the URL for a button. + /// + /// + /// You cannot have a button with a URL and a CustomId. + /// + public string Url { get; } + + /// + /// Gets whether this button is disabled or not. + /// + public bool IsDisabled { get; } + + /// + /// Turns this button into a button builder. + /// + /// + /// A newly created button builder with the same properties as this button. + /// + public ButtonBuilder ToBuilder() + => new ButtonBuilder(Label, CustomId, Style, Url, Emote, IsDisabled); + + internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled) + { + Style = style; + Label = label; + Emote = emote; + CustomId = customId; + Url = url; + IsDisabled = isDisabled; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs new file mode 100644 index 000000000..92d48ab4f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonStyle.cs @@ -0,0 +1,33 @@ +namespace Discord +{ + /// + /// Represents different styles to use with buttons. You can see an example of the different styles at + /// + public enum ButtonStyle + { + /// + /// A Blurple button + /// + Primary = 1, + + /// + /// A Grey (or gray) button + /// + Secondary = 2, + + /// + /// A Green button + /// + Success = 3, + + /// + /// A Red button + /// + Danger = 4, + + /// + /// A button with a little popup box indicating that this button is a link. + /// + Link = 5 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs new file mode 100644 index 000000000..4461a4205 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -0,0 +1,1064 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Discord.Utils; + +namespace Discord +{ + /// + /// Represents a builder for creating a . + /// + public class ComponentBuilder + { + /// + /// The max length of a . + /// + public const int MaxCustomIdLength = 100; + + /// + /// The max amount of rows a message can have. + /// + public const int MaxActionRowCount = 5; + + /// + /// Gets or sets the Action Rows for this Component Builder. + /// + /// cannot be null. + /// count exceeds . + public List ActionRows + { + get => _actionRows; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(ActionRows)} cannot be null."); + if (value.Count > MaxActionRowCount) + throw new ArgumentOutOfRangeException(nameof(value), $"Action row count must be less than or equal to {MaxActionRowCount}."); + _actionRows = value; + } + } + + private List _actionRows; + + /// + /// Creates a new builder from a message. + /// + /// The message to create the builder from. + /// The newly created builder. + public static ComponentBuilder FromMessage(IMessage message) + => FromComponents(message.Components); + + /// + /// Creates a new builder from the provided list of components. + /// + /// The components to create the builder from. + /// The newly created builder. + public static ComponentBuilder FromComponents(IReadOnlyCollection components) + { + var builder = new ComponentBuilder(); + for (int i = 0; i != components.Count; i++) + { + var component = components.ElementAt(i); + builder.AddComponent(component, i); + } + return builder; + } + + internal void AddComponent(IMessageComponent component, int row) + { + switch (component) + { + case ButtonComponent button: + WithButton(button.Label, button.CustomId, button.Style, button.Emote, button.Url, button.IsDisabled, row); + break; + case ActionRowComponent actionRow: + foreach (var cmp in actionRow.Components) + AddComponent(cmp, row); + break; + case SelectMenuComponent menu: + WithSelectMenu(menu.CustomId, menu.Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), menu.Placeholder, menu.MinValues, menu.MaxValues, menu.IsDisabled, row); + break; + } + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The row to add the menu to. + /// + public ComponentBuilder WithSelectMenu(string customId, List options, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled), + row); + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The menu to add. + /// The row to attempt to add this component on. + /// There is no more row to add a menu. + /// must be less than . + /// The current builder. + public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) + { + Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); + if (menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (_actionRows == null) + { + _actionRows = new List + { + new ActionRowBuilder().AddComponent(builtMenu) + }; + } + else + { + if (_actionRows.Count == row) + _actionRows.Add(new ActionRowBuilder().AddComponent(builtMenu)); + else + { + ActionRowBuilder actionRow; + if (_actionRows.Count > row) + actionRow = _actionRows.ElementAt(row); + else + { + actionRow = new ActionRowBuilder(); + _actionRows.Add(actionRow); + } + + if (actionRow.CanTakeComponent(builtMenu)) + actionRow.AddComponent(builtMenu); + else if (row < MaxActionRowCount) + WithSelectMenu(menu, row + 1); + else + throw new InvalidOperationException($"There is no more row to add a {nameof(builtMenu)}"); + } + } + + return this; + } + + /// + /// Adds a with specified parameters to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The row the button should be placed on. + /// The current builder. + public ComponentBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false, + int row = 0) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled); + + return WithButton(button, row); + } + + /// + /// Adds a to the at the specific row. + /// If the row cannot accept the component then it will add it to a row that can. + /// + /// The button to add. + /// The row to add the button. + /// There is no more row to add a menu. + /// must be less than . + /// The current builder. + public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) + { + Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); + + var builtButton = button.Build(); + + if (_actionRows == null) + { + _actionRows = new List + { + new ActionRowBuilder().AddComponent(builtButton) + }; + } + else + { + if (_actionRows.Count == row) + _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); + else + { + ActionRowBuilder actionRow; + if (_actionRows.Count > row) + actionRow = _actionRows.ElementAt(row); + else + { + actionRow = new ActionRowBuilder(); + _actionRows.Add(actionRow); + } + + if (actionRow.CanTakeComponent(builtButton)) + actionRow.AddComponent(builtButton); + else if (row < MaxActionRowCount) + WithButton(button, row + 1); + else + throw new InvalidOperationException($"There is no more row to add a {nameof(button)}"); + } + } + + return this; + } + + /// + /// Builds this builder into a used to send your components. + /// + /// A that can be sent with . + public MessageComponent Build() + { + return _actionRows != null + ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) + : MessageComponent.Empty; + } + } + + /// + /// Represents a class used to build Action rows. + /// + public class ActionRowBuilder + { + /// + /// The max amount of child components this row can hold. + /// + public const int MaxChildCount = 5; + + /// + /// Gets or sets the components inside this row. + /// + /// cannot be null. + /// count exceeds . + public List Components + { + get => _components; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); + + _components = value.Count switch + { + 0 => throw new ArgumentOutOfRangeException(nameof(value), "There must be at least 1 component in a row."), + > MaxChildCount => throw new ArgumentOutOfRangeException(nameof(value), $"Action row can only contain {MaxChildCount} child components!"), + _ => value + }; + } + } + + private List _components = new List(); + + /// + /// Adds a list of components to the current row. + /// + /// The list of components to add. + /// + /// The current builder. + public ActionRowBuilder WithComponents(List components) + { + Components = components; + return this; + } + + /// + /// Adds a component at the end of the current row. + /// + /// The component to add. + /// Components count reached + /// The current builder. + public ActionRowBuilder AddComponent(IMessageComponent component) + { + if (Components.Count >= MaxChildCount) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + Components.Add(component); + return this; + } + + /// + /// Builds the current builder to a that can be used within a + /// + /// A that can be used within a + public ActionRowComponent Build() + { + return new ActionRowComponent(_components); + } + + internal bool CanTakeComponent(IMessageComponent component) + { + switch (component.Type) + { + case ComponentType.ActionRow: + return false; + case ComponentType.Button: + if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + return false; + else + return Components.Count < 5; + case ComponentType.SelectMenu: + return Components.Count == 0; + default: + return false; + } + } + } + + /// + /// Represents a class used to build 's. + /// + public class ButtonBuilder + { + /// + /// The max length of a . + /// + public const int MaxButtonLabelLength = 80; + + /// + /// Gets or sets the label of the current button. + /// + /// length exceeds . + /// length exceeds . + public string Label + { + get => _label; + set => _label = value?.Length switch + { + > MaxButtonLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxButtonLabelLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the custom id of the current button. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the of the current button. + /// + public ButtonStyle Style { get; set; } + + /// + /// Gets or sets the of the current button. + /// + public IEmote Emote { get; set; } + + /// + /// Gets or sets the url of the current button. + /// + public string Url { get; set; } + + /// + /// Gets or sets whether the current button is disabled. + /// + public bool IsDisabled { get; set; } + + private string _label; + private string _customId; + + /// + /// Creates a new instance of a . + /// + public ButtonBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The label to use on the newly created link button. + /// The url of this button. + /// The custom ID of this button. + /// The custom ID of this button. + /// The emote of this button. + /// Disabled this button or not. + public ButtonBuilder(string label = null, string customId = null, ButtonStyle style = ButtonStyle.Primary, string url = null, IEmote emote = null, bool isDisabled = false) + { + CustomId = customId; + Style = style; + Url = url; + Label = label; + IsDisabled = isDisabled; + Emote = emote; + } + + /// + /// Creates a new instance of a from instance of a . + /// + public ButtonBuilder(ButtonComponent button) + { + CustomId = button.CustomId; + Style = button.Style; + Url = button.Url; + Label = button.Label; + IsDisabled = button.IsDisabled; + Emote = button.Emote; + } + + /// + /// Creates a button with the style. + /// + /// The label for this link button. + /// The url for this link button to go to. + /// The emote for this link button. + /// A builder with the newly created button. + public static ButtonBuilder CreateLinkButton(string label, string url, IEmote emote = null) + => new ButtonBuilder(label, null, ButtonStyle.Link, url, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this danger button. + /// The custom id for this danger button. + /// The emote for this danger button. + /// A builder with the newly created button. + public static ButtonBuilder CreateDangerButton(string label, string customId, IEmote emote = null) + => new ButtonBuilder(label, customId, ButtonStyle.Danger, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this primary button. + /// The custom id for this primary button. + /// The emote for this primary button. + /// A builder with the newly created button. + public static ButtonBuilder CreatePrimaryButton(string label, string customId, IEmote emote = null) + => new ButtonBuilder(label, customId, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this secondary button. + /// The custom id for this secondary button. + /// The emote for this secondary button. + /// A builder with the newly created button. + public static ButtonBuilder CreateSecondaryButton(string label, string customId, IEmote emote = null) + => new ButtonBuilder(label, customId, ButtonStyle.Secondary, emote: emote); + + /// + /// Creates a button with the style. + /// + /// The label for this success button. + /// The custom id for this success button. + /// The emote for this success button. + /// A builder with the newly created button. + public static ButtonBuilder CreateSuccessButton(string label, string customId, IEmote emote = null) + => new ButtonBuilder(label, customId, ButtonStyle.Success, emote: emote); + + /// + /// Sets the current buttons label to the specified text. + /// + /// The text for the label. + /// + /// The current builder. + public ButtonBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the current buttons style. + /// + /// The style for this builders button. + /// The current builder. + public ButtonBuilder WithStyle(ButtonStyle style) + { + Style = style; + return this; + } + + /// + /// Sets the current buttons emote. + /// + /// The emote to use for the current button. + /// The current builder. + public ButtonBuilder WithEmote(IEmote emote) + { + Emote = emote; + return this; + } + + /// + /// Sets the current buttons url. + /// + /// The url to use for the current button. + /// The current builder. + public ButtonBuilder WithUrl(string url) + { + Url = url; + return this; + } + + /// + /// Sets the custom id of the current button. + /// + /// The id to use for the current button. + /// + /// The current builder. + public ButtonBuilder WithCustomId(string id) + { + CustomId = id; + return this; + } + + /// + /// Sets whether the current button is disabled. + /// + /// Whether the current button is disabled or not. + /// The current builder. + public ButtonBuilder WithDisabled(bool isDisabled) + { + IsDisabled = isDisabled; + return this; + } + + /// + /// Builds this builder into a to be used in a . + /// + /// A to be used in a . + /// A button must contain either a or a , but not both. + /// A button must have an or a . + /// A link button must contain a URL. + /// A URL must include a protocol (http or https). + /// A non-link button must contain a custom id + public ButtonComponent Build() + { + if (string.IsNullOrEmpty(Label) && Emote == null) + throw new InvalidOperationException("A button must have an Emote or a label!"); + + if (!(string.IsNullOrEmpty(Url) ^ string.IsNullOrEmpty(CustomId))) + throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!"); + + if (Style == ButtonStyle.Link) + { + if (string.IsNullOrEmpty(Url)) + throw new InvalidOperationException("Link buttons must have a link associated with them"); + UrlValidation.ValidateButton(Url); + } + else if (string.IsNullOrEmpty(CustomId)) + throw new InvalidOperationException("Non-link buttons must have a custom id associated with them"); + + return new ButtonComponent(Style, Label, Emote, CustomId, Url, IsDisabled); + } + } + + /// + /// Represents a class used to build 's. + /// + public class SelectMenuBuilder + { + /// + /// The max length of a . + /// + public const int MaxPlaceholderLength = 100; + + /// + /// The maximum number of values for the and properties. + /// + public const int MaxValuesCount = 25; + + /// + /// The maximum number of options a can have. + /// + public const int MaxOptionCount = 25; + + /// + /// Gets or sets the custom id of the current select menu. + /// + /// length exceeds + /// length subceeds 1. + public string CustomId + { + get => _customId; + set => _customId = value?.Length switch + { + > ComponentBuilder.MaxCustomIdLength => throw new ArgumentOutOfRangeException(nameof(value), $"Custom Id length must be less or equal to {ComponentBuilder.MaxCustomIdLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Custom Id length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the placeholder text of the current select menu. + /// + /// length exceeds . + /// length subceeds 1. + public string Placeholder + { + get => _placeholder; + set => _placeholder = value?.Length switch + { + > MaxPlaceholderLength => throw new ArgumentOutOfRangeException(nameof(value), $"Placeholder length must be less or equal to {MaxPlaceholderLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Placeholder length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the minimum values of the current select menu. + /// + /// exceeds . + public int MinValues + { + get => _minValues; + set + { + Preconditions.AtMost(value, MaxValuesCount, nameof(MinValues)); + _minValues = value; + } + } + + /// + /// Gets or sets the maximum values of the current select menu. + /// + /// exceeds . + public int MaxValues + { + get => _maxValues; + set + { + Preconditions.AtMost(value, MaxValuesCount, nameof(MaxValues)); + _maxValues = value; + } + } + + /// + /// Gets or sets a collection of for this current select menu. + /// + /// count exceeds . + /// is null. + public List Options + { + get => _options; + set + { + if (value != null) + Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); + else + throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null."); + + _options = value; + } + } + + /// + /// Gets or sets whether the current menu is disabled. + /// + public bool IsDisabled { get; set; } + + private List _options = new List(); + private int _minValues = 1; + private int _maxValues = 1; + private string _placeholder; + private string _customId; + + /// + /// Creates a new instance of a . + /// + public SelectMenuBuilder() { } + + /// + /// Creates a new instance of a from instance of . + /// + public SelectMenuBuilder(SelectMenuComponent selectMenu) + { + Placeholder = selectMenu.Placeholder; + CustomId = selectMenu.Placeholder; + MaxValues = selectMenu.MaxValues; + MinValues = selectMenu.MinValues; + IsDisabled = selectMenu.IsDisabled; + Options = selectMenu.Options? + .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) + .ToList(); + } + + /// + /// Creates a new instance of a . + /// + /// The custom id of this select menu. + /// The options for this select menu. + /// The placeholder of this select menu. + /// The max values of this select menu. + /// The min values of this select menu. + /// Disabled this select menu or not. + public SelectMenuBuilder(string customId, List options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) + { + CustomId = customId; + Options = options; + Placeholder = placeholder; + IsDisabled = isDisabled; + MaxValues = maxValues; + MinValues = minValues; + } + + /// + /// Sets the field CustomId. + /// + /// The value to set the field CustomId to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithCustomId(string customId) + { + CustomId = customId; + return this; + } + + /// + /// Sets the field placeholder. + /// + /// The value to set the field placeholder to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithPlaceholder(string placeholder) + { + Placeholder = placeholder; + return this; + } + + /// + /// Sets the field minValues. + /// + /// The value to set the field minValues to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithMinValues(int minValues) + { + MinValues = minValues; + return this; + } + + /// + /// Sets the field maxValues. + /// + /// The value to set the field maxValues to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithMaxValues(int maxValues) + { + MaxValues = maxValues; + return this; + } + + /// + /// Sets the field options. + /// + /// The value to set the field options to. + /// + /// + /// The current builder. + /// + public SelectMenuBuilder WithOptions(List options) + { + Options = options; + return this; + } + + /// + /// Add one option to menu options. + /// + /// The option builder class containing the option properties. + /// Options count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddOption(SelectMenuOptionBuilder option) + { + if (Options.Count >= MaxOptionCount) + throw new InvalidOperationException($"Options count reached {MaxOptionCount}."); + + Options.Add(option); + return this; + } + + /// + /// Add one option to menu options. + /// + /// The label for this option. + /// The value of this option. + /// The description of this option. + /// The emote of this option. + /// Render this option as selected by default or not. + /// Options count reached . + /// + /// The current builder. + /// + public SelectMenuBuilder AddOption(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) + { + AddOption(new SelectMenuOptionBuilder(label, value, description, emote, isDefault)); + return this; + } + + /// + /// Sets whether the current menu is disabled. + /// + /// Whether the current menu is disabled or not. + /// + /// The current builder. + /// + public SelectMenuBuilder WithDisabled(bool isDisabled) + { + IsDisabled = isDisabled; + return this; + } + + /// + /// Builds a + /// + /// The newly built + public SelectMenuComponent Build() + { + var options = Options?.Select(x => x.Build()).ToList(); + + return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled); + } + } + + /// + /// Represents a class used to build 's. + /// + public class SelectMenuOptionBuilder + { + /// + /// The maximum length of a . + /// + public const int MaxSelectLabelLength = 100; + + /// + /// The maximum length of a . + /// + public const int MaxDescriptionLength = 100; + + /// + /// The maximum length of a . + /// + public const int MaxSelectValueLength = 100; + + /// + /// Gets or sets the label of the current select menu. + /// + /// length exceeds + /// length subceeds 1. + public string Label + { + get => _label; + set => _label = value?.Length switch + { + > MaxSelectLabelLength => throw new ArgumentOutOfRangeException(nameof(value), $"Label length must be less or equal to {MaxSelectLabelLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Label length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the value of the current select menu. + /// + /// length exceeds . + /// length subceeds 1. + public string Value + { + get => _value; + set => _value = value?.Length switch + { + > MaxSelectValueLength => throw new ArgumentOutOfRangeException(nameof(value), $"Value length must be less or equal to {MaxSelectValueLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Value length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets this menu options description. + /// + /// length exceeds . + /// length subceeds 1. + public string Description + { + get => _description; + set => _description = value?.Length switch + { + > MaxDescriptionLength => throw new ArgumentOutOfRangeException(nameof(value), $"Description length must be less or equal to {MaxDescriptionLength}."), + 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), + _ => value + }; + } + + /// + /// Gets or sets the emote of this option. + /// + public IEmote Emote { get; set; } + + /// + /// Gets or sets the whether or not this option will render selected by default. + /// + public bool? IsDefault { get; set; } + + private string _label; + private string _value; + private string _description; + + /// + /// Creates a new instance of a . + /// + public SelectMenuOptionBuilder() { } + + /// + /// Creates a new instance of a . + /// + /// The label for this option. + /// The value of this option. + /// The description of this option. + /// The emote of this option. + /// Render this option as selected by default or not. + public SelectMenuOptionBuilder(string label, string value, string description = null, IEmote emote = null, bool? isDefault = null) + { + Label = label; + Value = value; + Description = description; + Emote = emote; + IsDefault = isDefault; + } + + /// + /// Creates a new instance of a from instance of a . + /// + public SelectMenuOptionBuilder(SelectMenuOption option) + { + Label = option.Label; + Value = option.Value; + Description = option.Description; + Emote = option.Emote; + IsDefault = option.IsDefault; + } + + /// + /// Sets the field label. + /// + /// The value to set the field label to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithLabel(string label) + { + Label = label; + return this; + } + + /// + /// Sets the field value. + /// + /// The value to set the field value to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithValue(string value) + { + Value = value; + return this; + } + + /// + /// Sets the field description. + /// + /// The value to set the field description to. + /// + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDescription(string description) + { + Description = description; + return this; + } + + /// + /// Sets the field emote. + /// + /// The value to set the field emote to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithEmote(IEmote emote) + { + Emote = emote; + return this; + } + + /// + /// Sets the field default. + /// + /// The value to set the field default to. + /// + /// The current builder. + /// + public SelectMenuOptionBuilder WithDefault(bool isDefault) + { + IsDefault = isDefault; + return this; + } + + /// + /// Builds a . + /// + /// The newly built . + public SelectMenuOption Build() + { + return new SelectMenuOption(Label, Value, Description, Emote, IsDefault); + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs new file mode 100644 index 000000000..70bc1f301 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Represents a type of a component. + /// + public enum ComponentType + { + /// + /// A container for other components. + /// + ActionRow = 1, + + /// + /// A clickable button. + /// + Button = 2, + + /// + /// A select menu for picking from choices. + /// + SelectMenu = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs new file mode 100644 index 000000000..2a46e8f18 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Represents an interaction type for Message Components. + /// + public interface IComponentInteraction : IDiscordInteraction + { + /// + /// Gets the data received with this interaction, contains the button that was clicked. + /// + new IComponentInteractionData Data { get; } + + /// + /// Gets the message that contained the trigger for this interaction. + /// + IUserMessage Message { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs new file mode 100644 index 000000000..99b9b6f6c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents the data sent with the . + /// + public interface IComponentInteractionData : IDiscordInteractionData + { + /// + /// Gets the components Custom Id that was clicked. + /// + string CustomId { get; } + + /// + /// Gets the type of the component clicked. + /// + ComponentType Type { get; } + + /// + /// Gets the value(s) of a interaction response. + /// + IReadOnlyCollection Values { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs new file mode 100644 index 000000000..9366a44d6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Represents a message component on a message. + /// + public interface IMessageComponent + { + /// + /// Gets the of this Message Component. + /// + ComponentType Type { get; } + + /// + /// Gets the custom id of the component if possible; otherwise . + /// + string CustomId { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs new file mode 100644 index 000000000..720588681 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a component object used to send components with messages. + /// + public class MessageComponent + { + /// + /// Gets the components to be used in a message. + /// + public IReadOnlyCollection Components { get; } + + internal MessageComponent(List components) + { + Components = components; + } + + /// + /// Returns a empty . + /// + internal static MessageComponent Empty + => new MessageComponent(new List()); + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs new file mode 100644 index 000000000..229c1e148 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Discord +{ + /// + /// Represents a select menu component defined at + /// + public class SelectMenuComponent : IMessageComponent + { + /// + public ComponentType Type => ComponentType.SelectMenu; + + /// + public string CustomId { get; } + + /// + /// Gets the menus options to select from. + /// + public IReadOnlyCollection Options { get; } + + /// + /// Gets the custom placeholder text if nothing is selected. + /// + public string Placeholder { get; } + + /// + /// Gets the minimum number of items that must be chosen. + /// + public int MinValues { get; } + + /// + /// Gets the maximum number of items that can be chosen. + /// + public int MaxValues { get; } + + /// + /// Gets whether this menu is disabled or not. + /// + public bool IsDisabled { get; } + + /// + /// Turns this select menu into a builder. + /// + /// + /// A newly create builder with the same properties as this select menu. + /// + public SelectMenuBuilder ToBuilder() + => new SelectMenuBuilder( + CustomId, + Options.Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)).ToList(), + Placeholder, + MaxValues, + MinValues, + IsDisabled); + + internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled) + { + CustomId = customId; + Options = options; + Placeholder = placeholder; + MinValues = minValues; + MaxValues = maxValues; + IsDisabled = disabled; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuOption.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuOption.cs new file mode 100644 index 000000000..6856e1ee3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuOption.cs @@ -0,0 +1,42 @@ +namespace Discord +{ + /// + /// Represents a choice for a . + /// + public class SelectMenuOption + { + /// + /// Gets the user-facing name of the option. + /// + public string Label { get; } + + /// + /// Gets the dev-define value of the option. + /// + public string Value { get; } + + /// + /// Gets a description of the option. + /// + public string Description { get; } + + /// + /// Gets the displayed with this menu option. + /// + public IEmote Emote { get; } + + /// + /// Gets whether or not this option will render as selected by default. + /// + public bool? IsDefault { get; } + + internal SelectMenuOption(string label, string value, string description, IEmote emote, bool? defaultValue) + { + Label = label; + Value = value; + Description = description; + Emote = emote; + IsDefault = defaultValue; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteraction.cs new file mode 100644 index 000000000..bb5343d84 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteraction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents a . + /// + public interface IAutocompleteInteraction : IDiscordInteraction + { + /// + /// Gets the autocomplete data of this interaction. + /// + new IAutocompleteInteractionData Data { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteractionData.cs new file mode 100644 index 000000000..e6d1e9fae --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/IAutocompleteInteractionData.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents data for a slash commands autocomplete interaction. + /// + public interface IAutocompleteInteractionData : IDiscordInteractionData + { + /// + /// Gets the name of the invoked command. + /// + string CommandName { get; } + + /// + /// Gets the id of the invoked command. + /// + ulong CommandId { get; } + + /// + /// Gets the type of the invoked command. + /// + ApplicationCommandType Type { get; } + + /// + /// Gets the version of the invoked command. + /// + ulong Version { get; } + + /// + /// Gets the current autocomplete option that is actively being filled out. + /// + AutocompleteOption Current { get; } + + /// + /// Gets a collection of all the other options the executing users has filled out. + /// + IReadOnlyCollection Options { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/ISlashCommandInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/ISlashCommandInteraction.cs new file mode 100644 index 000000000..556182987 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/ISlashCommandInteraction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents a slash command interaction. + /// + public interface ISlashCommandInteraction : IDiscordInteraction + { + /// + /// Gets the data associated with this interaction. + /// + new IApplicationCommandInteractionData Data { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs new file mode 100644 index 000000000..b4fc89cc2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Discord +{ + /// + /// Represents a class used to build slash commands. + /// + public class SlashCommandBuilder + { + /// + /// Returns the maximum length a commands name allowed by Discord + /// + public const int MaxNameLength = 32; + /// + /// Returns the maximum length of a commands description allowed by Discord. + /// + public const int MaxDescriptionLength = 100; + /// + /// Returns the maximum count of command options allowed by Discord + /// + public const int MaxOptionsCount = 25; + + /// + /// Gets or sets the name of this slash command. + /// + public string Name + { + get => _name; + set + { + Preconditions.NotNullOrEmpty(value, nameof(value)); + Preconditions.AtLeast(value.Length, 1, nameof(value)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(value)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value)); + + _name = value; + } + } + + /// + /// Gets or sets a 1-100 length description of this slash command + /// + public string Description + { + get => _description; + set + { + Preconditions.NotNullOrEmpty(value, nameof(Description)); + Preconditions.AtLeast(value.Length, 1, nameof(Description)); + Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); + + _description = value; + } + } + + /// + /// Gets or sets the options for this command. + /// + public List Options + { + get => _options; + set + { + Preconditions.AtMost(value?.Count ?? 0, MaxOptionsCount, nameof(value)); + _options = value; + } + } + + /// + /// Gets or sets whether the command is enabled by default when the app is added to a guild + /// + public bool IsDefaultPermission { get; set; } = true; + + private string _name; + private string _description; + private List _options; + + /// + /// Build the current builder into a class. + /// + /// A that can be used to create slash commands. + public SlashCommandProperties Build() + { + var props = new SlashCommandProperties + { + Name = Name, + Description = Description, + IsDefaultPermission = IsDefaultPermission, + }; + + if (Options != null && Options.Any()) + { + var options = new List(); + + Options.OrderByDescending(x => x.IsRequired ?? false).ToList().ForEach(x => options.Add(x.Build())); + + props.Options = options; + } + + return props; + } + + /// + /// Sets the field name. + /// + /// The value to set the field name to. + /// + /// The current builder. + /// + public SlashCommandBuilder WithName(string name) + { + Name = name; + return this; + } + + /// + /// Sets the description of the current command. + /// + /// The description of this command. + /// The current builder. + public SlashCommandBuilder WithDescription(string description) + { + Description = description; + return this; + } + + /// + /// Sets the default permission of the current command. + /// + /// The default permission value to set. + /// The current builder. + public SlashCommandBuilder WithDefaultPermission(bool value) + { + IsDefaultPermission = value; + return this; + } + + /// + /// Adds an option to the current slash command. + /// + /// The name of the option to add. + /// The type of this option. + /// The description of this option. + /// If this option is required for this command. + /// If this option is the default option. + /// If this option is set to autocomplete. + /// The options of the option to add. + /// The allowed channel types for this option. + /// The choices of this option. + /// The smallest number value the user can input. + /// The largest number value the user can input. + /// The current builder. + public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, + string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, + List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + { + // Make sure the name matches the requirements from discord + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + // same with description + Preconditions.NotNullOrEmpty(description, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); + + // make sure theres only one option with default set to true + if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true) + throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); + + var option = new SlashCommandOptionBuilder + { + Name = name, + Description = description, + IsRequired = isRequired, + IsDefault = isDefault, + Options = options, + Type = type, + IsAutocomplete = isAutocomplete, + Choices = (choices ?? Array.Empty()).ToList(), + ChannelTypes = channelTypes, + MinValue = minValue, + MaxValue = maxValue, + }; + + return AddOption(option); + } + + /// + /// Adds an option to this slash command. + /// + /// The option to add. + /// The current builder. + public SlashCommandBuilder AddOption(SlashCommandOptionBuilder option) + { + Options ??= new List(); + + if (Options.Count >= MaxOptionsCount) + throw new InvalidOperationException($"Cannot have more than {MaxOptionsCount} options!"); + + Preconditions.NotNull(option, nameof(option)); + + Options.Add(option); + return this; + } + /// + /// Adds a collection of options to the current slash command. + /// + /// The collection of options to add. + /// The current builder. + public SlashCommandBuilder AddOptions(params SlashCommandOptionBuilder[] options) + { + if (options == null) + throw new ArgumentNullException(nameof(options), "Options cannot be null!"); + + if (options.Length == 0) + throw new ArgumentException("Options cannot be empty!", nameof(options)); + + Options ??= new List(); + + if (Options.Count + options.Length > MaxOptionsCount) + throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!"); + + Options.AddRange(options); + return this; + } + } + + /// + /// Represents a class used to build options for the . + /// + public class SlashCommandOptionBuilder + { + /// + /// The max length of a choice's name allowed by Discord. + /// + public const int ChoiceNameMaxLength = 100; + + /// + /// The maximum number of choices allowed by Discord. + /// + public const int MaxChoiceCount = 25; + + private string _name; + private string _description; + + /// + /// Gets or sets the name of this option. + /// + public string Name + { + get => _name; + set + { + if (value != null) + { + Preconditions.AtLeast(value.Length, 1, nameof(value)); + Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value)); + if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value)); + } + + _name = value; + } + } + + /// + /// Gets or sets the description of this option. + /// + public string Description + { + get => _description; + set + { + if (value != null) + { + Preconditions.AtLeast(value.Length, 1, nameof(value)); + Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); + } + + _description = value; + } + } + + /// + /// Gets or sets the type of this option. + /// + public ApplicationCommandOptionType Type { get; set; } + + /// + /// Gets or sets whether or not this options is the first required option for the user to complete. only one option can be default. + /// + public bool? IsDefault { get; set; } + + /// + /// Gets or sets if the option is required. + /// + public bool? IsRequired { get; set; } = null; + + /// + /// Gets or sets whether or not this option supports autocomplete. + /// + public bool IsAutocomplete { get; set; } + + /// + /// Gets or sets the smallest number value the user can input. + /// + public double? MinValue { get; set; } + + /// + /// Gets or sets the largest number value the user can input. + /// + public double? MaxValue { get; set; } + + /// + /// Gets or sets the choices for string and int types for the user to pick from. + /// + public List Choices { get; set; } + + /// + /// Gets or sets if this option is a subcommand or subcommand group type, these nested options will be the parameters. + /// + public List Options { get; set; } + + /// + /// Gets or sets the allowed channel types for this option. + /// + public List ChannelTypes { get; set; } + + /// + /// Builds the current option. + /// + /// The built version of this option. + public ApplicationCommandOptionProperties Build() + { + bool isSubType = Type == ApplicationCommandOptionType.SubCommandGroup; + bool isIntType = Type == ApplicationCommandOptionType.Integer; + + if (isSubType && (Options == null || !Options.Any())) + throw new InvalidOperationException("SubCommands/SubCommandGroups must have at least one option"); + + if (!isSubType && Options != null && Options.Any() && Type != ApplicationCommandOptionType.SubCommand) + throw new InvalidOperationException($"Cannot have options on {Type} type"); + + if (isIntType && MinValue != null && MinValue % 1 != 0) + throw new InvalidOperationException("MinValue cannot have decimals on Integer command options."); + + if (isIntType && MaxValue != null && MaxValue % 1 != 0) + throw new InvalidOperationException("MaxValue cannot have decimals on Integer command options."); + + return new ApplicationCommandOptionProperties + { + Name = Name, + Description = Description, + IsDefault = IsDefault, + IsRequired = IsRequired, + Type = Type, + Options = Options?.Count > 0 + ? Options.OrderByDescending(x => x.IsRequired ?? false).Select(x => x.Build()).ToList() + : new List(), + Choices = Choices, + IsAutocomplete = IsAutocomplete, + ChannelTypes = ChannelTypes, + MinValue = MinValue, + MaxValue = MaxValue + }; + } + + /// + /// Adds an option to the current slash command. + /// + /// The name of the option to add. + /// The type of this option. + /// The description of this option. + /// If this option is required for this command. + /// If this option is the default option. + /// If this option supports autocomplete. + /// The options of the option to add. + /// The allowed channel types for this option. + /// The choices of this option. + /// The smallest number value the user can input. + /// The largest number value the user can input. + /// The current builder. + public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, + string description, bool? required = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, + List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + { + // Make sure the name matches the requirements from discord + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + // same with description + Preconditions.NotNullOrEmpty(description, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + + // make sure theres only one option with default set to true + if (isDefault && Options?.Any(x => x.IsDefault == true) == true) + throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); + + var option = new SlashCommandOptionBuilder + { + Name = name, + Description = description, + IsRequired = required, + IsDefault = isDefault, + IsAutocomplete = isAutocomplete, + MinValue = minValue, + MaxValue = maxValue, + Options = options, + Type = type, + Choices = (choices ?? Array.Empty()).ToList(), + ChannelTypes = channelTypes + }; + + return AddOption(option); + } + /// + /// Adds a sub option to the current option. + /// + /// The sub option to add. + /// The current builder. + public SlashCommandOptionBuilder AddOption(SlashCommandOptionBuilder option) + { + Options ??= new List(); + + if (Options.Count >= SlashCommandBuilder.MaxOptionsCount) + throw new InvalidOperationException($"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); + + Preconditions.NotNull(option, nameof(option)); + + Options.Add(option); + return this; + } + + /// + /// Adds a choice to the current option. + /// + /// The name of the choice. + /// The value of the choice. + /// The current builder. + public SlashCommandOptionBuilder AddChoice(string name, int value) + { + return AddChoiceInternal(name, value); + } + + /// + /// Adds a choice to the current option. + /// + /// The name of the choice. + /// The value of the choice. + /// The current builder. + public SlashCommandOptionBuilder AddChoice(string name, string value) + { + return AddChoiceInternal(name, value); + } + + /// + /// Adds a choice to the current option. + /// + /// The name of the choice. + /// The value of the choice. + /// The current builder. + public SlashCommandOptionBuilder AddChoice(string name, double value) + { + return AddChoiceInternal(name, value); + } + + /// + /// Adds a choice to the current option. + /// + /// The name of the choice. + /// The value of the choice. + /// The current builder. + public SlashCommandOptionBuilder AddChoice(string name, float value) + { + return AddChoiceInternal(name, value); + } + + /// + /// Adds a choice to the current option. + /// + /// The name of the choice. + /// The value of the choice. + /// The current builder. + public SlashCommandOptionBuilder AddChoice(string name, long value) + { + return AddChoiceInternal(name, value); + } + + private SlashCommandOptionBuilder AddChoiceInternal(string name, object value) + { + Choices ??= new List(); + + if (Choices.Count >= MaxChoiceCount) + throw new InvalidOperationException($"Cannot add more than {MaxChoiceCount} choices!"); + + Preconditions.NotNull(name, nameof(name)); + Preconditions.NotNull(value, nameof(value)); + + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, 100, nameof(name)); + + if(value is string str) + { + Preconditions.AtLeast(str.Length, 1, nameof(value)); + Preconditions.AtMost(str.Length, 100, nameof(value)); + } + + Choices.Add(new ApplicationCommandOptionChoiceProperties + { + Name = name, + Value = value + }); + + return this; + } + + /// + /// Adds a channel type to the current option. + /// + /// The to add. + /// The current builder. + public SlashCommandOptionBuilder AddChannelType(ChannelType channelType) + { + ChannelTypes ??= new List(); + + ChannelTypes.Add(channelType); + + return this; + } + + /// + /// Sets the current builders name. + /// + /// The name to set the current option builder. + /// The current builder. + public SlashCommandOptionBuilder WithName(string name) + { + Name = name; + + return this; + } + + /// + /// Sets the current builders description. + /// + /// The description to set. + /// The current builder. + public SlashCommandOptionBuilder WithDescription(string description) + { + Description = description; + return this; + } + + /// + /// Sets the current builders required field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithRequired(bool value) + { + IsRequired = value; + return this; + } + + /// + /// Sets the current builders default field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithDefault(bool value) + { + IsDefault = value; + return this; + } + + /// + /// Sets the current builders autocomplete field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithAutocomplete(bool value) + { + IsAutocomplete = value; + return this; + } + + /// + /// Sets the current builders min value field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithMinValue(double value) + { + MinValue = value; + return this; + } + + /// + /// Sets the current builders max value field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithMaxValue(double value) + { + MaxValue = value; + return this; + } + + /// + /// Sets the current type of this builder. + /// + /// The type to set. + /// The current builder. + public SlashCommandOptionBuilder WithType(ApplicationCommandOptionType type) + { + Type = type; + return this; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandProperties.cs new file mode 100644 index 000000000..20ba2868f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandProperties.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a class used to create slash commands. + /// + public class SlashCommandProperties : ApplicationCommandProperties + { + internal override ApplicationCommandType Type => ApplicationCommandType.Slash; + + /// + /// Gets or sets the discription of this command. + /// + public Optional Description { get; set; } + + /// + /// Gets or sets the options for this command. + /// + public Optional> Options { get; set; } + + internal SlashCommandProperties() { } + } +} diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 1ab26de8f..47ffffacb 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -1,30 +1,108 @@ namespace Discord { + /// + /// Represents a generic invite object. + /// public interface IInvite : IEntity, IDeletable { - /// Gets the unique identifier for this invite. + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// string Code { get; } - /// Gets the url used to accept this invite, using Code. + /// + /// Gets the URL used to accept this invite using . + /// + /// + /// A string containing the full invite URL (e.g. https://discord.gg/FTqNnyS). + /// string Url { get; } - /// Gets the channel this invite is linked to. + /// + /// Gets the user that created this invite. + /// + /// + /// A user that created this invite. + /// + IUser Inviter { get; } + /// + /// Gets the channel this invite is linked to. + /// + /// + /// A generic channel that the invite points to. + /// IChannel Channel { get; } - /// Gets the type of the channel this invite is linked to. + /// + /// Gets the type of the channel this invite is linked to. + /// ChannelType ChannelType { get; } - /// Gets the id of the channel this invite is linked to. + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// An representing the channel snowflake identifier that the invite points to. + /// ulong ChannelId { get; } - /// Gets the name of the channel this invite is linked to. + /// + /// Gets the name of the channel this invite is linked to. + /// + /// + /// A string containing the name of the channel that the invite points to. + /// string ChannelName { get; } - /// Gets the guild this invite is linked to. + /// + /// Gets the guild this invite is linked to. + /// + /// + /// A guild object representing the guild that the invite points to. + /// IGuild Guild { get; } - /// Gets the id of the guild this invite is linked to. + /// + /// Gets the ID of the guild this invite is linked to. + /// + /// + /// An representing the guild snowflake identifier that the invite points to. + /// ulong? GuildId { get; } - /// Gets the name of the guild this invite is linked to. + /// + /// Gets the name of the guild this invite is linked to. + /// + /// + /// A string containing the name of the guild that the invite points to. + /// string GuildName { get; } - /// Gets the approximated count of online members in the guild. + /// + /// Gets the approximated count of online members in the guild. + /// + /// + /// An representing the approximated online member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// int? PresenceCount { get; } - /// Gets the approximated count of total members in the guild. + /// + /// Gets the approximated count of total members in the guild. + /// + /// + /// An representing the approximated total member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// int? MemberCount { get; } + /// + /// Gets the user this invite is linked to via . + /// + /// + /// A user that is linked to this invite. + /// + IUser TargetUser { get; } + /// + /// Gets the type of the linked for this invite. + /// + /// + /// The type of the linked user that is linked to this invite. + /// + TargetUserType TargetUserType { get; } } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index 0e026ab62..c2580c853 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -2,21 +2,48 @@ using System; namespace Discord { + /// + /// Represents additional information regarding the generic invite object. + /// public interface IInviteMetadata : IInvite { - /// Gets the user that created this invite. - IUser Inviter { get; } - /// Returns true if this invite was revoked. - bool IsRevoked { get; } - /// Returns true if users accepting this invite will be removed from the guild when they log off. + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// bool IsTemporary { get; } - /// Gets the time (in seconds) until the invite expires, or null if it never expires. + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite never expires. + /// int? MaxAge { get; } - /// Gets the max amount of times this invite may be used, or null if there is no limit. + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// int? MaxUses { get; } - /// Gets the amount of times this invite has been used. + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// int? Uses { get; } - /// Gets when this invite was created. + /// + /// Gets when this invite was created. + /// + /// + /// A representing the time of which the invite was first created. + /// DateTimeOffset? CreatedAt { get; } } } diff --git a/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs b/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs new file mode 100644 index 000000000..e1818d7a9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + public enum TargetUserType + { + /// + /// The invite whose target user type is not defined. + /// + Undefined = 0, + /// + /// The invite is for a Go Live stream. + /// + Stream = 1, + /// + /// The invite is for embedded application. + /// + EmbeddedApplication = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs new file mode 100644 index 000000000..ecd872d83 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs @@ -0,0 +1,34 @@ +using System; + +namespace Discord +{ + /// + /// Specifies the type of mentions that will be notified from the message content. + /// + [Flags] + public enum AllowedMentionTypes + { + /// + /// No flag is set. + /// + /// + /// This flag is not used to control mentions. + /// + /// It will always be present and does not mean mentions will not be allowed. + /// + /// + None = 0, + /// + /// Controls role mentions. + /// + Roles = 1, + /// + /// Controls user mentions. + /// + Users = 2, + /// + /// Controls @everyone and @here mentions. + /// + Everyone = 4, + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..0206ad7b1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Defines which mentions and types of mentions that will notify users from the message content. + /// + public class AllowedMentions + { + private static readonly Lazy none = new Lazy(() => new AllowedMentions()); + private static readonly Lazy all = new Lazy(() => + new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles)); + + /// + /// Gets a value which indicates that no mentions in the message content should notify users. + /// + public static AllowedMentions None => none.Value; + + /// + /// Gets a value which indicates that all mentions in the message content should notify users. + /// + public static AllowedMentions All => all.Value; + + /// + /// Gets or sets the type of mentions that will be parsed from the message content. + /// + /// + /// The flag is mutually exclusive with the + /// property, and the flag is mutually exclusive with the + /// property. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentionTypes? AllowedTypes { get; set; } + + /// + /// Gets or sets the list of all role ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List RoleIds { get; set; } = new List(); + + /// + /// Gets or sets the list of all user ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List UserIds { get; set; } = new List(); + + /// + /// Gets or sets whether to mention the author of the message you are replying to or not. + /// + /// + /// Specifically for inline replies. + /// + public bool? MentionRepliedUser { get; set; } = null; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The types of mentions to parse from the message content. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentions(AllowedMentionTypes? allowedTypes = null) + { + AllowedTypes = allowedTypes; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index dc62066eb..7fa6f6f36 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -5,22 +5,38 @@ using System.Linq; namespace Discord { + /// + /// Represents an embed object seen in an . + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Embed : IEmbed { + /// public EmbedType Type { get; } + /// public string Description { get; internal set; } + /// public string Url { get; internal set; } + /// public string Title { get; internal set; } + /// public DateTimeOffset? Timestamp { get; internal set; } + /// public Color? Color { get; internal set; } + /// public EmbedImage? Image { get; internal set; } + /// public EmbedVideo? Video { get; internal set; } + /// public EmbedAuthor? Author { get; internal set; } + /// public EmbedFooter? Footer { get; internal set; } + /// public EmbedProvider? Provider { get; internal set; } + /// public EmbedThumbnail? Thumbnail { get; internal set; } + /// public ImmutableArray Fields { get; internal set; } internal Embed(EmbedType type) @@ -57,6 +73,9 @@ namespace Discord Fields = fields; } + /// + /// Gets the total length of all embed properties. + /// public int Length { get @@ -70,6 +89,9 @@ namespace Discord } } + /// + /// Gets the title of the embed. + /// public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index c59473704..3b11f6a8b 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,14 +1,28 @@ -using System; using System.Diagnostics; namespace Discord { + /// + /// A author field of an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedAuthor { + /// + /// Gets the name of the author field. + /// public string Name { get; internal set; } + /// + /// Gets the URL of the author field. + /// public string Url { get; internal set; } + /// + /// Gets the icon URL of the author field. + /// public string IconUrl { get; internal set; } + /// + /// Gets the proxified icon URL of the author field. + /// public string ProxyIconUrl { get; internal set; } internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) @@ -20,6 +34,12 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Url})"; + /// + /// Gets the name of the author field. + /// + /// + /// + /// public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 82e94e39f..0304120f5 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -2,28 +2,48 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Discord.Utils; namespace Discord { + /// + /// Represents a builder class for creating a . + /// public class EmbedBuilder { private string _title; private string _description; - private string _url; private EmbedImage? _image; private EmbedThumbnail? _thumbnail; private List _fields; + /// + /// Returns the maximum number of fields allowed by Discord. + /// public const int MaxFieldCount = 25; + /// + /// Returns the maximum length of title allowed by Discord. + /// public const int MaxTitleLength = 256; - public const int MaxDescriptionLength = 2048; + /// + /// Returns the maximum length of description allowed by Discord. + /// + public const int MaxDescriptionLength = 4096; + /// + /// Returns the maximum length of total characters allowed by Discord. + /// public const int MaxEmbedLength = 6000; + /// Initializes a new class. public EmbedBuilder() { Fields = new List(); } + /// Gets or sets the title of an . + /// Title length exceeds . + /// + /// The title of the embed. public string Title { get => _title; @@ -33,6 +53,10 @@ namespace Discord _title = value; } } + + /// Gets or sets the description of an . + /// Description length exceeds . + /// The description of the embed. public string Description { get => _description; @@ -43,49 +67,80 @@ namespace Discord } } - public string Url - { - get => _url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(Url)); - _url = value; - } - } + /// Gets or sets the URL of an . + /// Url is not a well-formed . + /// The URL of the embed. + public string Url { get; set; } + /// Gets or sets the thumbnail URL of an . + /// Url is not a well-formed . + /// The thumbnail URL of the embed. public string ThumbnailUrl { get => _thumbnail?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(ThumbnailUrl)); - _thumbnail = new EmbedThumbnail(value, null, null, null); - } + set => _thumbnail = new EmbedThumbnail(value, null, null, null); } + /// Gets or sets the image URL of an . + /// Url is not a well-formed . + /// The image URL of the embed. public string ImageUrl { get => _image?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(ImageUrl)); - _image = new EmbedImage(value, null, null, null); - } + set => _image = new EmbedImage(value, null, null, null); } + + /// Gets or sets the list of of an . + /// An embed builder's fields collection is set to + /// null. + /// Fields count exceeds . + /// + /// The list of existing . public List Fields { get => _fields; set { - if (value == null) throw new ArgumentNullException(paramName: nameof(Fields), message: "Cannot set an embed builder's fields collection to null"); + if (value == null) throw new ArgumentNullException(paramName: nameof(Fields), message: "Cannot set an embed builder's fields collection to null."); if (value.Count > MaxFieldCount) throw new ArgumentException(message: $"Field count must be less than or equal to {MaxFieldCount}.", paramName: nameof(Fields)); _fields = value; } } + /// + /// Gets or sets the timestamp of an . + /// + /// + /// The timestamp of the embed, or null if none is set. + /// public DateTimeOffset? Timestamp { get; set; } + /// + /// Gets or sets the sidebar color of an . + /// + /// + /// The color of the embed, or null if none is set. + /// public Color? Color { get; set; } + /// + /// Gets or sets the of an . + /// + /// + /// The author field builder of the embed, or null if none is set. + /// public EmbedAuthorBuilder Author { get; set; } + /// + /// Gets or sets the of an . + /// + /// + /// The footer field builder of the embed, or null if none is set. + /// public EmbedFooterBuilder Footer { get; set; } + /// + /// Gets the total length of all embed properties. + /// + /// + /// The combined length of , , , + /// , , and . + /// public int Length { get @@ -100,52 +155,121 @@ namespace Discord } } + /// + /// Sets the title of an . + /// + /// The title to be set. + /// + /// The current builder. + /// public EmbedBuilder WithTitle(string title) { Title = title; return this; } + /// + /// Sets the description of an . + /// + /// The description to be set. + /// + /// The current builder. + /// public EmbedBuilder WithDescription(string description) { Description = description; return this; } + /// + /// Sets the URL of an . + /// + /// The URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithUrl(string url) { Url = url; return this; } + /// + /// Sets the thumbnail URL of an . + /// + /// The thumbnail URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) { ThumbnailUrl = thumbnailUrl; return this; } + /// + /// Sets the image URL of an . + /// + /// The image URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithImageUrl(string imageUrl) { ImageUrl = imageUrl; return this; } + /// + /// Sets the timestamp of an to the current time. + /// + /// + /// The current builder. + /// public EmbedBuilder WithCurrentTimestamp() { Timestamp = DateTimeOffset.UtcNow; return this; } + /// + /// Sets the timestamp of an . + /// + /// The timestamp to be set. + /// + /// The current builder. + /// public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) { Timestamp = dateTimeOffset; return this; } + /// + /// Sets the sidebar color of an . + /// + /// The color to be set. + /// + /// The current builder. + /// public EmbedBuilder WithColor(Color color) { Color = color; return this; } + /// + /// Sets the of an . + /// + /// The author builder class containing the author field properties. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) { Author = author; return this; } + /// + /// Sets the author field of an with the provided properties. + /// + /// The delegate containing the author field properties. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(Action action) { var author = new EmbedAuthorBuilder(); @@ -153,6 +277,15 @@ namespace Discord Author = author; return this; } + /// + /// Sets the author field of an with the provided name, icon URL, and URL. + /// + /// The title of the author field. + /// The icon URL of the author field. + /// The URL of the author field. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) { var author = new EmbedAuthorBuilder @@ -164,11 +297,25 @@ namespace Discord Author = author; return this; } + /// + /// Sets the of an . + /// + /// The footer builder class containing the footer field properties. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(EmbedFooterBuilder footer) { Footer = footer; return this; } + /// + /// Sets the footer field of an with the provided properties. + /// + /// The delegate containing the footer field properties. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(Action action) { var footer = new EmbedFooterBuilder(); @@ -176,6 +323,14 @@ namespace Discord Footer = footer; return this; } + /// + /// Sets the footer field of an with the provided name, icon URL. + /// + /// The title of the footer field. + /// The icon URL of the footer field. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(string text, string iconUrl = null) { var footer = new EmbedFooterBuilder @@ -187,6 +342,15 @@ namespace Discord return this; } + /// + /// Adds an field with the provided name and value. + /// + /// The title of the field. + /// The value of the field. + /// Indicates whether the field is in-line or not. + /// + /// The current builder. + /// public EmbedBuilder AddField(string name, object value, bool inline = false) { var field = new EmbedFieldBuilder() @@ -196,6 +360,16 @@ namespace Discord AddField(field); return this; } + + /// + /// Adds a field with the provided to an + /// . + /// + /// The field builder class containing the field properties. + /// Field count exceeds . + /// + /// The current builder. + /// public EmbedBuilder AddField(EmbedFieldBuilder field) { if (Fields.Count >= MaxFieldCount) @@ -206,6 +380,13 @@ namespace Discord Fields.Add(field); return this; } + /// + /// Adds an field with the provided properties. + /// + /// The delegate containing the field properties. + /// + /// The current builder. + /// public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); @@ -214,11 +395,36 @@ namespace Discord return this; } + /// + /// Builds the into a Rich Embed ready to be sent. + /// + /// + /// The built embed object. + /// + /// Total embed length exceeds . + /// Any Url must include its protocols (i.e http:// or https://). public Embed Build() { if (Length > MaxEmbedLength) - throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); - + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}."); + if (!string.IsNullOrEmpty(Url)) + UrlValidation.Validate(Url, true); + if (!string.IsNullOrEmpty(ThumbnailUrl)) + UrlValidation.Validate(ThumbnailUrl, true); + if (!string.IsNullOrEmpty(ImageUrl)) + UrlValidation.Validate(ImageUrl, true); + if (Author != null) + { + if (!string.IsNullOrEmpty(Author.Url)) + UrlValidation.Validate(Author.Url, true); + if (!string.IsNullOrEmpty(Author.IconUrl)) + UrlValidation.Validate(Author.IconUrl, true); + } + if(Footer != null) + { + if (!string.IsNullOrEmpty(Footer.IconUrl)) + UrlValidation.Validate(Footer.IconUrl, true); + } var fields = ImmutableArray.CreateBuilder(Fields.Count); for (int i = 0; i < Fields.Count; i++) fields.Add(Fields[i].Build()); @@ -227,13 +433,33 @@ namespace Discord } } + /// + /// Represents a builder class for an embed field. + /// public class EmbedFieldBuilder { private string _name; private string _value; + /// + /// Gets the maximum field length for name allowed by Discord. + /// public const int MaxFieldNameLength = 256; + /// + /// Gets the maximum field length for value allowed by Discord. + /// public const int MaxFieldValueLength = 1024; + /// + /// Gets or sets the field name. + /// + /// + /// Field name is null, empty or entirely whitespace. + /// - or - + /// Field name length exceeds . + /// + /// + /// The name of the field. + /// public string Name { get => _name; @@ -245,46 +471,104 @@ namespace Discord } } + /// + /// Gets or sets the field value. + /// + /// + /// Field value is null, empty or entirely whitespace. + /// - or - + /// Field value length exceeds . + /// + /// + /// The value of the field. + /// public object Value { get => _value; set { var stringValue = value?.ToString(); - if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException(message: "Field value must not be null or empty.", paramName: nameof(Value)); + if (string.IsNullOrWhiteSpace(stringValue)) throw new ArgumentException(message: "Field value must not be null or empty.", paramName: nameof(Value)); if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException(message: $"Field value length must be less than or equal to {MaxFieldValueLength}.", paramName: nameof(Value)); _value = stringValue; } } + /// + /// Gets or sets a value that indicates whether the field should be in-line with each other. + /// public bool IsInline { get; set; } + /// + /// Sets the field name. + /// + /// The name to set the field name to. + /// + /// The current builder. + /// public EmbedFieldBuilder WithName(string name) { Name = name; return this; } + /// + /// Sets the field value. + /// + /// The value to set the field value to. + /// + /// The current builder. + /// public EmbedFieldBuilder WithValue(object value) { Value = value; return this; } + /// + /// Determines whether the field should be in-line with each other. + /// + /// + /// The current builder. + /// public EmbedFieldBuilder WithIsInline(bool isInline) { IsInline = isInline; return this; } + /// + /// Builds the field builder into a class. + /// + /// + /// The current builder. + /// + /// + /// or is null, empty or entirely whitespace. + /// - or - + /// or exceeds the maximum length allowed by Discord. + /// public EmbedField Build() => new EmbedField(Name, Value.ToString(), IsInline); } + /// + /// Represents a builder class for a author field. + /// public class EmbedAuthorBuilder { private string _name; - private string _url; - private string _iconUrl; + /// + /// Gets the maximum author name length allowed by Discord. + /// public const int MaxAuthorNameLength = 256; + /// + /// Gets or sets the author name. + /// + /// + /// Author name length is longer than . + /// + /// + /// The author name. + /// public string Name { get => _name; @@ -294,52 +578,98 @@ namespace Discord _name = value; } } - public string Url - { - get => _url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(Url)); - _url = value; - } - } - public string IconUrl - { - get => _iconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(IconUrl)); - _iconUrl = value; - } - } + /// + /// Gets or sets the URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The URL of the author field. + /// + public string Url { get; set; } + /// + /// Gets or sets the icon URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the author field. + /// + public string IconUrl { get; set; } + /// + /// Sets the name of the author field. + /// + /// The name of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithName(string name) { Name = name; return this; } + /// + /// Sets the URL of the author field. + /// + /// The URL of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithUrl(string url) { Url = url; return this; } + /// + /// Sets the icon URL of the author field. + /// + /// The icon URL of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; } + /// + /// Builds the author field to be used. + /// + /// + /// Author name length is longer than . + /// - or - + /// is not a well-formed . + /// - or - + /// is not a well-formed . + /// + /// + /// The built author field. + /// public EmbedAuthor Build() => new EmbedAuthor(Name, Url, IconUrl, null); } + /// + /// Represents a builder class for an embed footer. + /// public class EmbedFooterBuilder { private string _text; - private string _iconUrl; + /// + /// Gets the maximum footer length allowed by Discord. + /// public const int MaxFooterTextLength = 2048; + /// + /// Gets or sets the footer text. + /// + /// + /// Author name length is longer than . + /// + /// + /// The footer text. + /// public string Text { get => _text; @@ -349,27 +679,52 @@ namespace Discord _text = value; } } - public string IconUrl - { - get => _iconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI", paramName: nameof(IconUrl)); - _iconUrl = value; - } - } + /// + /// Gets or sets the icon URL of the footer field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the footer field. + /// + public string IconUrl { get; set; } + /// + /// Sets the name of the footer field. + /// + /// The text of the footer field. + /// + /// The current builder. + /// public EmbedFooterBuilder WithText(string text) { Text = text; return this; } + /// + /// Sets the icon URL of the footer field. + /// + /// The icon URL of the footer field. + /// + /// The current builder. + /// public EmbedFooterBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; } + /// + /// Builds the footer field to be used. + /// + /// + /// + /// length is longer than . + /// - or - + /// is not a well-formed . + /// + /// + /// A built footer field. + /// public EmbedFooter Build() => new EmbedFooter(Text, IconUrl, null); } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index f7c1f8348..f6aa2af3b 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,12 +1,24 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// A field for an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedField { + /// + /// Gets the name of the field. + /// public string Name { get; internal set; } + /// + /// Gets the value of the field. + /// public string Value { get; internal set; } + /// + /// Gets a value that indicates whether the field should be in-line with each other. + /// public bool Inline { get; internal set; } internal EmbedField(string name, string value, bool inline) @@ -17,6 +29,12 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Value}"; + /// + /// Gets the name of the field. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 29d85cd90..4c507d017 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,14 +1,32 @@ -using System; using System.Diagnostics; namespace Discord { + /// A footer field for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedFooter { - public string Text { get; internal set; } - public string IconUrl { get; internal set; } - public string ProxyUrl { get; internal set; } + /// + /// Gets the text of the footer field. + /// + /// + /// A string containing the text of the footer field. + /// + public string Text { get; } + /// + /// Gets the URL of the footer icon. + /// + /// + /// A string containing the URL of the footer icon. + /// + public string IconUrl { get; } + /// + /// Gets the proxied URL of the footer icon link. + /// + /// + /// A string containing the proxied URL of the footer icon. + /// + public string ProxyUrl { get; } internal EmbedFooter(string text, string iconUrl, string proxyUrl) { @@ -18,6 +36,12 @@ namespace Discord } private string DebuggerDisplay => $"{Text} ({IconUrl})"; + /// + /// Gets the text of the footer field. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Text; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index e12ffc53f..9ce2bfe73 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,14 +1,40 @@ -using System; using System.Diagnostics; namespace Discord { + /// An image for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedImage { + /// + /// Gets the URL of the image. + /// + /// + /// A string containing the URL of the image. + /// public string Url { get; } + /// + /// Gets a proxied URL of this image. + /// + /// + /// A string containing the proxied URL of this image. + /// public string ProxyUrl { get; } + /// + /// Gets the height of this image. + /// + /// + /// A representing the height of this image if it can be retrieved; otherwise + /// null. + /// public int? Height { get; } + /// + /// Gets the width of this image. + /// + /// + /// A representing the width of this image if it can be retrieved; otherwise + /// null. + /// public int? Width { get; } internal EmbedImage(string url, string proxyUrl, int? height, int? width) @@ -20,6 +46,12 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 24722b158..960fb3d78 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,12 +1,24 @@ -using System; using System.Diagnostics; namespace Discord { + /// A provider field for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedProvider { + /// + /// Gets the name of the provider. + /// + /// + /// A string representing the name of the provider. + /// public string Name { get; } + /// + /// Gets the URL of the provider. + /// + /// + /// A string representing the link to the provider. + /// public string Url { get; } internal EmbedProvider(string name, string url) @@ -16,6 +28,12 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Url})"; + /// + /// Gets the name of the provider. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 9b3d6153a..7f7b582dc 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,14 +1,40 @@ -using System; using System.Diagnostics; namespace Discord { + /// A thumbnail featured in an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedThumbnail { + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string containing the URL of the thumbnail. + /// public string Url { get; } + /// + /// Gets a proxied URL of this thumbnail. + /// + /// + /// A string containing the proxied URL of this thumbnail. + /// public string ProxyUrl { get; } + /// + /// Gets the height of this thumbnail. + /// + /// + /// A representing the height of this thumbnail if it can be retrieved; otherwise + /// null. + /// public int? Height { get; } + /// + /// Gets the width of this thumbnail. + /// + /// + /// A representing the width of this thumbnail if it can be retrieved; otherwise + /// null. + /// public int? Width { get; } internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) @@ -20,6 +46,12 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index 5bb2653e2..978f45bc2 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -1,15 +1,45 @@ namespace Discord { + /// + /// Specifies the type of embed. + /// public enum EmbedType { + /// + /// An unknown embed type. + /// Unknown = -1, + /// + /// A rich embed type. + /// Rich, + /// + /// A link embed type. + /// Link, + /// + /// A video embed type. + /// Video, + /// + /// An image embed type. + /// Image, + /// + /// A GIFV embed type. + /// Gifv, + /// + /// An article embed type. + /// Article, + /// + /// A tweet embed type. + /// Tweet, + /// + /// A HTML embed type. + /// Html, } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index 5725e0e14..ca0300e80 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,13 +1,35 @@ -using System; using System.Diagnostics; namespace Discord { + /// + /// A video featured in an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedVideo { + /// + /// Gets the URL of the video. + /// + /// + /// A string containing the URL of the image. + /// public string Url { get; } + /// + /// Gets the height of the video. + /// + /// + /// A representing the height of this video if it can be retrieved; otherwise + /// null. + /// public int? Height { get; } + /// + /// Gets the weight of the video. + /// + /// + /// A representing the width of this video if it can be retrieved; otherwise + /// null. + /// public int? Width { get; } internal EmbedVideo(string url, int? height, int? width) @@ -18,6 +40,12 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the video. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs new file mode 100644 index 000000000..dc5437861 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public struct FileAttachment : IDisposable + { + public string FileName { get; set; } + public string Description { get; set; } + public bool IsSpoiler { get; set; } + +#pragma warning disable IDISP008 + public Stream Stream { get; } +#pragma warning restore IDISP008 + + private bool _isDisposed; + + /// + /// Creates a file attachment from a stream. + /// + /// The stream to create the attachment from. + /// The name of the attachment. + /// The description of the attachment. + public FileAttachment(Stream stream, string fileName, string description = null, bool isSpoiler = false) + { + _isDisposed = false; + FileName = fileName; + Description = description; + Stream = stream; + IsSpoiler = isSpoiler; + } + + /// + /// Create the file attachment from a file path. + /// + /// + /// This file path is NOT validated and is passed directly into a + /// . + /// + /// The path to the file. + /// + /// is a zero-length string, contains only white space, or contains one or more invalid + /// characters as defined by . + /// + /// is null. + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// is in an invalid format. + /// + /// The specified is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// The file specified in was not found. + /// + /// An I/O error occurred while opening the file. + public FileAttachment(string path, string description = null, bool isSpoiler = false) + { + _isDisposed = false; + Stream = File.OpenRead(path); + FileName = Path.GetFileName(path); + Description = description; + IsSpoiler = isSpoiler; + } + + public void Dispose() + { + if (!_isDisposed) + { + Stream?.Dispose(); + _isDisposed = true; + } + } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index 225e9cf2e..e94e9f97c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,14 +1,66 @@ -namespace Discord +namespace Discord { + /// + /// Represents a message attachment found in a . + /// public interface IAttachment { + /// + /// Gets the ID of this attachment. + /// + /// + /// A snowflake ID associated with this attachment. + /// ulong Id { get; } + /// + /// Gets the filename of this attachment. + /// + /// + /// A string containing the full filename of this attachment (e.g. textFile.txt). + /// string Filename { get; } + /// + /// Gets the URL of this attachment. + /// + /// + /// A string containing the URL of this attachment. + /// string Url { get; } + /// + /// Gets a proxied URL of this attachment. + /// + /// + /// A string containing the proxied URL of this attachment. + /// string ProxyUrl { get; } + /// + /// Gets the file size of this attachment. + /// + /// + /// The size of this attachment in bytes. + /// int Size { get; } + /// + /// Gets the height of this attachment. + /// + /// + /// The height of this attachment if it is a picture; otherwise null. + /// int? Height { get; } + /// + /// Gets the width of this attachment. + /// + /// + /// The width of this attachment if it is a picture; otherwise null. + /// int? Width { get; } + /// + /// Gets whether or not this attachment is ephemeral. + /// + /// + /// if the attachment is ephemeral; otherwise . + /// + bool Ephemeral { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index f390c4c28..4c1029a10 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -1,22 +1,104 @@ -using System; +using System; using System.Collections.Immutable; namespace Discord { + /// + /// Represents a Discord embed object. + /// public interface IEmbed { + /// + /// Gets the title URL of this embed. + /// + /// + /// A string containing the URL set in a title of the embed. + /// string Url { get; } + /// + /// Gets the title of this embed. + /// + /// + /// The title of the embed. + /// string Title { get; } + /// + /// Gets the description of this embed. + /// + /// + /// The description field of the embed. + /// string Description { get; } + /// + /// Gets the type of this embed. + /// + /// + /// The type of the embed. + /// EmbedType Type { get; } + /// + /// Gets the timestamp of this embed. + /// + /// + /// A based on the timestamp present at the bottom left of the embed, or + /// null if none is set. + /// DateTimeOffset? Timestamp { get; } + /// + /// Gets the color of this embed. + /// + /// + /// The color of the embed present on the side of the embed, or null if none is set. + /// Color? Color { get; } + /// + /// Gets the image of this embed. + /// + /// + /// The image of the embed, or null if none is set. + /// EmbedImage? Image { get; } + /// + /// Gets the video of this embed. + /// + /// + /// The video of the embed, or null if none is set. + /// EmbedVideo? Video { get; } + /// + /// Gets the author field of this embed. + /// + /// + /// The author field of the embed, or null if none is set. + /// EmbedAuthor? Author { get; } + /// + /// Gets the footer field of this embed. + /// + /// + /// The author field of the embed, or null if none is set. + /// EmbedFooter? Footer { get; } + /// + /// Gets the provider of this embed. + /// + /// + /// The source of the embed, or null if none is set. + /// EmbedProvider? Provider { get; } + /// + /// Gets the thumbnail featured in this embed. + /// + /// + /// The thumbnail featured in the embed, or null if none is set. + /// EmbedThumbnail? Thumbnail { get; } + /// + /// Gets the fields of the embed. + /// + /// + /// An array of the fields of the embed. + /// ImmutableArray Fields { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 4266f893a..f5f2ca007 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -1,41 +1,309 @@ -using System; +using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a message object. + /// public interface IMessage : ISnowflakeEntity, IDeletable { - /// Gets the type of this system message. + /// + /// Gets the type of this message. + /// MessageType Type { get; } - /// Gets the source of this message. + /// + /// Gets the source type of this message. + /// MessageSource Source { get; } - /// Returns true if this message was sent as a text-to-speech message. + /// + /// Gets the value that indicates whether this message was meant to be read-aloud by Discord. + /// + /// + /// true if this message was sent as a text-to-speech message; otherwise false. + /// bool IsTTS { get; } - /// Returns true if this message was added to its channel's pinned messages. + /// + /// Gets the value that indicates whether this message is pinned. + /// + /// + /// true if this message was added to its channel's pinned messages; otherwise false. + /// bool IsPinned { get; } - /// Returns the content for this message. + /// + /// Gets the value that indicates whether or not this message's embeds are suppressed. + /// + /// + /// true if the embeds in this message have been suppressed (made invisible); otherwise false. + /// + bool IsSuppressed { get; } + /// + /// Gets the value that indicates whether this message mentioned everyone. + /// + /// + /// true if this message mentioned everyone; otherwise false. + /// + bool MentionedEveryone { get; } + /// + /// Gets the content for this message. + /// + /// + /// A string that contains the body of the message; note that this field may be empty if there is an embed. + /// string Content { get; } - /// Gets the time this message was sent. + /// + /// Gets the clean content for this message. + /// + /// + /// A string that contains the body of the message stripped of mentions, markdown, emojis and pings; note that this field may be empty if there is an embed. + /// + string CleanContent { get; } + /// + /// Gets the time this message was sent. + /// + /// + /// Time of when the message was sent. + /// DateTimeOffset Timestamp { get; } - /// Gets the time of this message's last edit, if any. + /// + /// Gets the time of this message's last edit. + /// + /// + /// Time of when the message was last edited; null if the message is never edited. + /// DateTimeOffset? EditedTimestamp { get; } - /// Gets the channel this message was sent to. + /// + /// Gets the source channel of the message. + /// IMessageChannel Channel { get; } - /// Gets the author of this message. + /// + /// Gets the author of this message. + /// IUser Author { get; } - /// Returns all attachments included in this message. + /// + /// Gets all attachments included in this message. + /// + /// + /// This property gets a read-only collection of attachments associated with this message. Depending on the + /// user's end-client, a sent message may contain one or more attachments. For example, mobile users may + /// attach more than one file in their message, while the desktop client only allows for one. + /// + /// + /// A read-only collection of attachments. + /// IReadOnlyCollection Attachments { get; } - /// Returns all embeds included in this message. + /// + /// Gets all embeds included in this message. + /// + /// + /// This property gets a read-only collection of embeds associated with this message. Depending on the + /// message, a sent message may contain one or more embeds. This is usually true when multiple link previews + /// are generated; however, only one can be featured. + /// + /// + /// A read-only collection of embed objects. + /// IReadOnlyCollection Embeds { get; } - /// Returns all tags included in this message's content. + /// + /// Gets all tags included in this message's content. + /// IReadOnlyCollection Tags { get; } - /// Returns the ids of channels mentioned in this message. + /// + /// Gets the IDs of channels mentioned in this message. + /// + /// + /// A read-only collection of channel IDs. + /// IReadOnlyCollection MentionedChannelIds { get; } - /// Returns the ids of roles mentioned in this message. + /// + /// Gets the IDs of roles mentioned in this message. + /// + /// + /// A read-only collection of role IDs. + /// IReadOnlyCollection MentionedRoleIds { get; } - /// Returns the ids of users mentioned in this message. + /// + /// Gets the IDs of users mentioned in this message. + /// + /// + /// A read-only collection of user IDs. + /// IReadOnlyCollection MentionedUserIds { get; } + /// + /// Gets the activity associated with a message. + /// + /// + /// Sent with Rich Presence-related chat embeds. This often refers to activity that requires end-user's + /// interaction, such as a Spotify Invite activity. + /// + /// + /// A message's activity, if any is associated. + /// + MessageActivity Activity { get; } + /// + /// Gets the application associated with a message. + /// + /// + /// Sent with Rich-Presence-related chat embeds. + /// + /// + /// A message's application, if any is associated. + /// + MessageApplication Application { get; } + + /// + /// Gets the reference to the original message if it is a crosspost, channel follow add, pin, or reply message. + /// + /// + /// Sent with cross-posted messages, meaning they were published from news channels + /// and received by subscriber channels, channel follow adds, pins, and message replies. + /// + /// + /// A message's reference, if any is associated. + /// + MessageReference Reference { get; } + + /// + /// Gets all reactions included in this message. + /// + IReadOnlyDictionary Reactions { get; } + + /// + /// The 's attached to this message + /// + IReadOnlyCollection Components { get; } + + /// + /// Gets all stickers items included in this message. + /// + /// + /// A read-only collection of sticker item objects. + /// + IReadOnlyCollection Stickers { get; } + + /// + /// Gets the flags related to this message. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// A message's flags, if any is associated. + /// + MessageFlags? Flags { get; } + + /// + /// Gets the interaction this message is a response to. + /// + /// + /// A if the message is a response to an interaction; otherwise . + /// + IMessageInteraction Interaction { get; } + + /// + /// Adds a reaction to this message. + /// + /// + /// The following example adds the reaction, 💕, to the message. + /// + /// await msg.AddReactionAsync(new Emoji("\U0001f495")); + /// + /// + /// The emoji used to react to this message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for adding a reaction to this message. + /// + /// + Task AddReactionAsync(IEmote emote, RequestOptions options = null); + /// + /// Removes a reaction from message. + /// + /// + /// The following example removes the reaction, 💕, added by the message author from the message. + /// + /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); + /// + /// + /// The emoji used to react to this message. + /// The user that added the emoji. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); + /// + /// Removes a reaction from message. + /// + /// + /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. + /// + /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); + /// + /// + /// The emoji used to react to this message. + /// The ID of the user that added the emoji. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null); + /// + /// Removes all reactions from this message. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task RemoveAllReactionsAsync(RequestOptions options = null); + /// + /// Removes all reactions with a specific emoji from this message. + /// + /// The emoji used to react to this message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null); + + /// + /// Gets all users that reacted to a message with a given emote. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of reactions specified under . + /// The library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 reactions, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example gets the users that have reacted with the emoji 💕 to the message. + /// + /// var emoji = new Emoji("\U0001f495"); + /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); + /// + /// + /// The emoji that represents the reaction that you wish to get. + /// The number of users to request. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessageInteraction.cs b/src/Discord.Net.Core/Entities/Messages/IMessageInteraction.cs new file mode 100644 index 000000000..ebd03b627 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/IMessageInteraction.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a partial within a message. + /// + public interface IMessageInteraction + { + /// + /// Gets the snowflake id of the interaction. + /// + ulong Id { get; } + + /// + /// Gets the type of the interaction. + /// + InteractionType Type { get; } + + /// + /// Gets the name of the application command used. + /// + string Name { get; } + + /// + /// Gets the who invoked the interaction. + /// + IUser User { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs index 37ead42ae..b7d7128c0 100644 --- a/src/Discord.Net.Core/Entities/Messages/IReaction.cs +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -1,7 +1,13 @@ -namespace Discord +namespace Discord { + /// + /// Represents a generic reaction object. + /// public interface IReaction { + /// + /// The used in the reaction. + /// IEmote Emote { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs index 2dfaf8f2d..89cd17a35 100644 --- a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs @@ -1,5 +1,8 @@ -namespace Discord +namespace Discord { + /// + /// Represents a generic message sent by the system. + /// public interface ISystemMessage : IMessage { } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 36ee725ff..c2d0e13bc 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,31 +1,80 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic message sent by a user. + /// public interface IUserMessage : IMessage { - /// Modifies this message. + /// + /// Gets the referenced message if it is a crosspost, channel follow add, pin, or reply message. + /// + /// + /// The referenced message, if any is associated and still exists. + /// + IUserMessage ReferencedMessage { get; } + + /// + /// Modifies this message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Adds this message to its channel's pinned messages. + /// + /// Adds this message to its channel's pinned messages. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for pinning this message. + /// Task PinAsync(RequestOptions options = null); - /// Removes this message from its channel's pinned messages. + /// + /// Removes this message from its channel's pinned messages. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for unpinning this message. + /// Task UnpinAsync(RequestOptions options = null); - /// Returns all reactions included in this message. - IReadOnlyDictionary Reactions { get; } - - /// Adds a reaction to this message. - Task AddReactionAsync(IEmote emote, RequestOptions options = null); - /// Removes a reaction from message. - Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); - /// Removes all reactions from this message. - Task RemoveAllReactionsAsync(RequestOptions options = null); - /// Gets all users that reacted to a message with a given emote - IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); + /// + /// Publishes (crossposts) this message. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for publishing this message. + /// + /// + /// + /// This call will throw an if attempted in a non-news channel. + /// + /// This method will publish (crosspost) the message. Please note, publishing (crossposting), is only available in news channels. + /// + Task CrosspostAsync(RequestOptions options = null); - /// Transforms this message's text into a human readable form by resolving its tags. + /// + /// Transforms this message's text into a human-readable form by resolving its tags. + /// + /// Determines how the user tag should be handled. + /// Determines how the channel tag should be handled. + /// Determines how the role tag should be handled. + /// Determines how the @everyone tag should be handled. + /// Determines how the emoji tag should be handled. string Resolve( TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, diff --git a/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs new file mode 100644 index 000000000..ff4ae406b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// An activity object found in a sent message. + /// + /// + /// + /// This class refers to an activity object, visually similar to an embed within a message. However, a message + /// activity is interactive as opposed to a standard static embed. + /// + /// For example, a Spotify party invitation counts as a message activity. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageActivity + { + /// + /// Gets the type of activity of this message. + /// + public MessageActivityType Type { get; internal set; } + /// + /// Gets the party ID of this activity, if any. + /// + public string PartyId { get; internal set; } + + private string DebuggerDisplay + => $"{Type}{(string.IsNullOrWhiteSpace(PartyId) ? "" : $" {PartyId}")}"; + + public override string ToString() => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs b/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs new file mode 100644 index 000000000..68b99a9c1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum MessageActivityType + { + Join = 1, + Spectate = 2, + Listen = 3, + JoinRequest = 5 + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs new file mode 100644 index 000000000..39a599da3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + public ulong Id { get; internal set; } + /// + /// Gets the ID of the embed's image asset. + /// + public string CoverImage { get; internal set; } + /// + /// Gets the application's description. + /// + public string Description { get; internal set; } + /// + /// Gets the ID of the application's icon. + /// + public string Icon { get; internal set; } + /// + /// Gets the Url of the application's icon. + /// + public string IconUrl + => $"https://cdn.discordapp.com/app-icons/{Id}/{Icon}"; + /// + /// Gets the name of the application. + /// + public string Name { get; internal set; } + private string DebuggerDisplay + => $"{Name} ({Id}): {Description}"; + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs new file mode 100644 index 000000000..6f9450372 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs @@ -0,0 +1,48 @@ +using System; + +namespace Discord +{ + [Flags] + public enum MessageFlags + { + /// + /// Default value for flags, when none are given to a message. + /// + None = 0, + /// + /// Flag given to messages that have been published to subscribed + /// channels (via Channel Following). + /// + Crossposted = 1 << 0, + /// + /// Flag given to messages that originated from a message in another + /// channel (via Channel Following). + /// + IsCrosspost = 1 << 1, + /// + /// Flag given to messages that do not display any embeds. + /// + SuppressEmbeds = 1 << 2, + /// + /// Flag given to messages that the source message for this crosspost + /// has been deleted (via Channel Following). + /// + SourceMessageDeleted = 1 << 3, + /// + /// Flag given to messages that came from the urgent message system. + /// + Urgent = 1 << 4, + /// + /// Flag given to messages has an associated thread, with the same id as the message + /// + HasThread = 1 << 5, + /// + /// Flag given to messages that is only visible to the user who invoked the Interaction. + /// + Ephemeral = 1 << 6, + /// + /// Flag given to messages that is an Interaction Response and the bot is "thinking" + /// + Loading = 1 << 7 + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs b/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs new file mode 100644 index 000000000..cbbebd932 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a partial within a message. + /// + /// The type of the user. + public class MessageInteraction : IMessageInteraction where TUser : IUser + { + /// + /// Gets the snowflake id of the interaction. + /// + public ulong Id { get; } + + /// + /// Gets the type of the interaction. + /// + public InteractionType Type { get; } + + /// + /// Gets the name of the application command used. + /// + public string Name { get; } + + /// + /// Gets the who invoked the interaction. + /// + public TUser User { get; } + + internal MessageInteraction(ulong id, InteractionType type, string name, TUser user) + { + Id = id; + Type = type; + Name = name; + User = user; + } + + IUser IMessageInteraction.User => User; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index b3f3a9c89..1a4eaff2d 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -1,37 +1,59 @@ -namespace Discord +using System.Collections.Generic; + +namespace Discord { /// - /// Modify a message with the specified parameters. + /// Properties that are used to modify an with the specified changes. /// /// - /// The content of a message can be cleared with String.Empty; if and only if an Embed is present. + /// The content of a message can be cleared with if and only if an + /// is present. /// - /// - /// - /// var message = await ReplyAsync("abc"); - /// await message.ModifyAsync(x => - /// { - /// x.Content = ""; - /// x.Embed = new EmbedBuilder() - /// .WithColor(new Color(40, 40, 120)) - /// .WithAuthor(a => a.Name = "foxbot") - /// .WithTitle("Embed!") - /// .WithDescription("This is an embed."); - /// }); - /// - /// + /// public class MessageProperties { /// - /// The content of the message + /// Gets or sets the content of the message. /// /// - /// This must be less than 2000 characters. + /// This must be less than the constant defined by . /// public Optional Content { get; set; } + /// - /// The embed the message should display + /// Gets or sets a single embed for this message. /// + /// + /// This property will be added to the array, in the future please use the array rather than this property. + /// public Optional Embed { get; set; } + + /// + /// Gets or sets the embeds of the message. + /// + public Optional Embeds { get; set; } + + /// + /// Gets or sets the components for this message. + /// + public Optional Components { get; set; } + + /// + /// Gets or sets the flags of the message. + /// + /// + /// Only can be set/unset and you need to be + /// the author of the message. + /// + public Optional Flags { get; set; } + /// + /// Gets or sets the allowed mentions of the message. + /// + public Optional AllowedMentions { get; set; } + + /// + /// Gets or sets the attachments for the message. + /// + public Optional> Attachments { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs new file mode 100644 index 000000000..029910e56 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -0,0 +1,56 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// Contains the IDs sent from a crossposted message or inline reply. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageReference + { + /// + /// Gets the Message ID of the original message. + /// + public Optional MessageId { get; internal set; } + + /// + /// Gets the Channel ID of the original message. + /// + /// + /// It only will be the default value (zero) if it was instantiated with a in the constructor. + /// + public ulong ChannelId { get => InternalChannelId.GetValueOrDefault(); } + internal Optional InternalChannelId; + + /// + /// Gets the Guild ID of the original message. + /// + public Optional GuildId { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The ID of the message that will be referenced. Used to reply to specific messages and the only parameter required for it. + /// + /// + /// The ID of the channel that will be referenced. It will be validated if sent. + /// + /// + /// The ID of the guild that will be referenced. It will be validated if sent. + /// + public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null) + { + MessageId = messageId ?? Optional.Create(); + InternalChannelId = channelId ?? Optional.Create(); + GuildId = guildId ?? Optional.Create(); + } + + private string DebuggerDisplay + => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs index 1cb2f8b94..bd4f23727 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -1,10 +1,25 @@ namespace Discord { + /// + /// Specifies the source of the Discord message. + /// public enum MessageSource { + /// + /// The message is sent by the system. + /// System, + /// + /// The message is sent by a user. + /// User, + /// + /// The message is sent by a bot. + /// Bot, + /// + /// The message is sent by a webhook. + /// Webhook } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs index 687e69e14..b83f88434 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageType.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -1,13 +1,110 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the type of message. + /// public enum MessageType { + /// + /// The default message type. + /// Default = 0, + /// + /// The message when a recipient is added. + /// RecipientAdd = 1, + /// + /// The message when a recipient is removed. + /// RecipientRemove = 2, + /// + /// The message when a user is called. + /// Call = 3, + /// + /// The message when a channel name is changed. + /// ChannelNameChange = 4, + /// + /// The message when a channel icon is changed. + /// ChannelIconChange = 5, - ChannelPinnedMessage = 6 + /// + /// The message when another message is pinned. + /// + ChannelPinnedMessage = 6, + /// + /// The message when a new member joined. + /// + GuildMemberJoin = 7, + /// + /// The message for when a user boosts a guild. + /// + UserPremiumGuildSubscription = 8, + /// + /// The message for when a guild reaches Tier 1 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier1 = 9, + /// + /// The message for when a guild reaches Tier 2 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier2 = 10, + /// + /// The message for when a guild reaches Tier 3 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier3 = 11, + /// + /// The message for when a news channel subscription is added to a text channel. + /// + ChannelFollowAdd = 12, + /// + /// The message for when a guild is disqualified from discovery. + /// + GuildDiscoveryDisqualified = 14, + /// + /// The message for when a guild is requalified for discovery. + /// + GuildDiscoveryRequalified = 15, + /// + /// The message for when the initial warning is sent for the initial grace period discovery. + /// + GuildDiscoveryGracePeriodInitialWarning = 16, + /// + /// The message for when the final warning is sent for the initial grace period discovery. + /// + GuildDiscoveryGracePeriodFinalWarning = 17, + /// + /// The message for when a thread is created. + /// + ThreadCreated = 18, + /// + /// The message is an inline reply. + /// + /// + /// Only available in API v8. + /// + Reply = 19, + /// + /// The message is an Application Command. + /// + /// + /// Only available in API v8. + /// + ApplicationCommand = 20, + /// + /// The message that starts a thread. + /// + /// + /// Only available in API v9. + /// + ThreadStarterMessage = 21, + /// + /// The message for a invite reminder. + /// + GuildInviteReminder = 22, + /// + /// The message for a context menu command. + /// + ContextMenuCommand = 23, } } diff --git a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs index 005276202..850666921 100644 --- a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -1,11 +1,24 @@ -namespace Discord +namespace Discord { + /// + /// A metadata containing reaction information. + /// public struct ReactionMetadata { - /// Gets the number of reactions + /// + /// Gets the number of reactions. + /// + /// + /// An representing the number of this reactions that has been added to this message. + /// public int ReactionCount { get; internal set; } - /// Returns true if the current user has used this reaction + /// + /// Gets a value that indicates whether the current user has reacted to this. + /// + /// + /// true if the user has reacted to the message; otherwise false. + /// public bool IsMe { get; internal set; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/StickerFormatType.cs b/src/Discord.Net.Core/Entities/Messages/StickerFormatType.cs new file mode 100644 index 000000000..82e6b15a4 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/StickerFormatType.cs @@ -0,0 +1,25 @@ +namespace Discord +{ + /// + /// Defines the types of formats for stickers. + /// + public enum StickerFormatType + { + /// + /// Default value for a sticker format type. + /// + None = 0, + /// + /// The sticker format type is png. + /// + Png = 1, + /// + /// The sticker format type is apng. + /// + Apng = 2, + /// + /// The sticker format type is lottie. + /// + Lottie = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs index 492f05879..eaadd6400 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -1,13 +1,39 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the handling type the tag should use. + /// + /// + /// public enum TagHandling { - Ignore = 0, //<@53905483156684800> -> <@53905483156684800> - Remove, //<@53905483156684800> -> - Name, //<@53905483156684800> -> @Voltana - NameNoPrefix, //<@53905483156684800> -> Voltana - FullName, //<@53905483156684800> -> @Voltana#8252 - FullNameNoPrefix, //<@53905483156684800> -> Voltana#8252 - Sanitize //<@53905483156684800> -> <@53905483156684800> (w/ nbsp) + /// + /// Tag handling is ignored (e.g. <@53905483156684800> -> <@53905483156684800>). + /// + Ignore = 0, + /// + /// Removes the tag entirely. + /// + Remove, + /// + /// Resolves to username (e.g. <@53905483156684800> -> @Voltana). + /// + Name, + /// + /// Resolves to username without mention prefix (e.g. <@53905483156684800> -> Voltana). + /// + NameNoPrefix, + /// + /// Resolves to username with discriminator value. (e.g. <@53905483156684800> -> @Voltana#8252). + /// + FullName, + /// + /// Resolves to username with discriminator value without mention prefix. (e.g. <@53905483156684800> -> Voltana#8252). + /// + FullNameNoPrefix, + /// + /// Sanitizes the tag (e.g. <@53905483156684800> -> <@53905483156684800> (w/ nbsp)). + /// + Sanitize } } diff --git a/src/Discord.Net.Core/Entities/Messages/TagType.cs b/src/Discord.Net.Core/Entities/Messages/TagType.cs index 2d93bb3e3..177157251 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagType.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagType.cs @@ -1,12 +1,19 @@ -namespace Discord +namespace Discord { + /// Specifies the type of Discord tag. public enum TagType { + /// The object is an user mention. UserMention, + /// The object is a channel mention. ChannelMention, + /// The object is a role mention. RoleMention, + /// The object is an everyone mention. EveryoneMention, + /// The object is a here mention. HereMention, + /// The object is an emoji. Emoji } } diff --git a/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs new file mode 100644 index 000000000..347b0daaa --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs @@ -0,0 +1,47 @@ +using System; + +namespace Discord +{ + /// + /// Represents a class used to make timestamps in messages. see . + /// + public class TimestampTag + { + /// + /// Gets or sets the style of the timestamp tag. + /// + public TimestampTagStyles Style { get; set; } = TimestampTagStyles.ShortDateTime; + + /// + /// Gets or sets the time for this timestamp tag. + /// + public DateTime Time { get; set; } + + /// + /// Converts the current timestamp tag to the string representation supported by discord. + /// + /// If the is null then the default 0 will be used. + /// + /// + /// A string that is compatible in a discord message, ex: <t:1625944201:f> + public override string ToString() + { + return $""; + } + + /// + /// Creates a new timestamp tag with the specified datetime object. + /// + /// The time of this timestamp tag. + /// The style for this timestamp tag. + /// The newly create timestamp tag. + public static TimestampTag FromDateTime(DateTime time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime) + { + return new TimestampTag + { + Style = style, + Time = time + }; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/TimestampTagStyle.cs b/src/Discord.Net.Core/Entities/Messages/TimestampTagStyle.cs new file mode 100644 index 000000000..89f3c79b5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/TimestampTagStyle.cs @@ -0,0 +1,43 @@ +namespace Discord +{ + /// + /// Represents a set of styles to use with a + /// + public enum TimestampTagStyles + { + /// + /// A short time string: 16:20 + /// + ShortTime = 116, + + /// + /// A long time string: 16:20:30 + /// + LongTime = 84, + + /// + /// A short date string: 20/04/2021 + /// + ShortDate = 100, + + /// + /// A long date string: 20 April 2021 + /// + LongDate = 68, + + /// + /// A short datetime string: 20 April 2021 16:20 + /// + ShortDateTime = 102, + + /// + /// A long datetime string: Tuesday, 20 April 2021 16:20 + /// + LongDateTime = 70, + + /// + /// The relative time to the user: 2 months ago + /// + Relative = 82 + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissionTarget.cs b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissionTarget.cs new file mode 100644 index 000000000..9a99b34f1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissionTarget.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Specifies the target of the permission. + /// + public enum ApplicationCommandPermissionTarget + { + /// + /// The target of the permission is a role. + /// + Role = 1, + /// + /// The target of the permission is a user. + /// + User = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs new file mode 100644 index 000000000..28a6455e2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs @@ -0,0 +1,62 @@ +namespace Discord +{ + /// + /// Application command permissions allow you to enable or disable commands for specific users or roles within a guild. + /// + public class ApplicationCommandPermission + { + /// + /// The id of the role or user. + /// + public ulong TargetId { get; } + + /// + /// The target of this permission. + /// + public ApplicationCommandPermissionTarget TargetType { get; } + + /// + /// to allow, otherwise . + /// + public bool Permission { get; } + + internal ApplicationCommandPermission() { } + + /// + /// Creates a new . + /// + /// The id you want to target this permission value for. + /// The type of the targetId parameter. + /// The value of this permission. + public ApplicationCommandPermission(ulong targetId, ApplicationCommandPermissionTarget targetType, bool allow) + { + TargetId = targetId; + TargetType = targetType; + Permission = allow; + } + + /// + /// Creates a new targeting . + /// + /// The user you want to target this permission value for. + /// The value of this permission. + public ApplicationCommandPermission(IUser target, bool allow) + { + TargetId = target.Id; + Permission = allow; + TargetType = ApplicationCommandPermissionTarget.User; + } + + /// + /// Creates a new targeting . + /// + /// The role you want to target this permission value for. + /// The value of this permission. + public ApplicationCommandPermission(IRole target, bool allow) + { + TargetId = target.Id; + Permission = allow; + TargetType = ApplicationCommandPermissionTarget.Role; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 0fbd22c4e..45e24b7fa 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -2,38 +2,149 @@ using System; namespace Discord { - [FlagsAttribute] + /// Defines the available permissions for a channel. + [Flags] public enum ChannelPermission : ulong { // General - CreateInstantInvite = 0x00_00_00_01, - ManageChannels = 0x00_00_00_10, + /// + /// Allows creation of instant invites. + /// + CreateInstantInvite = 0x00_00_00_00_01, + /// + /// Allows management and editing of channels. + /// + ManageChannels = 0x00_00_00_00_10, // Text - AddReactions = 0x00_00_00_40, - [Obsolete("Use ViewChannel instead.")] - ReadMessages = ViewChannel, - ViewChannel = 0x00_00_04_00, - SendMessages = 0x00_00_08_00, - SendTTSMessages = 0x00_00_10_00, - ManageMessages = 0x00_00_20_00, - EmbedLinks = 0x00_00_40_00, - AttachFiles = 0x00_00_80_00, - ReadMessageHistory = 0x00_01_00_00, - MentionEveryone = 0x00_02_00_00, - UseExternalEmojis = 0x00_04_00_00, + /// + /// Allows for the addition of reactions to messages. + /// + AddReactions = 0x00_00_00_00_40, + /// + /// Allows guild members to view a channel, which includes reading messages in text channels. + /// + ViewChannel = 0x00_00_00_04_00, + /// + /// Allows for sending messages in a channel. + /// + SendMessages = 0x00_00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// + SendTTSMessages = 0x00_00_00_10_00, + /// + /// Allows for deletion of other users messages. + /// + ManageMessages = 0x00_00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_00_40_00, + /// + /// Allows for uploading images and files. + /// + AttachFiles = 0x00_00_00_80_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// + MentionEveryone = 0x00_00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// + UseExternalEmojis = 0x00_00_04_00_00, // Voice - Connect = 0x00_10_00_00, - Speak = 0x00_20_00_00, - MuteMembers = 0x00_40_00_00, - DeafenMembers = 0x00_80_00_00, - MoveMembers = 0x01_00_00_00, - UseVAD = 0x02_00_00_00, - PrioritySpeaker = 0x00_00_01_00, + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// + MoveMembers = 0x00_01_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x00_02_00_00_00, + + /// + /// Allows for using priority speaker in a voice channel. + /// + PrioritySpeaker = 0x00_00_00_01_00, + + /// + /// Allows video streaming in a voice channel. + /// + Stream = 0x00_00_00_02_00, // More General - ManageRoles = 0x10_00_00_00, - ManageWebhooks = 0x20_00_00_00, + /// + /// Allows management and editing of roles. + /// + ManageRoles = 0x00_10_00_00_00, + /// + /// Allows management and editing of webhooks. + /// + ManageWebhooks = 0x00_20_00_00_00, + + /// + /// Allows management and editing of emojis. + /// + ManageEmojis = 0x00_40_00_00_00, + + /// + /// Allows members to use slash commands in text channels. + /// + UseApplicationCommands = 0x00_80_00_00_00, + + /// + /// Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.) + /// + RequestToSpeak = 0x01_00_00_00_00, + + /// + /// Allows for deleting and archiving threads, and viewing all private threads + /// + ManageThreads = 0x04_00_00_00_00, + + /// + /// Allows for creating public threads. + /// + CreatePublicThreads = 0x08_00_00_00_00, + /// + /// Allows for creating private threads. + /// + CreatePrivateThreads = 0x10_00_00_00_00, + /// + /// Allows the usage of custom stickers from other servers. + /// + UseExternalStickers = 0x20_00_00_00_00, + /// + /// Allows for sending messages in threads. + /// + SendMessagesInThreads = 0x40_00_00_00_00, + /// + /// Allows for launching activities (applications with the EMBEDDED flag) in a voice channel. + /// + StartEmbeddedActivities = 0x80_00_00_00_00 + } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index ea56734ff..ee5c9984a 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,86 +7,106 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - /// Gets a blank ChannelPermissions that grants no permissions. + /// Gets a blank that grants no permissions. + /// A structure that does not contain any set permissions. public static readonly ChannelPermissions None = new ChannelPermissions(); - /// Gets a ChannelPermissions that grants all permissions for text channels. - public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); - /// Gets a ChannelPermissions that grants all permissions for voice channels. - public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000010100_010001); - /// Gets a ChannelPermissions that grants all permissions for category channels. + /// Gets a that grants all permissions for text channels. + public static readonly ChannelPermissions Text = new ChannelPermissions(0b0_11111_0101100_0000000_1111111110001_010001); + /// Gets a that grants all permissions for voice channels. + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b1_00000_0000100_1111110_0000000011100_010001); + /// Gets a that grants all permissions for stage channels. + public static readonly ChannelPermissions Stage = new ChannelPermissions(0b0_00000_1000100_0111010_0000000010000_010001); + /// Gets a that grants all permissions for category channels. public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); - /// Gets a ChannelPermissions that grants all permissions for direct message channels. - public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); - /// Gets a ChannelPermissions that grants all permissions for group channels. + /// Gets a that grants all permissions for direct message channels. + public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000); + /// Gets a that grants all permissions for group channels. public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); - /// Gets a ChannelPermissions that grants all permissions for a given channelType. + /// Gets a that grants all permissions for a given channel type. + /// Unknown channel type. public static ChannelPermissions All(IChannel channel) { - switch (channel) + return channel switch { - case ITextChannel _: return Text; - case IVoiceChannel _: return Voice; - case ICategoryChannel _: return Category; - case IDMChannel _: return DM; - case IGroupChannel _: return Group; - default: throw new ArgumentException(message: "Unknown channel type", paramName: nameof(channel)); - } + ITextChannel _ => Text, + IStageChannel _ => Stage, + IVoiceChannel _ => Voice, + ICategoryChannel _ => Category, + IDMChannel _ => DM, + IGroupChannel _ => Group, + _ => throw new ArgumentException(message: "Unknown channel type.", paramName: nameof(channel)), + }; } - /// Gets a packed value representing all the permissions in this ChannelPermissions. + /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If True, a user may create invites. + /// If true, a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); - /// If True, a user may create, delete and modify this channel. + /// If true, a user may create, delete and modify this channel. public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannels); - /// If true, a user may add reactions. + /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); - /// If True, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public bool ReadMessages => ViewChannel; - /// If True, a user may view channels. + /// If true, a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); - /// If True, a user may send messages. + /// If true, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); - /// If True, a user may send text-to-speech messages. + /// If true, a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, ChannelPermission.SendTTSMessages); - /// If True, a user may delete messages. + /// If true, a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, ChannelPermission.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. + /// If true, Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, ChannelPermission.EmbedLinks); - /// If True, a user may send files. + /// If true, a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, ChannelPermission.AttachFiles); - /// If True, a user may read previous messages. + /// If true, a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, ChannelPermission.ReadMessageHistory); - /// If True, a user may mention @everyone. + /// If true, a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, ChannelPermission.MentionEveryone); - /// If True, a user may use custom emoji from other guilds. + /// If true, a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, ChannelPermission.UseExternalEmojis); - /// If True, a user may connect to a voice channel. + /// If true, a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, ChannelPermission.Connect); - /// If True, a user may speak in a voice channel. + /// If true, a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, ChannelPermission.Speak); - /// If True, a user may mute users. + /// If true, a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, ChannelPermission.MuteMembers); - /// If True, a user may deafen users. + /// If true, a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, ChannelPermission.DeafenMembers); - /// If True, a user may move other users between voice channels. + /// If true, a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, ChannelPermission.MoveMembers); - /// If True, a user may use voice-activity-detection rather than push-to-talk. + /// If true, a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); - /// If True, a user may use priority speaker in a voice channel. + /// If true, a user may use priority speaker in a voice channel. public bool PrioritySpeaker => Permissions.GetValue(RawValue, ChannelPermission.PrioritySpeaker); + /// If true, a user may stream video in a voice channel. + public bool Stream => Permissions.GetValue(RawValue, ChannelPermission.Stream); - /// If True, a user may adjust role permissions. This also implictly grants all other permissions. + /// If true, a user may adjust role permissions. This also implicitly grants all other permissions. public bool ManageRoles => Permissions.GetValue(RawValue, ChannelPermission.ManageRoles); - /// If True, a user may edit the webhooks for this channel. + /// If true, a user may edit the webhooks for this channel. public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); + /// If true, a user may use application commands in this guild. + public bool UseApplicationCommands => Permissions.GetValue(RawValue, ChannelPermission.UseApplicationCommands); + /// If true, a user may request to speak in stage channels. + public bool RequestToSpeak => Permissions.GetValue(RawValue, ChannelPermission.RequestToSpeak); + /// If true, a user may manage threads in this guild. + public bool ManageThreads => Permissions.GetValue(RawValue, ChannelPermission.ManageThreads); + /// If true, a user may create public threads in this guild. + public bool CreatePublicThreads => Permissions.GetValue(RawValue, ChannelPermission.CreatePublicThreads); + /// If true, a user may create private threads in this guild. + public bool CreatePrivateThreads => Permissions.GetValue(RawValue, ChannelPermission.CreatePrivateThreads); + /// If true, a user may use external stickers in this guild. + public bool UseExternalStickers => Permissions.GetValue(RawValue, ChannelPermission.UseExternalStickers); + /// If true, a user may send messages in threads in this guild. + public bool SendMessagesInThreads => Permissions.GetValue(RawValue, ChannelPermission.SendMessagesInThreads); + /// If true, a user launch application activities in voice channels in this guild. + public bool StartEmbeddedActivities => Permissions.GetValue(RawValue, ChannelPermission.StartEmbeddedActivities); - /// Creates a new ChannelPermissions with the provided packed value. + /// Creates a new with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } private ChannelPermissions(ulong initialValue, @@ -109,8 +129,17 @@ namespace Discord bool? moveMembers = null, bool? useVoiceActivation = null, bool? prioritySpeaker = null, + bool? stream = null, bool? manageRoles = null, - bool? manageWebhooks = null) + bool? manageWebhooks = null, + bool? useApplicationCommands = null, + bool? requestToSpeak = null, + bool? manageThreads = null, + bool? createPublicThreads = null, + bool? createPrivateThreads = null, + bool? useExternalStickers = null, + bool? sendMessagesInThreads = null, + bool? startEmbeddedActivities = null) { ulong value = initialValue; @@ -133,13 +162,22 @@ namespace Discord Permissions.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); Permissions.SetValue(ref value, prioritySpeaker, ChannelPermission.PrioritySpeaker); + Permissions.SetValue(ref value, stream, ChannelPermission.Stream); Permissions.SetValue(ref value, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref value, manageWebhooks, ChannelPermission.ManageWebhooks); + Permissions.SetValue(ref value, useApplicationCommands, ChannelPermission.UseApplicationCommands); + Permissions.SetValue(ref value, requestToSpeak, ChannelPermission.RequestToSpeak); + Permissions.SetValue(ref value, manageThreads, ChannelPermission.ManageThreads); + Permissions.SetValue(ref value, createPublicThreads, ChannelPermission.CreatePublicThreads); + Permissions.SetValue(ref value, createPrivateThreads, ChannelPermission.CreatePrivateThreads); + Permissions.SetValue(ref value, useExternalStickers, ChannelPermission.UseExternalStickers); + Permissions.SetValue(ref value, sendMessagesInThreads, ChannelPermission.SendMessagesInThreads); + Permissions.SetValue(ref value, startEmbeddedActivities, ChannelPermission.StartEmbeddedActivities); RawValue = value; } - /// Creates a new ChannelPermissions with the provided permissions. + /// Creates a new with the provided permissions. public ChannelPermissions( bool createInstantInvite = false, bool manageChannel = false, @@ -160,14 +198,25 @@ namespace Discord bool moveMembers = false, bool useVoiceActivation = false, bool prioritySpeaker = false, + bool stream = false, bool manageRoles = false, - bool manageWebhooks = false) + bool manageWebhooks = false, + bool useApplicationCommands = false, + bool requestToSpeak = false, + bool manageThreads = false, + bool createPublicThreads = false, + bool createPrivateThreads = false, + bool useExternalStickers = false, + bool sendMessagesInThreads = false, + bool startEmbeddedActivities = false) : this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, manageRoles, manageWebhooks) + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, manageRoles, manageWebhooks, + useApplicationCommands, requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads, + startEmbeddedActivities) { } - /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. + /// Creates a new from this one, changing the provided non-null permissions. public ChannelPermissions Modify( bool? createInstantInvite = null, bool? manageChannel = null, @@ -188,8 +237,17 @@ namespace Discord bool? moveMembers = null, bool? useVoiceActivation = null, bool? prioritySpeaker = null, + bool? stream = null, bool? manageRoles = null, - bool? manageWebhooks = null) + bool? manageWebhooks = null, + bool? useApplicationCommands = null, + bool? requestToSpeak = null, + bool? manageThreads = null, + bool? createPublicThreads = null, + bool? createPrivateThreads = null, + bool? useExternalStickers = null, + bool? sendMessagesInThreads = null, + bool? startEmbeddedActivities = null) => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, @@ -210,8 +268,17 @@ namespace Discord moveMembers, useVoiceActivation, prioritySpeaker, + stream, manageRoles, - manageWebhooks); + manageWebhooks, + useApplicationCommands, + requestToSpeak, + manageThreads, + createPublicThreads, + createPrivateThreads, + useExternalStickers, + sendMessagesInThreads, + startEmbeddedActivities); public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..e738fec4c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Returned when fetching the permissions for a command in a guild. + /// + public class GuildApplicationCommandPermission + + { + /// + /// The id of the command. + /// + public ulong CommandId { get; } + + /// + /// The id of the application the command belongs to. + /// + public ulong ApplicationId { get; } + + /// + /// The id of the guild. + /// + public ulong GuildId { get; } + + /// + /// The permissions for the command in the guild. + /// + public IReadOnlyCollection Permissions { get; } + + internal GuildApplicationCommandPermission(ulong commandId, ulong appId, ulong guildId, ApplicationCommandPermission[] permissions) + { + CommandId = commandId; + ApplicationId = appId; + GuildId = guildId; + Permissions = permissions; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 13a9e32b1..5a5827c1d 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -2,46 +2,219 @@ using System; namespace Discord { - [FlagsAttribute] + /// Defines the available permissions for a channel. + [Flags] public enum GuildPermission : ulong { // General - CreateInstantInvite = 0x00_00_00_01, - KickMembers = 0x00_00_00_02, - BanMembers = 0x00_00_00_04, - Administrator = 0x00_00_00_08, - ManageChannels = 0x00_00_00_10, - ManageGuild = 0x00_00_00_20, + /// + /// Allows creation of instant invites. + /// + CreateInstantInvite = 0x00_00_00_01, + /// + /// Allows kicking members. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + KickMembers = 0x00_00_00_02, + /// + /// Allows banning members. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + BanMembers = 0x00_00_00_04, + /// + /// Allows all permissions and bypasses channel permission overwrites. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + Administrator = 0x00_00_00_08, + /// + /// Allows management and editing of channels. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageChannels = 0x00_00_00_10, + /// + /// Allows management and editing of the guild. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageGuild = 0x00_00_00_20, + /// + /// Allows for viewing of guild insights + /// + ViewGuildInsights = 0x00_08_00_00, // Text - AddReactions = 0x00_00_00_40, - ViewAuditLog = 0x00_00_00_80, - [Obsolete("Use ViewChannel instead.")] - ReadMessages = ViewChannel, - ViewChannel = 0x00_00_04_00, - SendMessages = 0x00_00_08_00, - SendTTSMessages = 0x00_00_10_00, - ManageMessages = 0x00_00_20_00, - EmbedLinks = 0x00_00_40_00, - AttachFiles = 0x00_00_80_00, - ReadMessageHistory = 0x00_01_00_00, - MentionEveryone = 0x00_02_00_00, - UseExternalEmojis = 0x00_04_00_00, + /// + /// Allows for the addition of reactions to messages. + /// + AddReactions = 0x00_00_00_40, + /// + /// Allows for viewing of audit logs. + /// + ViewAuditLog = 0x00_00_00_80, + /// + /// Allows guild members to view a channel, which includes reading messages in text channels. + /// + ViewChannel = 0x00_00_04_00, + /// + /// Allows for sending messages in a channel + /// + SendMessages = 0x00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// + SendTTSMessages = 0x00_00_10_00, + /// + /// Allows for deletion of other users messages. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageMessages = 0x00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_40_00, + /// + /// Allows for uploading images and files. + /// + AttachFiles = 0x00_00_80_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// + MentionEveryone = 0x00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// + UseExternalEmojis = 0x00_04_00_00, + // Voice - Connect = 0x00_10_00_00, - Speak = 0x00_20_00_00, - MuteMembers = 0x00_40_00_00, - DeafenMembers = 0x00_80_00_00, - MoveMembers = 0x01_00_00_00, - UseVAD = 0x02_00_00_00, - PrioritySpeaker = 0x00_00_01_00, + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// + MoveMembers = 0x01_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x02_00_00_00, + /// + /// Allows for using priority speaker in a voice channel. + /// + PrioritySpeaker = 0x00_00_01_00, + /// + /// Allows video streaming in a voice channel. + /// + Stream = 0x00_00_02_00, // General 2 - ChangeNickname = 0x04_00_00_00, - ManageNicknames = 0x08_00_00_00, - ManageRoles = 0x10_00_00_00, - ManageWebhooks = 0x20_00_00_00, - ManageEmojis = 0x40_00_00_00 + /// + /// Allows for modification of own nickname. + /// + ChangeNickname = 0x04_00_00_00, + /// + /// Allows for modification of other users nicknames. + /// + ManageNicknames = 0x08_00_00_00, + /// + /// Allows management and editing of roles. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageRoles = 0x10_00_00_00, + /// + /// Allows management and editing of webhooks. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageWebhooks = 0x20_00_00_00, + /// + /// Allows management and editing of emojis and stickers. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageEmojisAndStickers = 0x40_00_00_00, + /// + /// Allows members to use application commands like slash commands and context menus in text channels. + /// + UseApplicationCommands = 0x80_00_00_00, + /// + /// Allows for requesting to speak in stage channels. + /// + RequestToSpeak = 0x01_00_00_00_00, + /// + /// Allows for creating, editing, and deleting guild scheduled events. + /// + ManageEvents = 0x02_00_00_00_00, + /// + /// Allows for deleting and archiving threads, and viewing all private threads. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageThreads = 0x04_00_00_00_00, + /// + /// Allows for creating public threads. + /// + CreatePublicThreads = 0x08_00_00_00_00, + /// + /// Allows for creating private threads. + /// + CreatePrivateThreads = 0x10_00_00_00_00, + /// + /// Allows the usage of custom stickers from other servers. + /// + UseExternalStickers = 0x20_00_00_00_00, + /// + /// Allows for sending messages in threads. + /// + SendMessagesInThreads = 0x40_00_00_00_00, + /// + /// Allows for launching activities (applications with the EMBEDDED flag) in a voice channel. + /// + StartEmbeddedActivities = 0x80_00_00_00_00 + } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index c9cb90ec8..8a4ad2189 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,91 +1,114 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct GuildPermissions { - /// Gets a blank GuildPermissions that grants no permissions. + /// Gets a blank that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); - /// Gets a GuildPermissions that grants all guild permissions for webhook users. - public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); - /// Gets a GuildPermissions that grants all guild permissions. - public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111110111_111111); + /// Gets a that grants all guild permissions for webhook users. + public static readonly GuildPermissions Webhook = new GuildPermissions(0b0_00000_0000000_0000000_0001101100000_000000); + /// Gets a that grants all guild permissions. + public static readonly GuildPermissions All = new GuildPermissions(0b1_11111_1111111_1111111_1111111111111_111111); - /// Gets a packed value representing all the permissions in this GuildPermissions. + /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If True, a user may create invites. + /// If true, a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, GuildPermission.CreateInstantInvite); - /// If True, a user may ban users from the guild. + /// If true, a user may ban users from the guild. public bool BanMembers => Permissions.GetValue(RawValue, GuildPermission.BanMembers); - /// If True, a user may kick users from the guild. + /// If true, a user may kick users from the guild. public bool KickMembers => Permissions.GetValue(RawValue, GuildPermission.KickMembers); - /// If True, a user is granted all permissions, and cannot have them revoked via channel permissions. + /// If true, a user is granted all permissions, and cannot have them revoked via channel permissions. public bool Administrator => Permissions.GetValue(RawValue, GuildPermission.Administrator); - /// If True, a user may create, delete and modify channels. + /// If true, a user may create, delete and modify channels. public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); - /// If True, a user may adjust guild properties. + /// If true, a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - /// If true, a user may add reactions. + /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); - /// If true, a user may view the audit log. + /// If true, a user may view the audit log. public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog); + /// If true, a user may view the guild insights. + public bool ViewGuildInsights => Permissions.GetValue(RawValue, GuildPermission.ViewGuildInsights); - /// If True, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public bool ReadMessages => ViewChannel; /// If True, a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, GuildPermission.ViewChannel); /// If True, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, GuildPermission.SendMessages); - /// If True, a user may send text-to-speech messages. + /// If true, a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, GuildPermission.SendTTSMessages); - /// If True, a user may delete messages. + /// If true, a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, GuildPermission.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. + /// If true, Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, GuildPermission.EmbedLinks); - /// If True, a user may send files. + /// If true, a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, GuildPermission.AttachFiles); - /// If True, a user may read previous messages. + /// If true, a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, GuildPermission.ReadMessageHistory); - /// If True, a user may mention @everyone. + /// If true, a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, GuildPermission.MentionEveryone); - /// If True, a user may use custom emoji from other guilds. + /// If true, a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, GuildPermission.UseExternalEmojis); - /// If True, a user may connect to a voice channel. + /// If true, a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, GuildPermission.Connect); - /// If True, a user may speak in a voice channel. + /// If true, a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, GuildPermission.Speak); - /// If True, a user may mute users. + /// If true, a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, GuildPermission.MuteMembers); - /// If True, a user may deafen users. + /// If true, a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, GuildPermission.DeafenMembers); - /// If True, a user may move other users between voice channels. + /// If true, a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, GuildPermission.MoveMembers); - /// If True, a user may use voice-activity-detection rather than push-to-talk. + /// If true, a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, GuildPermission.UseVAD); /// If True, a user may use priority speaker in a voice channel. - public bool PrioritySpeaker => Permissions.GetValue(RawValue, ChannelPermission.PrioritySpeaker); + public bool PrioritySpeaker => Permissions.GetValue(RawValue, GuildPermission.PrioritySpeaker); + /// If True, a user may stream video in a voice channel. + public bool Stream => Permissions.GetValue(RawValue, GuildPermission.Stream); - /// If True, a user may change their own nickname. + /// If true, a user may change their own nickname. public bool ChangeNickname => Permissions.GetValue(RawValue, GuildPermission.ChangeNickname); - /// If True, a user may change the nickname of other users. + /// If true, a user may change the nickname of other users. public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); - /// If True, a user may adjust roles. + /// If true, a user may adjust roles. public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); - /// If True, a user may edit the webhooks for this guild. + /// If true, a user may edit the webhooks for this guild. public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); - /// If True, a user may edit the emojis for this guild. - public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); + /// If true, a user may edit the emojis and stickers for this guild. + public bool ManageEmojisAndStickers => Permissions.GetValue(RawValue, GuildPermission.ManageEmojisAndStickers); + /// If true, a user may use slash commands in this guild. + public bool UseApplicationCommands => Permissions.GetValue(RawValue, GuildPermission.UseApplicationCommands); + /// If true, a user may request to speak in stage channels. + public bool RequestToSpeak => Permissions.GetValue(RawValue, GuildPermission.RequestToSpeak); + /// If true, a user may create, edit, and delete events. + public bool ManageEvents => Permissions.GetValue(RawValue, GuildPermission.ManageEvents); + /// If true, a user may manage threads in this guild. + public bool ManageThreads => Permissions.GetValue(RawValue, GuildPermission.ManageThreads); + /// If true, a user may create public threads in this guild. + public bool CreatePublicThreads => Permissions.GetValue(RawValue, GuildPermission.CreatePublicThreads); + /// If true, a user may create private threads in this guild. + public bool CreatePrivateThreads => Permissions.GetValue(RawValue, GuildPermission.CreatePrivateThreads); + /// If true, a user may use external stickers in this guild. + public bool UseExternalStickers => Permissions.GetValue(RawValue, GuildPermission.UseExternalStickers); + /// If true, a user may send messages in threads in this guild. + public bool SendMessagesInThreads => Permissions.GetValue(RawValue, GuildPermission.SendMessagesInThreads); + /// If true, a user launch application activities in voice channels in this guild. + public bool StartEmbeddedActivities => Permissions.GetValue(RawValue, GuildPermission.StartEmbeddedActivities); - /// Creates a new GuildPermissions with the provided packed value. + /// Creates a new with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } + /// Creates a new with the provided packed value after converting to ulong. + public GuildPermissions(string rawValue) { RawValue = ulong.Parse(rawValue); } + private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, @@ -95,6 +118,7 @@ namespace Discord bool? manageGuild = null, bool? addReactions = null, bool? viewAuditLog = null, + bool? viewGuildInsights = null, bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, @@ -111,11 +135,21 @@ namespace Discord bool? moveMembers = null, bool? useVoiceActivation = null, bool? prioritySpeaker = null, + bool? stream = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, - bool? manageEmojis = null) + bool? manageEmojisAndStickers = null, + bool? useApplicationCommands = null, + bool? requestToSpeak = null, + bool? manageEvents = null, + bool? manageThreads = null, + bool? createPublicThreads = null, + bool? createPrivateThreads = null, + bool? useExternalStickers = null, + bool? sendMessagesInThreads = null, + bool? startEmbeddedActivities = null) { ulong value = initialValue; @@ -127,6 +161,7 @@ namespace Discord Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions); Permissions.SetValue(ref value, viewAuditLog, GuildPermission.ViewAuditLog); + Permissions.SetValue(ref value, viewGuildInsights, GuildPermission.ViewGuildInsights); Permissions.SetValue(ref value, viewChannel, GuildPermission.ViewChannel); Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); @@ -143,16 +178,26 @@ namespace Discord Permissions.SetValue(ref value, moveMembers, GuildPermission.MoveMembers); Permissions.SetValue(ref value, useVoiceActivation, GuildPermission.UseVAD); Permissions.SetValue(ref value, prioritySpeaker, GuildPermission.PrioritySpeaker); + Permissions.SetValue(ref value, stream, GuildPermission.Stream); Permissions.SetValue(ref value, changeNickname, GuildPermission.ChangeNickname); Permissions.SetValue(ref value, manageNicknames, GuildPermission.ManageNicknames); Permissions.SetValue(ref value, manageRoles, GuildPermission.ManageRoles); Permissions.SetValue(ref value, manageWebhooks, GuildPermission.ManageWebhooks); - Permissions.SetValue(ref value, manageEmojis, GuildPermission.ManageEmojis); + Permissions.SetValue(ref value, manageEmojisAndStickers, GuildPermission.ManageEmojisAndStickers); + Permissions.SetValue(ref value, useApplicationCommands, GuildPermission.UseApplicationCommands); + Permissions.SetValue(ref value, requestToSpeak, GuildPermission.RequestToSpeak); + Permissions.SetValue(ref value, manageEvents, GuildPermission.ManageEvents); + Permissions.SetValue(ref value, manageThreads, GuildPermission.ManageThreads); + Permissions.SetValue(ref value, createPublicThreads, GuildPermission.CreatePublicThreads); + Permissions.SetValue(ref value, createPrivateThreads, GuildPermission.CreatePrivateThreads); + Permissions.SetValue(ref value, useExternalStickers, GuildPermission.UseExternalStickers); + Permissions.SetValue(ref value, sendMessagesInThreads, GuildPermission.SendMessagesInThreads); + Permissions.SetValue(ref value, startEmbeddedActivities, GuildPermission.StartEmbeddedActivities); RawValue = value; } - /// Creates a new GuildPermissions with the provided permissions. + /// Creates a new structure with the provided permissions. public GuildPermissions( bool createInstantInvite = false, bool kickMembers = false, @@ -162,6 +207,7 @@ namespace Discord bool manageGuild = false, bool addReactions = false, bool viewAuditLog = false, + bool viewGuildInsights = false, bool viewChannel = false, bool sendMessages = false, bool sendTTSMessages = false, @@ -178,11 +224,21 @@ namespace Discord bool moveMembers = false, bool useVoiceActivation = false, bool prioritySpeaker = false, + bool stream = false, bool changeNickname = false, bool manageNicknames = false, bool manageRoles = false, bool manageWebhooks = false, - bool manageEmojis = false) + bool manageEmojisAndStickers = false, + bool useApplicationCommands = false, + bool requestToSpeak = false, + bool manageEvents = false, + bool manageThreads = false, + bool createPublicThreads = false, + bool createPrivateThreads = false, + bool useExternalStickers = false, + bool sendMessagesInThreads = false, + bool startEmbeddedActivities = false) : this(0, createInstantInvite: createInstantInvite, manageRoles: manageRoles, @@ -193,6 +249,7 @@ namespace Discord manageGuild: manageGuild, addReactions: addReactions, viewAuditLog: viewAuditLog, + viewGuildInsights: viewGuildInsights, viewChannel: viewChannel, sendMessages: sendMessages, sendTTSMessages: sendTTSMessages, @@ -209,13 +266,23 @@ namespace Discord moveMembers: moveMembers, useVoiceActivation: useVoiceActivation, prioritySpeaker: prioritySpeaker, + stream: stream, changeNickname: changeNickname, manageNicknames: manageNicknames, manageWebhooks: manageWebhooks, - manageEmojis: manageEmojis) + manageEmojisAndStickers: manageEmojisAndStickers, + useApplicationCommands: useApplicationCommands, + requestToSpeak: requestToSpeak, + manageEvents: manageEvents, + manageThreads: manageThreads, + createPublicThreads: createPublicThreads, + createPrivateThreads: createPrivateThreads, + useExternalStickers: useExternalStickers, + sendMessagesInThreads: sendMessagesInThreads, + startEmbeddedActivities: startEmbeddedActivities) { } - /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. + /// Creates a new from this one, changing the provided non-null permissions. public GuildPermissions Modify( bool? createInstantInvite = null, bool? kickMembers = null, @@ -225,6 +292,7 @@ namespace Discord bool? manageGuild = null, bool? addReactions = null, bool? viewAuditLog = null, + bool? viewGuildInsights = null, bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, @@ -241,18 +309,41 @@ namespace Discord bool? moveMembers = null, bool? useVoiceActivation = null, bool? prioritySpeaker = null, + bool? stream = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, - bool? manageEmojis = null) + bool? manageEmojisAndStickers = null, + bool? useApplicationCommands = null, + bool? requestToSpeak = null, + bool? manageEvents = null, + bool? manageThreads = null, + bool? createPublicThreads = null, + bool? createPrivateThreads = null, + bool? useExternalStickers = null, + bool? sendMessagesInThreads = null, + bool? startEmbeddedActivities = null) => new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions, - viewAuditLog, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, + viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, - useVoiceActivation, prioritySpeaker, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojis); + useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers, + useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads, + startEmbeddedActivities); + /// + /// Returns a value that indicates if a specific is enabled + /// in these permissions. + /// + /// The permission value to check for. + /// true if the permission is enabled, false otherwise. public bool Has(GuildPermission permission) => Permissions.GetValue(RawValue, permission); + /// + /// Returns a containing all of the + /// flags that are enabled. + /// + /// A containing flags. Empty if none are enabled. public List ToList() { var perms = new List(); @@ -268,6 +359,18 @@ namespace Discord return perms; } + internal void Ensure(GuildPermission permissions) + { + if (!Has(permissions)) + { + var vals = Enum.GetValues(typeof(GuildPermission)).Cast(); + var currentValues = RawValue; + var missingValues = vals.Where(x => permissions.HasFlag(x) && !Permissions.GetValue(currentValues, x)); + + throw new InvalidOperationException($"Missing required guild permission{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } + public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } diff --git a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs index bda67a870..f8f3fff44 100644 --- a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs +++ b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs @@ -1,15 +1,26 @@ -namespace Discord +namespace Discord { + /// + /// Represent a permission object. + /// public struct Overwrite { - /// Gets the unique identifier for the object this overwrite is targeting. + /// + /// Gets the unique identifier for the object this overwrite is targeting. + /// public ulong TargetId { get; } - /// Gets the type of object this overwrite is targeting. + /// + /// Gets the type of object this overwrite is targeting. + /// public PermissionTarget TargetType { get; } - /// Gets the permissions associated with this overwrite entry. + /// + /// Gets the permissions associated with this overwrite entry. + /// public OverwritePermissions Permissions { get; } - /// Creates a new Overwrite with provided target information and modified permissions. + /// + /// Initializes a new with provided target information and modified permissions. + /// public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions) { TargetId = targetId; diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index b8b4b83e2..0e634ad1a 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -1,24 +1,40 @@ +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics; namespace Discord { + /// + /// Represents a container for a series of overwrite permissions. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct OverwritePermissions { - /// Gets a blank OverwritePermissions that inherits all permissions. + /// + /// Gets a blank that inherits all permissions. + /// public static OverwritePermissions InheritAll { get; } = new OverwritePermissions(); - /// Gets a OverwritePermissions that grants all permissions for a given channelType. + /// + /// Gets a that grants all permissions for the given channel. + /// + /// Unknown channel type. public static OverwritePermissions AllowAll(IChannel channel) => new OverwritePermissions(ChannelPermissions.All(channel).RawValue, 0); - /// Gets a OverwritePermissions that denies all permissions for a given channelType. + /// + /// Gets a that denies all permissions for the given channel. + /// + /// Unknown channel type. public static OverwritePermissions DenyAll(IChannel channel) => new OverwritePermissions(0, ChannelPermissions.All(channel).RawValue); - /// Gets a packed value representing all the allowed permissions in this OverwritePermissions. + /// + /// Gets a packed value representing all the allowed permissions in this . + /// public ulong AllowValue { get; } - /// Gets a packed value representing all the denied permissions in this OverwritePermissions. + /// + /// Gets a packed value representing all the denied permissions in this . + /// public ulong DenyValue { get; } /// If Allowed, a user may create invites. @@ -28,9 +44,6 @@ namespace Discord /// If Allowed, a user may add reactions. public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public PermValue ReadMessages => ViewChannel; - /// If Allowed, a user may join channels. public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); /// If Allowed, a user may send messages. public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); @@ -61,11 +74,31 @@ namespace Discord public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); /// If Allowed, a user may use voice-activity-detection rather than push-to-talk. public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); + /// If Allowed, a user may use priority speaker in a voice channel. + public PermValue PrioritySpeaker => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.PrioritySpeaker); + /// If Allowed, a user may go live in a voice channel. + public PermValue Stream => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Stream); - /// If Allowed, a user may adjust role permissions. This also implictly grants all other permissions. + /// If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); /// If True, a user may edit the webhooks for this channel. public PermValue ManageWebhooks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageWebhooks); + /// If true, a user may use slash commands in this guild. + public PermValue UseApplicationCommands => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseApplicationCommands); + /// If true, a user may request to speak in stage channels. + public PermValue RequestToSpeak => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.RequestToSpeak); + /// If true, a user may manage threads in this guild. + public PermValue ManageThreads => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageThreads); + /// If true, a user may create public threads in this guild. + public PermValue CreatePublicThreads => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreatePublicThreads); + /// If true, a user may create private threads in this guild. + public PermValue CreatePrivateThreads => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreatePrivateThreads); + /// If true, a user may use external stickers in this guild. + public PermValue UseExternalStickers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseExternalStickers); + /// If true, a user may send messages in threads in this guild. + public PermValue SendMessagesInThreads => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessagesInThreads); + /// If true, a user launch application activities in voice channels in this guild. + public PermValue StartEmbeddedActivities => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.StartEmbeddedActivities); /// Creates a new OverwritePermissions with the provided allow and deny packed values. public OverwritePermissions(ulong allowValue, ulong denyValue) @@ -74,6 +107,13 @@ namespace Discord DenyValue = denyValue; } + /// Creates a new OverwritePermissions with the provided allow and deny packed values after converting to ulong. + public OverwritePermissions(string allowValue, string denyValue) + { + AllowValue = ulong.Parse(allowValue); + DenyValue = ulong.Parse(denyValue); + } + private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, @@ -94,7 +134,20 @@ namespace Discord PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, - PermValue? manageWebhooks = null) + PermValue? manageWebhooks = null, + PermValue? prioritySpeaker = null, + PermValue? stream = null, + PermValue? useSlashCommands = null, + PermValue? useApplicationCommands = null, + PermValue? requestToSpeak = null, + PermValue? manageThreads = null, + PermValue? createPublicThreads = null, + PermValue? createPrivateThreads = null, + PermValue? usePublicThreads = null, + PermValue? usePrivateThreads = null, + PermValue? useExternalStickers = null, + PermValue? sendMessagesInThreads = null, + PermValue? startEmbeddedActivities = null) { Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); @@ -114,14 +167,26 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref allowValue, ref denyValue, prioritySpeaker, ChannelPermission.PrioritySpeaker); + Permissions.SetValue(ref allowValue, ref denyValue, stream, ChannelPermission.Stream); Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); + Permissions.SetValue(ref allowValue, ref denyValue, useApplicationCommands, ChannelPermission.UseApplicationCommands); + Permissions.SetValue(ref allowValue, ref denyValue, requestToSpeak, ChannelPermission.RequestToSpeak); + Permissions.SetValue(ref allowValue, ref denyValue, manageThreads, ChannelPermission.ManageThreads); + Permissions.SetValue(ref allowValue, ref denyValue, createPublicThreads, ChannelPermission.CreatePublicThreads); + Permissions.SetValue(ref allowValue, ref denyValue, createPrivateThreads, ChannelPermission.CreatePrivateThreads); + Permissions.SetValue(ref allowValue, ref denyValue, useExternalStickers, ChannelPermission.UseExternalStickers); + Permissions.SetValue(ref allowValue, ref denyValue, sendMessagesInThreads, ChannelPermission.SendMessagesInThreads); + Permissions.SetValue(ref allowValue, ref denyValue, startEmbeddedActivities, ChannelPermission.StartEmbeddedActivities); AllowValue = allowValue; DenyValue = denyValue; } - /// Creates a new ChannelPermissions with the provided permissions. + /// + /// Initializes a new struct with the provided permissions. + /// public OverwritePermissions( PermValue createInstantInvite = PermValue.Inherit, PermValue manageChannel = PermValue.Inherit, @@ -142,12 +207,30 @@ namespace Discord PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue manageRoles = PermValue.Inherit, - PermValue manageWebhooks = PermValue.Inherit) + PermValue manageWebhooks = PermValue.Inherit, + PermValue prioritySpeaker = PermValue.Inherit, + PermValue stream = PermValue.Inherit, + PermValue useSlashCommands = PermValue.Inherit, + PermValue useApplicationCommands = PermValue.Inherit, + PermValue requestToSpeak = PermValue.Inherit, + PermValue manageThreads = PermValue.Inherit, + PermValue createPublicThreads = PermValue.Inherit, + PermValue createPrivateThreads = PermValue.Inherit, + PermValue usePublicThreads = PermValue.Inherit, + PermValue usePrivateThreads = PermValue.Inherit, + PermValue useExternalStickers = PermValue.Inherit, + PermValue sendMessagesInThreads = PermValue.Inherit, + PermValue startEmbeddedActivities = PermValue.Inherit) : this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } + moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream, useSlashCommands, useApplicationCommands, + requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, usePublicThreads, usePrivateThreads, useExternalStickers, + sendMessagesInThreads, startEmbeddedActivities) { } - /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. + /// + /// Initializes a new from the current one, changing the provided + /// non-null permissions. + /// public OverwritePermissions Modify( PermValue? createInstantInvite = null, PermValue? manageChannel = null, @@ -168,11 +251,30 @@ namespace Discord PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, - PermValue? manageWebhooks = null) + PermValue? manageWebhooks = null, + PermValue? prioritySpeaker = null, + PermValue? stream = null, + PermValue? useSlashCommands = null, + PermValue? useApplicationCommands = null, + PermValue? requestToSpeak = null, + PermValue? manageThreads = null, + PermValue? createPublicThreads = null, + PermValue? createPrivateThreads = null, + PermValue? usePublicThreads = null, + PermValue? usePrivateThreads = null, + PermValue? useExternalStickers = null, + PermValue? sendMessagesInThreads = null, + PermValue? startEmbeddedActivities = null) => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, manageRoles, manageWebhooks); + moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream, useSlashCommands, useApplicationCommands, + requestToSpeak, manageThreads, createPublicThreads, createPrivateThreads, usePublicThreads, usePrivateThreads, useExternalStickers, + sendMessagesInThreads, startEmbeddedActivities); + /// + /// Creates a of all the values that are allowed. + /// + /// A of all allowed flags. If none, the list will be empty. public List ToAllowList() { var perms = new List(); @@ -185,6 +287,11 @@ namespace Discord } return perms; } + + /// + /// Creates a of all the values that are denied. + /// + /// A of all denied flags. If none, the list will be empty. public List ToDenyList() { var perms = new List(); diff --git a/src/Discord.Net.Core/Entities/Permissions/PermValue.cs b/src/Discord.Net.Core/Entities/Permissions/PermValue.cs index fe048b016..6cea8270d 100644 --- a/src/Discord.Net.Core/Entities/Permissions/PermValue.cs +++ b/src/Discord.Net.Core/Entities/Permissions/PermValue.cs @@ -1,9 +1,13 @@ -namespace Discord +namespace Discord { + /// Specifies the permission value. public enum PermValue { + /// Allows this permission. Allow, + /// Denies this permission. Deny, + /// Inherits the permission settings. Inherit } } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 727049dcc..ee50710e8 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -1,58 +1,85 @@ using System; using System.Diagnostics; -#if NETSTANDARD2_0 || NET45 using StandardColor = System.Drawing.Color; -#endif namespace Discord { + /// + /// Represents a color used in Discord. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Color { + /// Gets the max decimal value of color. + public const uint MaxDecimalValue = 0xFFFFFF; /// Gets the default user color value. - public static readonly Color Default = new Color(0); - /// Gets the teal color value - public static readonly Color Teal = new Color(0x1ABC9C); - /// Gets the dark teal color value - public static readonly Color DarkTeal = new Color(0x11806A); - /// Gets the green color value - public static readonly Color Green = new Color(0x2ECC71); - /// Gets the dark green color value - public static readonly Color DarkGreen = new Color(0x1F8B4C); - /// Gets the blue color value - public static readonly Color Blue = new Color(0x3498DB); - /// Gets the dark blue color value - public static readonly Color DarkBlue = new Color(0x206694); - /// Gets the purple color value - public static readonly Color Purple = new Color(0x9B59B6); - /// Gets the dark purple color value - public static readonly Color DarkPurple = new Color(0x71368A); - /// Gets the magenta color value - public static readonly Color Magenta = new Color(0xE91E63); - /// Gets the dark magenta color value - public static readonly Color DarkMagenta = new Color(0xAD1457); - /// Gets the gold color value - public static readonly Color Gold = new Color(0xF1C40F); - /// Gets the light orange color value - public static readonly Color LightOrange = new Color(0xC27C0E); - /// Gets the orange color value - public static readonly Color Orange = new Color(0xE67E22); - /// Gets the dark orange color value - public static readonly Color DarkOrange = new Color(0xA84300); - /// Gets the red color value - public static readonly Color Red = new Color(0xE74C3C); - /// Gets the dark red color value - public static readonly Color DarkRed = new Color(0x992D22); - /// Gets the light grey color value - public static readonly Color LightGrey = new Color(0x979C9F); - /// Gets the lighter grey color value - public static readonly Color LighterGrey = new Color(0x95A5A6); - /// Gets the dark grey color value - public static readonly Color DarkGrey = new Color(0x607D8B); - /// Gets the darker grey color value - public static readonly Color DarkerGrey = new Color(0x546E7A); + public static readonly Color Default = new(0); + /// Gets the teal color value. + /// A color struct with the hex value of 1ABC9C. + public static readonly Color Teal = new(0x1ABC9C); + /// Gets the dark teal color value. + /// A color struct with the hex value of 11806A. + public static readonly Color DarkTeal = new(0x11806A); + /// Gets the green color value. + /// A color struct with the hex value of 2ECC71. + public static readonly Color Green = new(0x2ECC71); + /// Gets the dark green color value. + /// A color struct with the hex value of 1F8B4C. + public static readonly Color DarkGreen = new(0x1F8B4C); + /// Gets the blue color value. + /// A color struct with the hex value of 3498DB. + public static readonly Color Blue = new(0x3498DB); + /// Gets the dark blue color value. + /// A color struct with the hex value of 206694. + public static readonly Color DarkBlue = new(0x206694); + /// Gets the purple color value. + /// A color struct with the hex value of 9B59B6. + public static readonly Color Purple = new(0x9B59B6); + /// Gets the dark purple color value. + /// A color struct with the hex value of 71368A. + public static readonly Color DarkPurple = new(0x71368A); + /// Gets the magenta color value. + /// A color struct with the hex value of E91E63. + public static readonly Color Magenta = new(0xE91E63); + /// Gets the dark magenta color value. + /// A color struct with the hex value of AD1457. + public static readonly Color DarkMagenta = new(0xAD1457); + /// Gets the gold color value. + /// A color struct with the hex value of F1C40F. + public static readonly Color Gold = new(0xF1C40F); + /// Gets the light orange color value. + /// A color struct with the hex value of C27C0E. + public static readonly Color LightOrange = new(0xC27C0E); + /// Gets the orange color value. + /// A color struct with the hex value of E67E22. + public static readonly Color Orange = new(0xE67E22); + /// Gets the dark orange color value. + /// A color struct with the hex value of A84300. + public static readonly Color DarkOrange = new(0xA84300); + /// Gets the red color value. + /// A color struct with the hex value of E74C3C. + public static readonly Color Red = new(0xE74C3C); + /// Gets the dark red color value. + /// A color struct with the hex value of 992D22. + public static readonly Color DarkRed = new(0x992D22); + /// Gets the light grey color value. + /// A color struct with the hex value of 979C9F. + public static readonly Color LightGrey = new(0x979C9F); + /// Gets the lighter grey color value. + /// A color struct with the hex value of 95A5A6. + public static readonly Color LighterGrey = new(0x95A5A6); + /// Gets the dark grey color value. + /// A color struct with the hex value of 607D8B. + public static readonly Color DarkGrey = new(0x607D8B); + /// Gets the darker grey color value. + /// A color struct with the hex value of 546E7A. + public static readonly Color DarkerGrey = new(0x546E7A); /// Gets the encoded value for this color. + /// + /// This value is encoded as an unsigned integer value. The most-significant 8 bits contain the red value, + /// the middle 8 bits contain the green value, and the least-significant 8 bits contain the blue value. + /// public uint RawValue { get; } /// Gets the red component for this color. @@ -62,42 +89,103 @@ namespace Discord /// Gets the blue component for this color. public byte B => (byte)(RawValue); + /// + /// Initializes a struct with the given raw value. + /// + /// + /// The following will create a color that has a hex value of + /// #607D8B. + /// + /// Color darkGrey = new Color(0x607D8B); + /// + /// + /// The raw value of the color (e.g. 0x607D8B). + /// Value exceeds . public Color(uint rawValue) { + if (rawValue > MaxDecimalValue) + throw new ArgumentException($"{nameof(RawValue)} of color cannot be greater than {MaxDecimalValue}!", nameof(rawValue)); + RawValue = rawValue; } + + /// + /// Initializes a struct with the given RGB bytes. + /// + /// + /// The following will create a color that has a value of + /// #607D8B. + /// + /// Color darkGrey = new Color((byte)0b_01100000, (byte)0b_01111101, (byte)0b_10001011); + /// + /// + /// The byte that represents the red color. + /// The byte that represents the green color. + /// The byte that represents the blue color. + /// Value exceeds . public Color(byte r, byte g, byte b) { - RawValue = - ((uint)r << 16) | - ((uint)g << 8) | - (uint)b; + uint value = ((uint)r << 16) + | ((uint)g << 8) + | (uint)b; + + if (value > MaxDecimalValue) + throw new ArgumentException($"{nameof(RawValue)} of color cannot be greater than {MaxDecimalValue}!"); + + RawValue = value; } + + /// + /// Initializes a struct with the given RGB value. + /// + /// + /// The following will create a color that has a value of + /// #607D8B. + /// + /// Color darkGrey = new Color(96, 125, 139); + /// + /// + /// The value that represents the red color. Must be within 0~255. + /// The value that represents the green color. Must be within 0~255. + /// The value that represents the blue color. Must be within 0~255. + /// The argument value is not between 0 to 255. public Color(int r, int g, int b) { if (r < 0 || r > 255) - throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]."); if (g < 0 || g > 255) - throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]."); if (b < 0 || b > 255) - throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]"); - RawValue = - ((uint)r << 16) | - ((uint)g << 8) | - (uint)b; + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]."); + RawValue = ((uint)r << 16) + | ((uint)g << 8) + | (uint)b; } + /// + /// Initializes a struct with the given RGB float value. + /// + /// + /// The following will create a color that has a value of + /// #607c8c. + /// + /// Color darkGrey = new Color(0.38f, 0.49f, 0.55f); + /// + /// + /// The value that represents the red color. Must be within 0~1. + /// The value that represents the green color. Must be within 0~1. + /// The value that represents the blue color. Must be within 0~1. + /// The argument value is not between 0 to 1. public Color(float r, float g, float b) { if (r < 0.0f || r > 1.0f) - throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]."); if (g < 0.0f || g > 1.0f) - throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]."); if (b < 0.0f || b > 1.0f) - throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]"); - RawValue = - ((uint)(r * 255.0f) << 16) | - ((uint)(g * 255.0f) << 8) | - (uint)(b * 255.0f); + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]."); + RawValue = ((uint)(r * 255.0f) << 16) + | ((uint)(g * 255.0f) << 8) + | (uint)(b * 255.0f); } public static bool operator ==(Color lhs, Color rhs) @@ -106,21 +194,32 @@ namespace Discord public static bool operator !=(Color lhs, Color rhs) => lhs.RawValue != rhs.RawValue; + public static implicit operator Color(uint rawValue) + => new(rawValue); + + public static implicit operator uint(Color color) + => color.RawValue; + public override bool Equals(object obj) - => (obj is Color c && RawValue == c.RawValue); + => obj is Color c && RawValue == c.RawValue; public override int GetHashCode() => RawValue.GetHashCode(); -#if NETSTANDARD2_0 || NET45 - public static implicit operator StandardColor(Color color) => - StandardColor.FromArgb((int)color.RawValue); - public static explicit operator Color(StandardColor color) => - new Color((uint)color.ToArgb() << 8 >> 8); -#endif + public static implicit operator StandardColor(Color color) + => StandardColor.FromArgb((int)color.RawValue); + + public static explicit operator Color(StandardColor color) + => new((uint)color.ToArgb() << 8 >> 8); + /// + /// Gets the hexadecimal representation of the color (e.g. #000ccc). + /// + /// + /// A hexadecimal string of the color. + /// public override string ToString() => - $"#{Convert.ToString(RawValue, 16)}"; + string.Format("#{0:X6}", RawValue); private string DebuggerDisplay => - $"#{Convert.ToString(RawValue, 16)} ({RawValue})"; + string.Format("#{0:X6} ({0})", RawValue); } } diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index c40e0d716..59ca41e31 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -1,29 +1,112 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic role object to be given to a guild user. + /// public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable { - /// Gets the guild owning this role. + /// + /// Gets the guild that owns this role. + /// + /// + /// A guild representing the parent guild of this role. + /// IGuild Guild { get; } - /// Gets the color given to users of this role. + /// + /// Gets the color given to users of this role. + /// + /// + /// A struct representing the color of this role. + /// Color Color { get; } - /// Returns true if users of this role are separated in the user list. + /// + /// Gets a value that indicates whether the role can be separated in the user list. + /// + /// + /// true if users of this role are separated in the user list; otherwise false. + /// bool IsHoisted { get; } - /// Returns true if this role is automatically managed by Discord. + /// + /// Gets a value that indicates whether the role is managed by Discord. + /// + /// + /// true if this role is automatically managed by Discord; otherwise false. + /// bool IsManaged { get; } - /// Returns true if this role may be mentioned in messages. + /// + /// Gets a value that indicates whether the role is mentionable. + /// + /// + /// true if this role may be mentioned in messages; otherwise false. + /// bool IsMentionable { get; } - /// Gets the name of this role. + /// + /// Gets the name of this role. + /// + /// + /// A string containing the name of this role. + /// string Name { get; } - /// Gets the permissions granted to members of this role. + /// + /// Gets the icon of this role. + /// + /// + /// A string containing the hash of this role's icon. + /// + string Icon { get; } + /// + /// Gets the unicode emoji of this role. + /// + /// + /// This field is mutually exclusive with , either icon is set or emoji is set. + /// + Emoji Emoji { get; } + /// + /// Gets the permissions granted to members of this role. + /// + /// + /// A struct that this role possesses. + /// GuildPermissions Permissions { get; } - /// Gets this role's position relative to other roles in the same guild. + /// + /// Gets this role's position relative to other roles in the same guild. + /// + /// + /// An representing the position of the role in the role list of the guild. + /// int Position { get; } + /// + /// Gets the tags related to this role. + /// + /// + /// A object containing all tags related to this role. + /// + RoleTags Tags { get; } - ///// Modifies this role. + /// + /// Modifies this role. + /// + /// + /// This method modifies this role with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// A delegate containing the properties to modify the role with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Gets the image url of the icon role. + /// + /// + /// An image url of the icon role. + /// + string GetIconUrl(); } } diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs index 0c8afa24c..0074c0a3b 100644 --- a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -1,12 +1,30 @@ -namespace Discord +namespace Discord { + /// + /// Properties that are used to reorder an . + /// public class ReorderRoleProperties { - /// The id of the role to be edited + /// + /// Gets the identifier of the role to be edited. + /// + /// + /// A representing the snowflake identifier of the role to be modified. + /// public ulong Id { get; } - /// The new zero-based position of the role. + /// + /// Gets the new zero-based position of the role. + /// + /// + /// An representing the new zero-based position of the role. + /// public int Position { get; } + /// + /// Initializes a with the given role ID and position. + /// + /// The ID of the role to be edited. + /// The new zero-based position of the role. public ReorderRoleProperties(ulong id, int pos) { Id = id; diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index 8950a2634..93cda8d5b 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -1,57 +1,65 @@ -namespace Discord +namespace Discord { /// - /// Modify an IRole with the specified parameters + /// Properties that are used to modify an with the specified changes. /// /// - /// - /// await role.ModifyAsync(x => - /// { - /// x.Color = new Color(180, 15, 40); - /// x.Hoist = true; - /// }); - /// + /// The following example modifies the role to a mentionable one, renames the role into Sonic, and + /// changes the color to a light-blue. + /// + /// await role.ModifyAsync(x => + /// { + /// x.Name = "Sonic"; + /// x.Color = new Color(0x1A50BC); + /// x.Mentionable = true; + /// }); + /// /// - /// + /// public class RoleProperties { /// - /// The name of the role + /// Gets or sets the name of the role. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// This value may not be set if the role is an @everyone role. /// public Optional Name { get; set; } /// - /// The role's GuildPermissions + /// Gets or sets the role's . /// public Optional Permissions { get; set; } /// - /// The position of the role. This is 0-based! + /// Gets or sets the position of the role. This is 0-based! /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// This value may not be set if the role is an @everyone role. /// public Optional Position { get; set; } /// - /// The color of the Role. + /// Gets or sets the color of the role. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// This value may not be set if the role is an @everyone role. /// public Optional Color { get; set; } /// - /// Whether or not this role should be displayed independently in the userlist. + /// Gets or sets whether or not this role should be displayed independently in the user list. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// This value may not be set if the role is an @everyone role. /// public Optional Hoist { get; set; } + /// - /// Whether or not this role can be mentioned. + /// Gets or sets the icon of the role. + /// + public Optional Icon { get; set; } + /// + /// Gets or sets whether or not this role can be mentioned. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// This value may not be set if the role is an @everyone role. /// public Optional Mentionable { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Roles/RoleTags.cs b/src/Discord.Net.Core/Entities/Roles/RoleTags.cs new file mode 100644 index 000000000..d0cbd3580 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/RoleTags.cs @@ -0,0 +1,40 @@ +namespace Discord +{ + /// + /// Provides tags related to a discord role. + /// + public class RoleTags + { + /// + /// Gets the identifier of the bot that this role belongs to, if it does. + /// + /// + /// A if this role belongs to a bot; otherwise + /// . + /// + public ulong? BotId { get; } + /// + /// Gets the identifier of the integration that this role belongs to, if it does. + /// + /// + /// A if this role belongs to an integration; otherwise + /// . + /// + public ulong? IntegrationId { get; } + /// + /// Gets if this role is the guild's premium subscriber (booster) role. + /// + /// + /// if this role is the guild's premium subscriber role; + /// otherwise . + /// + public bool IsPremiumSubscriberRole { get; } + + internal RoleTags(ulong? botId, ulong? integrationId, bool isPremiumSubscriber) + { + BotId = botId; + IntegrationId = integrationId; + IsPremiumSubscriberRole = isPremiumSubscriber; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs b/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs new file mode 100644 index 000000000..9cba38c80 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/ICustomSticker.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a custom sticker within a guild. + /// + public interface ICustomSticker : ISticker + { + /// + /// Gets the users id who uploaded the sticker. + /// + /// + /// In order to get the author id, the bot needs the MANAGE_EMOJIS_AND_STICKERS permission. + /// + ulong? AuthorId { get; } + + /// + /// Gets the guild that this custom sticker is in. + /// + IGuild Guild { get; } + + /// + /// Modifies this sticker. + /// + /// + /// This method modifies this sticker with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + ///
+ ///
+ /// The bot needs the MANAGE_EMOJIS_AND_STICKERS permission within the guild in order to modify stickers. + ///
+ /// + /// The following example replaces the name of the sticker with kekw. + /// + /// await sticker.ModifyAsync(x => x.Name = "kekw"); + /// + /// + /// A delegate containing the properties to modify the sticker with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Deletes the current sticker. + /// + /// + /// The bot needs the MANAGE_EMOJIS_AND_STICKERS permission inside the guild in order to delete stickers. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous deletion operation. + /// + Task DeleteAsync(RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/ISticker.cs b/src/Discord.Net.Core/Entities/Stickers/ISticker.cs new file mode 100644 index 000000000..9deea753f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/ISticker.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a discord sticker. + /// + public interface ISticker : IStickerItem + { + /// + /// Gets the ID of this sticker. + /// + /// + /// A snowflake ID associated with this sticker. + /// + new ulong Id { get; } + /// + /// Gets the ID of the pack of this sticker. + /// + /// + /// A snowflake ID associated with the pack of this sticker. + /// + ulong PackId { get; } + /// + /// Gets the name of this sticker. + /// + /// + /// A with the name of this sticker. + /// + new string Name { get; } + /// + /// Gets the description of this sticker. + /// + /// + /// A with the description of this sticker. + /// + string Description { get; } + /// + /// Gets the list of tags of this sticker. + /// + /// + /// A read-only list with the tags of this sticker. + /// + IReadOnlyCollection Tags { get; } + /// + /// Gets the type of this sticker. + /// + StickerType Type { get; } + /// + /// Gets the format type of this sticker. + /// + /// + /// A with the format type of this sticker. + /// + new StickerFormatType Format { get; } + + /// + /// Gets whether this guild sticker can be used, may be false due to loss of Server Boosts. + /// + bool? IsAvailable { get; } + + /// + /// Gets the standard sticker's sort order within its pack. + /// + int? SortOrder { get; } + /// + /// Gets the image url for this sticker. + /// + string GetStickerUrl(); + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/IStickerItem.cs b/src/Discord.Net.Core/Entities/Stickers/IStickerItem.cs new file mode 100644 index 000000000..07ea63db9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/IStickerItem.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Represents a partial sticker item received with a message. + /// + public interface IStickerItem + { + /// + /// The id of the sticker. + /// + ulong Id { get; } + + /// + /// The name of the sticker. + /// + string Name { get; } + + /// + /// The format of the sticker. + /// + StickerFormatType Format { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/StickerPack.cs b/src/Discord.Net.Core/Entities/Stickers/StickerPack.cs new file mode 100644 index 000000000..c0c90aa69 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/StickerPack.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord +{ + /// + /// Represents a discord sticker pack. + /// + /// The type of the stickers within the collection. + public class StickerPack where TSticker : ISticker + { + /// + /// Gets the id of the sticker pack. + /// + public ulong Id { get; } + + /// + /// Gets a collection of the stickers in the pack. + /// + public IReadOnlyCollection Stickers { get; } + + /// + /// Gets the name of the sticker pack. + /// + public string Name { get; } + + /// + /// Gets the id of the pack's SKU. + /// + public ulong SkuId { get; } + + /// + /// Gets the id of a sticker in the pack which is shown as the pack's icon. + /// + public ulong? CoverStickerId { get; } + + /// + /// Gets the description of the sticker pack. + /// + public string Description { get; } + + /// + /// Gets the id of the sticker pack's banner image + /// + public ulong BannerAssetId { get; } + + internal StickerPack(string name, ulong id, ulong skuid, ulong? coverStickerId, string description, ulong bannerAssetId, IEnumerable stickers) + { + Name = name; + Id = id; + SkuId = skuid; + CoverStickerId = coverStickerId; + Description = description; + BannerAssetId = bannerAssetId; + + Stickers = stickers.ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs b/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs new file mode 100644 index 000000000..5f51e5f3d --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/StickerProperties.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a class used to modify stickers. + /// + public class StickerProperties + { + /// + /// Gets or sets the name of the sticker. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the description of the sticker. + /// + public Optional Description { get; set; } + + /// + /// Gets or sets the tags of the sticker. + /// + public Optional> Tags { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Stickers/StickerType.cs b/src/Discord.Net.Core/Entities/Stickers/StickerType.cs new file mode 100644 index 000000000..0db550772 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Stickers/StickerType.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Represents a type of sticker.. + /// + public enum StickerType + { + /// + /// Represents a discord standard sticker, this type of sticker cannot be modified by an application. + /// + Standard = 1, + + /// + /// Represents a sticker that was created within a guild. + /// + Guild = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Teams/ITeam.cs b/src/Discord.Net.Core/Entities/Teams/ITeam.cs new file mode 100644 index 000000000..b6e3d987b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Teams/ITeam.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a Discord Team. + /// + public interface ITeam + { + /// + /// Gets the team icon url. + /// + string IconUrl { get; } + /// + /// Gets the team unique identifier. + /// + ulong Id { get; } + /// + /// Gets the members of this team. + /// + IReadOnlyList TeamMembers { get; } + /// + /// Gets the name of this team. + /// + string Name { get; } + /// + /// Gets the user identifier that owns this team. + /// + ulong OwnerUserId { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Teams/ITeamMember.cs b/src/Discord.Net.Core/Entities/Teams/ITeamMember.cs new file mode 100644 index 000000000..fe0e499e5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Teams/ITeamMember.cs @@ -0,0 +1,25 @@ +namespace Discord +{ + /// + /// Represents a Discord Team member. + /// + public interface ITeamMember + { + /// + /// Gets the membership state of this team member. + /// + MembershipState MembershipState { get; } + /// + /// Gets the permissions of this team member. + /// + string[] Permissions { get; } + /// + /// Gets the team unique identifier for this team member. + /// + ulong TeamId { get; } + /// + /// Gets the Discord user of this team member. + /// + IUser User { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Teams/MembershipState.cs b/src/Discord.Net.Core/Entities/Teams/MembershipState.cs new file mode 100644 index 000000000..45b1693b0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Teams/MembershipState.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + /// + /// Represents the membership state of a team member. + /// + public enum MembershipState + { + Invited, + Accepted, + } +} diff --git a/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs new file mode 100644 index 000000000..e380d9027 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Properties that are used to add a new to the guild with the following parameters. + /// + /// + public class AddGuildUserProperties + { + /// + /// Gets or sets the user's nickname. + /// + /// + /// To clear the user's nickname, this value can be set to null or + /// . + /// + public Optional Nickname { get; set; } + /// + /// Gets or sets whether the user should be muted in a voice channel. + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Gets or sets whether the user should be deafened in a voice channel. + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> Roles { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/ClientType.cs b/src/Discord.Net.Core/Entities/Users/ClientType.cs new file mode 100644 index 000000000..d4afe39f3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/ClientType.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Defines the types of clients a user can be active on. + /// + public enum ClientType + { + /// + /// The user is active using the mobile application. + /// + Mobile, + /// + /// The user is active using the desktop application. + /// + Desktop, + /// + /// The user is active using the web application. + /// + Web + } +} diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 1c5e5482c..8f2d2111e 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -1,71 +1,77 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { /// - /// Modify an IGuildUser with the following parameters. + /// Properties that are used to modify an with the following parameters. /// - /// - /// - /// await (Context.User as IGuildUser)?.ModifyAsync(x => - /// { - /// x.Nickname = $"festive {Context.User.Username}"; - /// }); - /// - /// - /// + /// public class GuildUserProperties { /// - /// Should the user be guild-muted in a voice channel? + /// Gets or sets whether the user should be muted in a voice channel. /// /// - /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// If this value is set to true, no user will be able to hear this user speak in the guild. /// public Optional Mute { get; set; } /// - /// Should the user be guild-deafened in a voice channel? + /// Gets or sets whether the user should be deafened in a voice channel. /// /// - /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. /// public Optional Deaf { get; set; } /// - /// Should the user have a nickname set? + /// Gets or sets the user's nickname. /// /// - /// To clear the user's nickname, this value can be set to or . + /// To clear the user's nickname, this value can be set to null or + /// . /// public Optional Nickname { get; set; } /// - /// What roles should the user have? + /// Gets or sets the roles the user should have. /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// /// public Optional> Roles { get; set; } /// - /// What roles should the user have? + /// Gets or sets the roles the user should have. /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// /// public Optional> RoleIds { get; set; } /// - /// Move a user to a voice channel. + /// Moves a user to a voice channel. If null, this user will be disconnected from their current voice channel. /// /// - /// This user MUST already be in a Voice Channel for this to work. + /// This user MUST already be in a for this to work. + /// When set, this property takes precedence over . /// public Optional Channel { get; set; } /// - /// Move a user to a voice channel. + /// Moves a user to a voice channel. Set to null to disconnect this user from their current voice channel. /// /// - /// This user MUST already be in a Voice Channel for this to work. + /// This user MUST already be in a for this to work. /// - public Optional ChannelId { get; set; } + public Optional ChannelId { get; set; } // TODO: v3 breaking change, change ChannelId to ulong? to allow for kicking users from voice } } diff --git a/src/Discord.Net.Core/Entities/Users/IConnection.cs b/src/Discord.Net.Core/Entities/Users/IConnection.cs index cc981ccf0..1e65d971f 100644 --- a/src/Discord.Net.Core/Entities/Users/IConnection.cs +++ b/src/Discord.Net.Core/Entities/Users/IConnection.cs @@ -1,14 +1,27 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { public interface IConnection { + /// Gets the ID of the connection account. + /// A representing the unique identifier value of this connection. string Id { get; } + /// Gets the service of the connection (twitch, youtube). + /// A string containing the name of this type of connection. string Type { get; } + /// Gets the username of the connection account. + /// A string containing the name of this connection. string Name { get; } + /// Gets whether the connection is revoked. + /// A value which if true indicates that this connection has been revoked, otherwise false. bool IsRevoked { get; } + /// Gets a of integration IDs. + /// + /// An containing + /// representations of unique identifier values of integrations. + /// IReadOnlyCollection IntegrationIds { get; } } } diff --git a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs index dd046a5a8..ecf01f721 100644 --- a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs @@ -1,5 +1,8 @@ -namespace Discord +namespace Discord { + /// + /// Represents a Discord user that is in a group. + /// public interface IGroupUser : IUser, IVoiceState { ///// Kicks this user from this group. diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 57cad1333..947ff8521 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -1,41 +1,215 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { - /// A Guild-User pairing. + /// + /// Represents a generic guild user. + /// public interface IGuildUser : IUser, IVoiceState { - /// Gets when this user joined this guild. + /// + /// Gets when this user joined the guild. + /// + /// + /// A representing the time of which the user has joined the guild; + /// null when it cannot be obtained. + /// DateTimeOffset? JoinedAt { get; } - /// Gets the nickname for this user. + /// + /// Gets the nickname for this user. + /// + /// + /// A string representing the nickname of the user; null if none is set. + /// string Nickname { get; } - /// Gets the guild-level permissions for this user. + /// + /// Gets the guild specific avatar for this users. + /// + /// + /// The users guild avatar hash if they have one; otherwise . + /// + string GuildAvatarId { get; } + /// + /// Gets the guild-level permissions for this user. + /// + /// + /// A structure for this user, representing what + /// permissions this user has in the guild. + /// GuildPermissions GuildPermissions { get; } - /// Gets the guild for this user. + /// + /// Gets the guild for this user. + /// + /// + /// A guild object that this user belongs to. + /// IGuild Guild { get; } - /// Gets the id of the guild for this user. + /// + /// Gets the ID of the guild for this user. + /// + /// + /// An representing the snowflake identifier of the guild that this user belongs to. + /// ulong GuildId { get; } - /// Returns a collection of the ids of the roles this user is a member of in this guild, including the guild's @everyone role. + /// + /// Gets the date and time for when this user's guild boost began. + /// + /// + /// A for when the user began boosting this guild; null if they are not boosting the guild. + /// + DateTimeOffset? PremiumSince { get; } + /// + /// Gets a collection of IDs for the roles that this user currently possesses in the guild. + /// + /// + /// This property returns a read-only collection of the identifiers of the roles that this user possesses. + /// For WebSocket users, a Roles property can be found in place of this property. Due to the REST + /// implementation, only a collection of identifiers can be retrieved instead of the full role objects. + /// + /// + /// A read-only collection of , each representing a snowflake identifier for a role that + /// this user possesses. + /// IReadOnlyCollection RoleIds { get; } - /// Gets the level permissions granted to this user to a given channel. + /// + /// Whether the user has passed the guild's Membership Screening requirements. + /// + bool? IsPending { get; } + + /// + /// Gets the users position within the role hierarchy. + /// + int Hierarchy { get; } + + /// + /// Gets the level permissions granted to this user to a given channel. + /// + /// + /// The following example checks if the current user has the ability to send a message with attachment in + /// this channel; if so, uploads a file via . + /// + /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) + /// await targetChannel.SendFileAsync("fortnite.png"); + /// + /// + /// The channel to get the permission from. + /// + /// A structure representing the permissions that a user has in the + /// specified channel. + /// ChannelPermissions GetPermissions(IGuildChannel channel); - /// Kicks this user from this guild. + /// + /// Gets the guild avatar URL for this user. + /// + /// + /// This property retrieves a URL for this guild user's guild specific avatar. In event that the user does not have a valid guild avatar + /// (i.e. their avatar identifier is not set), this method will return null. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// + /// + /// A string representing the user's avatar URL; null if the user does not have an avatar in place. + /// + string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + /// + /// Kicks this user from this guild. + /// + /// The reason for the kick which will be recorded in the audit log. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous kick operation. + /// Task KickAsync(string reason = null, RequestOptions options = null); - /// Modifies this user's properties in this guild. + /// + /// Modifies this user's properties in this guild. + /// + /// + /// This method modifies the current guild user with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the user with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// Task ModifyAsync(Action func, RequestOptions options = null); - - /// Adds a role to this user in this guild. + /// + /// Adds the specified role to this user in the guild. + /// + /// The role to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRoleAsync(ulong roleId, RequestOptions options = null); + /// + /// Adds the specified role to this user in the guild. + /// + /// The role to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// Task AddRoleAsync(IRole role, RequestOptions options = null); - /// Adds roles to this user in this guild. + /// + /// Adds the specified to this user in the guild. + /// + /// The roles to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null); + /// + /// Adds the specified to this user in the guild. + /// + /// The roles to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); - /// Removes a role from this user in this guild. + /// + /// Removes the specified from this user in the guild. + /// + /// The role to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRoleAsync(ulong roleId, RequestOptions options = null); + /// + /// Removes the specified from this user in the guild. + /// + /// The role to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// Task RemoveRoleAsync(IRole role, RequestOptions options = null); - /// Removes roles from this user in this guild. + /// + /// Removes the specified from this user in the guild. + /// + /// The roles to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null); + /// + /// Removes the specified from this user in the guild. + /// + /// The roles to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 25adcc9c4..45babf481 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -1,10 +1,23 @@ -namespace Discord +using System.Collections.Generic; + +namespace Discord { + /// + /// Represents the user's presence status. This may include their online status and their activity. + /// public interface IPresence { - /// Gets the activity this user is currently doing. - IActivity Activity { get; } - /// Gets the current status of this user. + /// + /// Gets the current status of this user. + /// UserStatus Status { get; } + /// + /// Gets the set of clients where this user is currently active. + /// + IReadOnlyCollection ActiveClients { get; } + /// + /// Gets the list of activities that this user currently has available. + /// + IReadOnlyCollection Activities { get; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs index 7b91d4e3a..04c655212 100644 --- a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -3,15 +3,61 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents the logged-in Discord user. + /// public interface ISelfUser : IUser { - /// Gets the email associated with this user. + /// + /// Gets the email associated with this user. + /// string Email { get; } - /// Returns true if this user's email has been verified. + /// + /// Indicates whether or not this user has their email verified. + /// + /// + /// true if this user's email has been verified; false if not. + /// bool IsVerified { get; } - /// Returns true if this user has enabled MFA on their account. + /// + /// Indicates whether or not this user has MFA enabled on their account. + /// + /// + /// true if this user has enabled multi-factor authentication on their account; false if not. + /// bool IsMfaEnabled { get; } + /// + /// Gets the flags that are applied to a user's account. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of flags for this user. + /// + UserProperties Flags { get; } + /// + /// Gets the type of Nitro subscription that is active on this user's account. + /// + /// + /// This information may only be available with the identify OAuth scope. + /// + /// + /// The type of Nitro subscription the user subscribes to, if any. + /// + PremiumType PremiumType { get; } + /// + /// Gets the user's chosen language option. + /// + /// + /// The IETF language tag of the user's chosen region, if provided. + /// For example, a locale of "English, US" is "en-US", "Chinese (Taiwan)" is "zh-TW", etc. + /// + string Locale { get; } + /// + /// Modifies the user's properties. + /// Task ModifyAsync(Action func, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index c5cce7a25..2f79450f3 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -2,26 +2,115 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic user. + /// public interface IUser : ISnowflakeEntity, IMentionable, IPresence { - /// Gets the id of this user's avatar. - string AvatarId { get; } - /// Gets the url to this user's avatar. + /// + /// Gets the identifier of this user's avatar. + /// + string AvatarId { get; } + /// + /// Gets the avatar URL for this user. + /// + /// + /// This property retrieves a URL for this user's avatar. In event that the user does not have a valid avatar + /// (i.e. their avatar identifier is not set), this method will return null. If you wish to + /// retrieve the default avatar for this user, consider using (see + /// example). + /// + /// + /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is + /// not set, a default avatar for this user will be returned instead. + /// + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// + /// + /// A string representing the user's avatar URL; null if the user does not have an avatar in place. + /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); - /// Gets the url to this user's default avatar. + /// + /// Gets the default avatar URL for this user. + /// + /// + /// This property retrieves a URL for this user's default avatar generated by Discord (Discord logo followed + /// by a random color as its background). This property will always return a value as it is calculated based + /// on the user's (discriminator % 5). + /// + /// + /// A string representing the user's avatar URL. + /// string GetDefaultAvatarUrl(); - /// Gets the per-username unique id for this user. + /// + /// Gets the per-username unique ID for this user. + /// string Discriminator { get; } - /// Gets the per-username unique id for this user. + /// + /// Gets the per-username unique ID for this user. + /// ushort DiscriminatorValue { get; } - /// Returns true if this user is a bot user. + /// + /// Gets a value that indicates whether this user is identified as a bot. + /// + /// + /// This property retrieves a value that indicates whether this user is a registered bot application + /// (indicated by the blue BOT tag within the official chat client). + /// + /// + /// true if the user is a bot application; otherwise false. + /// bool IsBot { get; } - /// Returns true if this user is a webhook user. + /// + /// Gets a value that indicates whether this user is a webhook user. + /// + /// + /// true if the user is a webhook; otherwise false. + /// bool IsWebhook { get; } - /// Gets the username for this user. + /// + /// Gets the username for this user. + /// string Username { get; } + /// + /// Gets the public flags that are applied to this user's account. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of public flags for this user. + /// + UserProperties? PublicFlags { get; } - /// Returns a private message channel to this user, creating one if it does not already exist. - Task GetOrCreateDMChannelAsync(RequestOptions options = null); + /// + /// Creates the direct message channel of this user. + /// + /// + /// This method is used to obtain or create a channel used to send a direct message. + /// + /// In event that the current user cannot send a message to the target user, a channel can and will + /// still be created by Discord. However, attempting to send a message will yield a + /// with a 403 as its + /// . There are currently no official workarounds by + /// Discord. + /// + /// + /// + /// The following example attempts to send a direct message to the target user and logs the incident should + /// it fail. + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for getting or creating a DM channel. The task result + /// contains the DM channel associated with this user. + /// + Task CreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs index 428601f2a..c9a22761f 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -1,20 +1,72 @@ -namespace Discord +using System; + +namespace Discord { + /// + /// Represents a user's voice connection status. + /// public interface IVoiceState { - /// Returns true if the guild has deafened this user. + /// + /// Gets a value that indicates whether this user is deafened by the guild. + /// + /// + /// true if the user is deafened (i.e. not permitted to listen to or speak to others) by the guild; + /// otherwise false. + /// bool IsDeafened { get; } - /// Returns true if the guild has muted this user. + /// + /// Gets a value that indicates whether this user is muted (i.e. not permitted to speak via voice) by the + /// guild. + /// + /// + /// true if this user is muted by the guild; otherwise false. + /// bool IsMuted { get; } - /// Returns true if this user has marked themselves as deafened. + /// + /// Gets a value that indicates whether this user has marked themselves as deafened. + /// + /// + /// true if this user has deafened themselves (i.e. not permitted to listen to or speak to others); otherwise false. + /// bool IsSelfDeafened { get; } - /// Returns true if this user has marked themselves as muted. + /// + /// Gets a value that indicates whether this user has marked themselves as muted (i.e. not permitted to + /// speak via voice). + /// + /// + /// true if this user has muted themselves; otherwise false. + /// bool IsSelfMuted { get; } - /// Returns true if the guild is temporarily blocking audio to/from this user. + /// + /// Gets a value that indicates whether the user is muted by the current user. + /// + /// + /// true if the guild is temporarily blocking audio to/from this user; otherwise false. + /// bool IsSuppressed { get; } - /// Gets the voice channel this user is currently in, if any. + /// + /// Gets the voice channel this user is currently in. + /// + /// + /// A generic voice channel object representing the voice channel that the user is currently in; null + /// if none. + /// IVoiceChannel VoiceChannel { get; } - /// Gets the unique identifier for this user's voice session. + /// + /// Gets the unique identifier for this user's voice session. + /// string VoiceSessionId { get; } + /// + /// Gets a value that indicates if this user is streaming in a voice channel. + /// + /// + /// true if the user is streaming; otherwise false. + /// + bool IsStreaming { get; } + /// + /// Gets the time on which the user requested to speak. + /// + DateTimeOffset? RequestToSpeakTimestamp { get; } } } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index be769b944..7a10c6b6b 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,7 +1,9 @@ -namespace Discord +namespace Discord { + /// Represents a Webhook Discord user. public interface IWebhookUser : IGuildUser { + /// Gets the ID of a webhook. ulong WebhookId { get; } } } diff --git a/src/Discord.Net.Core/Entities/Users/PremiumType.cs b/src/Discord.Net.Core/Entities/Users/PremiumType.cs new file mode 100644 index 000000000..2b41e0b6a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/PremiumType.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Specifies the type of subscription a user is subscribed to. + /// + public enum PremiumType + { + /// + /// No subscription. + /// + None = 0, + /// + /// Nitro Classic subscription. Includes app perks like animated emojis and avatars, but not games. + /// + NitroClassic = 1, + /// + /// Nitro subscription. Includes app perks as well as the games subscription service. + /// + Nitro = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs index 9c4162780..e2ae12ba4 100644 --- a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -1,25 +1,17 @@ -namespace Discord +namespace Discord { /// - /// Modify the current user with the specified arguments + /// Properties that are used to modify the with the specified changes. /// - /// - /// - /// await Context.Client.CurrentUser.ModifyAsync(x => - /// { - /// x.Avatar = new Image(File.OpenRead("avatar.jpg")); - /// }); - /// - /// - /// + /// public class SelfUserProperties { /// - /// Your username + /// Gets or sets the username. /// public Optional Username { get; set; } /// - /// Your avatar + /// Gets or sets the avatar. /// public Optional Avatar { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/UserProperties.cs b/src/Discord.Net.Core/Entities/Users/UserProperties.cs new file mode 100644 index 000000000..4cf4162a9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -0,0 +1,73 @@ +using System; + +namespace Discord +{ + [Flags] + public enum UserProperties + { + /// + /// Default value for flags, when none are given to an account. + /// + None = 0, + /// + /// Flag given to users who are a Discord employee. + /// + Staff = 1 << 0, + /// + /// Flag given to users who are owners of a partnered Discord server. + /// + Partner = 1 << 1, + /// + /// Flag given to users in HypeSquad events. + /// + HypeSquadEvents = 1 << 2, + /// + /// Flag given to users who have participated in the bug report program and are level 1. + /// + BugHunterLevel1 = 1 << 3, + /// + /// Flag given to users who are in the HypeSquad House of Bravery. + /// + HypeSquadBravery = 1 << 6, + /// + /// Flag given to users who are in the HypeSquad House of Brilliance. + /// + HypeSquadBrilliance = 1 << 7, + /// + /// Flag given to users who are in the HypeSquad House of Balance. + /// + HypeSquadBalance = 1 << 8, + /// + /// Flag given to users who subscribed to Nitro before games were added. + /// + EarlySupporter = 1 << 9, + /// + /// Flag given to users who are part of a team. + /// + TeamUser = 1 << 10, + /// + /// Flag given to users who represent Discord (System). + /// + System = 1 << 12, + /// + /// Flag given to users who have participated in the bug report program and are level 2. + /// + BugHunterLevel2 = 1 << 14, + /// + /// Flag given to users who are verified bots. + /// + VerifiedBot = 1 << 16, + /// + /// Flag given to users that developed bots and early verified their accounts. + /// + EarlyVerifiedBotDeveloper = 1 << 17, + /// + /// Flag given to users that are discord certified moderators who has give discord's exam. + /// + DiscordCertifiedModerator = 1 << 18, + /// + /// Flag given to bots that use only outgoing webhooks, exclusively. + /// + BotHTTPInteractions = 1 << 19, + } +} diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index 74a52a0fa..09033261e 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -1,12 +1,33 @@ -namespace Discord +namespace Discord { + /// + /// Defines the available Discord user status. + /// public enum UserStatus { + /// + /// The user is offline. + /// Offline, + /// + /// The user is online. + /// Online, + /// + /// The user is idle. + /// Idle, + /// + /// The user is AFK. + /// AFK, + /// + /// The user is busy. + /// DoNotDisturb, + /// + /// The user is invisible. + /// Invisible, } } diff --git a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs index ef56f72b9..d5bc70d71 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -1,34 +1,62 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a webhook object on Discord. + /// public interface IWebhook : IDeletable, ISnowflakeEntity { - /// Gets the token of this webhook. + /// + /// Gets the token of this webhook. + /// string Token { get; } - /// Gets the default name of this webhook. + /// + /// Gets the default name of this webhook. + /// string Name { get; } - /// Gets the id of this webhook's default avatar. + /// + /// Gets the ID of this webhook's default avatar. + /// string AvatarId { get; } - /// Gets the url to this webhook's default avatar. + /// + /// Gets the URL to this webhook's default avatar. + /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); - /// Gets the channel for this webhook. + /// + /// Gets the channel for this webhook. + /// ITextChannel Channel { get; } - /// Gets the id of the channel for this webhook. + /// + /// Gets the ID of the channel for this webhook. + /// ulong ChannelId { get; } - /// Gets the guild owning this webhook. + /// + /// Gets the guild owning this webhook. + /// IGuild Guild { get; } - /// Gets the id of the guild owning this webhook. + /// + /// Gets the ID of the guild owning this webhook. + /// ulong? GuildId { get; } - /// Gets the user that created this webhook. + /// + /// Gets the user that created this webhook. + /// IUser Creator { get; } - /// Modifies this webhook. + /// + /// Gets the ID of the application owning this webhook. + /// + ulong? ApplicationId { get; } + + /// + /// Modifies this webhook. + /// Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs index 8759a1729..e5ee4d6aa 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -1,40 +1,31 @@ -namespace Discord +namespace Discord { /// - /// Modify an with the specified parameters. + /// Properties used to modify an with the specified changes. /// - /// - /// - /// await webhook.ModifyAsync(x => - /// { - /// x.Name = "Bob"; - /// x.Avatar = new Image("avatar.jpg"); - /// }); - /// - /// - /// + /// public class WebhookProperties { /// - /// The default name of the webhook. + /// Gets or sets the default name of the webhook. /// public Optional Name { get; set; } /// - /// The default avatar of the webhook. + /// Gets or sets the default avatar of the webhook. /// public Optional Image { get; set; } /// - /// The channel for this webhook. + /// Gets or sets the channel for this webhook. /// /// - /// This field is not used when authenticated with . + /// This field is not used when authenticated with . /// public Optional Channel { get; set; } /// - /// The channel id for this webhook. + /// Gets or sets the channel ID for this webhook. /// /// - /// This field is not used when authenticated with . + /// This field is not used when authenticated with . /// public Optional ChannelId { get; set; } } diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index dd16d2943..d96076259 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -4,19 +4,20 @@ using System.Threading.Tasks; namespace Discord { + /// An extension class for squashing . + /// + /// This set of extension methods will squash an into a + /// single . This is often associated with requests that has a + /// set limit when requesting. + /// public static class AsyncEnumerableExtensions { - /// - /// Flattens the specified pages into one asynchronously - /// - /// - /// - /// + /// Flattens the specified pages into one asynchronously. public static async Task> FlattenAsync(this IAsyncEnumerable> source) { - return await source.Flatten().ToArray().ConfigureAwait(false); + return await source.Flatten().ToArrayAsync().ConfigureAwait(false); } - + /// Flattens the specified pages into one . public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) { return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); diff --git a/src/Discord.Net.Core/Extensions/AttachmentExtensions.cs b/src/Discord.Net.Core/Extensions/AttachmentExtensions.cs new file mode 100644 index 000000000..605410769 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/AttachmentExtensions.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public static class AttachmentExtensions + { + /// + /// The prefix applied to files to indicate that it is a spoiler. + /// + public const string SpoilerPrefix = "SPOILER_"; + /// + /// Gets whether the message's attachments are spoilers or not. + /// + public static bool IsSpoiler(this IAttachment attachment) + => attachment.Filename.StartsWith(SpoilerPrefix); + } +} diff --git a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs index e5d6025c2..f6ba7624d 100644 --- a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -15,7 +15,7 @@ namespace Discord //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) - => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + => new CollectionWrapper(source.Values, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new CollectionWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) diff --git a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs index 81cd10b49..6ebdbac6e 100644 --- a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs +++ b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs @@ -1,24 +1,31 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Discord { + /// An extension class for the Discord client. public static class DiscordClientExtensions { + /// Gets the private channel with the provided ID. public static async Task GetPrivateChannelAsync(this IDiscordClient client, ulong id) => await client.GetChannelAsync(id).ConfigureAwait(false) as IPrivateChannel; + /// Gets the DM channel with the provided ID. public static async Task GetDMChannelAsync(this IDiscordClient client, ulong id) => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IDMChannel; + /// Gets all available DM channels for the client. public static async Task> GetDMChannelsAsync(this IDiscordClient client) => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).OfType(); + /// Gets the group channel with the provided ID. public static async Task GetGroupChannelAsync(this IDiscordClient client, ulong id) => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IGroupChannel; + /// Gets all available group channels for the client. public static async Task> GetGroupChannelsAsync(this IDiscordClient client) => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).OfType(); + /// Gets the most optimal voice region for the client. public static async Task GetOptimalVoiceRegionAsync(this IDiscordClient discord) { var regions = await discord.GetVoiceRegionsAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 2eb4ed473..c05df7cb7 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -1,27 +1,36 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace Discord { + /// An extension class for building an embed. public static class EmbedBuilderExtensions { + /// Adds embed color based on the provided raw value. public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => builder.WithColor(new Color(rawValue)); + /// Adds embed color based on the provided RGB value. public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => builder.WithColor(new Color(r, g, b)); + /// Adds embed color based on the provided RGB value. + /// The argument value is not between 0 to 255. public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => builder.WithColor(new Color(r, g, b)); + /// Adds embed color based on the provided RGB value. + /// The argument value is not between 0 to 1. public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => builder.WithColor(new Color(r, g, b)); + /// Fills the embed author field with the provided user's full username and avatar URL. public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => - builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); - - public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => - builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl()); + /// Converts a object to a . + /// The embed type is not . public static EmbedBuilder ToEmbedBuilder(this IEmbed embed) { if (embed.Type != EmbedType.Rich) @@ -35,7 +44,7 @@ namespace Discord IconUrl = embed.Author?.IconUrl, Url = embed.Author?.Url }, - Color = embed.Color ?? Color.Default, + Color = embed.Color, Description = embed.Description, Footer = new EmbedFooterBuilder { @@ -54,5 +63,22 @@ namespace Discord return builder; } + + /// + /// Adds the specified fields into this . + /// + /// Field count exceeds . + public static EmbedBuilder WithFields(this EmbedBuilder builder, IEnumerable fields) + { + foreach (var field in fields) + builder.AddField(field); + + return builder; + } + /// + /// Adds the specified fields into this . + /// + public static EmbedBuilder WithFields(this EmbedBuilder builder, params EmbedFieldBuilder[] fields) + => WithFields(builder, fields.AsEnumerable()); } } diff --git a/src/Discord.Net.Core/Extensions/GuildExtensions.cs b/src/Discord.Net.Core/Extensions/GuildExtensions.cs new file mode 100644 index 000000000..9dd8de82e --- /dev/null +++ b/src/Discord.Net.Core/Extensions/GuildExtensions.cs @@ -0,0 +1,40 @@ +namespace Discord +{ + /// + /// An extension class for . + /// + public static class GuildExtensions + { + /// + /// Gets if welcome system messages are enabled. + /// + /// The guild to check. + /// A bool indicating if the welcome messages are enabled in the system channel. + public static bool GetWelcomeMessagesEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.WelcomeMessage); + + /// + /// Gets if guild boost system messages are enabled. + /// + /// The guild to check. + /// A bool indicating if the guild boost messages are enabled in the system channel. + public static bool GetGuildBoostMessagesEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.GuildBoost); + + /// + /// Gets if guild setup system messages are enabled. + /// + /// The guild to check. + /// A bool indicating if the guild setup messages are enabled in the system channel. + public static bool GetGuildSetupTipMessagesEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.GuildSetupTip); + + /// + /// Gets if guild welcome messages have a reply with sticker button. + /// + /// The guild to check. + /// A bool indicating if the guild welcome messages have a reply with sticker button. + public static bool GetGuildWelcomeMessageReplyEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.WelcomeMessageReply); + } +} diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index c53ef9053..c187ecd5b 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -1,11 +1,100 @@ +using System.Threading.Tasks; + namespace Discord { + /// + /// Provides extension methods for . + /// public static class MessageExtensions { + /// + /// Gets a URL that jumps to the message. + /// + /// The message to jump to. + /// + /// A string that contains a URL for jumping to the message in chat. + /// public static string GetJumpUrl(this IMessage msg) { var channel = msg.Channel; - return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; + return $"https://discord.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; + } + + /// + /// Add multiple reactions to a message. + /// + /// + /// This method does not bulk add reactions! It will send a request for each reaction included. + /// + /// + /// + /// IEmote A = new Emoji("🅰"); + /// IEmote B = new Emoji("🅱"); + /// await msg.AddReactionsAsync(new[] { A, B }); + /// + /// + /// The message to add reactions to. + /// An array of reactions to add to the message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for adding a reaction to this message. + /// + /// + /// + public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.AddReactionAsync(rxn, options).ConfigureAwait(false); + } + /// + /// Remove multiple reactions from a message. + /// + /// + /// This method does not bulk remove reactions! If you want to clear reactions from a message, + /// + /// + /// + /// + /// await msg.RemoveReactionsAsync(currentUser, new[] { A, B }); + /// + /// + /// The message to remove reactions from. + /// The user who removed the reaction. + /// An array of reactions to remove from the message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + /// + public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.RemoveReactionAsync(rxn, user, options).ConfigureAwait(false); + } + + /// + /// Sends an inline reply that references a message. + /// + /// The message that is being replied on. + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// A array of s to send with this response. Max 10. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The options to be used when sending the request. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public static async Task ReplyAsync(this IUserMessage msg, string text = null, bool isTTS = false, Embed embed = null, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) + { + return await msg.Channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, new MessageReference(messageId: msg.Id), components, stickers, embeds).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/Extensions/ObjectExtensions.cs b/src/Discord.Net.Core/Extensions/ObjectExtensions.cs new file mode 100644 index 000000000..240fb47aa --- /dev/null +++ b/src/Discord.Net.Core/Extensions/ObjectExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class ObjectExtensions + { + public static bool IsNumericType(this object o) + { + switch (Type.GetTypeCode(o.GetType())) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + default: + return false; + } + } + } +} diff --git a/src/Discord.Net.Core/Extensions/StringExtensions.cs b/src/Discord.Net.Core/Extensions/StringExtensions.cs deleted file mode 100644 index c0ebb2626..000000000 --- a/src/Discord.Net.Core/Extensions/StringExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - internal static class StringExtensions - { - public static bool IsNullOrUri(this string url) => - string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); - } -} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 951e8ca4b..e268eae84 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -1,50 +1,179 @@ +using System; using System.Threading.Tasks; using System.IO; namespace Discord { + /// An extension class for various Discord user objects. public static class UserExtensions { /// - /// Sends a message to the user via DM. + /// Sends a message via DM. /// + /// + /// This method attempts to send a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// The user to send the DM to. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents the asynchronous send operation. The task result contains the sent message. + /// public static async Task SendMessageAsync(this IUser user, string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null) + RequestOptions options = null, + AllowedMentions allowedMentions = null, + MessageComponent component = null, + Embed[] embeds = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions, component: component, embeds: embeds).ConfigureAwait(false); } /// - /// Sends a file to the user via DM. + /// Sends a file to this message channel with an optional caption. /// + /// + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. + /// + /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", + /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + /// + /// + /// + /// This method attempts to send an attachment as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// The message component to be included with this message. Used for interactions. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// public static async Task SendFileAsync(this IUser user, Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null - ) + RequestOptions options = null, + MessageComponent component = null, + Embed[] embeds = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options, component: component, embeds: embeds).ConfigureAwait(false); } /// - /// Sends a file to the user via DM. + /// Sends a file via DM with an optional caption. /// + /// + /// The following example uploads a local file called wumpus.txt along with the text + /// good discord boi to the channel. + /// + /// await channel.SendFileAsync("wumpus.txt", "good discord boi"); + /// + /// + /// The following example uploads a local image called b1nzy.jpg embedded inside a rich embed to the + /// channel. + /// + /// await channel.SendFileAsync("b1nzy.jpg", + /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + /// + /// + /// + /// This method attempts to send an attachment as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// The message component to be included with this message. Used for interactions. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// public static async Task SendFileAsync(this IUser user, string filePath, string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null) + RequestOptions options = null, + MessageComponent component = null, + Embed[] embeds = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, component: component, embeds: embeds).ConfigureAwait(false); } + /// + /// Bans the user from the guild and optionally prunes their recent messages. + /// + /// The user to ban. + /// The number of days to remove messages from this for - must be between [0, 7] + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous operation for banning a user. + /// public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => user.Guild.AddBanAsync(user, pruneDays, reason, options); } diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index 729e37a8c..a5951aa73 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -1,10 +1,14 @@ +using System.Text; +using System.Text.RegularExpressions; + namespace Discord { + /// A helper class for formatting characters. public static class Format { // Characters which need escaping private static readonly string[] SensitiveCharacters = { - "\\", "*", "_", "~", "`", ".", ":", "/" }; + "\\", "*", "_", "~", "`", ".", ":", "/", ">", "|" }; /// Returns a markdown-formatted string with bold formatting. public static string Bold(string text) => $"**{text}**"; @@ -12,8 +16,14 @@ namespace Discord public static string Italics(string text) => $"*{text}*"; /// Returns a markdown-formatted string with underline formatting. public static string Underline(string text) => $"__{text}__"; - /// Returns a markdown-formatted string with strikethrough formatting. + /// Returns a markdown-formatted string with strike-through formatting. public static string Strikethrough(string text) => $"~~{text}~~"; + /// Returns a string with spoiler formatting. + public static string Spoiler(string text) => $"||{text}||"; + /// Returns a markdown-formatted URL. Only works in descriptions and fields. + public static string Url(string text, string url) => $"[{text}]({url})"; + /// Escapes a URL so that a preview is not generated. + public static string EscapeUrl(string url) => $"<{url}>"; /// Returns a markdown-formatted string with codeblock formatting. public static string Code(string text, string language = null) @@ -31,5 +41,79 @@ namespace Discord text = text.Replace(unsafeChar, $"\\{unsafeChar}"); return text; } + + /// + /// Formats a string as a quote. + /// + /// The text to format. + /// Gets the formatted quote text. + public static string Quote(string text) + { + // do not modify null or whitespace text + // whitespace does not get quoted properly + if (string.IsNullOrWhiteSpace(text)) + return text; + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int newLineIndex; + do + { + newLineIndex = text.IndexOf('\n', startIndex); + if (newLineIndex == -1) + { + // read the rest of the string + var str = text.Substring(startIndex); + result.Append($"> {str}"); + } + else + { + // read until the next newline + var str = text.Substring(startIndex, newLineIndex - startIndex); + result.Append($"> {str}\n"); + } + startIndex = newLineIndex + 1; + } + while (newLineIndex != -1 && startIndex != text.Length); + + return result.ToString(); + } + + /// + /// Formats a string as a block quote. + /// + /// The text to format. + /// Gets the formatted block quote text. + public static string BlockQuote(string text) + { + // do not modify null or whitespace + if (string.IsNullOrWhiteSpace(text)) + return text; + + return $">>> {text}"; + } + + /// + /// Remove discord supported markdown from text. + /// + /// The to remove markdown from. + /// Gets the unformatted text. + public static string StripMarkDown(string text) + { + //Remove discord supported markdown + var newText = Regex.Replace(text, @"(\*|_|`|~|>|\\)", ""); + return newText; + } + + /// + /// Formats a user's username + discriminator while maintaining bidirectional unicode + /// + /// The user whos username and discriminator to format + /// The username + discriminator + public static string UsernameAndDiscriminator(IUser user) + { + return $"\u2066{user.Username}\u2069#{user.Discriminator}"; + } } } diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs new file mode 100644 index 000000000..f2a99e44c --- /dev/null +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -0,0 +1,56 @@ +using System; + +namespace Discord +{ + [Flags] + public enum GatewayIntents + { + /// This intent includes no events + None = 0, + /// This intent includes GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE + Guilds = 1 << 0, + /// This intent includes GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE + /// This is a privileged intent and must be enabled in the Developer Portal. + GuildMembers = 1 << 1, + /// This intent includes GUILD_BAN_ADD, GUILD_BAN_REMOVE + GuildBans = 1 << 2, + /// This intent includes GUILD_EMOJIS_UPDATE + GuildEmojis = 1 << 3, + /// This intent includes GUILD_INTEGRATIONS_UPDATE + GuildIntegrations = 1 << 4, + /// This intent includes WEBHOOKS_UPDATE + GuildWebhooks = 1 << 5, + /// This intent includes INVITE_CREATE, INVITE_DELETE + GuildInvites = 1 << 6, + /// This intent includes VOICE_STATE_UPDATE + GuildVoiceStates = 1 << 7, + /// This intent includes PRESENCE_UPDATE + /// This is a privileged intent and must be enabled in the Developer Portal. + GuildPresences = 1 << 8, + /// This intent includes MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK + GuildMessages = 1 << 9, + /// This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI + GuildMessageReactions = 1 << 10, + /// This intent includes TYPING_START + GuildMessageTyping = 1 << 11, + /// This intent includes CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE + DirectMessages = 1 << 12, + /// This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI + DirectMessageReactions = 1 << 13, + /// This intent includes TYPING_START + DirectMessageTyping = 1 << 14, + /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + GuildScheduledEvents = 1 << 16, + /// + /// This intent includes all but and + /// which are privileged and must be enabled in the Developer Portal. + /// + AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | + GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | + DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, + /// + /// This intent includes all of them, including privileged ones. + /// + All = AllUnprivileged | GuildMembers | GuildPresences + } +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index a383c37da..f6981d552 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -5,38 +5,325 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic Discord client. + /// public interface IDiscordClient : IDisposable { + /// + /// Gets the current state of connection. + /// ConnectionState ConnectionState { get; } + /// + /// Gets the currently logged-in user. + /// ISelfUser CurrentUser { get; } + /// + /// Gets the token type of the logged-in user. + /// TokenType TokenType { get; } + /// + /// Starts the connection between Discord and the client.. + /// + /// + /// This method will initialize the connection between the client and Discord. + /// + /// This method will immediately return after it is called, as it will initialize the connection on + /// another thread. + /// + /// + /// + /// A task that represents the asynchronous start operation. + /// Task StartAsync(); + /// + /// Stops the connection between Discord and the client. + /// + /// + /// A task that represents the asynchronous stop operation. + /// Task StopAsync(); + /// + /// Gets a Discord application information for the logged-in user. + /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application + /// information. + /// Task GetApplicationInfoAsync(RequestOptions options = null); + /// + /// Gets a generic channel. + /// + /// + /// + /// var channel = await _client.GetChannelAsync(381889909113225237); + /// if (channel != null && channel is IMessageChannel msgChannel) + /// { + /// await msgChannel.SendMessageAsync($"{msgChannel} is created at {msgChannel.CreatedAt}"); + /// } + /// + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the channel associated + /// with the snowflake identifier; null when the channel cannot be found. + /// Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of private channels opened in this session. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// This method will retrieve all private channels (including direct-message, group channel and such) that + /// are currently opened in this session. + /// + /// This method will not return previously opened private channels outside of the current session! If + /// you have just started the client, this may return an empty collection. + /// + /// + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of private channels that the user currently partakes in. + /// Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of direct message channels opened in this session. + /// + /// + /// This method returns a collection of currently opened direct message channels. + /// + /// This method will not return previously opened DM channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of direct-message channels that the user currently partakes in. + /// Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of group channels opened in this session. + /// + /// + /// This method returns a collection of currently opened group channels. + /// + /// This method will not return previously opened group channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of group channels that the user currently partakes in. + /// Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the connections that the user has set up. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of connections. + /// Task> GetConnectionsAsync(RequestOptions options = null); + /// + /// Gets a global application command. + /// + /// The id of the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application command if found, otherwise + /// . + /// + Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null); + + /// + /// Gets a collection of all global commands. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global + /// application commands. + /// + Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null); + + /// + /// Creates a global application command. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created application command. + /// + Task CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options = null); + + /// + /// Bulk overwrites all global application commands. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of application commands that were created. + /// + Task> BulkOverwriteGlobalApplicationCommand(ApplicationCommandProperties[] properties, RequestOptions options = null); + + /// + /// Gets a guild. + /// + /// The guild snowflake identifier. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild associated + /// with the snowflake identifier; null when the guild cannot be found. + /// Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of guilds that the user is currently in. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of guilds that the current user is in. + /// Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Creates a guild for the logged-in user who is in less than 10 active guilds. + /// + /// + /// This method creates a new guild on behalf of the logged-in user. + /// + /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. + /// + /// + /// The name of the new guild. + /// The voice region to create the guild with. + /// The icon of the guild. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created guild. + /// Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); - + + /// + /// Gets an invite. + /// + /// The invitation identifier. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the invite information. + /// Task GetInviteAsync(string inviteId, RequestOptions options = null); + /// + /// Gets a user. + /// + /// + /// + /// var user = await _client.GetUserAsync(168693960628371456); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The snowflake identifier of the user (e.g. `168693960628371456`). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the snowflake identifier; null if the user is not found. + /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a user. + /// + /// + /// + /// var user = await _client.GetUserAsync("Still", "2876"); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The name of the user (e.g. `Still`). + /// The discriminator value of the user (e.g. `2876`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the name and the discriminator; null if the user is not found. + /// Task GetUserAsync(string username, string discriminator, RequestOptions options = null); + /// + /// Gets a collection of the available voice regions. + /// + /// + /// The following example gets the most optimal voice region from the collection. + /// + /// var regions = await client.GetVoiceRegionsAsync(); + /// var optimalRegion = regions.FirstOrDefault(x => x.IsOptimal); + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// with all of the available voice regions in this session. + /// Task> GetVoiceRegionsAsync(RequestOptions options = null); + /// + /// Gets a voice region. + /// + /// The identifier of the voice region (e.g. eu-central ). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice region + /// associated with the identifier; null if the voice region is not found. + /// Task GetVoiceRegionAsync(string id, RequestOptions options = null); + /// + /// Gets a webhook available. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// + /// Gets the recommended shard count as suggested by Discord. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains an + /// that represents the number of shards that should be used with this account. + /// Task GetRecommendedShardCountAsync(RequestOptions options = null); + + /// + /// Gets the gateway information related to the bot. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a + /// that represents the gateway information related to the bot. + /// + Task GetBotGatewayAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index a69519fa2..a99c45b39 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -24,7 +24,10 @@ namespace Discord.Logging if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { @@ -33,7 +36,10 @@ namespace Discord.Logging if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } } public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) diff --git a/src/Discord.Net.Core/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs index d1b3782be..715cac677 100644 --- a/src/Discord.Net.Core/Logging/LogMessage.cs +++ b/src/Discord.Net.Core/Logging/LogMessage.cs @@ -1,15 +1,50 @@ -using System; +using System; using System.Text; namespace Discord { + /// + /// Provides a message object used for logging purposes. + /// public struct LogMessage { + /// + /// Gets the severity of the log entry. + /// + /// + /// A enum to indicate the severeness of the incident or event. + /// public LogSeverity Severity { get; } + /// + /// Gets the source of the log entry. + /// + /// + /// A string representing the source of the log entry. + /// public string Source { get; } + /// + /// Gets the message of this log entry. + /// + /// + /// A string containing the message of this log entry. + /// public string Message { get; } + /// + /// Gets the exception of this log entry. + /// + /// + /// An object associated with an incident; otherwise null. + /// public Exception Exception { get; } + /// + /// Initializes a new struct with the severity, source, message of the event, and + /// optionally, an exception. + /// + /// The severity of the event. + /// The source of the event. + /// The message of the event. + /// The exception of the event. public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) { Severity = severity; @@ -17,8 +52,8 @@ namespace Discord Message = message; Exception = exception; } - - public override string ToString() => ToString(null); + + public override string ToString() => ToString(); public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11) { string sourceName = Source; diff --git a/src/Discord.Net.Core/Logging/LogSeverity.cs b/src/Discord.Net.Core/Logging/LogSeverity.cs index 785b0ef46..f9b518c17 100644 --- a/src/Discord.Net.Core/Logging/LogSeverity.cs +++ b/src/Discord.Net.Core/Logging/LogSeverity.cs @@ -1,12 +1,34 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the severity of the log message. + /// public enum LogSeverity { + /// + /// Logs that contain the most severe level of error. This type of error indicate that immediate attention + /// may be required. + /// Critical = 0, + /// + /// Logs that highlight when the flow of execution is stopped due to a failure. + /// Error = 1, + /// + /// Logs that highlight an abnormal activity in the flow of execution. + /// Warning = 2, + /// + /// Logs that track the general flow of the application. + /// Info = 3, + /// + /// Logs that are used for interactive investigation during development. + /// Verbose = 4, + /// + /// Logs that contain the most detailed messages. + /// Debug = 5 } } diff --git a/src/Discord.Net.Core/LoginState.cs b/src/Discord.Net.Core/LoginState.cs index 42b6ecac9..49f86c9fa 100644 --- a/src/Discord.Net.Core/LoginState.cs +++ b/src/Discord.Net.Core/LoginState.cs @@ -1,10 +1,15 @@ -namespace Discord +namespace Discord { + /// Specifies the state of the client's login status. public enum LoginState : byte { + /// The client is currently logged out. LoggedOut, + /// The client is currently logging in. LoggingIn, + /// The client is currently logged in. LoggedIn, + /// The client is currently logging out. LoggingOut } } diff --git a/src/Discord.Net.Core/Net/ApplicationCommandException.cs b/src/Discord.Net.Core/Net/ApplicationCommandException.cs new file mode 100644 index 000000000..4b4890d12 --- /dev/null +++ b/src/Discord.Net.Core/Net/ApplicationCommandException.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; + +namespace Discord.Net +{ + [Obsolete("Please use HttpException instead of this. Will be removed in next major version.", false)] + public class ApplicationCommandException : HttpException + { + public ApplicationCommandException(HttpException httpError) + : base(httpError.HttpCode, httpError.Request, httpError.DiscordCode, httpError.Reason, httpError.Errors.ToArray()) + { + + } + } +} diff --git a/src/Discord.Net.Core/Net/BucketId.cs b/src/Discord.Net.Core/Net/BucketId.cs new file mode 100644 index 000000000..96281a0ed --- /dev/null +++ b/src/Discord.Net.Core/Net/BucketId.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Net +{ + /// + /// Represents a ratelimit bucket. + /// + public class BucketId : IEquatable + { + /// + /// Gets the http method used to make the request if available. + /// + public string HttpMethod { get; } + /// + /// Gets the endpoint that is going to be requested if available. + /// + public string Endpoint { get; } + /// + /// Gets the major parameters of the route. + /// + public IOrderedEnumerable> MajorParameters { get; } + /// + /// Gets the hash of this bucket. + /// + /// + /// The hash is provided by Discord to group ratelimits. + /// + public string BucketHash { get; } + /// + /// Gets if this bucket is a hash type. + /// + public bool IsHashBucket { get => BucketHash != null; } + + private BucketId(string httpMethod, string endpoint, IEnumerable> majorParameters, string bucketHash) + { + HttpMethod = httpMethod; + Endpoint = endpoint; + MajorParameters = majorParameters.OrderBy(x => x.Key); + BucketHash = bucketHash; + } + + /// + /// Creates a new based on the + /// and . + /// + /// Http method used to make the request. + /// Endpoint that is going to receive requests. + /// Major parameters of the route of this endpoint. + /// + /// A based on the + /// and the with the provided data. + /// + public static BucketId Create(string httpMethod, string endpoint, Dictionary majorParams) + { + Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint)); + majorParams ??= new Dictionary(); + return new BucketId(httpMethod, endpoint, majorParams, null); + } + + /// + /// Creates a new based on a + /// and a previous . + /// + /// Bucket hash provided by Discord. + /// that is going to be upgraded to a hash type. + /// + /// A based on the + /// and . + /// + public static BucketId Create(string hash, BucketId oldBucket) + { + Preconditions.NotNullOrWhitespace(hash, nameof(hash)); + Preconditions.NotNull(oldBucket, nameof(oldBucket)); + return new BucketId(null, null, oldBucket.MajorParameters, hash); + } + + /// + /// Gets the string that will define this bucket as a hash based one. + /// + /// + /// A that defines this bucket as a hash based one. + /// + public string GetBucketHash() + => IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null; + + /// + /// Gets the string that will define this bucket as an endpoint based one. + /// + /// + /// A that defines this bucket as an endpoint based one. + /// + public string GetUniqueEndpoint() + => HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint; + + public override bool Equals(object obj) + => Equals(obj as BucketId); + + public override int GetHashCode() + => IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode(); + + public override string ToString() + => GetBucketHash() ?? GetUniqueEndpoint(); + + public bool Equals(BucketId other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (GetType() != other.GetType()) + return false; + return ToString() == other.ToString(); + } + } +} diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index d0ee65b23..07551f0e7 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -1,22 +1,61 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Net; namespace Discord.Net { + /// + /// The exception that is thrown if an error occurs while processing an Discord HTTP request. + /// public class HttpException : Exception { + /// + /// Gets the HTTP status code returned by Discord. + /// + /// + /// An + /// HTTP status code + /// from Discord. + /// public HttpStatusCode HttpCode { get; } - public int? DiscordCode { get; } + /// + /// Gets the JSON error code returned by Discord. + /// + /// + /// A + /// JSON error code + /// from Discord, or null if none. + /// + public DiscordErrorCode? DiscordCode { get; } + /// + /// Gets the reason of the exception. + /// public string Reason { get; } + /// + /// Gets the request object used to send the request. + /// public IRequest Request { get; } + /// + /// Gets a collection of json errors describing what went wrong with the request. + /// + public IReadOnlyCollection Errors { get; } - public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) - : base(CreateMessage(httpCode, discordCode, reason)) + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned. + /// The request that was sent prior to the exception. + /// The Discord status code returned. + /// The reason behind the exception. + public HttpException(HttpStatusCode httpCode, IRequest request, DiscordErrorCode? discordCode = null, string reason = null, DiscordJsonError[] errors = null) + : base(CreateMessage(httpCode, (int?)discordCode, reason)) { HttpCode = httpCode; Request = request; DiscordCode = discordCode; Reason = reason; + Errors = errors?.ToImmutableArray() ?? ImmutableArray.Empty; } private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) diff --git a/src/Discord.Net.Core/Net/IRequest.cs b/src/Discord.Net.Core/Net/IRequest.cs index d3c708dd5..1f23e65cd 100644 --- a/src/Discord.Net.Core/Net/IRequest.cs +++ b/src/Discord.Net.Core/Net/IRequest.cs @@ -2,6 +2,9 @@ using System; namespace Discord.Net { + /// + /// Represents a generic request to be sent to Discord. + /// public interface IRequest { DateTimeOffset? TimeoutAt { get; } diff --git a/src/Discord.Net.Core/Net/RateLimitedException.cs b/src/Discord.Net.Core/Net/RateLimitedException.cs index 2d34d7bc2..c19487fad 100644 --- a/src/Discord.Net.Core/Net/RateLimitedException.cs +++ b/src/Discord.Net.Core/Net/RateLimitedException.cs @@ -2,10 +2,20 @@ using System; namespace Discord.Net { + /// + /// The exception that is thrown when the user is being rate limited by Discord. + /// public class RateLimitedException : TimeoutException { + /// + /// Gets the request object used to send the request. + /// public IRequest Request { get; } + /// + /// Initializes a new instance of the class using the + /// sent. + /// public RateLimitedException(IRequest request) : base("You are being rate limited.") { diff --git a/src/Discord.Net.Core/Net/Rest/IRateLimitInfo.cs b/src/Discord.Net.Core/Net/Rest/IRateLimitInfo.cs new file mode 100644 index 000000000..816f25af4 --- /dev/null +++ b/src/Discord.Net.Core/Net/Rest/IRateLimitInfo.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic ratelimit info. + /// + public interface IRateLimitInfo + { + /// + /// Gets whether or not this ratelimit info is global. + /// + bool IsGlobal { get; } + + /// + /// Gets the number of requests that can be made. + /// + int? Limit { get; } + + /// + /// Gets the number of remaining requests that can be made. + /// + int? Remaining { get; } + + /// + /// Gets the total time (in seconds) of when the current rate limit bucket will reset. Can have decimals to match previous millisecond ratelimit precision. + /// + int? RetryAfter { get; } + + /// + /// Gets the at which the rate limit resets. + /// + DateTimeOffset? Reset { get; } + + /// + /// Gets the absolute time when this ratelimit resets. + /// + TimeSpan? ResetAfter { get; } + + /// + /// Gets a unique string denoting the rate limit being encountered (non-inclusive of major parameters in the route path). + /// + string Bucket { get; } + + /// + /// Gets the amount of lag for the request. This is used to denote the precise time of when the ratelimit expires. + /// + TimeSpan? Lag { get; } + + /// + /// Gets the endpoint that this ratelimit info came from. + /// + string Endpoint { get; } + } +} diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index addfa9061..71010f70d 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -1,14 +1,36 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Rest { - public interface IRestClient + /// + /// Represents a generic REST-based client. + /// + public interface IRestClient : IDisposable { + /// + /// Sets the HTTP header of this client for all requests. + /// + /// The field name of the header. + /// The value of the header. void SetHeader(string key, string value); + /// + /// Sets the cancellation token for this client. + /// + /// The cancellation token. void SetCancelToken(CancellationToken cancelToken); + /// + /// Sends a REST request. + /// + /// The method used to send this request (i.e. HTTP verb such as GET, POST). + /// The endpoint to send this request to. + /// The cancellation token used to cancel the task. + /// Indicates whether to send the header only. + /// The audit log reason. + /// Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); diff --git a/src/Discord.Net.Core/Net/RpcException.cs b/src/Discord.Net.Core/Net/RpcException.cs deleted file mode 100644 index 195fad73f..000000000 --- a/src/Discord.Net.Core/Net/RpcException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord -{ - public class RpcException : Exception - { - public int ErrorCode { get; } - public string Reason { get; } - - public RpcException(int errorCode, string reason = null) - : base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") - { - ErrorCode = errorCode; - Reason = reason; - } - } -} diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index 10ac652b3..ed2881d1f 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.Udp { - public interface IUdpSocket + public interface IUdpSocket : IDisposable { event Func ReceivedDatagram; diff --git a/src/Discord.Net.Core/Net/WebSocketClosedException.cs b/src/Discord.Net.Core/Net/WebSocketClosedException.cs index d647b6c8c..c743cd696 100644 --- a/src/Discord.Net.Core/Net/WebSocketClosedException.cs +++ b/src/Discord.Net.Core/Net/WebSocketClosedException.cs @@ -1,11 +1,29 @@ -using System; +using System; namespace Discord.Net { + /// + /// The exception that is thrown when the WebSocket session is closed by Discord. + /// public class WebSocketClosedException : Exception { + /// + /// Gets the close code sent by Discord. + /// + /// + /// A + /// close code + /// from Discord. + /// public int CloseCode { get; } + /// + /// Gets the reason of the interruption. + /// public string Reason { get; } + /// + /// Initializes a new instance of the using a Discord close code + /// and an optional reason. + /// public WebSocketClosedException(int closeCode, string reason = null) : base($"The server sent close {closeCode}{(reason != null ? $": \"{reason}\"" : "")}") { diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 7eccaabf2..6791af354 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public interface IWebSocketClient + public interface IWebSocketClient : IDisposable { event Func BinaryMessage; event Func TextMessage; @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets void SetCancelToken(CancellationToken cancelToken); Task ConnectAsync(string host); - Task DisconnectAsync(); + Task DisconnectAsync(int closeCode = 1000); Task SendAsync(byte[] data, int index, int count, bool isText); } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 5f3a8814b..46aa2681f 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,27 +1,74 @@ -using System.Threading; +using Discord.Net; +using System; +using System.Threading; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents options that should be used when sending a request. + /// public class RequestOptions { + /// + /// Creates a new class with its default settings. + /// public static RequestOptions Default => new RequestOptions(); - /// - /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. - /// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. + /// + /// Gets or sets the maximum time to wait for this request to complete. /// + /// + /// Gets or set the max time, in milliseconds, to wait for this request to complete. If + /// null, a request will not time out. If a rate limit has been triggered for this request's bucket + /// and will not be unpaused in time, this request will fail immediately. + /// + /// + /// A in milliseconds for when the request times out. + /// public int? Timeout { get; set; } + /// + /// Gets or sets the cancellation token for this request. + /// + /// + /// A for this request. + /// public CancellationToken CancelToken { get; set; } = CancellationToken.None; + /// + /// Gets or sets the retry behavior when the request fails. + /// public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } /// - /// The reason for this action in the guild's audit log + /// Gets or sets the reason for this action in the guild's audit log. /// + /// + /// Gets or sets the reason that will be written to the guild's audit log if applicable. This may not apply + /// to all actions. + /// public string AuditLogReason { get; set; } + /// + /// Gets or sets whether or not this request should use the system + /// clock for rate-limiting. Defaults to true. + /// + /// + /// This property can also be set in . + /// On a per-request basis, the system clock should only be disabled + /// when millisecond precision is especially important, and the + /// hosting system is known to have a desynced clock. + /// + public bool? UseSystemClock { get; set; } + + /// + /// Gets or sets the callback to execute regarding ratelimits for this request. + /// + public Func RatelimitCallback { get; set; } internal bool IgnoreState { get; set; } - internal string BucketId { get; set; } + internal BucketId BucketId { get; set; } internal bool IsClientBucket { get; set; } + internal bool IsReactionBucket { get; set; } + internal bool IsGatewayBucket { get; set; } internal static RequestOptions CreateOrClone(RequestOptions options) { @@ -31,11 +78,26 @@ namespace Discord return options.Clone(); } + internal void ExecuteRatelimitCallback(IRateLimitInfo info) + { + if (RatelimitCallback != null) + { + _ = Task.Run(async () => + { + await RatelimitCallback(info); + }); + } + } + + /// + /// Initializes a new class with the default request timeout set in + /// . + /// public RequestOptions() { Timeout = DiscordConfig.DefaultRequestTimeout; } - + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net.Core/RetryMode.cs b/src/Discord.Net.Core/RetryMode.cs index 65ae75fc3..1e09f4dd1 100644 --- a/src/Discord.Net.Core/RetryMode.cs +++ b/src/Discord.Net.Core/RetryMode.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord { @@ -12,7 +12,7 @@ namespace Discord RetryTimeouts = 0x1, // /// Retry if a request failed due to a network error. //RetryErrors = 0x2, - /// Retry if a request failed due to a ratelimit. + /// Retry if a request failed due to a rate-limit. RetryRatelimit = 0x4, /// Retry if a request failed due to an HTTP error 502. Retry502 = 0x8, diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 62181420a..03b840830 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -2,12 +2,20 @@ using System; namespace Discord { + /// Specifies the type of token to use with the client. public enum TokenType { - [Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)] - User, + /// + /// An OAuth2 token type. + /// Bearer, + /// + /// A bot token type. + /// Bot, + /// + /// A webhook token type. + /// Webhook } } diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index f17aa8699..4aa768292 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -1,30 +1,31 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { /// - /// Contains an entity that may be cached. + /// Represents a cached entity. /// - /// The type of entity that is cached - /// The type of this entity's ID + /// The type of entity that is cached. + /// The type of this entity's ID. public struct Cacheable where TEntity : IEntity where TId : IEquatable { /// - /// Is this entity cached? + /// Gets whether this entity is cached. /// public bool HasValue { get; } /// - /// The ID of this entity. + /// Gets the ID of this entity. /// public TId Id { get; } /// - /// The entity, if it could be pulled from cache. + /// Gets the entity if it could be pulled from cache. /// /// - /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is null. + /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is + /// null. /// public TEntity Value { get; } private Func> DownloadFunc { get; } @@ -38,22 +39,84 @@ namespace Discord } /// - /// Downloads this entity to cache. + /// Downloads this entity to cache. /// - /// An awaitable Task containing the downloaded entity. /// Thrown when used from a user account. /// Thrown when the message is deleted. + /// + /// A task that represents the asynchronous download operation. The task result contains the downloaded + /// entity. + /// public async Task DownloadAsync() { - return await DownloadFunc(); + return await DownloadFunc().ConfigureAwait(false); } /// - /// Returns the cached entity if it exists; otherwise downloads it. + /// Returns the cached entity if it exists; otherwise downloads it. /// - /// An awaitable Task containing a cached or downloaded entity. /// Thrown when used from a user account. /// Thrown when the message is deleted and is not in cache. - public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync(); + /// + /// A task that represents the asynchronous operation that attempts to get the message via cache or to + /// download the message. The task result contains the downloaded entity. + /// + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); } -} \ No newline at end of file + public struct Cacheable + where TCachedEntity : IEntity, TRelationship + where TDownloadableEntity : IEntity, TRelationship + where TId : IEquatable + { + /// + /// Gets whether this entity is cached. + /// + public bool HasValue { get; } + /// + /// Gets the ID of this entity. + /// + public TId Id { get; } + /// + /// Gets the entity if it could be pulled from cache. + /// + /// + /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is + /// null. + /// + public TCachedEntity Value { get; } + private Func> DownloadFunc { get; } + + internal Cacheable(TCachedEntity value, TId id, bool hasValue, Func> downloadFunc) + { + Value = value; + Id = id; + HasValue = hasValue; + DownloadFunc = downloadFunc; + } + + /// + /// Downloads this entity. + /// + /// Thrown when used from a user account. + /// Thrown when the message is deleted. + /// + /// A task that represents the asynchronous download operation. The task result contains the downloaded + /// entity. + /// + public async Task DownloadAsync() + { + return await DownloadFunc().ConfigureAwait(false); + } + + /// + /// Returns the cached entity if it exists; otherwise downloads it. + /// + /// Thrown when used from a user account. + /// Thrown when the message is deleted and is not in cache. + /// + /// A task that represents the asynchronous operation that attempts to get the message via cache or to + /// download the message. The task result contains the downloaded entity. + /// + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs index d7641e897..3c7b8aa3c 100644 --- a/src/Discord.Net.Core/Utils/Comparers.cs +++ b/src/Discord.Net.Core/Utils/Comparers.cs @@ -3,14 +3,31 @@ using System.Collections.Generic; namespace Discord { + /// + /// Represents a collection of for various Discord objects. + /// public static class DiscordComparers { - // TODO: simplify with '??=' slated for C# 8.0 - public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); - public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); - public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); - public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); - public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare users. + /// + public static IEqualityComparer UserComparer => _userComparer ??= new EntityEqualityComparer(); + /// + /// Gets an to be used to compare guilds. + /// + public static IEqualityComparer GuildComparer => _guildComparer ??= new EntityEqualityComparer(); + /// + /// Gets an to be used to compare channels. + /// + public static IEqualityComparer ChannelComparer => _channelComparer ??= new EntityEqualityComparer(); + /// + /// Gets an to be used to compare roles. + /// + public static IEqualityComparer RoleComparer => _roleComparer ??= new EntityEqualityComparer(); + /// + /// Gets an to be used to compare messages. + /// + public static IEqualityComparer MessageComparer => _messageComparer ??= new EntityEqualityComparer(); private static IEqualityComparer _userComparer; private static IEqualityComparer _guildComparer; @@ -24,16 +41,13 @@ namespace Discord { public override bool Equals(TEntity x, TEntity y) { - bool xNull = x == null; - bool yNull = y == null; - - if (xNull && yNull) - return true; - - if (xNull ^ yNull) - return false; - - return x.Id.Equals(y.Id); + return (x, y) switch + { + (null, null) => true, + (null, _) => false, + (_, null) => false, + var (l, r) => l.Id.Equals(r.Id) + }; } public override int GetHashCode(TEntity obj) diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index e9edfb772..308f08460 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -157,12 +157,16 @@ namespace Discord : this(collection, EqualityComparer.Default) { } public ConcurrentHashSet(IEqualityComparer comparer) : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { } + /// is null public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) : this(comparer) { if (collection == null) throw new ArgumentNullException(paramName: nameof(collection)); InitializeFromCollection(collection); } + /// + /// or is null + /// public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) : this(concurrencyLevel, DefaultCapacity, false, comparer) { @@ -206,7 +210,7 @@ namespace Discord if (_budget == 0) _budget = _tables._buckets.Length / _tables._locks.Length; } - + /// is null public bool ContainsKey(T value) { if (value == null) throw new ArgumentNullException(paramName: "key"); @@ -230,6 +234,7 @@ namespace Discord return false; } + /// is null public bool TryAdd(T value) { if (value == null) throw new ArgumentNullException(paramName: "key"); @@ -279,6 +284,7 @@ namespace Discord } } + /// is null public bool TryRemove(T value) { if (value == null) throw new ArgumentNullException(paramName: "key"); diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index e2a8faa75..608476889 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -2,13 +2,12 @@ using System; namespace Discord { - //Source: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs + /// internal static class DateTimeUtils { public static DateTimeOffset FromTicks(long ticks) => new DateTimeOffset(ticks, TimeSpan.Zero); public static DateTimeOffset? FromTicks(long? ticks) => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; - } } diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index a081b5a5a..059df6b5a 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -4,26 +4,55 @@ using System.Text; namespace Discord { + /// + /// Provides a series of helper methods for parsing mentions. + /// public static class MentionUtils { private const char SanitizeChar = '\x200b'; //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; + /// + /// Returns a mention string based on the user ID. + /// + /// + /// A user mention string (e.g. <@80351110224678912>). + /// public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); internal static string MentionChannel(string id) => $"<#{id}>"; + /// + /// Returns a mention string based on the channel ID. + /// + /// + /// A channel mention string (e.g. <#103735883630395392>). + /// public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); - internal static string MentionRole(string id) => $"<@&{id}>"; + internal static string MentionRole(string id) => $"<@&{id}>"; + /// + /// Returns a mention string based on the role ID. + /// + /// + /// A role mention string (e.g. <@&165511591545143296>). + /// public static string MentionRole(ulong id) => MentionRole(id.ToString()); - /// Parses a provided user mention string. + /// + /// Parses a provided user mention string. + /// + /// The user mention. + /// Invalid mention format. public static ulong ParseUser(string text) { if (TryParseUser(text, out ulong id)) return id; - throw new ArgumentException(message: "Invalid mention format", paramName: nameof(text)); + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); } - /// Tries to parse a provided user mention string. + /// + /// Tries to parse a provided user mention string. + /// + /// The user mention. + /// The UserId of the user. public static bool TryParseUser(string text, out ulong userId) { if (text.Length >= 3 && text[0] == '<' && text[1] == '@' && text[text.Length - 1] == '>') @@ -40,14 +69,19 @@ namespace Discord return false; } - /// Parses a provided channel mention string. + /// + /// Parses a provided channel mention string. + /// + /// Invalid mention format. public static ulong ParseChannel(string text) { if (TryParseChannel(text, out ulong id)) return id; - throw new ArgumentException(message: "Invalid mention format", paramName: nameof(text)); + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); } - /// Tries to parse a provided channel mention string. + /// + /// Tries to parse a provided channel mention string. + /// public static bool TryParseChannel(string text, out ulong channelId) { if (text.Length >= 3 && text[0] == '<' && text[1] == '#' && text[text.Length - 1] == '>') @@ -61,14 +95,19 @@ namespace Discord return false; } - /// Parses a provided role mention string. + /// + /// Parses a provided role mention string. + /// + /// Invalid mention format. public static ulong ParseRole(string text) { if (TryParseRole(text, out ulong id)) return id; - throw new ArgumentException(message: "Invalid mention format", paramName: nameof(text)); + throw new ArgumentException(message: "Invalid mention format.", paramName: nameof(text)); } - /// Tries to parse a provided role mention string. + /// + /// Tries to parse a provided role mention string. + /// public static bool TryParseRole(string text, out ulong roleId) { if (text.Length >= 4 && text[0] == '<' && text[1] == '@' && text[2] == '&' && text[text.Length - 1] == '>') diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index eb3cbdca2..985be9240 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Discord @@ -7,10 +7,11 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Optional { - public static Optional Unspecified => default(Optional); + public static Optional Unspecified => default; private readonly T _value; /// Gets the value for this parameter. + /// This property has no value set. public T Value { get @@ -42,18 +43,18 @@ namespace Discord public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0; public override string ToString() => IsSpecified ? _value?.ToString() : null; - private string DebuggerDisplay => IsSpecified ? (_value?.ToString() ?? "") : ""; + private string DebuggerDisplay => IsSpecified ? _value?.ToString() ?? "" : ""; - public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator Optional(T value) => new(value); public static explicit operator T(Optional value) => value.Value; } public static class Optional { public static Optional Create() => Optional.Unspecified; - public static Optional Create(T value) => new Optional(value); + public static Optional Create(T value) => new(value); public static T? ToNullable(this Optional val) where T : struct - => val.IsSpecified ? val.Value : (T?)null; + => val.IsSpecified ? val.Value : null; } } diff --git a/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs b/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs index a31721875..84209902a 100644 --- a/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs +++ b/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs @@ -25,26 +25,28 @@ namespace Discord _nextPage = nextPage; } - public IAsyncEnumerator> GetEnumerator() => new Enumerator(this); + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => new Enumerator(this, cancellationToken); internal class Enumerator : IAsyncEnumerator> { private readonly PagedAsyncEnumerable _source; + private readonly CancellationToken _token; private readonly PageInfo _info; public IReadOnlyCollection Current { get; private set; } - public Enumerator(PagedAsyncEnumerable source) + public Enumerator(PagedAsyncEnumerable source, CancellationToken token) { _source = source; + _token = token; _info = new PageInfo(source._start, source._count, source.PageSize); } - public async Task MoveNext(CancellationToken cancelToken) + public async ValueTask MoveNextAsync() { if (_info.Remaining == 0) return false; - var data = await _source._getPage(_info, cancelToken).ConfigureAwait(false); + var data = await _source._getPage(_info, _token).ConfigureAwait(false); Current = new Page(_info, data); _info.Page++; @@ -71,7 +73,11 @@ namespace Discord return true; } - public void Dispose() { Current = null; } + public ValueTask DisposeAsync() + { + Current = null; + return default; + } } } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index 6e7125ab7..fd0fe091a 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -165,7 +165,7 @@ namespace Discord resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; } } - resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) + resolvedPermissions &= mask; //Ensure we didn't get any permissions this channel doesn't support (from guildPerms, for example) } return resolvedPermissions; diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 6cbf4a173..ff8eb7c0d 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -4,8 +4,10 @@ namespace Discord { internal static class Preconditions { - //Objects + #region Objects + /// must not be . public static void NotNull(T obj, string name, string msg = null) where T : class { if (obj == null) throw CreateNotNullException(name, msg); } + /// must not be . public static void NotNull(Optional obj, string name, string msg = null) where T : class { if (obj.IsSpecified && obj.Value == null) throw CreateNotNullException(name, msg); } private static ArgumentNullException CreateNotNullException(string name, string msg) @@ -13,15 +15,22 @@ namespace Discord if (msg == null) return new ArgumentNullException(paramName: name); else return new ArgumentNullException(paramName: name, message: msg); } + #endregion - //Strings + #region Strings + /// cannot be blank. public static void NotEmpty(string obj, string name, string msg = null) { if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. public static void NotEmpty(Optional obj, string name, string msg = null) { if (obj.IsSpecified && obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. + /// must not be . public static void NotNullOrEmpty(string obj, string name, string msg = null) { if (obj == null) throw CreateNotNullException(name, msg); if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. + /// must not be . public static void NotNullOrEmpty(Optional obj, string name, string msg = null) { if (obj.IsSpecified) @@ -30,11 +39,15 @@ namespace Discord if (obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); } } + /// cannot be blank. + /// must not be . public static void NotNullOrWhitespace(string obj, string name, string msg = null) { if (obj == null) throw CreateNotNullException(name, msg); if (obj.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. + /// must not be . public static void NotNullOrWhitespace(Optional obj, string name, string msg = null) { if (obj.IsSpecified) @@ -46,125 +59,224 @@ namespace Discord private static ArgumentException CreateNotEmptyException(string name, string msg) => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); + #endregion - //Numerics + #region Numerics + /// Value may not be equal to . public static void NotEqual(sbyte obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(byte obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(short obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(ushort obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(int obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(uint obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(long obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(ulong obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(sbyte? obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(byte? obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(short? obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(ushort? obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(int? obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(uint? obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(long? obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(ulong? obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } private static ArgumentException CreateNotEqualException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value may not be equal to {value}", paramName: name); + => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); + /// Value must be at least . public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(byte obj, byte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(short obj, short value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(ushort obj, ushort value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(int obj, int value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(uint obj, uint value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(long obj, long value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(ulong obj, ulong value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . public static void AtLeast(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } private static ArgumentException CreateAtLeastException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be at least {value}", paramName: name); - + => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); + + /// Value must be greater than . public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(byte obj, byte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(short obj, short value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(ushort obj, ushort value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(int obj, int value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(uint obj, uint value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(long obj, long value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(ulong obj, ulong value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . public static void GreaterThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } private static ArgumentException CreateGreaterThanException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be greater than {value}", paramName: name); - + => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); + + /// Value must be at most . public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(byte obj, byte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(short obj, short value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(ushort obj, ushort value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(int obj, int value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(uint obj, uint value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(long obj, long value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(ulong obj, ulong value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . public static void AtMost(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } private static ArgumentException CreateAtMostException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be at most {value}", paramName: name); - + => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); + + /// Value must be less than . public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(byte obj, byte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(short obj, short value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(ushort obj, ushort value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(int obj, int value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(uint obj, uint value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(long obj, long value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(ulong obj, ulong value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . public static void LessThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } private static ArgumentException CreateLessThanException(string name, string msg, T value) - => new ArgumentException(message: msg ?? $"Value must be less than {value}", paramName: name); + => new ArgumentException(message: msg ?? $"Value must be less than {value}.", paramName: name); + #endregion - // Bulk Delete + #region Bulk Delete + /// Messages are younger than 2 weeks. public static void YoungerThanTwoWeeks(ulong[] collection, string name) { var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); @@ -175,13 +287,15 @@ namespace Discord throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); } } + /// The everyone role cannot be assigned to a user. public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) { for (var i = 0; i < roles.Length; i++) { if (roles[i] == guildId) - throw new ArgumentException(message: "The everyone role cannot be assigned to a user", paramName: name); + throw new ArgumentException(message: "The everyone role cannot be assigned to a user.", paramName: name); } } + #endregion } } diff --git a/src/Discord.Net.Core/Utils/SnowflakeUtils.cs b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs index eecebfb24..e52c99376 100644 --- a/src/Discord.Net.Core/Utils/SnowflakeUtils.cs +++ b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs @@ -2,10 +2,27 @@ using System; namespace Discord { + /// + /// Provides a series of helper methods for handling snowflake identifiers. + /// public static class SnowflakeUtils { + /// + /// Resolves the time of which the snowflake is generated. + /// + /// The snowflake identifier to resolve. + /// + /// A representing the time for when the object is generated. + /// public static DateTimeOffset FromSnowflake(ulong value) => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); + /// + /// Generates a pseudo-snowflake identifier with a . + /// + /// The time to be used in the new snowflake. + /// + /// A representing the newly generated snowflake identifier. + /// public static ulong ToSnowflake(DateTimeOffset value) => ((ulong)value.ToUnixTimeMilliseconds() - 1420070400000UL) << 22; } diff --git a/src/Discord.Net.Core/Utils/TokenUtils.cs b/src/Discord.Net.Core/Utils/TokenUtils.cs index 6decdc52f..c3dd39237 100644 --- a/src/Discord.Net.Core/Utils/TokenUtils.cs +++ b/src/Discord.Net.Core/Utils/TokenUtils.cs @@ -1,25 +1,159 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Globalization; using System.Text; -using System.Threading.Tasks; namespace Discord { + /// + /// Provides a series of helper methods for handling Discord login tokens. + /// public static class TokenUtils { + /// + /// The minimum length of a Bot token. + /// + /// + /// This value was determined by comparing against the examples in the Discord + /// documentation, and pre-existing tokens. + /// + internal const int MinBotTokenLength = 58; + + internal const char Base64Padding = '='; + + /// + /// Pads a base64-encoded string with 0, 1, or 2 '=' characters, + /// if the string is not a valid multiple of 4. + /// Does not ensure that the provided string contains only valid base64 characters. + /// Strings that already contain padding will not have any more padding applied. + /// + /// + /// A string that would require 3 padding characters is considered to be already corrupt. + /// Some older bot tokens may require padding, as the format provided by Discord + /// does not include this padding in the token. + /// + /// The base64 encoded string to pad with characters. + /// A string containing the base64 padding. + /// + /// Thrown if would require an invalid number of padding characters. + /// + /// + /// Thrown if is null, empty, or whitespace. + /// + internal static string PadBase64String(string encodedBase64) + { + if (string.IsNullOrWhiteSpace(encodedBase64)) + throw new ArgumentNullException(paramName: encodedBase64, + message: "The supplied base64-encoded string was null or whitespace."); + + // do not pad if already contains padding characters + if (encodedBase64.IndexOf(Base64Padding) != -1) + return encodedBase64; + + // based from https://stackoverflow.com/a/1228744 + var padding = (4 - (encodedBase64.Length % 4)) % 4; + if (padding == 3) + // can never have 3 characters of padding + throw new FormatException("The provided base64 string is corrupt, as it requires an invalid amount of padding."); + else if (padding == 0) + return encodedBase64; + return encodedBase64.PadRight(encodedBase64.Length + padding, Base64Padding); + } + + /// + /// Decodes a base 64 encoded string into a ulong value. + /// + /// A base 64 encoded string containing a User Id. + /// A ulong containing the decoded value of the string, or null if the value was invalid. + internal static ulong? DecodeBase64UserId(string encoded) + { + if (string.IsNullOrWhiteSpace(encoded)) + return null; + + try + { + // re-add base64 padding if missing + encoded = PadBase64String(encoded); + // decode the base64 string + var bytes = Convert.FromBase64String(encoded); + var idStr = Encoding.UTF8.GetString(bytes); + // try to parse a ulong from the resulting string + if (ulong.TryParse(idStr, NumberStyles.None, CultureInfo.InvariantCulture, out var id)) + return id; + } + catch (DecoderFallbackException) + { + // ignore exception, can be thrown by GetString + } + catch (FormatException) + { + // ignore exception, can be thrown if base64 string is invalid + } + catch (ArgumentException) + { + // ignore exception, can be thrown by BitConverter, or by PadBase64String + } + return null; + } + + /// + /// Checks the validity of a bot token by attempting to decode a ulong userid + /// from the bot token. + /// + /// + /// The bot token to validate. + /// + /// + /// True if the bot token was valid, false if it was not. + /// + internal static bool CheckBotTokenValidity(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + // split each component of the JWT + var segments = message.Split('.'); + + // ensure that there are three parts + if (segments.Length != 3) + return false; + // return true if the user id could be determined + return DecodeBase64UserId(segments[0]).HasValue; + } + + /// + /// The set of all characters that are not allowed inside of a token. + /// + internal static char[] IllegalTokenCharacters = new char[] + { + ' ', '\t', '\r', '\n' + }; + + /// + /// Checks if the given token contains a whitespace or newline character + /// that would fail to log in. + /// + /// The token to validate. + /// + /// True if the token contains a whitespace or newline character. + /// + internal static bool CheckContainsIllegalCharacters(string token) + => token.IndexOfAny(IllegalTokenCharacters) != -1; + /// /// Checks the validity of the supplied token of a specific type. /// /// The type of token to validate. /// The token value to validate. - /// Thrown when the supplied token string is null, empty, or contains only whitespace. - /// Thrown when the supplied TokenType or token value is invalid. + /// Thrown when the supplied token string is null, empty, or contains only whitespace. + /// Thrown when the supplied or token value is invalid. public static void ValidateToken(TokenType tokenType, string token) { // A Null or WhiteSpace token of any type is invalid. if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(paramName: nameof(token), message: "A token cannot be null, empty, or contain only whitespace."); + // ensure that there are no whitespace or newline characters + if (CheckContainsIllegalCharacters(token)) + throw new ArgumentException(message: "The token contains a whitespace or newline character. Ensure that the token has been properly trimmed.", paramName: nameof(token)); switch (tokenType) { @@ -30,17 +164,21 @@ namespace Discord // no validation is performed on Bearer tokens break; case TokenType.Bot: - // bot tokens are assumed to be at least 59 characters in length + // bot tokens are assumed to be at least 58 characters in length // this value was determined by referencing examples in the discord documentation, and by comparing with // pre-existing tokens - if (token.Length < 59) - throw new ArgumentException(message: "A Bot token must be at least 59 characters in length.", paramName: nameof(token)); + if (token.Length < MinBotTokenLength) + throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); + // check the validity of the bot token by decoding the ulong userid from the jwt + if (!CheckBotTokenValidity(token)) + throw new ArgumentException(message: "The Bot token was invalid. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); break; default: // All unrecognized TokenTypes (including User tokens) are considered to be invalid. throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); } } - } } diff --git a/src/Discord.Net.Core/Utils/UrlValidation.cs b/src/Discord.Net.Core/Utils/UrlValidation.cs new file mode 100644 index 000000000..8e877bd4e --- /dev/null +++ b/src/Discord.Net.Core/Utils/UrlValidation.cs @@ -0,0 +1,42 @@ +using System; + +namespace Discord.Utils +{ + internal static class UrlValidation + { + /// + /// Not full URL validation right now. Just ensures protocol is present and that it's either http or https + /// should be used for url buttons. + /// + /// The URL to validate before sending to Discord. + /// to allow the attachment:// protocol; otherwise . + /// A URL must include a protocol (http or https). + /// true if URL is valid by our standard, false if null, throws an error upon invalid. + public static bool Validate(string url, bool allowAttachments = false) + { + if (string.IsNullOrEmpty(url)) + return false; + if (!(url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || (allowAttachments ? url.StartsWith("attachment://", StringComparison.Ordinal) : false))) + throw new InvalidOperationException($"The url {url} must include a protocol (either {(allowAttachments ? "HTTP, HTTPS, or ATTACHMENT" : "HTTP or HTTPS")})"); + return true; + } + + /// + /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord + /// should be used everything other than url buttons. + /// + /// The URL to validate before sending to discord. + /// A URL must include a protocol (either http, https, or discord). + /// true if the URL is valid by our standard, false if null, throws an error upon invalid. + public static bool ValidateButton(string url) + { + if (string.IsNullOrEmpty(url)) + return false; + if (!(url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("discord://", StringComparison.OrdinalIgnoreCase))) + throw new InvalidOperationException($"The url {url} must include a protocol (either HTTP, HTTPS, or DISCORD)"); + return true; + } + } +} diff --git a/src/Discord.Net.Examples/Core/Entities/Channels/IGuildChannel.Examples.cs b/src/Discord.Net.Examples/Core/Entities/Channels/IGuildChannel.Examples.cs new file mode 100644 index 000000000..d382ddbf3 --- /dev/null +++ b/src/Discord.Net.Examples/Core/Entities/Channels/IGuildChannel.Examples.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Discord.Net.Examples.Core.Entities.Channels +{ + [PublicAPI] + internal class GuildChannelExamples + { + #region AddPermissionOverwriteAsyncRole + + public async Task MuteRoleAsync(IRole role, IGuildChannel channel) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + if (channel == null) throw new ArgumentNullException(nameof(channel)); + + // Fetches the previous overwrite and bail if one is found + var previousOverwrite = channel.GetPermissionOverwrite(role); + if (previousOverwrite.HasValue && previousOverwrite.Value.SendMessages == PermValue.Deny) + throw new InvalidOperationException($"Role {role.Name} had already been muted in this channel."); + + // Creates a new OverwritePermissions with send message set to deny and pass it into the method + await channel.AddPermissionOverwriteAsync(role, new OverwritePermissions(sendMessages: PermValue.Deny)); + } + + #endregion + + #region AddPermissionOverwriteAsyncUser + + public async Task MuteUserAsync(IGuildUser user, IGuildChannel channel) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + if (channel == null) throw new ArgumentNullException(nameof(channel)); + + // Fetches the previous overwrite and bail if one is found + var previousOverwrite = channel.GetPermissionOverwrite(user); + if (previousOverwrite.HasValue && previousOverwrite.Value.SendMessages == PermValue.Deny) + throw new InvalidOperationException($"User {user.Username} had already been muted in this channel."); + + // Creates a new OverwritePermissions with send message set to deny and pass it into the method + await channel.AddPermissionOverwriteAsync(user, new OverwritePermissions(sendMessages: PermValue.Deny)); + } + + #endregion + } +} diff --git a/src/Discord.Net.Examples/Core/Entities/Channels/IMessageChannel.Examples.cs b/src/Discord.Net.Examples/Core/Entities/Channels/IMessageChannel.Examples.cs new file mode 100644 index 000000000..d920e9710 --- /dev/null +++ b/src/Discord.Net.Examples/Core/Entities/Channels/IMessageChannel.Examples.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Discord.Net.Examples.Core.Entities.Channels +{ + [PublicAPI] + internal class MessageChannelExamples + { + #region GetMessagesAsync.FromId.BeginningMessages + + public async Task PrintFirstMessages(IMessageChannel channel, int messageCount) + { + // Although the library does attempt to divide the messageCount by 100 + // to comply to Discord's maximum message limit per request, sending + // too many could still cause the queue to clog up. + // The purpose of this exception is to discourage users from sending + // too many requests at once. + if (messageCount > 1000) + throw new InvalidOperationException("Too many messages requested."); + + // Setting fromMessageId to 0 will make Discord + // default to the first message in channel. + var messages = await channel.GetMessagesAsync( + 0, Direction.After, messageCount) + .FlattenAsync(); + + // Print message content + foreach (var message in messages) + Console.WriteLine($"{message.Author} posted '{message.Content}' at {message.CreatedAt}."); + } + + #endregion + + public async Task GetMessagesExampleBody(IMessageChannel channel) + { +#pragma warning disable IDISP001 +#pragma warning disable IDISP014 + // We're just declaring this for the sample below. + // Ideally, you want to get or create your HttpClient + // from IHttpClientFactory. + // You get a bonus for reading the example source though! + var httpClient = new HttpClient(); +#pragma warning restore IDISP014 +#pragma warning restore IDISP001 + + // Another dummy method + Task LongRunningAsync() + { + return Task.Delay(0); + } + + #region GetMessagesAsync.FromLimit.Standard + + var messages = await channel.GetMessagesAsync(300).FlattenAsync(); + var userMessages = messages.Where(x => x.Author.Id == 53905483156684800); + + #endregion + + #region GetMessagesAsync.FromMessage + + var oldMessage = await channel.SendMessageAsync("boi"); + var messagesFromMsg = await channel.GetMessagesAsync(oldMessage, Direction.Before, 5).FlattenAsync(); + + #endregion + + + #region GetMessagesAsync.FromId.FromMessage + + await channel.GetMessagesAsync(442012544660537354, Direction.Before, 5).FlattenAsync(); + + #endregion + + #region SendMessageAsync + + var message = await channel.SendMessageAsync(DateTimeOffset.UtcNow.ToString("R")); + await Task.Delay(TimeSpan.FromSeconds(5)) + .ContinueWith(x => message.DeleteAsync()); + + #endregion + + #region SendFileAsync.FilePath + + await channel.SendFileAsync("wumpus.txt", "good discord boi"); + + #endregion + + #region SendFileAsync.FilePath.EmbeddedImage + + await channel.SendFileAsync("b1nzy.jpg", + embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + + #endregion + + + #region SendFileAsync.FileStream.EmbeddedImage + + using (var b1nzyStream = await httpClient.GetStreamAsync("https://example.com/b1nzy")) + await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", + embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + + #endregion + + #region EnterTypingState + + using (channel.EnterTypingState()) await LongRunningAsync(); + + #endregion + } + } +} diff --git a/src/Discord.Net.Examples/Core/Entities/Guilds/IGuild.Examples.cs b/src/Discord.Net.Examples/Core/Entities/Guilds/IGuild.Examples.cs new file mode 100644 index 000000000..03144b232 --- /dev/null +++ b/src/Discord.Net.Examples/Core/Entities/Guilds/IGuild.Examples.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Discord.Net.Examples.Core.Entities.Guilds +{ + [PublicAPI] + internal class GuildExamples + { + #region CreateTextChannelAsync + public async Task CreateTextChannelUnderWumpus(IGuild guild, string name) + { + var categories = await guild.GetCategoriesAsync(); + var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); + if (targetCategory == null) return; + await guild.CreateTextChannelAsync(name, x => + { + x.CategoryId = targetCategory.Id; + x.Topic = $"This channel was created at {DateTimeOffset.UtcNow}."; + }); + } + #endregion + } +} diff --git a/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs b/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs new file mode 100644 index 000000000..83daedaa0 --- /dev/null +++ b/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs @@ -0,0 +1,38 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Discord.Net.Examples.Core.Entities.Users +{ + [PublicAPI] + internal class UserExamples + { + #region GetAvatarUrl + + public async Task GetAvatarAsync(IUser user, ITextChannel textChannel) + { + var userAvatarUrl = user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl(); + await textChannel.SendMessageAsync(userAvatarUrl); + } + + #endregion + + #region CreateDMChannelAsync + + public async Task MessageUserAsync(IUser user) + { + var channel = await user.CreateDMChannelAsync(); + try + { + await channel.SendMessageAsync("Awesome stuff!"); + } + catch (Discord.Net.HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) + { + Console.WriteLine($"Boo, I cannot message {user}."); + } + } + + #endregion + } +} diff --git a/src/Discord.Net.Examples/Discord.Net.Examples.csproj b/src/Discord.Net.Examples/Discord.Net.Examples.csproj new file mode 100644 index 000000000..3371432b8 --- /dev/null +++ b/src/Discord.Net.Examples/Discord.Net.Examples.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + diff --git a/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs new file mode 100644 index 000000000..27d393c07 --- /dev/null +++ b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord.WebSocket; +using JetBrains.Annotations; + +namespace Discord.Net.Examples.WebSocket +{ + [PublicAPI] + internal class BaseSocketClientExamples + { + #region ReactionAdded + + public void HookReactionAdded(BaseSocketClient client) + => client.ReactionAdded += HandleReactionAddedAsync; + + public async Task HandleReactionAddedAsync(Cacheable cachedMessage, + Cacheable originChannel, SocketReaction reaction) + { + var message = await cachedMessage.GetOrDownloadAsync(); + if (message != null && reaction.User.IsSpecified) + Console.WriteLine($"{reaction.User.Value} just added a reaction '{reaction.Emote}' " + + $"to {message.Author}'s message ({message.Id})."); + } + + #endregion + + #region ChannelCreated + + public void HookChannelCreated(BaseSocketClient client) + => client.ChannelCreated += HandleChannelCreated; + + public Task HandleChannelCreated(SocketChannel channel) + { + if (channel is SocketGuildChannel guildChannel) + Console.WriteLine($"A new channel '{guildChannel.Name}'({guildChannel.Id}, {guildChannel.GetType()})" + + $"has been created at {guildChannel.CreatedAt}."); + return Task.CompletedTask; + } + + #endregion + + #region ChannelDestroyed + + public void HookChannelDestroyed(BaseSocketClient client) + => client.ChannelDestroyed += HandleChannelDestroyed; + + public Task HandleChannelDestroyed(SocketChannel channel) + { + if (channel is SocketGuildChannel guildChannel) + Console.WriteLine( + $"A new channel '{guildChannel.Name}'({guildChannel.Id}, {guildChannel.GetType()}) has been deleted."); + return Task.CompletedTask; + } + + #endregion + + #region ChannelUpdated + + public void HookChannelUpdated(BaseSocketClient client) + => client.ChannelUpdated += HandleChannelRename; + + public Task HandleChannelRename(SocketChannel beforeChannel, SocketChannel afterChannel) + { + if (beforeChannel is SocketGuildChannel beforeGuildChannel && + afterChannel is SocketGuildChannel afterGuildChannel) + if (beforeGuildChannel.Name != afterGuildChannel.Name) + Console.WriteLine( + $"A channel ({beforeChannel.Id}) is renamed from {beforeGuildChannel.Name} to {afterGuildChannel.Name}."); + return Task.CompletedTask; + } + + #endregion + + #region MessageReceived + + private readonly ulong[] _targetUserIds = {168693960628371456, 53905483156684800}; + + public void HookMessageReceived(BaseSocketClient client) + => client.MessageReceived += HandleMessageReceived; + + public Task HandleMessageReceived(SocketMessage message) + { + // check if the message is a user message as opposed to a system message (e.g. Clyde, pins, etc.) + if (!(message is SocketUserMessage userMessage)) return Task.CompletedTask; + // check if the message origin is a guild message channel + if (!(userMessage.Channel is SocketTextChannel textChannel)) return Task.CompletedTask; + // check if the target user was mentioned + var targetUsers = userMessage.MentionedUsers.Where(x => _targetUserIds.Contains(x.Id)); + foreach (var targetUser in targetUsers) + Console.WriteLine( + $"{targetUser} was mentioned in the message '{message.Content}' by {message.Author} in {textChannel.Name}."); + return Task.CompletedTask; + } + + #endregion + + #region MessageDeleted + + public void HookMessageDeleted(BaseSocketClient client) + => client.MessageDeleted += HandleMessageDelete; + + public async Task HandleMessageDelete(Cacheable cachedMessage, Cacheable cachedChannel) + { + // check if the message exists in cache; if not, we cannot report what was removed + if (!cachedMessage.HasValue) return; + // gets or downloads the channel if it's not in the cache + IMessageChannel channel = await cachedChannel.GetOrDownloadAsync(); + var message = cachedMessage.Value; + Console.WriteLine( + $"A message ({message.Id}) from {message.Author} was removed from the channel {channel.Name} ({channel.Id}):" + + Environment.NewLine + + message.Content); + } + + #endregion + } +} diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj index bfd0983ce..e143340e1 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -1,10 +1,10 @@ - + Discord.Net.Providers.WS4Net Discord.Providers.WS4Net An optional WebSocket client provider for Discord.Net using WebSocket4Net - netstandard1.3 + netstandard2.0 diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index 1894a8906..50f19b778 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -1,4 +1,4 @@ -using Discord.Net.WebSockets; +using Discord.Net.WebSockets; using System; using System.Collections.Generic; using System.Linq; @@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private WS4NetSocket _client; + private CancellationTokenSource _disconnectCancelTokenSource; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private ManualResetEventSlim _waitUntilConnect; @@ -28,7 +29,7 @@ namespace Discord.Net.Providers.WS4Net { _headers = new Dictionary(); _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _disconnectCancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _waitUntilConnect = new ManualResetEventSlim(); @@ -38,7 +39,11 @@ namespace Discord.Net.Providers.WS4Net if (!_isDisposed) { if (disposing) - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + { + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); + _lock?.Dispose(); + _cancelTokenSource?.Dispose(); + } _isDisposed = true; } } @@ -63,8 +68,13 @@ namespace Discord.Net.Providers.WS4Net { await DisconnectInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _disconnectCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _client?.Dispose(); + + _disconnectCancelTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) { @@ -82,27 +92,27 @@ namespace Discord.Net.Providers.WS4Net _waitUntilConnect.Wait(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private Task DisconnectInternalAsync(bool isDisposing = false) + private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { - _cancelTokenSource.Cancel(); + _disconnectCancelTokenSource.Cancel(); if (_client == null) return Task.Delay(0); if (_client.State == WebSocketState.Open) { - try { _client.Close(1000, ""); } + try { _client.Close(closeCode, ""); } catch { } } @@ -125,8 +135,10 @@ namespace Discord.Net.Providers.WS4Net } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count, bool isText) @@ -151,7 +163,7 @@ namespace Discord.Net.Providers.WS4Net } private void OnBinaryMessage(object sender, DataReceivedEventArgs e) { - BinaryMessage(e.Data, 0, e.Data.Count()).GetAwaiter().GetResult(); + BinaryMessage(e.Data, 0, e.Data.Length).GetAwaiter().GetResult(); } private void OnConnected(object sender, object e) { diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs b/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs index 166e767d0..b56f3b4f0 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetProvider.cs @@ -1,4 +1,4 @@ -using Discord.Net.WebSockets; +using Discord.Net.WebSockets; namespace Discord.Net.Providers.WS4Net { diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs new file mode 100644 index 000000000..9dede7e03 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API +{ + internal class ActionRowComponent : IMessageComponent + { + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("components")] + public IMessageComponent[] Components { get; set; } + + internal ActionRowComponent() { } + internal ActionRowComponent(Discord.ActionRowComponent c) + { + Type = c.Type; + Components = c.Components?.Select(x => + { + return x.Type switch + { + ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), + ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), + _ => null + }; + }).ToArray(); + } + + [JsonIgnore] + string IMessageComponent.CustomId => null; + } +} diff --git a/src/Discord.Net.Rest/API/Common/AllowedMentions.cs b/src/Discord.Net.Rest/API/Common/AllowedMentions.cs new file mode 100644 index 000000000..7737a464f --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AllowedMentions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AllowedMentions + { + [JsonProperty("parse")] + public Optional Parse { get; set; } + // Roles and Users have a max size of 100 + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("users")] + public Optional Users { get; set; } + [JsonProperty("replied_user")] + public Optional RepliedUser { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Application.cs b/src/Discord.Net.Rest/API/Common/Application.cs index ca4c443f1..4ef6940a2 100644 --- a/src/Discord.Net.Rest/API/Common/Application.cs +++ b/src/Discord.Net.Rest/API/Common/Application.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -8,17 +7,27 @@ namespace Discord.API [JsonProperty("description")] public string Description { get; set; } [JsonProperty("rpc_origins")] - public string[] RPCOrigins { get; set; } + public Optional RPCOrigins { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("icon")] public string Icon { get; set; } + [JsonProperty("bot_public")] + public bool IsBotPublic { get; set; } + [JsonProperty("bot_require_code_grant")] + public bool BotRequiresCodeGrant { get; set; } + [JsonProperty("install_params")] + public Optional InstallParams { get; set; } + + [JsonProperty("team")] + public Team Team { get; set; } [JsonProperty("flags"), Int53] - public Optional Flags { get; set; } + public Optional Flags { get; set; } [JsonProperty("owner")] public Optional Owner { get; set; } + public Optional Tags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs new file mode 100644 index 000000000..81598b96e --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ApplicationCommand + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } = ApplicationCommandType.Slash; // defaults to 1 which is slash. + + [JsonProperty("application_id")] + public ulong ApplicationId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("default_permission")] + public Optional DefaultPermissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs new file mode 100644 index 000000000..a98ed77d6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ApplicationCommandInteractionData : IResolvable, IDiscordInteractionData + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } + + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs new file mode 100644 index 000000000..1e488c4e6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ApplicationCommandInteractionDataOption + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public ApplicationCommandOptionType Type { get; set; } + + [JsonProperty("value")] + public Optional Value { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs new file mode 100644 index 000000000..5b4b83e23 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API +{ + internal class ApplicationCommandInteractionDataResolved + { + [JsonProperty("users")] + public Optional> Users { get; set; } + + [JsonProperty("members")] + public Optional> Members { get; set; } + + [JsonProperty("channels")] + public Optional> Channels { get; set; } + + [JsonProperty("roles")] + public Optional> Roles { get; set; } + [JsonProperty("messages")] + public Optional> Messages { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs new file mode 100644 index 000000000..1207df282 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -0,0 +1,88 @@ +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API +{ + internal class ApplicationCommandOption + { + [JsonProperty("type")] + public ApplicationCommandOptionType Type { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("default")] + public Optional Default { get; set; } + + [JsonProperty("required")] + public Optional Required { get; set; } + + [JsonProperty("choices")] + public Optional Choices { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("autocomplete")] + public Optional Autocomplete { get; set; } + + [JsonProperty("min_value")] + public Optional MinValue { get; set; } + + [JsonProperty("max_value")] + public Optional MaxValue { get; set; } + + [JsonProperty("channel_types")] + public Optional ChannelTypes { get; set; } + + public ApplicationCommandOption() { } + + public ApplicationCommandOption(IApplicationCommandOption cmd) + { + Choices = cmd.Choices.Select(x => new ApplicationCommandOptionChoice + { + Name = x.Name, + Value = x.Value + }).ToArray(); + + Options = cmd.Options.Select(x => new ApplicationCommandOption(x)).ToArray(); + + ChannelTypes = cmd.ChannelTypes.ToArray(); + + Required = cmd.IsRequired ?? Optional.Unspecified; + Default = cmd.IsDefault ?? Optional.Unspecified; + MinValue = cmd.MinValue ?? Optional.Unspecified; + MaxValue = cmd.MaxValue ?? Optional.Unspecified; + + Name = cmd.Name; + Type = cmd.Type; + Description = cmd.Description; + } + public ApplicationCommandOption(ApplicationCommandOptionProperties option) + { + Choices = option.Choices?.Select(x => new ApplicationCommandOptionChoice + { + Name = x.Name, + Value = x.Value + }).ToArray() ?? Optional.Unspecified; + + Options = option.Options?.Select(x => new ApplicationCommandOption(x)).ToArray() ?? Optional.Unspecified; + + Required = option.IsRequired ?? Optional.Unspecified; + + Default = option.IsDefault ?? Optional.Unspecified; + MinValue = option.MinValue ?? Optional.Unspecified; + MaxValue = option.MaxValue ?? Optional.Unspecified; + + ChannelTypes = option.ChannelTypes?.ToArray() ?? Optional.Unspecified; + + Name = option.Name; + Type = option.Type; + Description = option.Description; + Autocomplete = option.IsAutocomplete; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs new file mode 100644 index 000000000..6f84437f6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ApplicationCommandOptionChoice + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("value")] + public object Value { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs new file mode 100644 index 000000000..8bde80f50 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ApplicationCommandPermissions + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("type")] + public ApplicationCommandPermissionTarget Type { get; set; } + + [JsonProperty("permission")] + public bool Permission { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Attachment.cs b/src/Discord.Net.Rest/API/Common/Attachment.cs index 4a651d9fa..7970dc9a5 100644 --- a/src/Discord.Net.Rest/API/Common/Attachment.cs +++ b/src/Discord.Net.Rest/API/Common/Attachment.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -9,6 +8,10 @@ namespace Discord.API public ulong Id { get; set; } [JsonProperty("filename")] public string Filename { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("content_type")] + public Optional ContentType { get; set; } [JsonProperty("size")] public int Size { get; set; } [JsonProperty("url")] @@ -19,5 +22,7 @@ namespace Discord.API public Optional Height { get; set; } [JsonProperty("width")] public Optional Width { get; set; } + [JsonProperty("ephemeral")] + public Optional Ephemeral { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/AuditLog.cs b/src/Discord.Net.Rest/API/Common/AuditLog.cs index cd8ad147d..c8bd6d3e2 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLog.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLog.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { @@ -7,6 +7,12 @@ namespace Discord.API [JsonProperty("webhooks")] public Webhook[] Webhooks { get; set; } + [JsonProperty("threads")] + public Channel[] Threads { get; set; } + + [JsonProperty("integrations")] + public Integration[] Integrations { get; set; } + [JsonProperty("users")] public User[] Users { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs index 80d9a9e97..9626ad67e 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { @@ -7,7 +7,7 @@ namespace Discord.API [JsonProperty("target_id")] public ulong? TargetId { get; set; } [JsonProperty("user_id")] - public ulong UserId { get; set; } + public ulong? UserId { get; set; } [JsonProperty("changes")] public AuditLogChange[] Changes { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs index 24141d90c..b666215e2 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -4,11 +4,12 @@ namespace Discord.API { internal class AuditLogOptions { - //Message delete [JsonProperty("count")] - public int? MessageDeleteCount { get; set; } + public int? Count { get; set; } [JsonProperty("channel_id")] - public ulong? MessageDeleteChannelId { get; set; } + public ulong? ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong? MessageId { get; set; } //Prune [JsonProperty("delete_member_days")] diff --git a/src/Discord.Net.Rest/API/Common/AutocompleteInteractionData.cs b/src/Discord.Net.Rest/API/Common/AutocompleteInteractionData.cs new file mode 100644 index 000000000..2184a0e98 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AutocompleteInteractionData.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AutocompleteInteractionData : IDiscordInteractionData + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } + + [JsonProperty("version")] + public ulong Version { get; set; } + + [JsonProperty("options")] + public AutocompleteInteractionDataOption[] Options { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AutocompleteInteractionDataOption.cs b/src/Discord.Net.Rest/API/Common/AutocompleteInteractionDataOption.cs new file mode 100644 index 000000000..1419f93b6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AutocompleteInteractionDataOption.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AutocompleteInteractionDataOption + { + [JsonProperty("type")] + public ApplicationCommandOptionType Type { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("value")] + public Optional Value { get; set; } + + [JsonProperty("focused")] + public Optional Focused { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Ban.cs b/src/Discord.Net.Rest/API/Common/Ban.cs index 202004f53..ff47c7904 100644 --- a/src/Discord.Net.Rest/API/Common/Ban.cs +++ b/src/Discord.Net.Rest/API/Common/Ban.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs new file mode 100644 index 000000000..7f737d7ad --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ButtonComponent : IMessageComponent + { + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("style")] + public ButtonStyle Style { get; set; } + + [JsonProperty("label")] + public Optional Label { get; set; } + + [JsonProperty("emoji")] + public Optional Emote { get; set; } + + [JsonProperty("custom_id")] + public Optional CustomId { get; set; } + + [JsonProperty("url")] + public Optional Url { get; set; } + + [JsonProperty("disabled")] + public Optional Disabled { get; set; } + + public ButtonComponent() { } + + public ButtonComponent(Discord.ButtonComponent c) + { + Type = c.Type; + Style = c.Style; + Label = c.Label; + CustomId = c.CustomId; + Url = c.Url; + Disabled = c.IsDisabled; + + if (c.Emote != null) + { + if (c.Emote is Emote e) + { + Emote = new Emoji + { + Name = e.Name, + Animated = e.Animated, + Id = e.Id + }; + } + else + { + Emote = new Emoji + { + Name = c.Emote.Name + }; + } + } + } + + [JsonIgnore] + string IMessageComponent.CustomId => CustomId.GetValueOrDefault(); + } +} diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 57a5ce9ab..afd219b63 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -49,5 +48,21 @@ namespace Discord.API //GroupChannel [JsonProperty("icon")] public Optional Icon { get; set; } + + //ThreadChannel + [JsonProperty("member")] + public Optional ThreadMember { get; set; } + + [JsonProperty("thread_metadata")] + public Optional ThreadMetadata { get; set; } + + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } + + [JsonProperty("message_count")] + public Optional MessageCount { get; set; } + + [JsonProperty("member_count")] + public Optional MemberCount { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs new file mode 100644 index 000000000..94b2396bf --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ChannelThreads + { + [JsonProperty("threads")] + public Channel[] Threads { get; set; } + + [JsonProperty("members")] + public ThreadMember[] Members { get; set; } + + [JsonProperty("has_more")] + public bool HasMore { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Connection.cs b/src/Discord.Net.Rest/API/Common/Connection.cs index ad0a76ac1..bd8de3902 100644 --- a/src/Discord.Net.Rest/API/Common/Connection.cs +++ b/src/Discord.Net.Rest/API/Common/Connection.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; diff --git a/src/Discord.Net.Rest/API/Common/DiscordError.cs b/src/Discord.Net.Rest/API/Common/DiscordError.cs new file mode 100644 index 000000000..ac1e5e13d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/DiscordError.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + [JsonConverter(typeof(Discord.Net.Converters.DiscordErrorConverter))] + internal class DiscordError + { + [JsonProperty("message")] + public string Message { get; set; } + [JsonProperty("code")] + public DiscordErrorCode Code { get; set; } + [JsonProperty("errors")] + public Optional Errors { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index 1c9fa34e2..77efa12aa 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -1,7 +1,6 @@ -#pragma warning disable CS1591 using System; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using Discord.Net.Converters; namespace Discord.API { @@ -15,7 +14,7 @@ namespace Discord.API public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } - [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("type"), JsonConverter(typeof(EmbedTypeConverter))] public EmbedType Type { get; set; } [JsonProperty("timestamp")] public DateTimeOffset? Timestamp { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index 4381a9da3..d7f3ae68d 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -1,4 +1,3 @@ -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 3dd7020d9..cd08e7e26 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -1,4 +1,3 @@ -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index c6b3562a3..6b5db0681 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index 1658eda1a..ed0f7c3c8 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index 993beb72b..dd25a1a26 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index 610cf58a8..f668217f0 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 -using System; using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index 2bdfdcc36..ff0baa73e 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -17,5 +16,7 @@ namespace Discord.API public bool RequireColons { get; set; } [JsonProperty("managed")] public bool Managed { get; set; } + [JsonProperty("user")] + public Optional User { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Error.cs b/src/Discord.Net.Rest/API/Common/Error.cs new file mode 100644 index 000000000..a2b1777a3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Error.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Error + { + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 4cde8444a..105ce0d73 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Runtime.Serialization; @@ -33,6 +32,16 @@ namespace Discord.API public Optional SyncId { get; set; } [JsonProperty("session_id")] public Optional SessionId { get; set; } + [JsonProperty("Flags")] + public Optional Flags { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + [JsonProperty("created_at")] + public Optional CreatedAt { get; set; } + //[JsonProperty("buttons")] + //public Optional Buttons { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index 0ca1bc236..d550c54a0 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -13,6 +12,8 @@ namespace Discord.API public string Icon { get; set; } [JsonProperty("splash")] public string Splash { get; set; } + [JsonProperty("discovery_splash")] + public string DiscoverySplash { get; set; } [JsonProperty("owner_id")] public ulong OwnerId { get; set; } [JsonProperty("region")] @@ -21,14 +22,12 @@ namespace Discord.API 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("system_channel_id")] - public ulong? SystemChannelId { get; set; } [JsonProperty("verification_level")] public VerificationLevel VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public DefaultMessageNotifications DefaultMessageNotifications { get; set; } + [JsonProperty("explicit_content_filter")] + public ExplicitContentFilterLevel ExplicitContentFilter { get; set; } [JsonProperty("voice_states")] public VoiceState[] VoiceStates { get; set; } [JsonProperty("roles")] @@ -36,10 +35,53 @@ namespace Discord.API [JsonProperty("emojis")] public Emoji[] Emojis { get; set; } [JsonProperty("features")] - public string[] Features { get; set; } + public GuildFeatures Features { get; set; } [JsonProperty("mfa_level")] public MfaLevel MfaLevel { get; set; } - [JsonProperty("default_message_notifications")] - public DefaultMessageNotifications DefaultMessageNotifications { get; set; } + [JsonProperty("application_id")] + public ulong? ApplicationId { get; set; } + [JsonProperty("widget_enabled")] + public Optional WidgetEnabled { get; set; } + [JsonProperty("widget_channel_id")] + public Optional WidgetChannelId { get; set; } + [JsonProperty("system_channel_id")] + public ulong? SystemChannelId { get; set; } + [JsonProperty("premium_tier")] + public PremiumTier PremiumTier { get; set; } + [JsonProperty("vanity_url_code")] + public string VanityURLCode { get; set; } + [JsonProperty("banner")] + public string Banner { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + // this value is inverted, flags set will turn OFF features + [JsonProperty("system_channel_flags")] + public SystemChannelMessageDeny SystemChannelFlags { get; set; } + [JsonProperty("rules_channel_id")] + public ulong? RulesChannelId { get; set; } + [JsonProperty("max_presences")] + public Optional MaxPresences { get; set; } + [JsonProperty("max_members")] + public Optional MaxMembers { get; set; } + [JsonProperty("premium_subscription_count")] + public int? PremiumSubscriptionCount { get; set; } + [JsonProperty("preferred_locale")] + public string PreferredLocale { get; set; } + [JsonProperty("public_updates_channel_id")] + public ulong? PublicUpdatesChannelId { get; set; } + [JsonProperty("max_video_channel_users")] + public Optional MaxVideoChannelUsers { get; set; } + [JsonProperty("approximate_member_count")] + public Optional ApproximateMemberCount { get; set; } + [JsonProperty("approximate_presence_count")] + public Optional ApproximatePresenceCount { get; set; } + [JsonProperty("threads")] + public Optional Threads { get; set; } + [JsonProperty("nsfw_level")] + public NsfwLevel NsfwLevel { get; set; } + [JsonProperty("stickers")] + public Sticker[] Stickers { get; set; } + [JsonProperty("premium_progress_bar_enabled")] + public Optional IsBoostProgressBarEnabled { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..cc74299f7 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GuildApplicationCommandPermission + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("application_id")] + public ulong ApplicationId { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("permissions")] + public ApplicationCommandPermissions[] Permissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 24ad17c14..9b888e86a 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -10,6 +9,8 @@ namespace Discord.API public User User { get; set; } [JsonProperty("nick")] public Optional Nick { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } [JsonProperty("roles")] public Optional Roles { get; set; } [JsonProperty("joined_at")] @@ -18,5 +19,9 @@ namespace Discord.API public Optional Deaf { get; set; } [JsonProperty("mute")] public Optional Mute { get; set; } + [JsonProperty("pending")] + public Optional Pending { get; set; } + [JsonProperty("premium_since")] + public Optional PremiumSince { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs new file mode 100644 index 000000000..338c24dc9 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("creator_id")] + public Optional CreatorId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("scheduled_start_time")] + public DateTimeOffset ScheduledStartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public DateTimeOffset? ScheduledEndTime { get; set; } + [JsonProperty("privacy_level")] + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; set; } + [JsonProperty("status")] + public GuildScheduledEventStatus Status { get; set; } + [JsonProperty("entity_type")] + public GuildScheduledEventType EntityType { get; set; } + [JsonProperty("entity_id")] + public ulong? EntityId { get; set; } + [JsonProperty("entity_metadata")] + public GuildScheduledEventEntityMetadata EntityMetadata { get; set; } + [JsonProperty("creator")] + public Optional Creator { get; set; } + [JsonProperty("user_count")] + public Optional UserCount { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs new file mode 100644 index 000000000..1db38c0ae --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEventEntityMetadata.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEventEntityMetadata + { + [JsonProperty("location")] + public Optional Location { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs new file mode 100644 index 000000000..1b0b93763 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEventUser.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class GuildScheduledEventUser + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("member")] + public Optional Member { get; set; } + [JsonProperty("guild_scheduled_event_id")] + public ulong GuildScheduledEventId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildEmbed.cs b/src/Discord.Net.Rest/API/Common/GuildWidget.cs similarity index 61% rename from src/Discord.Net.Rest/API/Common/GuildEmbed.cs rename to src/Discord.Net.Rest/API/Common/GuildWidget.cs index ff8b8e180..6b1d29cce 100644 --- a/src/Discord.Net.Rest/API/Common/GuildEmbed.cs +++ b/src/Discord.Net.Rest/API/Common/GuildWidget.cs @@ -1,13 +1,12 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API { - internal class GuildEmbed + internal class GuildWidget { [JsonProperty("enabled")] public bool Enabled { get; set; } [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } + public ulong? ChannelId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InstallParams.cs b/src/Discord.Net.Rest/API/Common/InstallParams.cs new file mode 100644 index 000000000..1fb987f30 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InstallParams.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class InstallParams + { + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + [JsonProperty("permissions")] + public ulong Permission { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Integration.cs b/src/Discord.Net.Rest/API/Common/Integration.cs index 821359975..47d67e149 100644 --- a/src/Discord.Net.Rest/API/Common/Integration.cs +++ b/src/Discord.Net.Rest/API/Common/Integration.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; diff --git a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs index 22831e795..a8d33931c 100644 --- a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs +++ b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/Interaction.cs b/src/Discord.Net.Rest/API/Common/Interaction.cs new file mode 100644 index 000000000..7f953384d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Interaction.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + [JsonConverter(typeof(Net.Converters.InteractionConverter))] + internal class Interaction + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("application_id")] + public ulong ApplicationId { get; set; } + + [JsonProperty("type")] + public InteractionType Type { get; set; } + + [JsonProperty("data")] + public Optional Data { get; set; } + + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + + [JsonProperty("member")] + public Optional Member { get; set; } + + [JsonProperty("user")] + public Optional User { get; set; } + + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("version")] + public int Version { get; set; } + + [JsonProperty("message")] + public Optional Message { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs new file mode 100644 index 000000000..b07ebff49 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class InteractionCallbackData + { + [JsonProperty("tts")] + public Optional TTS { get; set; } + + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("choices")] + public Optional Choices { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/InteractionResponse.cs b/src/Discord.Net.Rest/API/Common/InteractionResponse.cs new file mode 100644 index 000000000..93d4cd307 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InteractionResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class InteractionResponse + { + [JsonProperty("type")] + public InteractionResponseType Type { get; set; } + + [JsonProperty("data")] + public Optional Data { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Invite.cs b/src/Discord.Net.Rest/API/Common/Invite.cs index 649bc37ec..f9d53bad6 100644 --- a/src/Discord.Net.Rest/API/Common/Invite.cs +++ b/src/Discord.Net.Rest/API/Common/Invite.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -11,9 +10,15 @@ namespace Discord.API public Optional Guild { get; set; } [JsonProperty("channel")] public InviteChannel Channel { get; set; } + [JsonProperty("inviter")] + public Optional Inviter { get; set; } [JsonProperty("approximate_presence_count")] public Optional PresenceCount { get; set; } [JsonProperty("approximate_member_count")] public Optional MemberCount { get; set; } + [JsonProperty("target_user")] + public Optional TargetUser { get; set; } + [JsonProperty("target_user_type")] + public Optional TargetUserType { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InviteChannel.cs b/src/Discord.Net.Rest/API/Common/InviteChannel.cs index f8f2a34f2..d601e65fe 100644 --- a/src/Discord.Net.Rest/API/Common/InviteChannel.cs +++ b/src/Discord.Net.Rest/API/Common/InviteChannel.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/InviteGuild.cs b/src/Discord.Net.Rest/API/Common/InviteGuild.cs index 3d6d7cd74..f5c634e4e 100644 --- a/src/Discord.Net.Rest/API/Common/InviteGuild.cs +++ b/src/Discord.Net.Rest/API/Common/InviteGuild.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs index ca019b79b..a06d06969 100644 --- a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -6,19 +5,15 @@ namespace Discord.API { internal class InviteMetadata : Invite { - [JsonProperty("inviter")] - public User Inviter { get; set; } [JsonProperty("uses")] - public Optional Uses { get; set; } + public int Uses { get; set; } [JsonProperty("max_uses")] - public Optional MaxUses { get; set; } + public int MaxUses { get; set; } [JsonProperty("max_age")] - public Optional MaxAge { get; set; } + public int MaxAge { get; set; } [JsonProperty("temporary")] public bool Temporary { get; set; } [JsonProperty("created_at")] - public Optional CreatedAt { get; set; } - [JsonProperty("revoked")] - public bool Revoked { get; set; } + public DateTimeOffset CreatedAt { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InviteVanity.cs b/src/Discord.Net.Rest/API/Common/InviteVanity.cs new file mode 100644 index 000000000..412795aa6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/InviteVanity.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + /// + /// Represents a vanity invite. + /// + public class InviteVanity + { + /// + /// The unique code for the invite link. + /// + [JsonProperty("code")] + public string Code { get; set; } + + /// + /// The total amount of vanity invite uses. + /// + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MembershipState.cs b/src/Discord.Net.Rest/API/Common/MembershipState.cs new file mode 100644 index 000000000..67fcc8908 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MembershipState.cs @@ -0,0 +1,9 @@ +namespace Discord.API +{ + internal enum MembershipState + { + None = 0, + Invited = 1, + Accepted = 2, + } +} diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index 229249ccf..d33a03fe5 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -33,7 +32,7 @@ namespace Discord.API [JsonProperty("mention_everyone")] public Optional MentionEveryone { get; set; } [JsonProperty("mentions")] - public Optional[]> UserMentions { get; set; } + public Optional UserMentions { get; set; } [JsonProperty("mention_roles")] public Optional RoleMentions { get; set; } [JsonProperty("attachments")] @@ -44,5 +43,24 @@ namespace Discord.API public Optional Pinned { get; set; } [JsonProperty("reactions")] public Optional Reactions { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("activity")] + public Optional Activity { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("application")] + public Optional Application { get; set; } + [JsonProperty("message_reference")] + public Optional Reference { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + [JsonProperty("referenced_message")] + public Optional ReferencedMessage { get; set; } + [JsonProperty("components")] + public Optional Components { get; set; } + public Optional Interaction { get; set; } + [JsonProperty("sticker_items")] + public Optional StickerItems { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageActivity.cs b/src/Discord.Net.Rest/API/Common/MessageActivity.cs new file mode 100644 index 000000000..701f6fc03 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageActivity.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageActivity + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("party_id")] + public Optional PartyId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageApplication.cs b/src/Discord.Net.Rest/API/Common/MessageApplication.cs new file mode 100644 index 000000000..7302185ad --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageApplication.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + [JsonProperty("id")] + public ulong Id { get; set; } + /// + /// Gets the ID of the embed's image asset. + /// + [JsonProperty("cover_image")] + public string CoverImage { get; set; } + /// + /// Gets the application's description. + /// + [JsonProperty("description")] + public string Description { get; set; } + /// + /// Gets the ID of the application's icon. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + /// + /// Gets the name of the application. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs new file mode 100644 index 000000000..a7760911c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class MessageComponentInteractionData : IDiscordInteractionData + { + [JsonProperty("custom_id")] + public string CustomId { get; set; } + + [JsonProperty("component_type")] + public ComponentType ComponentType { get; set; } + + [JsonProperty("values")] + public Optional Values { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageInteraction.cs b/src/Discord.Net.Rest/API/Common/MessageInteraction.cs new file mode 100644 index 000000000..48f278396 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageInteraction.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class MessageInteraction + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("type")] + public InteractionType Type { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs new file mode 100644 index 000000000..6cc7603e0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class MessageReference + { + [JsonProperty("message_id")] + public Optional MessageId { get; set; } + + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } // Optional when sending, always present when receiving + + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs b/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs new file mode 100644 index 000000000..cc2f0d963 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/NitroStickerPacks.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API +{ + internal class NitroStickerPacks + { + [JsonProperty("sticker_packs")] + public List StickerPacks { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Overwrite.cs b/src/Discord.Net.Rest/API/Common/Overwrite.cs index 1ba836127..a1fb534d8 100644 --- a/src/Discord.Net.Rest/API/Common/Overwrite.cs +++ b/src/Discord.Net.Rest/API/Common/Overwrite.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -10,8 +9,8 @@ namespace Discord.API [JsonProperty("type")] public PermissionTarget TargetType { get; set; } [JsonProperty("deny"), Int53] - public ulong Deny { get; set; } + public string Deny { get; set; } [JsonProperty("allow"), Int53] - public ulong Allow { get; set; } + public string Allow { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs index 2902b7ce3..23f871ae6 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -1,5 +1,6 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; +using System; +using System.Collections.Generic; namespace Discord.API { @@ -11,12 +12,21 @@ namespace Discord.API public Optional GuildId { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } - [JsonProperty("game")] - public Game Game { get; set; } [JsonProperty("roles")] public Optional Roles { get; set; } [JsonProperty("nick")] public Optional Nick { get; set; } + // This property is a Dictionary where each key is the ClientType + // and the values are the current client status. + // The client status values are all the same. + // Example: + // "client_status": { "desktop": "dnd", "mobile": "dnd" } + [JsonProperty("client_status")] + public Optional> ClientStatus { get; set; } + [JsonProperty("activities")] + public List Activities { get; set; } + [JsonProperty("premium_since")] + public Optional PremiumSince { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/PropertyErrorDescription.cs b/src/Discord.Net.Rest/API/Common/PropertyErrorDescription.cs new file mode 100644 index 000000000..145288e5d --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/PropertyErrorDescription.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ErrorDetails + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("errors")] + public Error[] Errors { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ReadState.cs b/src/Discord.Net.Rest/API/Common/ReadState.cs index 6ea6e4bd0..9a66880c6 100644 --- a/src/Discord.Net.Rest/API/Common/ReadState.cs +++ b/src/Discord.Net.Rest/API/Common/ReadState.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/Relationship.cs b/src/Discord.Net.Rest/API/Common/Relationship.cs index ecbb96f80..d17f766af 100644 --- a/src/Discord.Net.Rest/API/Common/Relationship.cs +++ b/src/Discord.Net.Rest/API/Common/Relationship.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/RelationshipType.cs b/src/Discord.Net.Rest/API/Common/RelationshipType.cs index 0ed99f396..776ba156b 100644 --- a/src/Discord.Net.Rest/API/Common/RelationshipType.cs +++ b/src/Discord.Net.Rest/API/Common/RelationshipType.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API { internal enum RelationshipType diff --git a/src/Discord.Net.Rest/API/Common/Role.cs b/src/Discord.Net.Rest/API/Common/Role.cs index 856a8695f..81f54ccc0 100644 --- a/src/Discord.Net.Rest/API/Common/Role.cs +++ b/src/Discord.Net.Rest/API/Common/Role.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -9,6 +8,10 @@ namespace Discord.API public ulong Id { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("unicode_emoji")] + public Optional Emoji { get; set; } [JsonProperty("color")] public uint Color { get; set; } [JsonProperty("hoist")] @@ -18,8 +21,10 @@ namespace Discord.API [JsonProperty("position")] public int Position { get; set; } [JsonProperty("permissions"), Int53] - public ulong Permissions { get; set; } + public string Permissions { get; set; } [JsonProperty("managed")] public bool Managed { get; set; } + [JsonProperty("tags")] + public Optional Tags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/RoleTags.cs b/src/Discord.Net.Rest/API/Common/RoleTags.cs new file mode 100644 index 000000000..9ddd39a64 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/RoleTags.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class RoleTags + { + [JsonProperty("bot_id")] + public Optional BotId { get; set; } + [JsonProperty("integration_id")] + public Optional IntegrationId { get; set; } + [JsonProperty("premium_subscriber")] + public Optional IsPremiumSubscriber { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs new file mode 100644 index 000000000..0886a8fe9 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API +{ + internal class SelectMenuComponent : IMessageComponent + { + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("custom_id")] + public string CustomId { get; set; } + + [JsonProperty("options")] + public SelectMenuOption[] Options { get; set; } + + [JsonProperty("placeholder")] + public Optional Placeholder { get; set; } + + [JsonProperty("min_values")] + public int MinValues { get; set; } + + [JsonProperty("max_values")] + public int MaxValues { get; set; } + + [JsonProperty("disabled")] + public bool Disabled { get; set; } + + public SelectMenuComponent() { } + + public SelectMenuComponent(Discord.SelectMenuComponent component) + { + Type = component.Type; + CustomId = component.CustomId; + Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray(); + Placeholder = component.Placeholder; + MinValues = component.MinValues; + MaxValues = component.MaxValues; + Disabled = component.IsDisabled; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs new file mode 100644 index 000000000..d0a25a829 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SelectMenuOption.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class SelectMenuOption + { + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + + [JsonProperty("default")] + public Optional Default { get; set; } + + public SelectMenuOption() { } + + public SelectMenuOption(Discord.SelectMenuOption option) + { + Label = option.Label; + Value = option.Value; + Description = option.Description; + + if (option.Emote != null) + { + if (option.Emote is Emote e) + { + Emoji = new Emoji + { + Name = e.Name, + Animated = e.Animated, + Id = e.Id + }; + } + else + { + Emoji = new Emoji + { + Name = option.Emote.Name + }; + } + } + + Default = option.IsDefault ?? Optional.Unspecified; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/SessionStartLimit.cs b/src/Discord.Net.Rest/API/Common/SessionStartLimit.cs new file mode 100644 index 000000000..29d5ddf85 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SessionStartLimit.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class SessionStartLimit + { + [JsonProperty("total")] + public int Total { get; set; } + [JsonProperty("remaining")] + public int Remaining { get; set; } + [JsonProperty("reset_after")] + public int ResetAfter { get; set; } + [JsonProperty("max_concurrency")] + public int MaxConcurrency { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/StageInstance.cs b/src/Discord.Net.Rest/API/Common/StageInstance.cs new file mode 100644 index 000000000..3ec623949 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/StageInstance.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class StageInstance + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("topic")] + public string Topic { get; set; } + + [JsonProperty("privacy_level")] + public StagePrivacyLevel PrivacyLevel { get; set; } + + [JsonProperty("discoverable_disabled")] + public bool DiscoverableDisabled { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Sticker.cs b/src/Discord.Net.Rest/API/Common/Sticker.cs new file mode 100644 index 000000000..b2c58d57c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Sticker.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Sticker + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("pack_id")] + public ulong PackId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("tags")] + public Optional Tags { get; set; } + [JsonProperty("type")] + public StickerType Type { get; set; } + [JsonProperty("format_type")] + public StickerFormatType FormatType { get; set; } + [JsonProperty("available")] + public bool? Available { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("user")] + public Optional User { get; set; } + [JsonProperty("sort_value")] + public int? SortValue { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/StickerItem.cs b/src/Discord.Net.Rest/API/Common/StickerItem.cs new file mode 100644 index 000000000..4b24f711b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/StickerItem.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class StickerItem + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("format_type")] + public StickerFormatType FormatType { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/StickerPack.cs b/src/Discord.Net.Rest/API/Common/StickerPack.cs new file mode 100644 index 000000000..3daaac5bf --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/StickerPack.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class StickerPack + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("stickers")] + public Sticker[] Stickers { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("sku_id")] + public ulong SkuId { get; set; } + [JsonProperty("cover_sticker_id")] + public Optional CoverStickerId { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("banner_asset_id")] + public ulong BannerAssetId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Team.cs b/src/Discord.Net.Rest/API/Common/Team.cs new file mode 100644 index 000000000..b421dc18c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Team.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Team + { + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("members")] + public TeamMember[] TeamMembers { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("owner_user_id")] + public ulong OwnerUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/TeamMember.cs b/src/Discord.Net.Rest/API/Common/TeamMember.cs new file mode 100644 index 000000000..f3cba608e --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/TeamMember.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class TeamMember + { + [JsonProperty("membership_state")] + public MembershipState MembershipState { get; set; } + [JsonProperty("permissions")] + public string[] Permissions { get; set; } + [JsonProperty("team_id")] + public ulong TeamId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs new file mode 100644 index 000000000..3e30d2c95 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class ThreadMember + { + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("user_id")] + public Optional UserId { get; set; } + + [JsonProperty("join_timestamp")] + public DateTimeOffset JoinTimestamp { get; set; } + + [JsonProperty("presence")] + public Optional Presence { get; set; } + + [JsonProperty("member")] + public Optional Member { get; set; } + + [JsonProperty("flags")] + public int Flags { get; set; } // No enum type (yet?) + } +} diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs new file mode 100644 index 000000000..39e9bd13e --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class ThreadMetadata + { + [JsonProperty("archived")] + public bool Archived { get; set; } + + [JsonProperty("auto_archive_duration")] + public ThreadArchiveDuration AutoArchiveDuration { get; set; } + + [JsonProperty("archive_timestamp")] + public DateTimeOffset ArchiveTimestamp { get; set; } + + [JsonProperty("locked")] + public Optional Locked { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/User.cs b/src/Discord.Net.Rest/API/Common/User.cs index d49d24623..08fe88cb0 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -15,6 +14,10 @@ namespace Discord.API public Optional Bot { get; set; } [JsonProperty("avatar")] public Optional Avatar { get; set; } + [JsonProperty("banner")] + public Optional Banner { get; set; } + [JsonProperty("accent_color")] + public Optional AccentColor { get; set; } //CurrentUser [JsonProperty("verified")] @@ -23,5 +26,13 @@ namespace Discord.API public Optional Email { get; set; } [JsonProperty("mfa_enabled")] public Optional MfaEnabled { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("premium_type")] + public Optional PremiumType { get; set; } + [JsonProperty("locale")] + public Optional Locale { get; set; } + [JsonProperty("public_flags")] + public Optional PublicFlags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/UserGuild.cs b/src/Discord.Net.Rest/API/Common/UserGuild.cs index f4f763885..fc1fe833d 100644 --- a/src/Discord.Net.Rest/API/Common/UserGuild.cs +++ b/src/Discord.Net.Rest/API/Common/UserGuild.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -14,6 +13,6 @@ namespace Discord.API [JsonProperty("owner")] public bool Owner { get; set; } [JsonProperty("permissions"), Int53] - public ulong Permissions { get; set; } + public string Permissions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/VoiceRegion.cs b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs index 606af07bd..3cc66a0ef 100644 --- a/src/Discord.Net.Rest/API/Common/VoiceRegion.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceRegion.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Common/VoiceState.cs b/src/Discord.Net.Rest/API/Common/VoiceState.cs index b1f937b09..f7cd54a72 100644 --- a/src/Discord.Net.Rest/API/Common/VoiceState.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceState.cs @@ -1,5 +1,5 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; +using System; namespace Discord.API { @@ -26,5 +26,9 @@ namespace Discord.API public bool SelfMute { get; set; } [JsonProperty("suppress")] public bool Suppress { get; set; } + [JsonProperty("self_stream")] + public bool SelfStream { get; set; } + [JsonProperty("request_to_speak_timestamp")] + public Optional RequestToSpeakTimestamp { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Webhook.cs b/src/Discord.Net.Rest/API/Common/Webhook.cs index cbd5fdad5..23b682bd3 100644 --- a/src/Discord.Net.Rest/API/Common/Webhook.cs +++ b/src/Discord.Net.Rest/API/Common/Webhook.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -21,5 +20,7 @@ namespace Discord.API [JsonProperty("user")] public Optional Creator { get; set; } + [JsonProperty("application_id")] + public ulong? ApplicationId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Int53Attribute.cs b/src/Discord.Net.Rest/API/Int53Attribute.cs index 70ef2f185..3a21b583d 100644 --- a/src/Discord.Net.Rest/API/Int53Attribute.cs +++ b/src/Discord.Net.Rest/API/Int53Attribute.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using System; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Net/IResolvable.cs b/src/Discord.Net.Rest/API/Net/IResolvable.cs new file mode 100644 index 000000000..7485f5de8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Net/IResolvable.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal interface IResolvable + { + Optional Resolved { get; } + } +} diff --git a/src/Discord.Net.Rest/API/Net/MultipartFile.cs b/src/Discord.Net.Rest/API/Net/MultipartFile.cs index 604852e90..d6bc4c7ab 100644 --- a/src/Discord.Net.Rest/API/Net/MultipartFile.cs +++ b/src/Discord.Net.Rest/API/Net/MultipartFile.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; namespace Discord.Net.Rest { @@ -6,11 +6,13 @@ namespace Discord.Net.Rest { public Stream Stream { get; } public string Filename { get; } + public string ContentType { get; } - public MultipartFile(Stream stream, string filename) + public MultipartFile(Stream stream, string filename, string contentType = null) { Stream = stream; Filename = filename; + ContentType = contentType; } } } diff --git a/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs new file mode 100644 index 000000000..ef6229edb --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class AddGuildMemberParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + [JsonProperty("mute")] + public Optional IsMuted { get; set; } + [JsonProperty("deaf")] + public Optional IsDeafened { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs new file mode 100644 index 000000000..82f0befcd --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class CreateApplicationCommandParams + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("default_permission")] + public Optional DefaultPermission { get; set; } + + public CreateApplicationCommandParams() { } + public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) + { + Name = name; + Description = description; + Options = Optional.Create(options); + Type = type; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs b/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs index db79bc314..852abe301 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -14,5 +13,11 @@ namespace Discord.API.Rest public Optional IsTemporary { get; set; } [JsonProperty("unique")] public Optional IsUnique { get; set; } + [JsonProperty("target_type")] + public Optional TargetType { get; set; } + [JsonProperty("target_user_id")] + public Optional TargetUserId { get; set; } + [JsonProperty("target_application_id")] + public Optional TargetApplicationId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs index f32796e02..0a710dd1b 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs index f0432e517..fce9df11f 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Rest { internal class CreateGuildBanParams diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index 05cdf4b8a..57816e448 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -12,12 +11,18 @@ namespace Discord.API.Rest public ChannelType Type { get; } [JsonProperty("parent_id")] public Optional CategoryId { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + [JsonProperty("permission_overwrites")] + public Optional Overwrites { get; set; } //Text channels [JsonProperty("topic")] public Optional Topic { get; set; } [JsonProperty("nsfw")] public Optional IsNsfw { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional SlowModeInterval { get; set; } //Voice channels [JsonProperty("bitrate")] diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs index 308199820..c81f62f4c 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs index 1053a0ed3..7358e5201 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs index cda6caedf..e89c2b119 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs new file mode 100644 index 000000000..a207d3374 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreateGuildScheduledEventParams + { + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("entity_metadata")] + public Optional EntityMetadata { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("privacy_level")] + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; set; } + [JsonProperty("scheduled_start_time")] + public DateTimeOffset StartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public Optional EndTime { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("entity_type")] + public GuildScheduledEventType Type { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index d77bff8ca..5996c7e83 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -11,10 +10,24 @@ namespace Discord.API.Rest [JsonProperty("nonce")] public Optional Nonce { get; set; } + [JsonProperty("tts")] public Optional IsTTS { get; set; } - [JsonProperty("embed")] - public Optional Embed { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("message_reference")] + public Optional MessageReference { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("sticker_ids")] + public Optional Stickers { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/API/Rest/CreateStageInstanceParams.cs b/src/Discord.Net.Rest/API/Rest/CreateStageInstanceParams.cs new file mode 100644 index 000000000..a1d59bb51 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateStageInstanceParams.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class CreateStageInstanceParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("topic")] + public string Topic { get; set; } + + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs new file mode 100644 index 000000000..b330a0111 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs @@ -0,0 +1,35 @@ +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; +namespace Discord.API.Rest +{ + internal class CreateStickerParams + { + public Stream File { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Tags { get; set; } + public string FileName { get; set; } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary + { + ["name"] = $"{Name}", + ["description"] = Description, + ["tags"] = Tags + }; + + string contentType = "image/png"; + + if (File is FileStream fileStream) + contentType = $"image/{Path.GetExtension(fileStream.Name)}"; + else if (FileName != null) + contentType = $"image/{Path.GetExtension(FileName)}"; + + d["file"] = new MultipartFile(File, FileName ?? "image", contentType.Replace(".", "")); + + return d; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs index 970a30201..bda0f7ff1 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -1,28 +1,84 @@ -#pragma warning disable CS1591 +using Discord.Net.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Text; namespace Discord.API.Rest { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] internal class CreateWebhookMessageParams { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + [JsonProperty("content")] - public string Content { get; } + public string Content { get; set; } [JsonProperty("nonce")] public Optional Nonce { get; set; } + [JsonProperty("tts")] public Optional IsTTS { get; set; } + [JsonProperty("embeds")] public Optional Embeds { get; set; } + [JsonProperty("username")] public Optional Username { get; set; } + [JsonProperty("avatar_url")] public Optional AvatarUrl { get; set; } - public CreateWebhookMessageParams(string content) + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("file")] + public Optional File { get; set; } + + public IReadOnlyDictionary ToDictionary() { - Content = content; + var d = new Dictionary(); + + if (File.IsSpecified) + { + d["file"] = File.Value; + } + + var payload = new Dictionary + { + ["content"] = Content + }; + + if (IsTTS.IsSpecified) + payload["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + payload["nonce"] = Nonce.Value; + if (Username.IsSpecified) + payload["username"] = Username.Value; + if (AvatarUrl.IsSpecified) + payload["avatar_url"] = AvatarUrl.Value; + if (Embeds.IsSpecified) + payload["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + payload["allowed_mentions"] = AllowedMentions.Value; + if (Components.IsSpecified) + payload["components"] = Components.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + + return d; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs index 0d1059fab..242f451cb 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs b/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs index ca9d8c26e..ca6b78406 100644 --- a/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs index ceabccbc8..f136fa7aa 100644 --- a/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs @@ -1,8 +1,10 @@ -namespace Discord.API.Rest +namespace Discord.API.Rest { class GetAuditLogsParams { public Optional Limit { get; set; } public Optional BeforeEntryId { get; set; } + public Optional UserId { get; set; } + public Optional ActionType { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs b/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs index 111fcf3db..3f8318cd1 100644 --- a/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs +++ b/src/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -9,5 +8,7 @@ namespace Discord.API.Rest public string Url { get; set; } [JsonProperty("shards")] public int Shards { get; set; } + [JsonProperty("session_start_limit")] + public SessionStartLimit SessionStartLimit { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs index ea5327667..52dd84836 100644 --- a/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Rest { internal class GetChannelMessagesParams diff --git a/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs b/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs new file mode 100644 index 000000000..db3ac666e --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetEventUsersParams.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class GetEventUsersParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs b/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs index ce3630170..11207633d 100644 --- a/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs index 66023cb43..67d380035 100644 --- a/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Rest { internal class GetGuildMembersParams diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs b/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs index 4af85acfa..1e7fc8c7b 100644 --- a/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs index f770ef398..1d3f70f07 100644 --- a/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Rest { internal class GetGuildSummariesParams diff --git a/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs b/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs index 6a98d3758..c6caa1eb1 100644 --- a/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs +++ b/src/Discord.Net.Rest/API/Rest/GuildPruneParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -9,9 +8,13 @@ namespace Discord.API.Rest [JsonProperty("days")] public int Days { get; } - public GuildPruneParams(int days) + [JsonProperty("include_roles")] + public ulong[] IncludeRoleIds { get; } + + public GuildPruneParams(int days, ulong[] includeRoleIds) { Days = days; + IncludeRoleIds = includeRoleIds; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs new file mode 100644 index 000000000..5891c2c28 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyApplicationCommandParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("options")] + public Optional Options { get; set; } + + [JsonProperty("default_permission")] + public Optional DefaultPermission { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs index 0fe5f7e5a..acb81034a 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -7,13 +6,13 @@ namespace Discord.API.Rest internal class ModifyChannelPermissionsParams { [JsonProperty("type")] - public string Type { get; } + public int Type { get; } [JsonProperty("allow")] - public ulong Allow { get; } + public string Allow { get; } [JsonProperty("deny")] - public ulong Deny { get; } + public string Deny { get; } - public ModifyChannelPermissionsParams(string type, ulong allow, ulong deny) + public ModifyChannelPermissionsParams(int type, string allow, string deny) { Type = type; Allow = allow; diff --git a/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs index ba44e34cf..c10f2e4ec 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs index 7ba27c3a5..e28deb32b 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..a557061f3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyGuildApplicationCommandPermissions + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("permissions")] + public ApplicationCommandPermissions[] Permissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissionsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissionsParams.cs new file mode 100644 index 000000000..322875b8e --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissionsParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyGuildApplicationCommandPermissionsParams + { + [JsonProperty("permissions")] + public ApplicationCommandPermissions[] Permissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index 120eeb3a8..dfe9cd980 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -12,5 +11,7 @@ namespace Discord.API.Rest public Optional Position { get; set; } [JsonProperty("parent_id")] public Optional CategoryId { get; set; } + [JsonProperty("permission_overwrites")] + public Optional Overwrites { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs index f97fbda0b..91567be3d 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs index 487744c65..420bdbeaf 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs index a2295dd5d..08b196daa 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs index 0a1b4f9fa..cf869c838 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs index 159670afb..37625de09 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -15,6 +14,6 @@ namespace Discord.API.Rest [JsonProperty("roles")] public Optional RoleIds { get; set; } [JsonProperty("channel_id")] - public Optional ChannelId { get; set; } + public Optional ChannelId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index 8de10f534..c1a20cb83 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -22,11 +21,21 @@ namespace Discord.API.Rest public Optional SystemChannelId { get; set; } [JsonProperty("icon")] public Optional Icon { get; set; } + [JsonProperty("banner")] + public Optional Banner { get; set; } [JsonProperty("splash")] public Optional Splash { get; set; } [JsonProperty("afk_channel_id")] public Optional AfkChannelId { get; set; } [JsonProperty("owner_id")] public Optional OwnerId { get; set; } + [JsonProperty("explicit_content_filter")] + public Optional ExplicitContentFilter { get; set; } + [JsonProperty("system_channel_flags")] + public Optional SystemChannelFlags { get; set; } + [JsonProperty("preferred_locale")] + public string PreferredLocale { get; set; } + [JsonProperty("premium_progress_bar_enabled")] + public Optional IsBoostProgressBarEnabled { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index 287e1cafe..fbb9c3e48 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -9,11 +8,13 @@ namespace Discord.API.Rest [JsonProperty("name")] public Optional Name { get; set; } [JsonProperty("permissions")] - public Optional Permissions { get; set; } + public Optional Permissions { get; set; } [JsonProperty("color")] public Optional Color { get; set; } [JsonProperty("hoist")] public Optional Hoist { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } [JsonProperty("mentionable")] public Optional Mentionable { get; set; } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs index 0e816a260..eeb724523 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs new file mode 100644 index 000000000..3d191a0b3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyGuildScheduledEventParams + { + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + [JsonProperty("entity_metadata")] + public Optional EntityMetadata { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } + [JsonProperty("scheduled_start_time")] + public Optional StartTime { get; set; } + [JsonProperty("scheduled_end_time")] + public Optional EndTime { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("entity_type")] + public Optional Type { get; set; } + [JsonProperty("status")] + public Optional Status { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildWidgetParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildWidgetParams.cs new file mode 100644 index 000000000..2e5658d51 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildWidgetParams.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildWidgetParams + { + [JsonProperty("enabled")] + public Optional Enabled { get; set; } + [JsonProperty("channel")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs new file mode 100644 index 000000000..a2c7cbee6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyInteractionResponseParams + { + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs index fdff4de15..3dba45a5b 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -8,7 +7,13 @@ namespace Discord.API.Rest { [JsonProperty("content")] public Optional Content { get; set; } - [JsonProperty("embed")] - public Optional Embed { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("components")] + public Optional Components { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyStageInstanceParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyStageInstanceParams.cs new file mode 100644 index 000000000..c09d8f216 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyStageInstanceParams.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyStageInstanceParams + { + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs new file mode 100644 index 000000000..bd538c72e --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyStickerParams.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyStickerParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } + [JsonProperty("tags")] + public Optional Tags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs index 94f149fc1..409d90c3f 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs new file mode 100644 index 000000000..8c9216c3f --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyThreadParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("archived")] + public Optional Archived { get; set; } + + [JsonProperty("auto_archive_duration")] + public Optional AutoArchiveDuration { get; set; } + + [JsonProperty("locked")] + public Optional Locked { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional Slowmode { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs index ce36eb11f..2f8cacc69 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -10,5 +9,7 @@ namespace Discord.API.Rest public Optional Bitrate { get; set; } [JsonProperty("user_limit")] public Optional UserLimit { get; set; } + [JsonProperty("rtc_region")] + public Optional RTCRegion { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyVoiceStateParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyVoiceStateParams.cs new file mode 100644 index 000000000..1ff0f3e08 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyVoiceStateParams.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rest +{ + internal class ModifyVoiceStateParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("suppress")] + public Optional Suppressed { get; set; } + + [JsonProperty("request_to_speak_timestamp")] + public Optional RequestToSpeakTimestamp { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs new file mode 100644 index 000000000..e73efaf36 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookMessageParams + { + [JsonProperty("content")] + public Optional Content { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + [JsonProperty("components")] + public Optional Components { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs index 0f2d6e33b..2e4e6a4c4 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest diff --git a/src/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs b/src/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs new file mode 100644 index 000000000..56b3595fa --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + internal class SearchGuildMembersParams + { + public string Query { get; set; } + public Optional Limit { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs new file mode 100644 index 000000000..a13161cd4 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class StartThreadParams + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("auto_archive_duration")] + public ThreadArchiveDuration Duration { get; set; } + + [JsonProperty("type")] + public ThreadType Type { get; set; } + + [JsonProperty("invitable")] + public Optional Invitable { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional Ratelimit { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 9e909b50c..6340c3e38 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -1,10 +1,9 @@ -#pragma warning disable CS1591 using Discord.Net.Converters; using Discord.Net.Rest; using Newtonsoft.Json; using System.Collections.Generic; -using System.Globalization; using System.IO; +using System.Linq; using System.Text; namespace Discord.API.Rest @@ -13,24 +12,27 @@ namespace Discord.API.Rest { private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - public Stream File { get; } + public FileAttachment[] Files { get; } - public Optional Filename { get; set; } public Optional Content { get; set; } public Optional Nonce { get; set; } public Optional IsTTS { get; set; } - public Optional Embed { get; set; } + public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageReference { get; set; } + public Optional MessageComponent { get; set; } + public Optional Flags { get; set; } + public Optional Stickers { get; set; } - public UploadFileParams(Stream file) + public UploadFileParams(params Discord.FileAttachment[] attachments) { - File = file; + Files = attachments; } public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); - d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); - + var payload = new Dictionary(); if (Content.IsSpecified) payload["content"] = Content.Value; @@ -38,8 +40,39 @@ namespace Discord.API.Rest payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) payload["nonce"] = Nonce.Value; - if (Embed.IsSpecified) - payload["embed"] = Embed.Value; + if (Embeds.IsSpecified) + payload["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + payload["allowed_mentions"] = AllowedMentions.Value; + if (MessageComponent.IsSpecified) + payload["components"] = MessageComponent.Value; + if (MessageReference.IsSpecified) + payload["message_reference"] = MessageReference.Value; + if (Stickers.IsSpecified) + payload["sticker_ids"] = Stickers.Value; + if (Flags.IsSpecified) + payload["flags"] = Flags; + + List attachments = new(); + + for(int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + payload["attachments"] = attachments; var json = new StringBuilder(); using (var text = new StringWriter(json)) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 6d6eb29b2..3d09ad145 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,44 +1,82 @@ -#pragma warning disable CS1591 using System.Collections.Generic; using System.IO; +using System.Text; +using Discord.Net.Converters; using Discord.Net.Rest; +using Newtonsoft.Json; namespace Discord.API.Rest { internal class UploadWebhookFileParams { - public Stream File { get; } + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + public FileAttachment[] Files { get; } - public Optional Filename { get; set; } public Optional Content { get; set; } public Optional Nonce { get; set; } public Optional IsTTS { get; set; } public Optional Username { get; set; } public Optional AvatarUrl { get; set; } public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageComponents { get; set; } - public UploadWebhookFileParams(Stream file) + public UploadWebhookFileParams(params FileAttachment[] files) { - File = file; + Files = files; } public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); - d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + var payload = new Dictionary(); if (Content.IsSpecified) - d["content"] = Content.Value; + payload["content"] = Content.Value; if (IsTTS.IsSpecified) - d["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) - d["nonce"] = Nonce.Value; + payload["nonce"] = Nonce.Value; if (Username.IsSpecified) - d["username"] = Username.Value; + payload["username"] = Username.Value; if (AvatarUrl.IsSpecified) - d["avatar_url"] = AvatarUrl.Value; + payload["avatar_url"] = AvatarUrl.Value; + if (MessageComponents.IsSpecified) + payload["components"] = MessageComponents.Value; if (Embeds.IsSpecified) - d["embeds"] = Embeds.Value; + payload["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + payload["allowed_mentions"] = AllowedMentions.Value; + + List attachments = new(); + + for (int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + payload["attachments"] = attachments; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + return d; } } diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 9c90919af..5c9351d64 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] [assembly: TypeForwardedTo(typeof(Discord.Embed))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 8a3db3e6a..525875232 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -10,6 +10,7 @@ namespace Discord.Rest { public abstract class BaseDiscordClient : IDiscordClient { + #region BaseDiscordClient public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); @@ -24,11 +25,19 @@ namespace Discord.Rest internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } + /// + /// Gets the login state of the client. + /// public LoginState LoginState { get; private set; } + /// + /// Gets the logged-in user. + /// public ISelfUser CurrentUser { get; protected set; } + /// public TokenType TokenType => ApiClient.AuthTokenType; - - /// Creates a new REST-only discord client. + internal bool UseInteractionSnowflakeDate { get; private set; } + + /// Creates a new REST-only Discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) { ApiClient = client; @@ -39,17 +48,18 @@ namespace Discord.Rest _restLogger = LogManager.CreateLogger("Rest"); _isFirstLogin = config.DisplayInitialLog; - ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + UseInteractionSnowflakeDate = config.UseInteractionSnowflakeDate; + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } - /// public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) { await _stateLock.WaitAsync().ConfigureAwait(false); @@ -59,7 +69,7 @@ namespace Discord.Rest } finally { _stateLock.Release(); } } - private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + internal virtual async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) { if (_isFirstLogin) { @@ -84,7 +94,7 @@ namespace Discord.Rest catch (ArgumentException ex) { // log these ArgumentExceptions and allow for the client to attempt to log in anyways - await LogManager.WarningAsync("Discord", "A supplied token was invalid", ex).ConfigureAwait(false); + await LogManager.WarningAsync("Discord", "A supplied token was invalid.", ex).ConfigureAwait(false); } } @@ -100,10 +110,9 @@ namespace Discord.Rest await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLoginAsync(TokenType tokenType, string token) + internal virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.Delay(0); - /// public async Task LogoutAsync() { await _stateLock.WaitAsync().ConfigureAwait(false); @@ -113,7 +122,7 @@ namespace Discord.Rest } finally { _stateLock.Release(); } } - private async Task LogoutInternalAsync() + internal virtual async Task LogoutInternalAsync() { if (LoginState == LoginState.LoggedOut) return; LoginState = LoginState.LoggingOut; @@ -126,14 +135,17 @@ namespace Discord.Rest await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLogoutAsync() + internal virtual Task OnLogoutAsync() => Task.Delay(0); internal virtual void Dispose(bool disposing) { if (!_isDisposed) { +#pragma warning disable IDISP007 ApiClient.Dispose(); +#pragma warning restore IDISP007 + _stateLock?.Dispose(); _isDisposed = true; } } @@ -144,51 +156,90 @@ namespace Discord.Rest public Task GetRecommendedShardCountAsync(RequestOptions options = null) => ClientHelper.GetRecommendShardCountAsync(this, options); - //IDiscordClient + /// + public Task GetBotGatewayAsync(RequestOptions options = null) + => ClientHelper.GetBotGatewayAsync(this, options); + #endregion + + #region IDiscordClient + /// ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + /// ISelfUser IDiscordClient.CurrentUser => CurrentUser; - Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + /// + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => throw new NotSupportedException(); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// + /// Creating a guild is not supported with the base client. Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => throw new NotSupportedException(); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) => Task.FromResult(null); + /// + Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + + /// + Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) + => Task.FromResult(null); + Task> IDiscordClient.BulkOverwriteGlobalApplicationCommand(ApplicationCommandProperties[] properties, + RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + + /// Task IDiscordClient.StartAsync() => Task.Delay(0); + /// Task IDiscordClient.StopAsync() => Task.Delay(0); + #endregion } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index aa99fe005..5debea27e 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -1,3 +1,4 @@ +using System; using Discord.API.Rest; using System.Collections.Generic; using System.Collections.Immutable; @@ -9,14 +10,14 @@ namespace Discord.Rest { internal static class ClientHelper { - //Applications + #region Applications public static async Task GetApplicationInfoAsync(BaseDiscordClient client, RequestOptions options) { var model = await client.ApiClient.GetMyApplicationAsync(options).ConfigureAwait(false); return RestApplication.Create(client, model); } - public static async Task GetChannelAsync(BaseDiscordClient client, + public static async Task GetChannelAsync(BaseDiscordClient client, ulong id, RequestOptions options) { var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false); @@ -24,6 +25,7 @@ namespace Discord.Rest return RestChannel.Create(client, model); return null; } + /// Unexpected channel type. public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); @@ -43,13 +45,13 @@ namespace Discord.Rest .Where(x => x.Type == ChannelType.Group) .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } - + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); return models.Select(RestConnection.Create).ToImmutableArray(); } - + public static async Task GetInviteAsync(BaseDiscordClient client, string inviteId, RequestOptions options) { @@ -58,24 +60,24 @@ namespace Discord.Rest return RestInviteMetadata.Create(client, null, null, model); return null; } - + public static async Task GetGuildAsync(BaseDiscordClient client, - ulong id, RequestOptions options) + ulong id, bool withCounts, RequestOptions options) { - var model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildAsync(id, withCounts, options).ConfigureAwait(false); if (model != null) return RestGuild.Create(client, model); return null; } - public static async Task GetGuildEmbedAsync(BaseDiscordClient client, + public static async Task GetGuildWidgetAsync(BaseDiscordClient client, ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildWidgetAsync(id, options).ConfigureAwait(false); if (model != null) - return RestGuildEmbed.Create(model); + return RestGuildWidget.Create(model); return null; } - public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, + public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, ulong? fromGuildId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( @@ -104,13 +106,13 @@ namespace Discord.Rest count: limit ); } - public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) + public static async Task> GetGuildsAsync(BaseDiscordClient client, bool withCounts, RequestOptions options) { var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false); var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { - var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); + var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id, withCounts).ConfigureAwait(false); if (guildModel != null) guilds.Add(RestGuild.Create(client, guildModel)); } @@ -126,7 +128,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } - + public static async Task GetUserAsync(BaseDiscordClient client, ulong id, RequestOptions options) { @@ -138,7 +140,7 @@ namespace Discord.Rest public static async Task GetGuildUserAsync(BaseDiscordClient client, ulong guildId, ulong id, RequestOptions options) { - var guild = await GetGuildAsync(client, guildId, options).ConfigureAwait(false); + var guild = await GetGuildAsync(client, guildId, false, options).ConfigureAwait(false); if (guild == null) return null; @@ -151,7 +153,7 @@ namespace Discord.Rest public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) { - var model = await client.ApiClient.GetWebhookAsync(id); + var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); if (model != null) return RestWebhook.Create(client, (IGuild)null, model); return null; @@ -174,5 +176,93 @@ namespace Discord.Rest var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); return response.Shards; } + + public static async Task GetBotGatewayAsync(BaseDiscordClient client, RequestOptions options) + { + var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); + return new BotGateway + { + Url = response.Url, + Shards = response.Shards, + SessionStartLimit = new SessionStartLimit + { + Total = response.SessionStartLimit.Total, + Remaining = response.SessionStartLimit.Remaining, + ResetAfter = response.SessionStartLimit.ResetAfter, + MaxConcurrency = response.SessionStartLimit.MaxConcurrency + } + }; + } + + public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, + RequestOptions options = null) + { + var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); + + if (!response.Any()) + return Array.Empty(); + + return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); + } + public static async Task GetGlobalApplicationCommandAsync(BaseDiscordClient client, ulong id, + RequestOptions options = null) + { + var model = await client.ApiClient.GetGlobalApplicationCommandAsync(id, options); + + return model != null ? RestGlobalCommand.Create(client, model) : null; + } + + public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, + RequestOptions options = null) + { + var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); + + if (!response.Any()) + return ImmutableArray.Create(); + + return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); + } + public static async Task GetGuildApplicationCommandAsync(BaseDiscordClient client, ulong id, ulong guildId, + RequestOptions options = null) + { + var model = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, id, options); + + return model != null ? RestGuildCommand.Create(client, model, guildId) : null; + } + public static async Task CreateGuildApplicationCommandAsync(BaseDiscordClient client, ulong guildId, ApplicationCommandProperties properties, + RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommandAsync(client, guildId, properties, options); + + return RestGuildCommand.Create(client, model, guildId); + } + public static async Task CreateGlobalApplicationCommandAsync(BaseDiscordClient client, ApplicationCommandProperties properties, + RequestOptions options = null) + { + var model = await InteractionHelper.CreateGlobalCommandAsync(client, properties, options); + + return RestGlobalCommand.Create(client, model); + } + public static async Task> BulkOverwriteGlobalApplicationCommandAsync(BaseDiscordClient client, ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGlobalCommandsAsync(client, properties, options); + + return models.Select(x => RestGlobalCommand.Create(client, x)).ToImmutableArray(); + } + public static async Task> BulkOverwriteGuildApplicationCommandAsync(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties[] properties, RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommandsAsync(client, guildId, properties, options); + + return models.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); + } + + public static Task AddRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + => client.ApiClient.AddRoleAsync(guildId, userId, roleId, options); + + public static Task RemoveRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + => client.ApiClient.RemoveRoleAsync(guildId, userId, roleId, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 75b69bd04..8407abfd6 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,19 +1,17 @@ + Discord.Net.Rest Discord.Rest A core Discord.Net library containing the REST client and models. - net46;netstandard1.3;netstandard2.0 - netstandard1.3;netstandard2.0 + net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 - - - - + diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 35fa0e989..abe059c64 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 + using Discord.API.Rest; using Discord.Net; using Discord.Net.Converters; @@ -23,7 +23,8 @@ namespace Discord.API { internal class DiscordRestApiClient : IDisposable { - private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); + #region DiscordRestApiClient + private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); @@ -44,42 +45,43 @@ namespace Discord.API internal string AuthToken { get; private set; } internal IRestClient RestClient { get; private set; } internal ulong? CurrentUserId { get; set; } - + internal bool UseSystemClock { get; set; } internal JsonSerializer Serializer => _serializer; + /// Unknown OAuth token type. public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, - JsonSerializer serializer = null) + JsonSerializer serializer = null, bool useSystemClock = true) { _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + UseSystemClock = useSystemClock; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); SetBaseUrl(DiscordConfig.APIUrl); } + + /// Unknown OAuth token type. internal void SetBaseUrl(string baseUrl) { + RestClient?.Dispose(); RestClient = _restClientProvider(baseUrl); RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); } + /// Unknown OAuth token type. internal static string GetPrefixedToken(TokenType tokenType, string token) { - switch (tokenType) + return tokenType switch { - case default(TokenType): - return token; - case TokenType.Bot: - return $"Bot {token}"; - case TokenType.Bearer: - return $"Bearer {token}"; - default: - throw new ArgumentException(message: "Unknown OAuth token type", paramName: nameof(tokenType)); - } + TokenType.Bot => $"Bot {token}", + TokenType.Bearer => $"Bearer {token}", + _ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)), + }; } internal virtual void Dispose(bool disposing) { @@ -88,7 +90,9 @@ namespace Discord.API if (disposing) { _loginCancelToken?.Dispose(); - (RestClient as IDisposable)?.Dispose(); + RestClient?.Dispose(); + RequestQueue?.Dispose(); + _stateLock?.Dispose(); } _isDisposed = true; } @@ -112,6 +116,7 @@ namespace Discord.API try { + _loginCancelToken?.Dispose(); _loginCancelToken = new CancellationTokenSource(); AuthToken = null; @@ -119,7 +124,7 @@ namespace Discord.API RestClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; - AuthToken = token; + AuthToken = token?.TrimEnd(); if (tokenType != TokenType.Webhook) RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); @@ -150,7 +155,7 @@ namespace Discord.API try { _loginCancelToken?.Cancel(false); } catch { } - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); @@ -161,16 +166,17 @@ namespace Discord.API } internal virtual Task ConnectInternalAsync() => Task.Delay(0); - internal virtual Task DisconnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); + #endregion - //Core + #region Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.HeaderOnly = true; options.BucketId = bucketId; @@ -180,11 +186,11 @@ namespace Discord.API internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.HeaderOnly = true; options.BucketId = bucketId; @@ -195,11 +201,11 @@ namespace Discord.API internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.HeaderOnly = true; options.BucketId = bucketId; @@ -209,11 +215,11 @@ namespace Discord.API internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.BucketId = bucketId; var request = new RestRequest(RestClient, method, endpoint, options); @@ -222,25 +228,26 @@ namespace Discord.API internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { - options = options ?? new RequestOptions(); + options ??= new RequestOptions(); options.BucketId = bucketId; var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); @@ -253,6 +260,8 @@ namespace Discord.API CheckState(); if (request.Options.RetryMode == null) request.Options.RetryMode = DefaultRetryMode; + if (request.Options.UseSystemClock == null) + request.Options.UseSystemClock = UseSystemClock; var stopwatch = Stopwatch.StartNew(); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); @@ -263,15 +272,17 @@ namespace Discord.API return responseStream; } + #endregion - //Auth + #region Auth public async Task ValidateTokenAsync(RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); } + #endregion - //Gateway + #region Gateway public async Task GetGatewayAsync(RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -282,8 +293,9 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); } + #endregion - //Channels + #region Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -308,6 +320,7 @@ namespace Discord.API var model = await SendAsync("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); if (!model.GuildId.IsSpecified || model.GuildId.Value != guildId) return null; + return model; } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } @@ -326,11 +339,16 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + Preconditions.AtMost(args.Name.Length, 100, nameof(args.Name)); + if (args.Topic.IsSpecified) + Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); return await SendJsonAsync("POST", () => $"guilds/{guildId}/channels", args, ids, options: options).ConfigureAwait(false); } + public async Task DeleteChannelAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -339,30 +357,52 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync("DELETE", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); } + /// + /// must not be equal to zero. + /// -and- + /// must be greater than zero. + /// + /// + /// must not be . + /// -and- + /// must not be or empty. + /// public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + + if(args.Name.IsSpecified) + Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyTextChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + + if(args.Name.IsSpecified) + Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); + if(args.Topic.IsSpecified) + Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name)); + Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval)); - Preconditions.AtMost(args.SlowModeInterval, 120, nameof(args.SlowModeInterval)); + Preconditions.AtMost(args.SlowModeInterval, 21600, nameof(args.SlowModeInterval)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.ModifyVoiceChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -370,12 +410,13 @@ namespace Discord.API Preconditions.AtLeast(args.Bitrate, 8000, nameof(args.Bitrate)); Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); } + public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -396,6 +437,266 @@ namespace Discord.API break; } } + #endregion + + #region Threads + public async Task ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, bucket, options: options); + } + + public async Task StartThreadAsync(ulong channelId, ulong messageId, StartThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(0, channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/threads", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task StartThreadAsync(ulong channelId, StartThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task JoinThreadAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + await SendAsync("PUT", () => $"channels/{channelId}/thread-members/@me", bucket, options: options).ConfigureAwait(false); + } + + public async Task AddThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + await SendAsync("PUT", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); + } + + public async Task LeaveThreadAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/thread-members/@me", bucket, options: options).ConfigureAwait(false); + } + + public async Task RemoveThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + var bucket = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); + } + + public async Task ListThreadMembersAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendAsync("GET", () => $"channels/{channelId}/thread-members", bucket, options: options).ConfigureAwait(false); + } + + public async Task GetThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); + } + + public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendAsync("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); + } + + public async Task GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/public{query}", bucket, options: options); + } + + public async Task GetPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, + RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/private{query}", bucket, options: options); + } + + public async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, + RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={SnowflakeUtils.ToSnowflake(before.GetValueOrDefault(DateTimeOffset.UtcNow))}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/users/@me/threads/archived/private{query}", bucket, options: options); + } + #endregion + + #region Stage + public async Task CreateStageInstanceAsync(CreateStageInstanceParams args, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(); + + return await SendJsonAsync("POST", () => $"stage-instances", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task ModifyStageInstanceAsync(ulong channelId, ModifyStageInstanceParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("PATCH", () => $"stage-instances/{channelId}", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task DeleteStageInstanceAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + try + { + await SendAsync("DELETE", $"stage-instances/{channelId}", options: options).ConfigureAwait(false); + } + catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) { } + } + + public async Task GetStageInstanceAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + try + { + return await SendAsync("POST", () => $"stage-instances/{channelId}", bucket, options: options).ConfigureAwait(false); + } + catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) + { + return null; + } + } + + public async Task ModifyMyVoiceState(ulong guildId, ModifyVoiceStateParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(); + + await SendJsonAsync("PATCH", () => $"guilds/{guildId}/voice-states/@me", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task ModifyUserVoiceState(ulong guildId, ulong userId, ModifyVoiceStateParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(); + + await SendJsonAsync("PATCH", () => $"guilds/{guildId}/voice-states/{userId}", args, bucket, options: options).ConfigureAwait(false); + } + #endregion + + #region Roles public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -405,7 +706,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); } public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) { @@ -416,10 +717,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); } + #endregion - //Channel Messages + #region Channel Messages public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -443,22 +745,12 @@ namespace Discord.API int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxMessagesPerBatch); ulong? relativeId = args.RelativeMessageId.IsSpecified ? args.RelativeMessageId.Value : (ulong?)null; - string relativeDir; - - switch (args.RelativeDirection.GetValueOrDefault(Direction.Before)) + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch { - case Direction.Before: - default: - relativeDir = "before"; - break; - case Direction.After: - relativeDir = "after"; - break; - case Direction.Around: - relativeDir = "around"; - break; - } - + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; var ids = new BucketIds(channelId: channelId); Expression> endpoint; if (relativeId != null) @@ -467,11 +759,12 @@ namespace Discord.API endpoint = () => $"channels/{channelId}/messages?limit={limit}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); - if (!args.Embed.IsSpecified || args.Embed.Value == null) + if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && (!args.Stickers.IsSpecified || args.Stickers.Value == null || args.Stickers.Value.Length == 0)) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); if (args.Content?.Length > DiscordConfig.MaxMessageSize) @@ -481,6 +774,8 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) @@ -495,8 +790,47 @@ namespace Discord.API throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + var ids = new BucketIds(webhookId: webhookId); + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, 10, nameof(args.Embeds), "A max of 10 Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(webhookId: webhookId); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// This operation may only be called with a token. + public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(webhookId: webhookId); + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, options: options).ConfigureAwait(false); + } + + /// Message content is too long, length must be less or equal to . public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -511,6 +845,9 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) @@ -530,7 +867,8 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + var ids = new BucketIds(webhookId: webhookId); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -563,167 +901,519 @@ namespace Discord.API break; } } + /// Message content is too long, length must be less or equal to . public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified) - { - if (!args.Embed.IsSpecified) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.UploadFileParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNull(args, nameof(args)); + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendMultipartAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + #endregion + + #region Stickers, Reactions, Crosspost, and Acks + public async Task GetStickerAsync(ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + + options = RequestOptions.CreateOrClone(options); + + return await NullifyNotFound(SendAsync("GET", () => $"stickers/{id}", new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task GetGuildStickerAsync(ulong guildId, ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await NullifyNotFound(SendAsync("GET", () => $"guilds/{guildId}/stickers/{id}", new BucketIds(guildId), options: options)).ConfigureAwait(false); + } + public async Task ListGuildStickersAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"guilds/{guildId}/stickers", new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task ListNitroStickerPacksAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"sticker-packs", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task CreateGuildStickerAsync(CreateStickerParams args, ulong guildId, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendMultipartAsync("POST", () => $"guilds/{guildId}/stickers", args.ToDictionary(), new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task ModifyStickerAsync(ModifyStickerParams args, ulong guildId, ulong stickerId, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(stickerId, 0, nameof(stickerId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/stickers/{stickerId}", args, new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task DeleteStickerAsync(ulong guildId, ulong stickerId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(stickerId, 0, nameof(stickerId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"guilds/{guildId}/stickers/{stickerId}", new BucketIds(guildId), options: options).ConfigureAwait(false); + } + public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + options = RequestOptions.CreateOrClone(options); + options.IsReactionBucket = true; + + var ids = new BucketIds(channelId: channelId); + + // @me is non-const to fool the ratelimiter, otherwise it will put add/remove in separate buckets + var me = "@me"; + await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{me}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + options.IsReactionBucket = true; + + var ids = new BucketIds(channelId: channelId); + + var user = CurrentUserId.HasValue ? (userId == CurrentUserId.Value ? "@me" : userId.ToString()) : userId.ToString(); + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{user}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false); + } + public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUserReactionsPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUserReactionsPerBatch); + ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + + var ids = new BucketIds(channelId: channelId); + Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/ack", ids, options: options).ConfigureAwait(false); + } + public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); + } + public async Task CrosspostAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/crosspost", ids, options: options).ConfigureAwait(false); + } + #endregion + + #region Channel Permissions + public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendJsonAsync("PUT", () => $"channels/{channelId}/permissions/{targetId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/permissions/{targetId}", ids, options: options).ConfigureAwait(false); + } + #endregion + + #region Channel Pins + public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.GreaterThan(channelId, 0, nameof(channelId)); + Preconditions.GreaterThan(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("PUT", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + + } + public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + } + public async Task> GetPinsAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/pins", ids, options: options).ConfigureAwait(false); + } + #endregion + + #region Channel Recipients + public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.GreaterThan(channelId, 0, nameof(channelId)); + Preconditions.GreaterThan(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("PUT", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + + } + public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + } + #endregion + + #region Interactions + public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"applications/{CurrentUserId}/commands", new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + + options = RequestOptions.CreateOrClone(options); + + try + { + return await SendAsync("GET", () => $"applications/{CurrentUserId}/commands/{id}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) { return null; } + } + + public async Task CreateGlobalApplicationCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 1, nameof(command.Name)); + + if (command.Type == ApplicationCommandType.Slash) + { + Preconditions.NotNullOrEmpty(command.Description, nameof(command.Description)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + } + + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommandAsync(SendJsonAsync("POST", () => $"applications/{CurrentUserId}/commands", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task ModifyGlobalApplicationCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommandAsync(SendJsonAsync("PATCH", () => $"applications/{CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task ModifyGlobalApplicationUserCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommandAsync(SendJsonAsync("PATCH", () => $"applications/{CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task ModifyGlobalApplicationMessageCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommandAsync(SendJsonAsync("PATCH", () => $"applications/{CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"applications/{CurrentUserId}/commands/{commandId}", new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task BulkOverwriteGlobalApplicationCommandsAsync(CreateApplicationCommandParams[] commands, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommandAsync(SendJsonAsync("PUT", () => $"applications/{CurrentUserId}/commands", commands, new BucketIds(), options: options)).ConfigureAwait(false); + } + + public async Task GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await SendAsync("GET", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false); + } + + public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + try + { + return await SendAsync("GET", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options); + } + catch (HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) { return null; } + } + + public async Task CreateGuildApplicationCommandAsync(CreateApplicationCommandParams command, ulong guildId, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 1, nameof(command.Name)); + + if (command.Type == ApplicationCommandType.Slash) + { + Preconditions.NotNullOrEmpty(command.Description, nameof(command.Description)); + Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description)); + Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description)); + } + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await TrySendApplicationCommandAsync(SendJsonAsync("POST", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options)).ConfigureAwait(false); + } + public async Task ModifyGuildApplicationCommandAsync(ModifyApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(guildId: guildId); + + return await TrySendApplicationCommandAsync(SendJsonAsync("PATCH", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options)).ConfigureAwait(false); + } + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) + { options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); + var bucket = new BucketIds(guildId: guildId); - await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options).ConfigureAwait(false); } - public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + public async Task BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, CreateApplicationCommandParams[] commands, RequestOptions options = null) + { options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); + var bucket = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{userId}", ids, options: options).ConfigureAwait(false); + return await TrySendApplicationCommandAsync(SendJsonAsync("PUT", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands", commands, bucket, options: options)).ConfigureAwait(false); } - public async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + #endregion + + #region Interaction Responses + public async Task CreateInteractionResponseAsync(InteractionResponse response, ulong interactionId, string interactionToken, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); + if (response.Data.IsSpecified && response.Data.Value.Content.IsSpecified) + Preconditions.AtMost(response.Data.Value.Content.Value?.Length ?? 0, 2000, nameof(response.Data.Value.Content)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - - await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); + await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options); } - public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) + public async Task GetInteractionResponseAsync(string interactionToken, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); - Preconditions.AtMost(args.Limit, DiscordConfig.MaxUserReactionsPerBatch, nameof(args.Limit)); - Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); - options = RequestOptions.CreateOrClone(options); + Preconditions.NotNullOrEmpty(interactionToken, nameof(interactionToken)); - int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUserReactionsPerBatch); - ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}"; - return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"webhooks/{CurrentUserId}/{interactionToken}/messages/@original", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + public async Task ModifyInteractionResponseAsync(ModifyInteractionResponseParams args, string interactionToken, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/ack", ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("PATCH", () => $"webhooks/{CurrentUserId}/{interactionToken}/messages/@original", args, new BucketIds(), options: options); } - public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null) + public async Task DeleteInteractionResponseAsync(string interactionToken, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"webhooks/{CurrentUserId}/{interactionToken}/messages/@original", new BucketIds(), options: options); } - //Channel Permissions - public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) + public async Task CreateInteractionFollowupMessageAsync(CreateWebhookMessageParams args, string token, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(targetId, 0, nameof(targetId)); - Preconditions.NotNull(args, nameof(args)); + if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.File.IsSpecified) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendJsonAsync("PUT", () => $"channels/{channelId}/permissions/{targetId}", args, ids, options: options).ConfigureAwait(false); + if (!args.File.IsSpecified) + return await SendJsonAsync("POST", () => $"webhooks/{CurrentUserId}/{token}?wait=true", args, new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendMultipartAsync("POST", () => $"webhooks/{CurrentUserId}/{token}?wait=true", args.ToDictionary(), new BucketIds(), options: options).ConfigureAwait(false); } - public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) + + public async Task ModifyInteractionFollowupMessageAsync(ModifyInteractionResponseParams args, ulong id, string token, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(targetId, 0, nameof(targetId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(id, 0, nameof(id)); + + if (args.Content.IsSpecified) + if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendAsync("DELETE", () => $"channels/{channelId}/permissions/{targetId}", ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("PATCH", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", args, new BucketIds(), options: options).ConfigureAwait(false); } - //Channel Pins - public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + public async Task DeleteInteractionFollowupMessageAsync(ulong id, string token, RequestOptions options = null) { - Preconditions.GreaterThan(channelId, 0, nameof(channelId)); - Preconditions.GreaterThan(messageId, 0, nameof(messageId)); - options = RequestOptions.CreateOrClone(options); + Preconditions.NotEqual(id, 0, nameof(id)); - var ids = new BucketIds(channelId: channelId); - await SendAsync("PUT", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + await SendAsync("DELETE", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + #endregion + + #region Application Command permissions + public async Task GetGuildApplicationCommandPermissionsAsync(ulong guildId, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendAsync("DELETE", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/permissions", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task> GetPinsAsync(ulong channelId, RequestOptions options = null) + + public async Task GetGuildApplicationCommandPermissionAsync(ulong guildId, ulong commandId, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(commandId, 0, nameof(commandId)); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - return await SendAsync>("GET", () => $"channels/{channelId}/pins", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/{commandId}/permissions", new BucketIds(), options: options).ConfigureAwait(false); } - //Channel Recipients - public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + public async Task ModifyApplicationCommandPermissionsAsync(ModifyGuildApplicationCommandPermissionsParams permissions, ulong guildId, ulong commandId, RequestOptions options = null) { - Preconditions.GreaterThan(channelId, 0, nameof(channelId)); - Preconditions.GreaterThan(userId, 0, nameof(userId)); - options = RequestOptions.CreateOrClone(options); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(commandId, 0, nameof(commandId)); - var ids = new BucketIds(channelId: channelId); - await SendAsync("PUT", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + return await SendJsonAsync("PUT", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/{commandId}/permissions", permissions, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + + public async Task> BatchModifyApplicationCommandPermissionsAsync(ModifyGuildApplicationCommandPermissions[] permissions, ulong guildId, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(permissions, nameof(permissions)); + options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(channelId: channelId); - await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("PUT", () => $"applications/{CurrentUserId}/guilds/{guildId}/commands/permissions", permissions, new BucketIds(), options: options).ConfigureAwait(false); } + #endregion - //Guilds - public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) + #region Guilds + public async Task GetGuildAsync(ulong guildId, bool withCounts, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -731,7 +1421,7 @@ namespace Discord.API try { var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"guilds/{guildId}?with_counts={(withCounts ? "true" : "false")}", ids, options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } @@ -789,13 +1479,15 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Days, 1, nameof(args.Days)); + string endpointRoleIds = args.IncludeRoleIds?.Length > 0 ? $"&include_roles={string.Join(",", args.IncludeRoleIds)}" : ""; options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/prune?days={args.Days}", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"guilds/{guildId}/prune?days={args.Days}{endpointRoleIds}", ids, options: options).ConfigureAwait(false); } + #endregion - //Guild Bans + #region Guild Bans public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -810,9 +1502,19 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } + /// + /// and must not be equal to zero. + /// -and- + /// must be between 0 to 7. + /// + /// must not be . public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -824,8 +1526,9 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}"; - await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete_message_days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); } + /// and must not be equal to zero. public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -835,9 +1538,11 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); await SendAsync("DELETE", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); } + #endregion - //Guild Embeds - public async Task GetGuildEmbedAsync(ulong guildId, RequestOptions options = null) + #region Guild Widget + /// must not be equal to zero. + public async Task GetGuildWidgetAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -845,21 +1550,25 @@ namespace Discord.API try { var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"guilds/{guildId}/widget", ids, options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } - public async Task ModifyGuildEmbedAsync(ulong guildId, Rest.ModifyGuildEmbedParams args, RequestOptions options = null) + /// must not be equal to zero. + /// must not be . + public async Task ModifyGuildWidgetAsync(ulong guildId, Rest.ModifyGuildWidgetParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/widget", args, ids, options: options).ConfigureAwait(false); } + #endregion - //Guild Integrations + #region Guild Integrations + /// must not be equal to zero. public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -868,6 +1577,8 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); } + /// and must not be equal to zero. + /// must not be . public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -908,8 +1619,11 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false); } + #endregion - //Guild Invites + #region Guild Invites + /// cannot be blank. + /// must not be . public async Task GetInviteAsync(string inviteId, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); @@ -929,14 +1643,16 @@ namespace Discord.API } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetVanityInviteAsync(ulong guildId, RequestOptions options = null) + /// may not be equal to zero. + public async Task GetVanityInviteAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/vanity-url", ids, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"guilds/{guildId}/vanity-url", ids, options: options).ConfigureAwait(false); } + /// may not be equal to zero. public async Task> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -945,6 +1661,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/invites", ids, options: options).ConfigureAwait(false); } + /// may not be equal to zero. public async Task> GetChannelInvitesAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -953,12 +1670,28 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync>("GET", () => $"channels/{channelId}/invites", ids, options: options).ConfigureAwait(false); } + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// + /// must not be . public async Task CreateChannelInviteAsync(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); + Preconditions.AtMost(args.MaxAge, 86400, nameof(args.MaxAge), + "The maximum age of an invite must be less than or equal to a day (86400 seconds)."); + if (args.TargetType.IsSpecified) + { + Preconditions.NotEqual((int)args.TargetType.Value, (int)TargetUserType.Undefined, nameof(args.TargetType)); + if (args.TargetType.Value == TargetUserType.Stream) Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); + if (args.TargetType.Value == TargetUserType.EmbeddedApplication) Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); + } options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); @@ -971,8 +1704,28 @@ namespace Discord.API return await SendAsync("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); } + #endregion + + #region Guild Members + public async Task AddGuildMemberAsync(ulong guildId, ulong userId, AddGuildMemberParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.AccessToken, nameof(args.AccessToken)); + + if (args.RoleIds.IsSpecified) + { + foreach (var roleId in args.RoleIds.Value) + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + } + + options = RequestOptions.CreateOrClone(options); - //Guild Members + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); + } public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -1035,8 +1788,25 @@ namespace Discord.API await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false); } } + public async Task> SearchGuildMembersAsync(ulong guildId, SearchGuildMembersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.NotNullOrEmpty(args.Query, nameof(args.Query)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUsersPerBatch); + string query = args.Query; + + var ids = new BucketIds(guildId: guildId); + Expression> endpoint = () => $"guilds/{guildId}/members/search?limit={limit}&query={query}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + #endregion - //Guild Roles + #region Guild Roles public async Task> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -1045,13 +1815,13 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); } - public async Task CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) + public async Task CreateGuildRoleAsync(ulong guildId, Rest.ModifyGuildRoleParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) { @@ -1083,8 +1853,18 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } + #endregion + + #region Guild emoji + public async Task> GetGuildEmotesAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/emojis", ids, options: options).ConfigureAwait(false); + } - //Guild emoji public async Task GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -1092,7 +1872,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options).ConfigureAwait(false); } public async Task CreateGuildEmoteAsync(ulong guildId, Rest.CreateGuildEmoteParams args, RequestOptions options = null) @@ -1104,7 +1884,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options).ConfigureAwait(false); } public async Task ModifyGuildEmoteAsync(ulong guildId, ulong emoteId, ModifyGuildEmoteParams args, RequestOptions options = null) @@ -1115,7 +1895,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options).ConfigureAwait(false); } public async Task DeleteGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) @@ -1125,10 +1905,110 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options).ConfigureAwait(false); + } + #endregion + + #region Guild Events + + public async Task ListGuildScheduledEventsAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/scheduled-events?with_user_count=true", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventAsync(ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await NullifyNotFound(SendAsync("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}?with_user_count=true", ids, options: options)).ConfigureAwait(false); + } + + public async Task CreateGuildScheduledEventAsync(CreateGuildScheduledEventParams args, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("POST", () => $"guilds/{guildId}/scheduled-events", args, ids, options: options).ConfigureAwait(false); + } + + public async Task ModifyGuildScheduledEventAsync(ModifyGuildScheduledEventParams args, ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + Preconditions.NotNull(args, nameof(args)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/scheduled-events/{eventId}", args, ids, options: options).ConfigureAwait(false); + } + + public async Task DeleteGuildScheduledEventAsync(ulong eventId, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + await SendAsync("DELETE", () => $"guilds/{guildId}/scheduled-events/{eventId}", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventUsersAsync(ulong eventId, ulong guildId, int limit = 100, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendAsync("GET", () => $"guilds/{guildId}/scheduled-events/{eventId}/users?limit={limit}&with_member=true", ids, options: options).ConfigureAwait(false); + } + + public async Task GetGuildScheduledEventUsersAsync(ulong eventId, ulong guildId, GetEventUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(eventId, 0, nameof(eventId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxMessagesPerBatch, nameof(args.Limit)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxGuildEventUsersPerBatch); + ulong? relativeId = args.RelativeUserId.IsSpecified ? args.RelativeUserId.Value : (ulong?)null; + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch + { + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; + var ids = new BucketIds(guildId: guildId); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"guilds/{guildId}/scheduled-events/{eventId}/users?with_member=true&limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () => $"guilds/{guildId}/scheduled-events/{eventId}/users?with_member=true&limit={limit}"; + + return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false); } - //Users + #endregion + + #region Users public async Task GetUserAsync(ulong userId, RequestOptions options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); @@ -1140,8 +2020,9 @@ namespace Discord.API } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } + #endregion - //Current User/DMs + #region Current User/DMs public async Task GetMyUserAsync(RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -1200,8 +2081,9 @@ namespace Discord.API return await SendJsonAsync("POST", () => "users/@me/channels", args, new BucketIds(), options: options).ConfigureAwait(false); } + #endregion - //Voice Regions + #region Voice Regions public async Task> GetVoiceRegionsAsync(RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -1215,8 +2097,9 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); } + #endregion - //Audit logs + #region Audit logs public async Task GetAuditLogsAsync(ulong guildId, GetAuditLogsParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); @@ -1228,15 +2111,30 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); Expression> endpoint; + var queryArgs = new StringBuilder(); if (args.BeforeEntryId.IsSpecified) - endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&before={args.BeforeEntryId.Value}"; - else - endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}"; + { + queryArgs.Append("&before=") + .Append(args.BeforeEntryId); + } + if (args.UserId.IsSpecified) + { + queryArgs.Append("&user_id=") + .Append(args.UserId.Value); + } + if (args.ActionType.IsSpecified) + { + queryArgs.Append("&action_type=") + .Append(args.ActionType.Value); + } + // Still use string interp for the query w/o params, as this is necessary for CreateBucketId + endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}{queryArgs.ToString()}"; return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false); } + #endregion - //Webhooks + #region Webhooks public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -1245,7 +2143,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); - return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options); + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options).ConfigureAwait(false); } public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) { @@ -1299,8 +2197,10 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendAsync>("GET", () => $"channels/{channelId}/webhooks", ids, options: options).ConfigureAwait(false); } + #endregion - //Helpers + #region Helpers + /// Client is not logged in. protected void CheckState() { if (LoginState != LoginState.LoggedIn) @@ -1322,28 +2222,105 @@ namespace Discord.API return _serializer.Deserialize(reader); } + protected async Task TrySendApplicationCommandAsync(Task sendTask) + { + try + { + var result = await sendTask.ConfigureAwait(false); + + if (sendTask.Exception != null) + { + if (sendTask.Exception.InnerException is HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(x); + } + } + + throw sendTask.Exception; + } + else + return result; + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(x); + } + + throw; + } + } + + protected async Task NullifyNotFound(Task sendTask) where T : class + { + try + { + var result = await sendTask.ConfigureAwait(false); + + if (sendTask.Exception != null) + { + if (sendTask.Exception.InnerException is HttpException x) + { + if (x.HttpCode == HttpStatusCode.NotFound) + { + return null; + } + } + + throw sendTask.Exception; + } + else + return result; + } + catch (HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) + { + return null; + } + } internal class BucketIds { public ulong GuildId { get; internal set; } public ulong ChannelId { get; internal set; } + public ulong WebhookId { get; internal set; } + public string HttpMethod { get; internal set; } - internal BucketIds(ulong guildId = 0, ulong channelId = 0) + internal BucketIds(ulong guildId = 0, ulong channelId = 0, ulong webhookId = 0) { GuildId = guildId; ChannelId = channelId; + WebhookId = webhookId; } + internal object[] ToArray() - => new object[] { GuildId, ChannelId }; + => new object[] { HttpMethod, GuildId, ChannelId, WebhookId }; + + internal Dictionary ToMajorParametersDictionary() + { + var dict = new Dictionary(); + if (GuildId != 0) + dict["GuildId"] = GuildId.ToString(); + if (ChannelId != 0) + dict["ChannelId"] = ChannelId.ToString(); + if (WebhookId != 0) + dict["WebhookId"] = WebhookId.ToString(); + return dict; + } internal static int? GetIndex(string name) { - switch (name) + return name switch { - case "guildId": return 0; - case "channelId": return 1; - default: - return null; - } + "httpMethod" => 0, + "guildId" => 1, + "channelId" => 2, + "webhookId" => 3, + _ => null, + }; } } @@ -1351,18 +2328,19 @@ namespace Discord.API { return endpointExpr.Compile()(); } - private static string GetBucketId(BucketIds ids, Expression> endpointExpr, TokenType tokenType, string callingMethod) + private static BucketId GetBucketId(string httpMethod, BucketIds ids, Expression> endpointExpr, string callingMethod) { + ids.HttpMethod ??= httpMethod; return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); } - private static Func CreateBucketId(Expression> endpoint) + private static Func CreateBucketId(Expression> endpoint) { try { //Is this a constant string? if (endpoint.Body.NodeType == ExpressionType.Constant) - return x => (endpoint.Body as ConstantExpression).Value.ToString(); + return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary()); var builder = new StringBuilder(); var methodCall = endpoint.Body as MethodCallExpression; @@ -1378,7 +2356,7 @@ namespace Discord.API Array.Copy(elements, 0, methodArgs, 1, elements.Length); } - int endIndex = format.IndexOf('?'); //Dont include params + int endIndex = format.IndexOf('?'); //Don't include params if (endIndex == -1) endIndex = format.Length; @@ -1394,12 +2372,12 @@ namespace Discord.API builder.Append(format, lastIndex, leftIndex - lastIndex); int rightIndex = format.IndexOf("}", leftIndex); - int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1)); + int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1), NumberStyles.None, CultureInfo.InvariantCulture); string fieldName = GetFieldName(methodArgs[argId + 1]); var mappedId = BucketIds.GetIndex(fieldName); - if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash + if (!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash rightIndex++; if (mappedId.HasValue) @@ -1407,13 +2385,16 @@ namespace Discord.API lastIndex = rightIndex + 1; } + if (builder[builder.Length - 1] == '/') + builder.Remove(builder.Length - 1, 1); format = builder.ToString(); - return x => string.Format(format, x.ToArray()); + + return x => BucketId.Create(x.HttpMethod, string.Format(format, x.ToArray()), x.ToMajorParametersDictionary()); } catch (Exception ex) { - throw new InvalidOperationException("Failed to generate the bucket id for this operation", ex); + throw new InvalidOperationException("Failed to generate the bucket id for this operation.", ex); } } @@ -1427,5 +2408,6 @@ namespace Discord.API return (expr as MemberExpression).Member.Name; } + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index d4dee9f0a..93183161b 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,49 +1,142 @@ +//using Discord.Rest.Entities.Interactions; +using Discord.Net; +using Discord.Net.Converters; +using Discord.Net.ED25519; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Text; using System.Threading.Tasks; namespace Discord.Rest { + /// + /// Provides a client to send REST-based requests to Discord. + /// public class DiscordRestClient : BaseDiscordClient, IDiscordClient { + #region DiscordRestClient private RestApplication _applicationInfo; + internal static JsonSerializer Serializer = new JsonSerializer() { ContractResolver = new DiscordContractResolver(), NullValueHandling = NullValueHandling.Include }; - public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; - + /// + /// Gets the logged-in user. + /// + public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } + + /// public DiscordRestClient() : this(new DiscordRestConfig()) { } + /// + /// Initializes a new with the provided configuration. + /// + /// The configuration to be used with the client. public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + // used for socket client rest access + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) - => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, serializer: Serializer, useSystemClock: config.UseSystemClock); + internal override void Dispose(bool disposing) { if (disposing) ApiClient.Dispose(); + + base.Dispose(disposing); } + /// internal override async Task OnLoginAsync(TokenType tokenType, string token) { var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); ApiClient.CurrentUserId = user.Id; base.CurrentUser = RestSelfUser.Create(this, user); } + + internal void CreateRestSelfUser(API.User user) + { + base.CurrentUser = RestSelfUser.Create(this, user); + } + /// internal override Task OnLogoutAsync() { _applicationInfo = null; return Task.Delay(0); } - /// + #region Rest interactions + + public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, string body) + => IsValidHttpInteraction(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); + public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, byte[] body) + { + var key = HexConverter.HexToByteArray(publicKey); + var sig = HexConverter.HexToByteArray(signature); + var tsp = Encoding.UTF8.GetBytes(timestamp); + + var message = new List(); + message.AddRange(tsp); + message.AddRange(body); + + return IsValidHttpInteraction(key, sig, message.ToArray()); + } + + private bool IsValidHttpInteraction(byte[] publicKey, byte[] signature, byte[] message) + { + return Ed25519.Verify(signature, message, publicKey); + } + + /// + /// Creates a from a http message. + /// + /// The public key of your application + /// The signature sent with the interaction. + /// The timestamp sent with the interaction. + /// The body of the http message. + /// + /// A that represents the incoming http interaction. + /// + /// Thrown when the signature doesn't match the public key. + public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body) + => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); + + /// + /// Creates a from a http message. + /// + /// The public key of your application + /// The signature sent with the interaction. + /// The timestamp sent with the interaction. + /// The body of the http message. + /// + /// A that represents the incoming http interaction. + /// + /// Thrown when the signature doesn't match the public key. + public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body) + { + if (!IsValidHttpInteraction(publicKey, signature, timestamp, body)) + { + throw new BadSignatureException(); + } + + using (var textReader = new StringReader(Encoding.UTF8.GetString(body))) + using (var jsonReader = new JsonTextReader(textReader)) + { + var model = Serializer.Deserialize(jsonReader); + return await RestInteraction.CreateAsync(this, model); + } + } + + #endregion + public async Task GetApplicationInfoAsync(RequestOptions options = null) { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options)); + return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false); } - - /// + public Task GetChannelAsync(ulong id, RequestOptions options = null) => ClientHelper.GetChannelAsync(this, id, options); - /// public Task> GetPrivateChannelsAsync(RequestOptions options = null) => ClientHelper.GetPrivateChannelsAsync(this, options); public Task> GetDMChannelsAsync(RequestOptions options = null) @@ -51,54 +144,79 @@ namespace Discord.Rest public Task> GetGroupChannelsAsync(RequestOptions options = null) => ClientHelper.GetGroupChannelsAsync(this, options); - /// public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options); - /// public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options); - /// public Task GetGuildAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetGuildAsync(this, id, options); - /// - public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetGuildEmbedAsync(this, id, options); - /// + => ClientHelper.GetGuildAsync(this, id, false, options); + public Task GetGuildAsync(ulong id, bool withCounts, RequestOptions options = null) + => ClientHelper.GetGuildAsync(this, id, withCounts, options); + public Task GetGuildWidgetAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildWidgetAsync(this, id, options); public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) => ClientHelper.GetGuildSummariesAsync(this, null, null, options); - /// public IAsyncEnumerable> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null) => ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options); - /// public Task> GetGuildsAsync(RequestOptions options = null) - => ClientHelper.GetGuildsAsync(this, options); - /// + => ClientHelper.GetGuildsAsync(this, false, options); + public Task> GetGuildsAsync(bool withCounts, RequestOptions options = null) + => ClientHelper.GetGuildsAsync(this, withCounts, options); public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); - /// public Task GetUserAsync(ulong id, RequestOptions options = null) => ClientHelper.GetUserAsync(this, id, options); - /// public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) => ClientHelper.GetGuildUserAsync(this, guildId, id, options); - /// public Task> GetVoiceRegionsAsync(RequestOptions options = null) => ClientHelper.GetVoiceRegionsAsync(this, options); - /// public Task GetVoiceRegionAsync(string id, RequestOptions options = null) => ClientHelper.GetVoiceRegionAsync(this, id, options); - /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); - //IDiscordClient + public Task CreateGlobalCommand(ApplicationCommandProperties properties, RequestOptions options = null) + => ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); + public Task CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) + => ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options); + public Task> GetGlobalApplicationCommands(RequestOptions options = null) + => ClientHelper.GetGlobalApplicationCommandsAsync(this, options); + public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) + => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, options); + public Task> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) + => ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); + public Task> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) + => ClientHelper.BulkOverwriteGuildApplicationCommandAsync(this, guildId, commandProperties, options); + public Task> BatchEditGuildCommandPermissions(ulong guildId, IDictionary permissions, RequestOptions options = null) + => InteractionHelper.BatchEditGuildCommandPermissionsAsync(this, guildId, permissions, options); + public Task DeleteAllGlobalCommandsAsync(RequestOptions options = null) + => InteractionHelper.DeleteAllGlobalCommandsAsync(this, options); + + public Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId) + => ClientHelper.AddRoleAsync(this, guildId, userId, roleId); + public Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId) + => ClientHelper.RemoveRoleAsync(this, guildId, userId, roleId); + + public Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(channelId, messageId, emote, this, options); + public Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(channelId, messageId, userId, emote, this, options); + public Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(channelId, messageId, this, options); + public Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote, this, options); +#endregion + + #region IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -106,6 +224,7 @@ namespace Discord.Rest else return null; } + /// async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -113,6 +232,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -120,6 +240,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -128,12 +249,14 @@ namespace Discord.Rest return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -141,6 +264,7 @@ namespace Discord.Rest else return null; } + /// async Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -148,9 +272,11 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -159,12 +285,23 @@ namespace Discord.Rest return null; } + /// async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); + + /// + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => await GetGlobalApplicationCommands(options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 4a7aae287..7bf7440ce 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -1,7 +1,10 @@ -using Discord.Net.Rest; +using Discord.Net.Rest; namespace Discord.Rest { + /// + /// Represents a configuration class for . + /// public class DiscordRestConfig : DiscordConfig { /// Gets or sets the provider used to generate new REST connections. diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index 7936343f3..b3aaf582c 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Model = Discord.API.AuditLog; @@ -27,6 +27,9 @@ namespace Discord.Rest [ActionType.Unban] = UnbanAuditLogData.Create, [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + [ActionType.MemberMoved] = MemberMoveAuditLogData.Create, + [ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create, + [ActionType.BotAdded] = BotAddAuditLogData.Create, [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, @@ -45,6 +48,10 @@ namespace Discord.Rest [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + [ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create, + [ActionType.MessagePinned] = MessagePinAuditLogData.Create, + [ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create, + }; public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index 4b9d5875f..fc807cac0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a ban. + /// public class BanAuditLogData : IAuditLogData { private BanAuditLogData(IUser user) @@ -18,6 +21,12 @@ namespace Discord.Rest return new BanAuditLogData(RestUser.Create(discord, userInfo)); } + /// + /// Gets the user that was banned. + /// + /// + /// A user object representing the banned user. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs new file mode 100644 index 000000000..0d12e4609 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a adding a bot to a guild. + /// + public class BotAddAuditLogData : IAuditLogData + { + private BotAddAuditLogData(IUser bot) + { + Target = bot; + } + + internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the bot that was added. + /// + /// + /// A user object representing the bot. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index 51e72c414..5c2f81ae6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using System.Collections.Generic; using System.Linq; @@ -7,44 +6,100 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a channel creation. + /// public class ChannelCreateAuditLogData : IAuditLogData { - private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) + private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, int? rateLimit, bool? nsfw, int? bitrate, IReadOnlyCollection overwrites) { ChannelId = id; ChannelName = name; ChannelType = type; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; + Bitrate = bitrate; Overwrites = overwrites; } internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var changes = entry.Changes; - var overwrites = new List(); var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var overwrites = overwritesModel.NewValue.ToObject(discord.ApiClient.Serializer) + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); + int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? nsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? bitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var id = entry.TargetId.Value; - foreach (var overwrite in overwritesModel.NewValue) - { - var deny = overwrite.Value("deny"); - var permType = overwrite.Value("type"); - var id = overwrite.Value("id"); - var allow = overwrite.Value("allow"); - - overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); - } - - return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, overwrites.ToReadOnlyCollection()); + return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); } + /// + /// Gets the snowflake ID of the created channel. + /// + /// + /// A representing the snowflake identifier for the created channel. + /// public ulong ChannelId { get; } + /// + /// Gets the name of the created channel. + /// + /// + /// A string containing the name of the created channel. + /// public string ChannelName { get; } + /// + /// Gets the type of the created channel. + /// + /// + /// The type of channel that was created. + /// public ChannelType ChannelType { get; } + /// + /// Gets the current slow-mode delay of the created channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether the created channel is NSFW. + /// + /// + /// true if the created channel has the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate that the clients in the created voice channel are requested to use. + /// + /// + /// An representing the bit-rate (bps) that the created voice channel defines and requests the + /// client(s) to use. + /// null if this is not mentioned in this entry. + /// + public int? Bitrate { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the created channel. + /// + /// + /// A collection of permission , containing the permission overwrites that were + /// assigned to the created channel. + /// public IReadOnlyCollection Overwrites { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 7af5ca10c..81ae7155b 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -1,21 +1,24 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a channel deletion. + /// public class ChannelDeleteAuditLogData : IAuditLogData { - private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) + private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, int? rateLimit, bool? nsfw, int? bitrate, IReadOnlyCollection overwrites) { ChannelId = id; ChannelName = name; ChannelType = type; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; + Bitrate = bitrate; Overwrites = overwrites; } @@ -26,20 +29,75 @@ namespace Discord.Rest var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); var overwrites = overwritesModel.OldValue.ToObject(discord.ApiClient.Serializer) .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) .ToList(); var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); var name = nameModel.OldValue.ToObject(discord.ApiClient.Serializer); + int? rateLimitPerUser = rateLimitPerUserModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + bool? nsfw = nsfwModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + int? bitrate = bitrateModel?.OldValue?.ToObject(discord.ApiClient.Serializer); var id = entry.TargetId.Value; - return new ChannelDeleteAuditLogData(id, name, type, overwrites.ToReadOnlyCollection()); + return new ChannelDeleteAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); } + /// + /// Gets the snowflake ID of the deleted channel. + /// + /// + /// A representing the snowflake identifier for the deleted channel. + /// public ulong ChannelId { get; } + /// + /// Gets the name of the deleted channel. + /// + /// + /// A string containing the name of the deleted channel. + /// public string ChannelName { get; } + /// + /// Gets the type of the deleted channel. + /// + /// + /// The type of channel that was deleted. + /// public ChannelType ChannelType { get; } + /// + /// Gets the slow-mode delay of the deleted channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether the deleted channel was NSFW. + /// + /// + /// true if this channel had the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate of this channel if applicable. + /// + /// + /// An representing the bit-rate set of the voice channel. + /// null if this is not mentioned in this entry. + /// + public int? Bitrate { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the deleted channel. + /// + /// + /// A collection of permission . + /// public IReadOnlyCollection Overwrites { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index e2d6064a9..f50d9eeb3 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -1,18 +1,65 @@ namespace Discord.Rest { + /// + /// Represents information for a channel. + /// public struct ChannelInfo { - internal ChannelInfo(string name, string topic, int? bitrate, int? limit) + internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate, ChannelType? type) { Name = name; Topic = topic; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; Bitrate = bitrate; - UserLimit = limit; + ChannelType = type; } + /// + /// Gets the name of this channel. + /// + /// + /// A string containing the name of this channel. + /// public string Name { get; } + /// + /// Gets the topic of this channel. + /// + /// + /// A string containing the topic of this channel, if any. + /// public string Topic { get; } + /// + /// Gets the current slow-mode delay of this channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether this channel is NSFW. + /// + /// + /// true if this channel has the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate of this channel if applicable. + /// + /// + /// An representing the bit-rate set for the voice channel; + /// null if this is not mentioned in this entry. + /// public int? Bitrate { get; } - public int? UserLimit { get; } + /// + /// Gets the type of this channel. + /// + /// + /// The channel type of this channel; null if not applicable. + /// + public ChannelType? ChannelType { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs index 36fe82084..b2294f183 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a channel update. + /// public class ChannelUpdateAuditLogData : IAuditLogData { private ChannelUpdateAuditLogData(ulong id, ChannelInfo before, ChannelInfo after) @@ -20,26 +23,50 @@ namespace Discord.Rest var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); var topicModel = changes.FirstOrDefault(x => x.ChangedProperty == "topic"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); - var userLimitModel = changes.FirstOrDefault(x => x.ChangedProperty == "user_limit"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); string oldTopic = topicModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newTopic = topicModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? oldRateLimitPerUser = rateLimitPerUserModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newRateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldNsfw = nsfwModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newNsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); int? oldBitrate = bitrateModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newBitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); - int? oldLimit = userLimitModel?.OldValue?.ToObject(discord.ApiClient.Serializer), - newLimit = userLimitModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ChannelType? oldType = typeModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newType = typeModel?.NewValue?.ToObject(discord.ApiClient.Serializer); - var before = new ChannelInfo(oldName, oldTopic, oldBitrate, oldLimit); - var after = new ChannelInfo(newName, newTopic, newBitrate, newLimit); + var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate, oldType); + var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate, newType); return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after); } + /// + /// Gets the snowflake ID of the updated channel. + /// + /// + /// A representing the snowflake identifier for the updated channel. + /// public ulong ChannelId { get; } + /// + /// Gets the channel information before the changes. + /// + /// + /// An information object containing the original channel information before the changes were made. + /// public ChannelInfo Before { get; } + /// + /// Gets the channel information after the changes. + /// + /// + /// An information object containing the channel information after the changes were made. + /// public ChannelInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs index dac2d90ef..92e92574f 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an emoji creation. + /// public class EmoteCreateAuditLogData : IAuditLogData { private EmoteCreateAuditLogData(ulong id, string name) @@ -25,7 +24,19 @@ namespace Discord.Rest return new EmoteCreateAuditLogData(entry.TargetId.Value, emoteName); } + /// + /// Gets the snowflake ID of the created emoji. + /// + /// + /// A representing the snowflake identifier for the created emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the name of the created emoji. + /// + /// + /// A string containing the name of the created emoji. + /// public string Name { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs index 73cb31af9..fd307d5a9 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an emoji deletion. + /// public class EmoteDeleteAuditLogData : IAuditLogData { private EmoteDeleteAuditLogData(ulong id, string name) @@ -22,7 +25,19 @@ namespace Discord.Rest return new EmoteDeleteAuditLogData(entry.TargetId.Value, emoteName); } + /// + /// Gets the snowflake ID of the deleted emoji. + /// + /// + /// A representing the snowflake identifier for the deleted emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the name of the deleted emoji. + /// + /// + /// A string containing the name of the deleted emoji. + /// public string Name { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs index 84898013d..96e791d81 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an emoji update. + /// public class EmoteUpdateAuditLogData : IAuditLogData { private EmoteUpdateAuditLogData(ulong id, string oldName, string newName) @@ -24,8 +27,26 @@ namespace Discord.Rest return new EmoteUpdateAuditLogData(entry.TargetId.Value, oldName, newName); } + /// + /// Gets the snowflake ID of the updated emoji. + /// + /// + /// A representing the snowflake identifier of the updated emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the new name of the updated emoji. + /// + /// + /// A string containing the new name of the updated emoji. + /// public string NewName { get; } + /// + /// Gets the old name of the updated emoji. + /// + /// + /// A string containing the old name of the updated emoji. + /// public string OldName { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs index 90865ef72..85c7ac438 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs @@ -1,10 +1,14 @@ namespace Discord.Rest { + /// + /// Represents information for a guild. + /// public struct GuildInfo { internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs, ulong? afkChannel, string name, string region, string icon, - VerificationLevel? verification, IUser owner, MfaLevel? mfa, int? filter) + VerificationLevel? verification, IUser owner, MfaLevel? mfa, ExplicitContentFilterLevel? filter, + ulong? systemChannel, ulong? widgetChannel, bool? widget) { AfkTimeout = afkTimeout; DefaultMessageNotifications = defaultNotifs; @@ -15,18 +19,110 @@ namespace Discord.Rest VerificationLevel = verification; Owner = owner; MfaLevel = mfa; - ContentFilterLevel = filter; + ExplicitContentFilter = filter; + SystemChannelId = systemChannel; + EmbedChannelId = widgetChannel; + IsEmbeddable = widget; } + /// + /// 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. + /// + /// + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. + /// null if this is not mentioned in this entry. + /// public int? AfkTimeout { get; } + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// + /// + /// The default message notifications setting of this guild. + /// null if this is not mentioned in this entry. + /// public DefaultMessageNotifications? DefaultMessageNotifications { get; } + /// + /// Gets the ID of the AFK voice channel for this guild. + /// + /// + /// A representing the snowflake identifier of the AFK voice channel; null if + /// none is set. + /// public ulong? AfkChannelId { get; } + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// public string Name { get; } + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// public string RegionId { get; } + /// + /// Gets the ID of this guild's icon. + /// + /// + /// A string containing the identifier for the splash image; null if none is set. + /// public string IconHash { get; } + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// null if this is not mentioned in this entry. + /// public VerificationLevel? VerificationLevel { get; } + /// + /// Gets the owner of this guild. + /// + /// + /// A user object representing the owner of this guild. + /// public IUser Owner { get; } + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// + /// + /// The level of MFA requirement. + /// null if this is not mentioned in this entry. + /// public MfaLevel? MfaLevel { get; } - public int? ContentFilterLevel { get; } + /// + /// Gets the level of content filtering applied to user's content in a Guild. + /// + /// + /// The level of explicit content filtering. + /// + public ExplicitContentFilterLevel? ExplicitContentFilter { get; } + /// + /// Gets the ID of the channel where system messages are sent. + /// + /// + /// A representing the snowflake identifier of the channel where system + /// messages are sent; null if none is set. + /// + public ulong? SystemChannelId { get; } + /// + /// Gets the ID of the widget embed channel of this guild. + /// + /// + /// A representing the snowflake identifier of the embedded channel found within the + /// widget settings of this guild; null if none is set. + /// + public ulong? EmbedChannelId { get; } + /// + /// Gets a value that indicates whether this guild is embeddable (i.e. can use widget). + /// + /// + /// true if this guild can be embedded via widgets; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsEmbeddable { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs index 09c1eda18..80b719a6d 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a guild update. + /// public class GuildUpdateAuditLogData : IAuditLogData { private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after) @@ -18,15 +21,18 @@ namespace Discord.Rest var changes = entry.Changes; var afkTimeoutModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var defaultMessageNotificationsModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var afkChannelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var regionIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var iconHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var verificationLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var ownerIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var mfaLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); - var contentFilterModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var defaultMessageNotificationsModel = changes.FirstOrDefault(x => x.ChangedProperty == "default_message_notifications"); + var afkChannelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_channel_id"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var regionIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "region"); + var iconHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "icon_hash"); + var verificationLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "verification_level"); + var ownerIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "owner_id"); + var mfaLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "mfa_level"); + var contentFilterModel = changes.FirstOrDefault(x => x.ChangedProperty == "explicit_content_filter"); + var systemChannelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "system_channel_id"); + var widgetChannelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "widget_channel_id"); + var widgetEnabledModel = changes.FirstOrDefault(x => x.ChangedProperty == "widget_enabled"); int? oldAfkTimeout = afkTimeoutModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newAfkTimeout = afkTimeoutModel?.NewValue?.ToObject(discord.ApiClient.Serializer); @@ -46,8 +52,14 @@ namespace Discord.Rest newOwnerId = ownerIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); MfaLevel? oldMfaLevel = mfaLevelModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newMfaLevel = mfaLevelModel?.NewValue?.ToObject(discord.ApiClient.Serializer); - int? oldContentFilter = contentFilterModel?.OldValue?.ToObject(discord.ApiClient.Serializer), - newContentFilter = contentFilterModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ExplicitContentFilterLevel? oldContentFilter = contentFilterModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newContentFilter = contentFilterModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldSystemChannelId = systemChannelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newSystemChannelId = systemChannelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldWidgetChannelId = widgetChannelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newWidgetChannelId = widgetChannelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldWidgetEnabled = widgetEnabledModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newWidgetEnabled = widgetEnabledModel?.NewValue?.ToObject(discord.ApiClient.Serializer); IUser oldOwner = null; if (oldOwnerId != null) @@ -65,15 +77,27 @@ namespace Discord.Rest var before = new GuildInfo(oldAfkTimeout, oldDefaultMessageNotifications, oldAfkChannelId, oldName, oldRegionId, oldIconHash, oldVerificationLevel, oldOwner, - oldMfaLevel, oldContentFilter); + oldMfaLevel, oldContentFilter, oldSystemChannelId, oldWidgetChannelId, oldWidgetEnabled); var after = new GuildInfo(newAfkTimeout, newDefaultMessageNotifications, newAfkChannelId, newName, newRegionId, newIconHash, newVerificationLevel, newOwner, - newMfaLevel, newContentFilter); + newMfaLevel, newContentFilter, newSystemChannelId, newWidgetChannelId, newWidgetEnabled); return new GuildUpdateAuditLogData(before, after); } + /// + /// Gets the guild information before the changes. + /// + /// + /// An information object containing the original guild information before the changes were made. + /// public GuildInfo Before { get; } + /// + /// Gets the guild information after the changes. + /// + /// + /// An information object containing the guild information after the changes were made. + /// public GuildInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index 1d7f48e93..b177b2435 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an invite creation. + /// public class InviteCreateAuditLogData : IAuditLogData { private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) @@ -33,23 +36,71 @@ namespace Discord.Rest var maxAge = maxAgeModel.NewValue.ToObject(discord.ApiClient.Serializer); var code = codeModel.NewValue.ToObject(discord.ApiClient.Serializer); var temporary = temporaryModel.NewValue.ToObject(discord.ApiClient.Serializer); - var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var channelId = channelIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var uses = usesModel.NewValue.ToObject(discord.ApiClient.Serializer); var maxUses = maxUsesModel.NewValue.ToObject(discord.ApiClient.Serializer); - var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - var inviter = RestUser.Create(discord, inviterInfo); + RestUser inviter = null; + if (inviterIdModel != null) + { + var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + inviter = RestUser.Create(discord, inviterInfo); + } return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Gets a value that determines whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// public bool Temporary { get; } + /// + /// Gets the user that created this invite if available. + /// + /// + /// A user that created this invite or . + /// public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to. + /// public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite was used. + /// public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// public int MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 091285532..9d0aed12b 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an invite removal. + /// public class InviteDeleteAuditLogData : IAuditLogData { private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) @@ -33,23 +36,71 @@ namespace Discord.Rest var maxAge = maxAgeModel.OldValue.ToObject(discord.ApiClient.Serializer); var code = codeModel.OldValue.ToObject(discord.ApiClient.Serializer); var temporary = temporaryModel.OldValue.ToObject(discord.ApiClient.Serializer); - var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var channelId = channelIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var uses = usesModel.OldValue.ToObject(discord.ApiClient.Serializer); var maxUses = maxUsesModel.OldValue.ToObject(discord.ApiClient.Serializer); - var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - var inviter = RestUser.Create(discord, inviterInfo); + RestUser inviter = null; + if (inviterIdModel != null) + { + var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + inviter = RestUser.Create(discord, inviterInfo); + } return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// public bool Temporary { get; } + /// + /// Gets the user that created this invite if available. + /// + /// + /// A user that created this invite or . + /// public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to. + /// public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// public int MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs index c9840f6cc..aaad362da 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for an invite. + /// public struct InviteInfo { internal InviteInfo(int? maxAge, string code, bool? temporary, ulong? channelId, int? maxUses) @@ -11,10 +14,44 @@ namespace Discord.Rest MaxUses = maxUses; } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite never expires or not specified. + /// public int? MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off, + /// false if not; null if not specified. + /// public bool? Temporary { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to; + /// null if not specified. + /// public ulong? ChannelId { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is specified. + /// public int? MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs index 35088be98..95bfb845a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data relating to an invite update. + /// public class InviteUpdateAuditLogData : IAuditLogData { private InviteUpdateAuditLogData(InviteInfo before, InviteInfo after) @@ -40,7 +43,19 @@ namespace Discord.Rest return new InviteUpdateAuditLogData(before, after); } + /// + /// Gets the invite information before the changes. + /// + /// + /// An information object containing the original invite information before the changes were made. + /// public InviteInfo Before { get; } + /// + /// Gets the invite information after the changes. + /// + /// + /// An information object containing the invite information after the changes were made. + /// public InviteInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs index 41b5526b8..dceb73d0a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a kick. + /// public class KickAuditLogData : IAuditLogData { private KickAuditLogData(RestUser user) @@ -18,6 +21,12 @@ namespace Discord.Rest return new KickAuditLogData(RestUser.Create(discord, userInfo)); } + /// + /// Gets the user that was kicked. + /// + /// + /// A user object representing the kicked user. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs new file mode 100644 index 000000000..b0374dc86 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to disconnecting members from voice channels. + /// + public class MemberDisconnectAuditLogData : IAuditLogData + { + private MemberDisconnectAuditLogData(int count) + { + MemberCount = count; + } + + internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberDisconnectAuditLogData(entry.Options.Count.Value); + } + + /// + /// Gets the number of members that were disconnected. + /// + /// + /// An representing the number of members that were disconnected from a voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs index b0a1a8e5a..ffa316faa 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs @@ -1,18 +1,41 @@ namespace Discord.Rest { + /// + /// Represents information for a member. + /// public struct MemberInfo { - internal MemberInfo(string nick, bool? deaf, bool? mute, string avatar_hash) + internal MemberInfo(string nick, bool? deaf, bool? mute) { Nickname = nick; Deaf = deaf; Mute = mute; - AvatarHash = avatar_hash; } + /// + /// Gets the nickname of the updated member. + /// + /// + /// A string representing the nickname of the updated member; null if none is set. + /// public string Nickname { get; } + /// + /// Gets a value that indicates whether the updated member is deafened by the guild. + /// + /// + /// true if the updated member is deafened (i.e. not permitted to listen to or speak to others) by the guild; + /// otherwise false. + /// null if this is not mentioned in this entry. + /// public bool? Deaf { get; } + /// + /// Gets a value that indicates whether the updated member is muted (i.e. not permitted to speak via voice) by the + /// guild. + /// + /// + /// true if the updated member is muted by the guild; otherwise false. + /// null if this is not mentioned in this entry. + /// public bool? Mute { get; } - public string AvatarHash { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs new file mode 100644 index 000000000..f5373d34d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs @@ -0,0 +1,37 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to moving members between voice channels. + /// + public class MemberMoveAuditLogData : IAuditLogData + { + private MemberMoveAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MemberCount = count; + } + + internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the members were moved to. + /// + /// + /// A representing the snowflake identifier for the channel that the members were moved to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of members that were moved. + /// + /// + /// An representing the number of members that were moved to another voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs index 48adb1833..763c90c68 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; @@ -7,6 +6,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a change in a guild member's roles. + /// public class MemberRoleAuditLogData : IAuditLogData { private MemberRoleAuditLogData(IReadOnlyCollection roles, IUser target) @@ -30,7 +32,20 @@ namespace Discord.Rest return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); } + /// + /// Gets a collection of role changes that were performed on the member. + /// + /// + /// A read-only collection of , containing the roles that were changed on + /// the member. + /// public IReadOnlyCollection Roles { get; } + /// + /// Gets the user that the roles changes were performed on. + /// + /// + /// A user object representing the user that the role changes were performed on. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs index 4838b75c9..b0abf2d95 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// An information object representing a change in one of a guild member's roles. + /// public struct MemberRoleEditInfo { internal MemberRoleEditInfo(string name, ulong roleId, bool added) @@ -9,8 +12,26 @@ namespace Discord.Rest Added = added; } + /// + /// Gets the name of the role that was changed. + /// + /// + /// A string containing the name of the role that was changed. + /// public string Name { get; } + /// + /// Gets the ID of the role that was changed. + /// + /// + /// A representing the snowflake identifier of the role that was changed. + /// public ulong RoleId { get; } + /// + /// Gets a value that indicates whether the role was added to the user. + /// + /// + /// true if the role was added to the user; otherwise false. + /// public bool Added { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs index 96d34610e..f22b83e4c 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -1,11 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; -using ChangeModel = Discord.API.AuditLogChange; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a change in a guild member. + /// public class MemberUpdateAuditLogData : IAuditLogData { private MemberUpdateAuditLogData(IUser target, MemberInfo before, MemberInfo after) @@ -22,7 +24,6 @@ namespace Discord.Rest var nickModel = changes.FirstOrDefault(x => x.ChangedProperty == "nick"); var deafModel = changes.FirstOrDefault(x => x.ChangedProperty == "deaf"); var muteModel = changes.FirstOrDefault(x => x.ChangedProperty == "mute"); - var avatarModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash"); string oldNick = nickModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newNick = nickModel?.NewValue?.ToObject(discord.ApiClient.Serializer); @@ -30,20 +31,36 @@ namespace Discord.Rest newDeaf = deafModel?.NewValue?.ToObject(discord.ApiClient.Serializer); bool? oldMute = muteModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newMute = muteModel?.NewValue?.ToObject(discord.ApiClient.Serializer); - string oldAvatar = avatarModel?.OldValue?.ToObject(discord.ApiClient.Serializer), - newAvatar = avatarModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); var user = RestUser.Create(discord, targetInfo); - var before = new MemberInfo(oldNick, oldDeaf, oldMute, oldAvatar); - var after = new MemberInfo(newNick, newDeaf, newMute, newAvatar); + var before = new MemberInfo(oldNick, oldDeaf, oldMute); + var after = new MemberInfo(newNick, newDeaf, newMute); return new MemberUpdateAuditLogData(user, before, after); } + /// + /// Gets the user that the changes were performed on. + /// + /// + /// A user object representing the user who the changes were performed on. + /// public IUser Target { get; } + /// + /// Gets the member information before the changes. + /// + /// + /// An information object containing the original member information before the changes were made. + /// public MemberInfo Before { get; } + /// + /// Gets the member information after the changes. + /// + /// + /// An information object containing the member information after the changes were made. + /// public MemberInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs new file mode 100644 index 000000000..7a9846349 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs @@ -0,0 +1,38 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to message deletion(s). + /// + public class MessageBulkDeleteAuditLogData : IAuditLogData + { + private MessageBulkDeleteAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MessageCount = count; + } + + internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// + public int MessageCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index 3949cdd68..66b3f7d83 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -1,22 +1,49 @@ -using Model = Discord.API.AuditLog; +using System.Linq; + +using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to message deletion(s). + /// public class MessageDeleteAuditLogData : IAuditLogData { - private MessageDeleteAuditLogData(ulong channelId, int count) + private MessageDeleteAuditLogData(ulong channelId, int count, IUser user) { ChannelId = channelId; MessageCount = count; + Target = user; } internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value); + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); } + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// public int MessageCount { get; } + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// public ulong ChannelId { get; } + /// + /// Gets the user of the messages that were deleted. + /// + /// + /// A user object representing the user that created the deleted messages. + /// + public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs new file mode 100644 index 000000000..be66ac846 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -0,0 +1,54 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a pinned message. + /// + public class MessagePinAuditLogData : IAuditLogData + { + private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + RestUser user = null; + if (entry.TargetId.HasValue) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + user = RestUser.Create(discord, userInfo); + } + + return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); + } + + /// + /// Gets the ID of the messages that was pinned. + /// + /// + /// A representing the snowflake identifier for the messages that was pinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was pinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was pinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was pinned if available. + /// + /// + /// A user object representing the user that created the pinned message or . + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs new file mode 100644 index 000000000..b4fa389cc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -0,0 +1,54 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an unpinned message. + /// + public class MessageUnpinAuditLogData : IAuditLogData + { + private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + RestUser user = null; + if (entry.TargetId.HasValue) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + user = RestUser.Create(discord, userInfo); + } + + return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); + } + + /// + /// Gets the ID of the messages that was unpinned. + /// + /// + /// A representing the snowflake identifier for the messages that was unpinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was unpinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was unpinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was unpinned if available. + /// + /// + /// A user object representing the user that created the unpinned message or . + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs index b13f4b8fd..3f391187d 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs @@ -5,10 +5,14 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data for a permissions overwrite creation. + /// public class OverwriteCreateAuditLogData : IAuditLogData { - private OverwriteCreateAuditLogData(Overwrite overwrite) + private OverwriteCreateAuditLogData(ulong channelId, Overwrite overwrite) { + ChannelId = channelId; Overwrite = overwrite; } @@ -27,9 +31,23 @@ namespace Discord.Rest var id = entry.Options.OverwriteTargetId.Value; var type = entry.Options.OverwriteType; - return new OverwriteCreateAuditLogData(new Overwrite(id, type, permissions)); + return new OverwriteCreateAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); } + /// + /// Gets the ID of the channel that the overwrite was created from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// created from. + /// + public ulong ChannelId { get; } + /// + /// Gets the permission overwrite object that was created. + /// + /// + /// An object representing the overwrite that was created. + /// public Overwrite Overwrite { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index 5d5177d9a..dc8948d37 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -1,20 +1,18 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; -using ChangeModel = Discord.API.AuditLogChange; -using OptionModel = Discord.API.AuditLogOptions; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to the deletion of a permission overwrite. + /// public class OverwriteDeleteAuditLogData : IAuditLogData { - private OverwriteDeleteAuditLogData(Overwrite deletedOverwrite) + private OverwriteDeleteAuditLogData(ulong channelId, Overwrite deletedOverwrite) { + ChannelId = channelId; Overwrite = deletedOverwrite; } @@ -23,18 +21,33 @@ namespace Discord.Rest var changes = entry.Changes; var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); - var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); - var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id"); var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); var deny = denyModel.OldValue.ToObject(discord.ApiClient.Serializer); - var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); - var id = idModel.OldValue.ToObject(discord.ApiClient.Serializer); var allow = allowModel.OldValue.ToObject(discord.ApiClient.Serializer); - return new OverwriteDeleteAuditLogData(new Overwrite(id, type, new OverwritePermissions(allow, deny))); + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); } + /// + /// Gets the ID of the channel that the overwrite was deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the permission overwrite object that was deleted. + /// + /// + /// An object representing the overwrite that was deleted. + /// public Overwrite Overwrite { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs index d05e1feff..c2b8d423e 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs @@ -5,10 +5,14 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to the update of a permission overwrite. + /// public class OverwriteUpdateAuditLogData : IAuditLogData { - private OverwriteUpdateAuditLogData(OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType) + private OverwriteUpdateAuditLogData(ulong channelId, OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType) { + ChannelId = channelId; OldPermissions = before; NewPermissions = after; OverwriteTargetId = targetId; @@ -25,20 +29,53 @@ namespace Discord.Rest var beforeAllow = allowModel?.OldValue?.ToObject(discord.ApiClient.Serializer); var afterAllow = allowModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var beforeDeny = denyModel?.OldValue?.ToObject(discord.ApiClient.Serializer); - var afterDeny = denyModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var afterDeny = denyModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var beforePermissions = new OverwritePermissions(beforeAllow ?? 0, beforeDeny ?? 0); var afterPermissions = new OverwritePermissions(afterAllow ?? 0, afterDeny ?? 0); var type = entry.Options.OverwriteType; - return new OverwriteUpdateAuditLogData(beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, type); + return new OverwriteUpdateAuditLogData(entry.TargetId.Value, beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, type); } + /// + /// Gets the ID of the channel that the overwrite was updated from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// updated from. + /// + public ulong ChannelId { get; } + /// + /// Gets the overwrite permissions before the changes. + /// + /// + /// An overwrite permissions object representing the overwrite permissions that the overwrite had before + /// the changes were made. + /// public OverwritePermissions OldPermissions { get; } + /// + /// Gets the overwrite permissions after the changes. + /// + /// + /// An overwrite permissions object representing the overwrite permissions that the overwrite had after the + /// changes. + /// public OverwritePermissions NewPermissions { get; } - + /// + /// Gets the ID of the overwrite that was updated. + /// + /// + /// A representing the snowflake identifier of the overwrite that was updated. + /// public ulong OverwriteTargetId { get; } + /// + /// Gets the target of the updated permission overwrite. + /// + /// + /// The target of the updated permission overwrite. + /// public PermissionTarget OverwriteType { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs index 0005e304d..c32d12b3f 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs @@ -3,6 +3,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a guild prune. + /// public class PruneAuditLogData : IAuditLogData { private PruneAuditLogData(int pruneDays, int membersRemoved) @@ -16,7 +19,22 @@ namespace Discord.Rest return new PruneAuditLogData(entry.Options.PruneDeleteMemberDays.Value, entry.Options.PruneMembersRemoved.Value); } + /// + /// Gets the threshold for a guild member to not be kicked. + /// + /// + /// An representing the amount of days that a member must have been seen in the server, + /// to avoid being kicked. (i.e. If a user has not been seen for more than , they will be + /// kicked from the server) + /// public int PruneDays { get; } + /// + /// Gets the number of members that were kicked during the purge. + /// + /// + /// An representing the number of members that were removed from this guild for having + /// not been seen within . + /// public int MembersRemoved { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs index 69e72fdb0..cee255fb0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a role creation. + /// public class RoleCreateAuditLogData : IAuditLogData { private RoleCreateAuditLogData(ulong id, RoleEditInfo props) @@ -41,7 +44,19 @@ namespace Discord.Rest new RoleEditInfo(color, mentionable, hoist, name, permissions)); } + /// + /// Gets the ID of the role that was created. + /// + /// + /// A representing the snowflake identifier to the role that was created. + /// public ulong RoleId { get; } + /// + /// Gets the role information that was created. + /// + /// + /// An information object representing the properties of the role that was created. + /// public RoleEditInfo Properties { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs index f812567cb..78b5efc87 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data relating to a role deletion. + /// public class RoleDeleteAuditLogData : IAuditLogData { private RoleDeleteAuditLogData(ulong id, RoleEditInfo props) @@ -41,7 +44,19 @@ namespace Discord.Rest new RoleEditInfo(color, mentionable, hoist, name, permissions)); } + /// + /// Gets the ID of the role that was deleted. + /// + /// + /// A representing the snowflake identifier to the role that was deleted. + /// public ulong RoleId { get; } + /// + /// Gets the role information that was deleted. + /// + /// + /// An information object representing the properties of the role that was deleted. + /// public RoleEditInfo Properties { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs index 186ea8d11..6f3d8d387 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for a role edit. + /// public struct RoleEditInfo { internal RoleEditInfo(Color? color, bool? mentionable, bool? hoist, string name, @@ -12,10 +15,45 @@ namespace Discord.Rest Permissions = permissions; } + /// + /// Gets the color of this role. + /// + /// + /// A color object representing the color assigned to this role; null if this role does not have a + /// color. + /// public Color? Color { get; } + /// + /// Gets a value that indicates whether this role is mentionable. + /// + /// + /// true if other members can mention this role in a text channel; otherwise false; + /// null if this is not mentioned in this entry. + /// public bool? Mentionable { get; } + /// + /// Gets a value that indicates whether this role is hoisted (i.e. its members will appear in a separate + /// section on the user list). + /// + /// + /// true if this role's members will appear in a separate section in the user list; otherwise + /// false; null if this is not mentioned in this entry. + /// public bool? Hoist { get; } + /// + /// Gets the name of this role. + /// + /// + /// A string containing the name of this role. + /// public string Name { get; } + /// + /// Gets the permissions assigned to this role. + /// + /// + /// A guild permissions object representing the permissions that have been assigned to this role; null + /// if no permissions have been assigned. + /// public GuildPermissions? Permissions { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs index 5cea865f1..094e1e0e0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a role update. + /// public class RoleUpdateAuditLogData : IAuditLogData { private RoleUpdateAuditLogData(ulong id, RoleEditInfo oldProps, RoleEditInfo newProps) @@ -33,7 +36,7 @@ namespace Discord.Rest string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); ulong? oldPermissionsRaw = permissionsModel?.OldValue?.ToObject(discord.ApiClient.Serializer), - newPermissionsRaw = permissionsModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + newPermissionsRaw = permissionsModel?.NewValue?.ToObject(discord.ApiClient.Serializer); Color? oldColor = null, newColor = null; @@ -55,8 +58,26 @@ namespace Discord.Rest return new RoleUpdateAuditLogData(entry.TargetId.Value, oldProps, newProps); } + /// + /// Gets the ID of the role that was changed. + /// + /// + /// A representing the snowflake identifier of the role that was changed. + /// public ulong RoleId { get; } + /// + /// Gets the role information before the changes. + /// + /// + /// A role information object containing the role information before the changes were made. + /// public RoleEditInfo Before { get; } + /// + /// Gets the role information after the changes. + /// + /// + /// A role information object containing the role information after the changes were made. + /// public RoleEditInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInfo.cs new file mode 100644 index 000000000..3700796e6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInfo.cs @@ -0,0 +1,30 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a stage. + /// + public class StageInfo + { + /// + /// Gets the topic of the stage channel. + /// + public string Topic { get; } + + /// + /// Gets the privacy level of the stage channel. + /// + public StagePrivacyLevel? PrivacyLevel { get; } + + /// + /// Gets the user who started the stage channel. + /// + public IUser User { get; } + + internal StageInfo(IUser user, StagePrivacyLevel? level, string topic) + { + Topic = topic; + PrivacyLevel = level; + User = user; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceCreateAuditLogData.cs new file mode 100644 index 000000000..eac99e87b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceCreateAuditLogData.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a stage going live. + /// + public class StageInstanceCreateAuditLogData : IAuditLogData + { + /// + /// Gets the topic of the stage channel. + /// + public string Topic { get; } + + /// + /// Gets the privacy level of the stage channel. + /// + public StagePrivacyLevel PrivacyLevel { get; } + + /// + /// Gets the user who started the stage channel. + /// + public IUser User { get; } + + /// + /// Gets the Id of the stage channel. + /// + public ulong StageChannelId { get; } + + internal StageInstanceCreateAuditLogData(string topic, StagePrivacyLevel privacyLevel, IUser user, ulong channelId) + { + Topic = topic; + PrivacyLevel = privacyLevel; + User = user; + StageChannelId = channelId; + } + + internal static StageInstanceCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var topic = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "topic").NewValue.ToObject(discord.ApiClient.Serializer); + var privacyLevel = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "privacy_level").NewValue.ToObject(discord.ApiClient.Serializer); + var user = log.Users.FirstOrDefault(x => x.Id == entry.UserId); + var channelId = entry.Options.ChannelId; + + return new StageInstanceCreateAuditLogData(topic, privacyLevel, RestUser.Create(discord, user), channelId ?? 0); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceDeleteAuditLogData.cs new file mode 100644 index 000000000..d22c56010 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceDeleteAuditLogData.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a stage instance deleted. + /// + public class StageInstanceDeleteAuditLogData + { + /// + /// Gets the topic of the stage channel. + /// + public string Topic { get; } + + /// + /// Gets the privacy level of the stage channel. + /// + public StagePrivacyLevel PrivacyLevel { get; } + + /// + /// Gets the user who started the stage channel. + /// + public IUser User { get; } + + /// + /// Gets the Id of the stage channel. + /// + public ulong StageChannelId { get; } + + internal StageInstanceDeleteAuditLogData(string topic, StagePrivacyLevel privacyLevel, IUser user, ulong channelId) + { + Topic = topic; + PrivacyLevel = privacyLevel; + User = user; + StageChannelId = channelId; + } + + internal static StageInstanceDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var topic = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "topic").OldValue.ToObject(discord.ApiClient.Serializer); + var privacyLevel = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "privacy_level").OldValue.ToObject(discord.ApiClient.Serializer); + var user = log.Users.FirstOrDefault(x => x.Id == entry.UserId); + var channelId = entry.Options.ChannelId; + + return new StageInstanceDeleteAuditLogData(topic, privacyLevel, RestUser.Create(discord, user), channelId ?? 0); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceUpdatedAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceUpdatedAuditLogData.cs new file mode 100644 index 000000000..93a0344b2 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/StageInstanceUpdatedAuditLogData.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a stage instance update. + /// + public class StageInstanceUpdatedAuditLogData + { + /// + /// Gets the Id of the stage channel. + /// + public ulong StageChannelId { get; } + + /// + /// Gets the stage information before the changes. + /// + public StageInfo Before { get; } + + /// + /// Gets the stage information after the changes. + /// + public StageInfo After { get; } + + internal StageInstanceUpdatedAuditLogData(ulong channelId, StageInfo before, StageInfo after) + { + StageChannelId = channelId; + Before = before; + After = after; + } + + internal static StageInstanceUpdatedAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var channelId = entry.Options.ChannelId.Value; + + var topic = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "topic"); + var privacy = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "privacy"); + + var user = RestUser.Create(discord, log.Users.FirstOrDefault(x => x.Id == entry.UserId)); + + var oldTopic = topic?.OldValue.ToObject(); + var newTopic = topic?.NewValue.ToObject(); + var oldPrivacy = privacy?.OldValue.ToObject(); + var newPrivacy = privacy?.NewValue.ToObject(); + + return new StageInstanceUpdatedAuditLogData(channelId, new StageInfo(user, oldPrivacy, oldTopic), new StageInfo(user, newPrivacy, newTopic)); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs index c94f18271..bc7e7fd4f 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -5,6 +5,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to an unban. + /// public class UnbanAuditLogData : IAuditLogData { private UnbanAuditLogData(IUser user) @@ -18,6 +21,12 @@ namespace Discord.Rest return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); } + /// + /// Gets the user that was unbanned. + /// + /// + /// A user object representing the user that was unbanned. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs index 06932bfc4..81d902fc0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs @@ -5,11 +5,15 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a webhook creation. + /// public class WebhookCreateAuditLogData : IAuditLogData { - private WebhookCreateAuditLogData(IWebhook webhook, WebhookType type, string name, ulong channelId) + private WebhookCreateAuditLogData(IWebhook webhook, ulong webhookId, WebhookType type, string name, ulong channelId) { Webhook = webhook; + WebhookId = webhookId; Name = name; Type = type; ChannelId = channelId; @@ -28,17 +32,53 @@ namespace Discord.Rest var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId); - var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo); + var webhook = webhookInfo == null ? null : RestWebhook.Create(discord, (IGuild)null, webhookInfo); - return new WebhookCreateAuditLogData(webhook, type, name, channelId); + return new WebhookCreateAuditLogData(webhook, entry.TargetId.Value, type, name, channelId); } - //Corresponds to the *current* data + // Doc Note: Corresponds to the *current* data + + /// + /// Gets the webhook that was created if it still exists. + /// + /// + /// A webhook object representing the webhook that was created if it still exists, otherwise returns null. + /// public IWebhook Webhook { get; } - //Corresponds to the *audit log* data + // Doc Note: Corresponds to the *audit log* data + + /// + /// Gets the webhook id. + /// + /// + /// The webhook identifier. + /// + public ulong WebhookId { get; } + + /// + /// Gets the type of webhook that was created. + /// + /// + /// The type of webhook that was created. + /// public WebhookType Type { get; } + + /// + /// Gets the name of the webhook. + /// + /// + /// A string containing the name of the webhook. + /// public string Name { get; } + /// + /// Gets the ID of the channel that the webhook could send to. + /// + /// + /// A representing the snowflake identifier of the channel that the webhook could send + /// to. + /// public ulong ChannelId { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs index 8fc4da578..308020c95 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a webhook deletion. + /// public class WebhookDeleteAuditLogData : IAuditLogData { private WebhookDeleteAuditLogData(ulong id, ulong channel, WebhookType type, string name, string avatar) @@ -37,10 +36,41 @@ namespace Discord.Rest return new WebhookDeleteAuditLogData(entry.TargetId.Value, channelId, type, name, avatarHash); } + /// + /// Gets the ID of the webhook that was deleted. + /// + /// + /// A representing the snowflake identifier of the webhook that was deleted. + /// public ulong WebhookId { get; } + /// + /// Gets the ID of the channel that the webhook could send to. + /// + /// + /// A representing the snowflake identifier of the channel that the webhook could send + /// to. + /// public ulong ChannelId { get; } + /// + /// Gets the type of the webhook that was deleted. + /// + /// + /// The type of webhook that was deleted. + /// public WebhookType Type { get; } + /// + /// Gets the name of the webhook that was deleted. + /// + /// + /// A string containing the name of the webhook that was deleted. + /// public string Name { get; } + /// + /// Gets the hash value of the webhook's avatar. + /// + /// + /// A string containing the hash of the webhook's avatar. + /// public string Avatar { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs index 26975cc7c..e60157b1a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for a webhook. + /// public struct WebhookInfo { internal WebhookInfo(string name, ulong? channelId, string avatar) @@ -9,8 +12,27 @@ namespace Discord.Rest Avatar = avatar; } + /// + /// Gets the name of this webhook. + /// + /// + /// A string containing the name of this webhook. + /// public string Name { get; } + /// + /// Gets the ID of the channel that this webhook sends to. + /// + /// + /// A representing the snowflake identifier of the channel that this webhook can send + /// to. + /// public ulong? ChannelId { get; } + /// + /// Gets the hash value of this webhook's avatar. + /// + /// + /// A string containing the hash of this webhook's avatar. + /// public string Avatar { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs index ad7db53e2..18fe865df 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Contains a piece of audit log data related to a webhook update. + /// public class WebhookUpdateAuditLogData : IAuditLogData { private WebhookUpdateAuditLogData(IWebhook webhook, WebhookInfo before, WebhookInfo after) @@ -42,11 +41,28 @@ namespace Discord.Rest return new WebhookUpdateAuditLogData(webhook, before, after); } - //Again, the *current* data + /// + /// Gets the webhook that was updated. + /// + /// + /// A webhook object representing the webhook that was updated. + /// public IWebhook Webhook { get; } - //And the *audit log* data + /// + /// Gets the webhook information before the changes. + /// + /// + /// A webhook information object representing the webhook before the changes were made. + /// public WebhookInfo Before { get; } + + /// + /// Gets the webhook information after the changes. + /// + /// + /// A webhook information object representing the webhook after the changes were made. + /// public WebhookInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs index d01e964ae..2176eab71 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs @@ -6,6 +6,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents a REST-based audit log entry. + /// public class RestAuditLogEntry : RestEntity, IAuditLogEntry { private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user) @@ -19,7 +22,7 @@ namespace Discord.Rest internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model) { - var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId); + var userInfo = model.UserId != null ? fullLog.Users.FirstOrDefault(x => x.Id == model.UserId) : null; IUser user = null; if (userInfo != null) user = RestUser.Create(discord, userInfo); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 74ce7870f..2956d6443 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -6,14 +6,13 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; -using UserModel = Discord.API.User; -using WebhookModel = Discord.API.Webhook; +using StageInstance = Discord.API.StageInstance; namespace Discord.Rest { internal static class ChannelHelper { - //General + #region General public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, RequestOptions options) { @@ -29,7 +28,16 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, - CategoryId = args.CategoryId + CategoryId = args.CategoryId, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -47,6 +55,15 @@ namespace Discord.Rest Topic = args.Topic, IsNsfw = args.IsNsfw, SlowModeInterval = args.SlowModeInterval, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -60,37 +77,117 @@ namespace Discord.Rest { Bitrate = args.Bitrate, Name = args.Name, + RTCRegion = args.RTCRegion, Position = args.Position, CategoryId = args.CategoryId, - UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() + UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create(), + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - //Invites + public static async Task ModifyAsync(IStageChannel channel, BaseDiscordClient client, + Action func, RequestOptions options = null) + { + var args = new StageInstanceProperties(); + func(args); + + var apiArgs = new ModifyStageInstanceParams() + { + PrivacyLevel = args.PrivacyLevel, + Topic = args.Topic + }; + + return await client.ApiClient.ModifyStageInstanceAsync(channel.Id, apiArgs, options); + } + #endregion + + #region Invites public static async Task> GetInvitesAsync(IGuildChannel channel, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id, options).ConfigureAwait(false); return models.Select(x => RestInviteMetadata.Create(client, null, channel, x)).ToImmutableArray(); } + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// public static async Task CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) { - var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique }; - if (maxAge.HasValue) - args.MaxAge = maxAge.Value; - else - args.MaxAge = 0; - if (maxUses.HasValue) - args.MaxUses = maxUses.Value; - else - args.MaxUses = 0; + var args = new API.Rest.CreateChannelInviteParams + { + IsTemporary = isTemporary, + IsUnique = isUnique, + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0 + }; + var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); + return RestInviteMetadata.Create(client, null, channel, model); + } + + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// + public static async Task CreateInviteToStreamAsync(IGuildChannel channel, BaseDiscordClient client, + int? maxAge, int? maxUses, bool isTemporary, bool isUnique, IUser user, + RequestOptions options) + { + var args = new API.Rest.CreateChannelInviteParams + { + IsTemporary = isTemporary, + IsUnique = isUnique, + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + TargetType = TargetUserType.Stream, + TargetUserId = user.Id + }; + var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); + return RestInviteMetadata.Create(client, null, channel, model); + } + + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// + public static async Task CreateInviteToApplicationAsync(IGuildChannel channel, BaseDiscordClient client, + int? maxAge, int? maxUses, bool isTemporary, bool isUnique, ulong applicationId, + RequestOptions options) + { + var args = new API.Rest.CreateChannelInviteParams + { + IsTemporary = isTemporary, + IsUnique = isUnique, + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + TargetType = TargetUserType.EmbeddedApplication, + TargetApplicationId = applicationId + }; var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); return RestInviteMetadata.Create(client, null, channel, model); } + #endregion - //Messages + #region Messages public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) { @@ -99,18 +196,25 @@ namespace Discord.Rest var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); if (model == null) return null; - var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + var author = MessageHelper.GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { - if (dir == Direction.Around) - throw new NotImplementedException(); //TODO: Impl - var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch) + { + int around = limit / 2; + if (fromMessageId.HasValue) + return GetMessagesAsync(channel, client, fromMessageId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself + .Concat(GetMessagesAsync(channel, client, fromMessageId, Direction.After, around, options)); + else //Shouldn't happen since there's no public overload for ulong? and Direction + return GetMessagesAsync(channel, client, null, Direction.Before, around + 1, options); + } + return new PagedAsyncEnumerable( DiscordConfig.MaxMessagesPerBatch, async (info, ct) => @@ -127,7 +231,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + var author = MessageHelper.GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -155,36 +259,151 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + var author = MessageHelper.GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); } + /// Message content is too long, length must be less or equal to . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, RequestOptions options) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, RequestOptions options, Embed[] embeds) { - var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); + } + + var args = new CreateMessageParams(text) + { + IsTTS = isTTS, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel(), + MessageReference = messageReference?.ToModel(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified + }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds).ConfigureAwait(false); } - public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + /// Message content is too long, length must be less or equal to . + public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional.Unspecified }; + return SendFileAsync(channel, client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + } + + /// Message content is too long, length must be less or equal to . + public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, RequestOptions options, Embed[] embeds) + => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + public static async Task SendFilesAsync(IMessageChannel channel, BaseDiscordClient client, + IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, RequestOptions options, Embed[] embeds) + { + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + foreach(var attachment in attachments) + { + Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); + } + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); + } + + var args = new UploadFileParams(attachments.ToArray()) { Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, MessageComponent = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } + public static async Task ModifyMessageAsync(IMessageChannel channel, ulong messageId, Action func, + BaseDiscordClient client, RequestOptions options) + { + var msgModel = await MessageHelper.ModifyAsync(channel.Id, messageId, client, func, options).ConfigureAwait(false); + return RestUserMessage.Create(client, channel, msgModel.Author.IsSpecified ? RestUser.Create(client, msgModel.Author.Value) : client.CurrentUser, msgModel); + } + public static Task DeleteMessageAsync(IMessageChannel channel, ulong messageId, BaseDiscordClient client, RequestOptions options) => MessageHelper.DeleteAsync(channel.Id, messageId, client, options); @@ -215,18 +434,19 @@ namespace Discord.Rest await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); } } + #endregion - //Permission Overwrites + #region Permission Overwrites public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, IUser user, OverwritePermissions perms, RequestOptions options) { - var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue); + var args = new ModifyChannelPermissionsParams((int)PermissionTarget.User, perms.AllowValue.ToString(), perms.DenyValue.ToString()); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args, options).ConfigureAwait(false); } public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, IRole role, OverwritePermissions perms, RequestOptions options) { - var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue); + var args = new ModifyChannelPermissionsParams((int)PermissionTarget.Role, perms.AllowValue.ToString(), perms.DenyValue.ToString()); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args, options).ConfigureAwait(false); } public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, @@ -239,8 +459,10 @@ namespace Discord.Rest { await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, role.Id, options).ConfigureAwait(false); } + #endregion - //Users + #region Users + /// Resolving permissions requires the parent guild to be downloaded. public static async Task GetUserAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { @@ -253,6 +475,7 @@ namespace Discord.Rest return user; } + /// Resolving permissions requires the parent guild to be downloaded. public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, ulong? fromUserId, int? limit, RequestOptions options) { @@ -283,8 +506,9 @@ namespace Discord.Rest count: limit ); } + #endregion - //Typing + #region Typing public static async Task TriggerTypingAsync(IMessageChannel channel, BaseDiscordClient client, RequestOptions options = null) { @@ -292,9 +516,10 @@ namespace Discord.Rest } public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) - => new TypingNotifier(client, channel, options); + => new TypingNotifier(channel, options); + #endregion - //Webhooks + #region Webhooks public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) { var args = new CreateWebhookParams { Name = name }; @@ -317,7 +542,9 @@ namespace Discord.Rest return models.Select(x => RestWebhook.Create(client, channel, x)) .ToImmutableArray(); } - // Categories + #endregion + + #region Categories public static async Task GetCategoryAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) { // if no category id specified, return null @@ -327,16 +554,26 @@ namespace Discord.Rest var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, model) as ICategoryChannel; } - - //Helpers - private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) + /// This channel does not have a parent channel. + public static async Task SyncPermissionsAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) { - IUser author = null; - if (guild != null) - author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; - if (author == null) - author = RestUser.Create(client, guild, model, webhookId); - return author; + var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false); + if (category == null) + throw new InvalidOperationException("This channel does not have a parent channel."); + + var apiArgs = new ModifyGuildChannelParams + { + Overwrites = category.PermissionOverwrites + .Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + }; + await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index e0095c7b1..159735798 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -4,25 +4,159 @@ using System.Threading.Tasks; namespace Discord.Rest { + /// + /// Represents a REST-based channel that can send and receive messages. + /// public interface IRestMessageChannel : IMessageChannel { - /// Sends a message to this message channel. - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a message to this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in + /// . Please visit + /// its documentation for more details on this method. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); - /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - - /// Gets a message from this message channel with the given id, or null if not found. + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// Task GetMessageAsync(ulong id, RequestOptions options = null); - /// Gets the last N messages from this message channel. + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a collection of pinned messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a collection of messages found in the pinned messages. + /// new Task> GetPinnedMessagesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs index a6939f81e..f387ac2d4 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.Rest { + /// + /// Represents a REST-based channel that is private to select recipients. + /// public interface IRestPrivateChannel : IPrivateChannel { + /// + /// Users that can access this channel. + /// new IReadOnlyCollection Recipients { get; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs index 321f1f1d2..9f944501b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based category channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestCategoryChannel : RestGuildChannel, ICategoryChannel { + #region RestCategoryChannel internal RestCategoryChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -23,17 +25,17 @@ namespace Discord.Rest } private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + #endregion - // IGuildChannel - Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => throw new NotSupportedException(); - Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => throw new NotSupportedException(); - - //IChannel + #region IChannel + /// + /// This method is not supported with category channels. IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotSupportedException(); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 5860d8283..83c6d8bfb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -6,52 +6,80 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a generic REST-based channel. + /// public class RestChannel : RestEntity, IChannel, IUpdateable { + #region RestChannel + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) { } + /// Unexpected channel type. internal static RestChannel Create(BaseDiscordClient discord, Model model) { - switch (model.Type) + return model.Type switch { - case ChannelType.Text: - case ChannelType.Voice: - return RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model); - case ChannelType.DM: - case ChannelType.Group: - return CreatePrivate(discord, model) as RestChannel; - case ChannelType.Category: - return RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model); - default: - return new RestChannel(discord, model.Id); - } + ChannelType.News or + ChannelType.Text or + ChannelType.Voice or + ChannelType.Stage or + ChannelType.NewsThread or + ChannelType.PrivateThread or + ChannelType.PublicThread + => RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), + ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, + ChannelType.Category => RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), + _ => new RestChannel(discord, model.Id), + }; } + internal static RestChannel Create(BaseDiscordClient discord, Model model, IGuild guild) + { + return model.Type switch + { + ChannelType.News or + ChannelType.Text or + ChannelType.Voice or + ChannelType.Stage or + ChannelType.NewsThread or + ChannelType.PrivateThread or + ChannelType.PublicThread + => RestGuildChannel.Create(discord, guild, model), + ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, + ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), + _ => new RestChannel(discord, model.Id), + }; + } + /// Unexpected channel type. internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) { - switch (model.Type) + return model.Type switch { - case ChannelType.DM: - return RestDMChannel.Create(discord, model); - case ChannelType.Group: - return RestGroupChannel.Create(discord, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } + ChannelType.DM => RestDMChannel.Create(discord, model), + ChannelType.Group => RestGroupChannel.Create(discord, model), + _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), + }; } internal virtual void Update(Model model) { } + /// public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); + #endregion - //IChannel + #region IChannel + /// string IChannel.Name => null; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 20a76aaf0..1b91c6e62 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -9,12 +9,26 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based direct-message channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel { - public RestUser CurrentUser { get; private set; } - public RestUser Recipient { get; private set; } - + #region RestDMChannel + /// + /// Gets the current logged-in user. + /// + public RestUser CurrentUser { get; } + + /// + /// Gets the recipient of the channel. + /// + public RestUser Recipient { get; } + + /// + /// Gets a collection that is the current logged-in user and the recipient. + /// public IReadOnlyCollection Users => ImmutableArray.Create(CurrentUser, Recipient); internal RestDMChannel(BaseDiscordClient discord, ulong id, ulong recipientId) @@ -34,14 +48,24 @@ namespace Discord.Rest Recipient.Update(model.Recipients.Value[0]); } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public Task CloseAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + + /// + /// Gets a user in this channel from the provided . + /// + /// The snowflake identifier of the user. + /// + /// A object that is a recipient of this channel; otherwise null. + /// public RestUser GetUser(ulong id) { if (id == Recipient.Id) @@ -52,49 +76,114 @@ namespace Discord.Rest return null; } + /// public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); - + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); + /// + /// Gets a string that represents the Username#Discriminator of the recipient. + /// + /// + /// A string that resolves to the Recipient of this channel. + /// public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + #endregion - //IDMChannel + #region IDMChannel + /// IUser IDMChannel.Recipient => Recipient; + #endregion - //IRestPrivateChannel + #region IRestPrivateChannel + /// IReadOnlyCollection IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + #endregion - //IPrivateChannel + #region IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + #endregion - //IMessageChannel + #region IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -102,6 +191,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -109,6 +199,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -116,6 +207,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -123,25 +215,36 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); - - //IChannel + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion + + #region IChannel + /// string IChannel.Name => $"@{Recipient}"; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index c6b5d6ad0..83ff3f558 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -10,12 +10,17 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based group-message channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel { + #region RestGroupChannel private string _iconId; private ImmutableDictionary _users; + /// public string Name { get; private set; } public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); @@ -49,12 +54,13 @@ namespace Discord.Rest users[models[i].Id] = RestGroupUser.Create(Discord, models[i]); _users = users.ToImmutable(); } - + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -65,46 +71,97 @@ namespace Discord.Rest return null; } + /// public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); - + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}, Group)"; + #endregion - //ISocketPrivateChannel + #region ISocketPrivateChannel IReadOnlyCollection IRestPrivateChannel.Recipients => Recipients; + #endregion - //IPrivateChannel + #region IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => Recipients; + #endregion - //IMessageChannel + #region IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -136,24 +193,31 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); - - //IAudioChannel + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion + + #region IAudioChannel + /// + /// Connecting to a group channel is not supported. Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); } + #endregion - //IChannel + #region IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 1dbcf7e9a..bc9d4110a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -7,15 +7,23 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a private REST-based group channel. + /// public class RestGuildChannel : RestChannel, IGuildChannel { + #region RestGuildChannel private ImmutableArray _overwrites; - public IReadOnlyCollection PermissionOverwrites => _overwrites; + /// + public virtual IReadOnlyCollection PermissionOverwrites => _overwrites; internal IGuild Guild { get; } + /// public string Name { get; private set; } + /// public int Position { get; private set; } + /// public ulong GuildId => Guild.Id; internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -25,44 +33,60 @@ namespace Discord.Rest } internal static RestGuildChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - switch (model.Type) + return model.Type switch { - case ChannelType.Text: - return RestTextChannel.Create(discord, guild, model); - case ChannelType.Voice: - return RestVoiceChannel.Create(discord, guild, model); - case ChannelType.Category: - return RestCategoryChannel.Create(discord, guild, model); - default: - return new RestGuildChannel(discord, guild, model.Id); - } + ChannelType.News => RestNewsChannel.Create(discord, guild, model), + ChannelType.Text => RestTextChannel.Create(discord, guild, model), + ChannelType.Voice => RestVoiceChannel.Create(discord, guild, model), + ChannelType.Stage => RestStageChannel.Create(discord, guild, model), + ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), + ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread => RestThreadChannel.Create(discord, guild, model), + _ => new RestGuildChannel(discord, guild, model.Id), + }; } internal override void Update(Model model) { Name = model.Name.Value; - Position = model.Position.Value; - var overwrites = model.PermissionOverwrites.Value; - var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); - for (int i = 0; i < overwrites.Length; i++) - newOverwrites.Add(overwrites[i].ToEntity()); - _overwrites = newOverwrites.ToImmutable(); + if (model.Position.IsSpecified) + { + Position = model.Position.Value; + } + + if (model.PermissionOverwrites.IsSpecified) + { + var overwrites = model.PermissionOverwrites.Value; + var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); + for (int i = 0; i < overwrites.Length; i++) + newOverwrites.Add(overwrites[i].ToEntity()); + _overwrites = newOverwrites.ToImmutable(); + } } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); - public OverwritePermissions? GetPermissionOverwrite(IUser user) + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) { for (int i = 0; i < _overwrites.Length; i++) { @@ -71,7 +95,15 @@ namespace Discord.Rest } return null; } - public OverwritePermissions? GetPermissionOverwrite(IRole role) + + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) { for (int i = 0; i < _overwrites.Length; i++) { @@ -80,17 +112,45 @@ namespace Discord.Rest } return null; } - public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) + + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) { - await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); } - public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { - await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); } - public async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); @@ -103,7 +163,15 @@ namespace Discord.Rest } } } - public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); @@ -117,14 +185,17 @@ namespace Discord.Rest } } - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - + /// + /// Gets the name of this channel. + /// + /// + /// A string that is the name of this channel. + /// public override string ToString() => Name; + #endregion - //IGuildChannel + #region IGuildChannel + /// IGuild IGuildChannel.Guild { get @@ -135,33 +206,40 @@ namespace Discord.Rest } } - async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => await GetInvitesAsync(options).ConfigureAwait(false); - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice + #endregion - //IChannel + #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden in Text/Voice + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs new file mode 100644 index 000000000..fad3358dc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based news channel in a guild that has the same properties as a . + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestNewsChannel : RestTextChannel, INewsChannel + { + internal RestNewsChannel(BaseDiscordClient discord, IGuild guild, ulong id) + :base(discord, guild, id) + { + } + internal new static RestNewsChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestNewsChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs new file mode 100644 index 000000000..c01df96fd --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs @@ -0,0 +1,150 @@ +using Discord.API; +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based stage channel in a guild. + /// + public class RestStageChannel : RestVoiceChannel, IStageChannel + { + /// + public string Topic { get; private set; } + + /// + public StagePrivacyLevel? PrivacyLevel { get; private set; } + + /// + public bool? IsDiscoverableDisabled { get; private set; } + + /// + public bool IsLive { get; private set; } + internal RestStageChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) { } + + internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestStageChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(StageInstance model, bool isLive = false) + { + IsLive = isLive; + if(isLive) + { + Topic = model.Topic; + PrivacyLevel = model.PrivacyLevel; + IsDiscoverableDisabled = model.DiscoverableDisabled; + } + else + { + Topic = null; + PrivacyLevel = null; + IsDiscoverableDisabled = null; + } + } + + /// + public async Task ModifyInstanceAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options); + + Update(model, true); + } + + /// + public async Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null) + { + var args = new CreateStageInstanceParams + { + ChannelId = Id, + PrivacyLevel = privacyLevel, + Topic = topic + }; + + var model = await Discord.ApiClient.CreateStageInstanceAsync(args, options); + + Update(model, true); + } + + /// + public async Task StopStageAsync(RequestOptions options = null) + { + await Discord.ApiClient.DeleteStageInstanceAsync(Id, options); + + Update(null); + } + + /// + public override async Task UpdateAsync(RequestOptions options = null) + { + await base.UpdateAsync(options); + + var model = await Discord.ApiClient.GetStageInstanceAsync(Id, options); + + Update(model, model != null); + } + + /// + public Task RequestToSpeakAsync(RequestOptions options = null) + { + var args = new ModifyVoiceStateParams + { + ChannelId = Id, + RequestToSpeakTimestamp = DateTimeOffset.UtcNow + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task BecomeSpeakerAsync(RequestOptions options = null) + { + var args = new ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = false + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task StopSpeakingAsync(RequestOptions options = null) + { + var args = new ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = true + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task MoveToSpeakerAsync(IGuildUser user, RequestOptions options = null) + { + var args = new ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = false + }; + + return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args); + } + + /// + public Task RemoveFromSpeakerAsync(IGuildUser user, RequestOptions options = null) + { + var args = new ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = true + }; + + return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 12437c969..57de0eb45 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -8,17 +8,24 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based channel in a guild that can send and receive messages. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel { + #region RestTextChannel + /// public string Topic { get; private set; } - public int SlowModeInterval { get; private set; } + /// + public virtual int SlowModeInterval { get; private set; } + /// public ulong? CategoryId { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); - - private bool _nsfw; - public bool IsNsfw => _nsfw; + /// + public bool IsNsfw { get; private set; } internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) @@ -30,82 +37,264 @@ namespace Discord.Rest entity.Update(model); return entity; } + /// internal override void Update(Model model) { base.Update(model); CategoryId = model.CategoryId; - Topic = model.Topic.Value; - SlowModeInterval = model.SlowMode.Value; - _nsfw = model.Nsfw.GetValueOrDefault(); + Topic = model.Topic.GetValueOrDefault(); + if (model.SlowMode.IsSpecified) + SlowModeInterval = model.SlowMode.Value; + IsNsfw = model.Nsfw.GetValueOrDefault(); } - public async Task ModifyAsync(Action func, RequestOptions options = null) + /// + public virtual async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user. + /// The options to be used when sending the request. + /// + /// Resolving permissions requires the parent guild to be downloaded. + /// + /// + /// A task representing the asynchronous get operation. The task result contains a guild user object that + /// represents the user; null if none is found. + /// public Task GetUserAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetUserAsync(this, Guild, Discord, id, options); + + /// + /// Gets a collection of users that are able to view the channel. + /// + /// The options to be used when sending the request. + /// + /// Resolving permissions requires the parent guild to be downloaded. + /// + /// + /// A paged collection containing a collection of guild users that can access this channel. Flattening the + /// paginated response into a collection of users with + /// is required if you wish to access the users. + /// public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); + /// public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); - public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// + public virtual Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); - public Task GetWebhookAsync(ulong id, RequestOptions options = null) + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + public virtual Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); - public Task> GetWebhooksAsync(RequestOptions options = null) + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// + public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); - public Task GetCategoryAsync(RequestOptions options = null) + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public virtual Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion + + #region Invites + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - //ITextChannel + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + public async Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + { + var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); + return RestThreadChannel.Create(Discord, Guild, model); + } + #endregion + + #region ITextChannel + /// async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); - //IMessageChannel + async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, bool? invitable, int? slowmode, RequestOptions options) + => await CreateThreadAsync(name, type, autoArchiveDuration, message, invitable, slowmode, options); + #endregion + + #region IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -113,6 +302,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -121,6 +311,7 @@ namespace Discord.Rest return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -128,6 +319,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -135,20 +327,33 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion - //IGuildChannel + #region IGuildChannel + /// async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -156,6 +361,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -163,8 +369,10 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + #endregion - //IChannel + #region IChannel + /// async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -172,6 +380,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -179,13 +388,16 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + #endregion - // INestedChannel + #region INestedChannel + /// async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) { if (CategoryId.HasValue && mode == CacheMode.AllowDownload) return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; return null; } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs new file mode 100644 index 000000000..63071b9a5 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a thread channel received over REST. + /// + public class RestThreadChannel : RestTextChannel, IThreadChannel + { + public ThreadType Type { get; private set; } + /// + public bool HasJoined { get; private set; } + + /// + public bool IsArchived { get; private set; } + + /// + public ThreadArchiveDuration AutoArchiveDuration { get; private set; } + + /// + public DateTimeOffset ArchiveTimestamp { get; private set; } + + /// + public bool IsLocked { get; private set; } + + /// + public int MemberCount { get; private set; } + + /// + public int MessageCount { get; private set; } + + /// + /// Gets the parent text channel id. + /// + public ulong ParentChannelId { get; private set; } + + internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) { } + + internal new static RestThreadChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestThreadChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + base.Update(model); + + HasJoined = model.ThreadMember.IsSpecified; + + if (model.ThreadMetadata.IsSpecified) + { + IsArchived = model.ThreadMetadata.Value.Archived; + AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; + ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; + IsLocked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); + } + + MemberCount = model.MemberCount.GetValueOrDefault(0); + MessageCount = model.MessageCount.GetValueOrDefault(0); + Type = (ThreadType)model.Type; + ParentChannelId = model.CategoryId.Value; + } + + /// + /// Gets a user within this thread. + /// + /// The id of the user to fetch. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous get operation. The task returns a + /// if found, otherwise . + /// + public new Task GetUserAsync(ulong userId, RequestOptions options = null) + => ThreadHelper.GetUserAsync(userId, this, Discord, options); + + /// + /// Gets a collection of users within this thread. + /// + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous get operation. The task returns a + /// of 's. + /// + public new async Task> GetUsersAsync(RequestOptions options = null) + => (await ThreadHelper.GetUsersAsync(this, Discord, options).ConfigureAwait(false)).ToImmutableArray(); + + /// + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ThreadHelper.ModifyAsync(this, Discord, func, options); + Update(model); + } + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task GetCategoryAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetInvitesAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IRole role) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IUser user) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetWebhooksAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override IReadOnlyCollection PermissionOverwrites + => throw new NotSupportedException("This method is not supported in threads."); + + /// + public Task JoinAsync(RequestOptions options = null) + => Discord.ApiClient.JoinThreadAsync(Id, options); + + /// + public Task LeaveAsync(RequestOptions options = null) + => Discord.ApiClient.LeaveThreadAsync(Id, options); + + /// + public Task AddUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.AddThreadMemberAsync(Id, user.Id, options); + + /// + public Task RemoveUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.RemoveThreadMemberAsync(Id, user.Id, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 13f3b5efa..239c00467 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -8,13 +8,23 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based voice channel in a guild. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel { + #region RestVoiceChannel + /// public int Bitrate { get; private set; } + /// public int? UserLimit { get; private set; } + /// public ulong? CategoryId { get; private set; } + /// + public string Mention => MentionUtils.MentionChannel(Id); + internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -25,41 +35,85 @@ namespace Discord.Rest entity.Update(model); return entity; } + /// internal override void Update(Model model) { base.Update(model); CategoryId = model.CategoryId; - Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + + if(model.Bitrate.IsSpecified) + Bitrate = model.Bitrate.Value; + + if(model.UserLimit.IsSpecified) + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// public Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion + + #region Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// + public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + #endregion - //IAudioChannel + #region IAudioChannel + /// + /// Connecting to a REST-based channel is not supported. Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); } + #endregion - //IGuildChannel + #region IGuildChannel + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); + #endregion - // INestedChannel + #region INestedChannel + /// async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) { if (CategoryId.HasValue && mode == CacheMode.AllowDownload) return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; return null; } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs deleted file mode 100644 index e8b939e65..000000000 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class RestVirtualMessageChannel : RestEntity, IMessageChannel - { - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Mention => MentionUtils.MentionChannel(Id); - - internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) - : base(discord, id) - { - } - internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulong id) - { - return new RestVirtualMessageChannel(discord, id); - } - - public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, options); - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); - public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); - - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); - - public Task TriggerTypingAsync(RequestOptions options = null) - => ChannelHelper.TriggerTypingAsync(this, Discord, options); - public IDisposable EnterTypingState(RequestOptions options = null) - => ChannelHelper.EnterTypingState(this, Discord, options); - - private string DebuggerDisplay => $"({Id}, Text)"; - - //IMessageChannel - async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options); - else - return null; - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessageId, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessage, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options); - - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); - - //IChannel - string IChannel.Name { get { throw new NotSupportedException(); } } - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { - throw new NotSupportedException(); - } - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs new file mode 100644 index 000000000..917410f98 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -0,0 +1,74 @@ +using Discord.API.Rest; +using System; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + internal static class ThreadHelper + { + public static async Task CreateThreadAsync(BaseDiscordClient client, ITextChannel channel, string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + { + var features = channel.Guild.Features; + if (autoArchiveDuration == ThreadArchiveDuration.OneWeek && !features.HasFeature(GuildFeature.SevenDayThreadArchive)) + throw new ArgumentException($"The guild {channel.Guild.Name} does not have the SEVEN_DAY_THREAD_ARCHIVE feature!", nameof(autoArchiveDuration)); + + if (autoArchiveDuration == ThreadArchiveDuration.ThreeDays && !features.HasFeature(GuildFeature.ThreeDayThreadArchive)) + throw new ArgumentException($"The guild {channel.Guild.Name} does not have the THREE_DAY_THREAD_ARCHIVE feature!", nameof(autoArchiveDuration)); + + if (type == ThreadType.PrivateThread && !features.HasFeature(GuildFeature.PrivateThreads)) + throw new ArgumentException($"The guild {channel.Guild.Name} does not have the PRIVATE_THREADS feature!", nameof(type)); + + var args = new StartThreadParams + { + Name = name, + Duration = autoArchiveDuration, + Type = type, + Invitable = invitable.HasValue ? invitable.Value : Optional.Unspecified, + Ratelimit = slowmode.HasValue ? slowmode.Value : Optional.Unspecified, + }; + + Model model; + + if (message != null) + model = await client.ApiClient.StartThreadAsync(channel.Id, message.Id, args, options).ConfigureAwait(false); + else + model = await client.ApiClient.StartThreadAsync(channel.Id, args, options).ConfigureAwait(false); + + return model; + } + + public static async Task ModifyAsync(IThreadChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new TextChannelProperties(); + func(args); + var apiArgs = new ModifyThreadParams + { + Name = args.Name, + Archived = args.Archived, + AutoArchiveDuration = args.AutoArchiveDuration, + Locked = args.Locked, + Slowmode = args.SlowModeInterval + }; + return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) + { + var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); + + return users.Select(x => RestThreadUser.Create(client, channel.Guild, x, channel)).ToArray(); + } + + public static async Task GetUserAsync(ulong userId, IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) + { + var model = await client.ApiClient.GetThreadMemberAsync(channel.Id, userId, options).ConfigureAwait(false); + + return RestThreadUser.Create(client, channel.Guild, model, channel); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index bde36922a..2cdbbb7b5 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -4,20 +4,22 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using EmbedModel = Discord.API.GuildEmbed; +using WidgetModel = Discord.API.GuildWidget; using Model = Discord.API.Guild; using RoleModel = Discord.API.Role; using ImageModel = Discord.API.Image; +using System.IO; namespace Discord.Rest { internal static class GuildHelper { - //General + #region General + /// is null. public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { - if (func == null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new ArgumentNullException(nameof(func)); var args = new GuildProperties(); func(args); @@ -31,8 +33,11 @@ namespace Discord.Rest Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), - Username = args.Username, - VerificationLevel = args.VerificationLevel + Banner = args.Banner.IsSpecified ? args.Banner.Value?.ToModel() : Optional.Create(), + VerificationLevel = args.VerificationLevel, + ExplicitContentFilter = args.ExplicitContentFilter, + SystemChannelFlags = args.SystemChannelFlags, + IsBoostProgressBarEnabled = args.IsBoostProgressBarEnabled }; if (args.AfkChannel.IsSpecified) @@ -55,21 +60,37 @@ namespace Discord.Rest else if (args.RegionId.IsSpecified) apiArgs.RegionId = args.RegionId.Value; + if (!apiArgs.Banner.IsSpecified && guild.BannerId != null) + apiArgs.Banner = new ImageModel(guild.BannerId); if (!apiArgs.Splash.IsSpecified && guild.SplashId != null) apiArgs.Splash = new ImageModel(guild.SplashId); if (!apiArgs.Icon.IsSpecified && guild.IconId != null) apiArgs.Icon = new ImageModel(guild.IconId); + if (args.ExplicitContentFilter.IsSpecified) + apiArgs.ExplicitContentFilter = args.ExplicitContentFilter.Value; + + if (args.SystemChannelFlags.IsSpecified) + apiArgs.SystemChannelFlags = args.SystemChannelFlags.Value; + + // PreferredLocale takes precedence over PreferredCulture + if (args.PreferredLocale.IsSpecified) + apiArgs.PreferredLocale = args.PreferredLocale.Value; + else if (args.PreferredCulture.IsSpecified) + apiArgs.PreferredLocale = args.PreferredCulture.Value.Name; + return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, - Action func, RequestOptions options) + /// is null. + public static async Task ModifyWidgetAsync(IGuild guild, BaseDiscordClient client, + Action func, RequestOptions options) { - if (func == null) throw new NullReferenceException(nameof(func)); + if (func == null) + throw new ArgumentNullException(nameof(func)); - var args = new GuildEmbedProperties(); + var args = new GuildWidgetProperties(); func(args); - var apiArgs = new API.Rest.ModifyGuildEmbedParams + var apiArgs = new API.Rest.ModifyGuildWidgetParams { Enabled = args.Enabled }; @@ -79,7 +100,7 @@ namespace Discord.Rest else if (args.ChannelId.IsSpecified) apiArgs.ChannelId = args.ChannelId.Value; - return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + return await client.ApiClient.ModifyGuildWidgetAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } public static async Task ReorderChannelsAsync(IGuild guild, BaseDiscordClient client, IEnumerable args, RequestOptions options) @@ -103,8 +124,9 @@ namespace Discord.Rest { await client.ApiClient.DeleteGuildAsync(guild.Id, options).ConfigureAwait(false); } + #endregion - //Bans + #region Bans public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { @@ -114,7 +136,7 @@ namespace Discord.Rest public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); - return RestBan.Create(client, model); + return model == null ? null : RestBan.Create(client, model); } public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, @@ -128,8 +150,9 @@ namespace Discord.Rest { await client.ApiClient.RemoveGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); } + #endregion - //Channels + #region Channels public static async Task GetChannelAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { @@ -144,6 +167,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); } + /// is null. public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options, Action func = null) { @@ -157,10 +181,22 @@ namespace Discord.Rest CategoryId = props.CategoryId, Topic = props.Topic, IsNsfw = props.IsNsfw, + Position = props.Position, + SlowModeInterval = props.SlowModeInterval, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); } + /// is null. public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options, Action func = null) { @@ -173,22 +209,87 @@ namespace Discord.Rest { CategoryId = props.CategoryId, Bitrate = props.Bitrate, - UserLimit = props.UserLimit + UserLimit = props.UserLimit, + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } + public static async Task CreateStageChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); + + var props = new VoiceChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Stage) + { + CategoryId = props.CategoryId, + Bitrate = props.Bitrate, + UserLimit = props.UserLimit, + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + }; + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestStageChannel.Create(client, guild, model); + } + /// is null. public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, - string name, RequestOptions options) + string name, RequestOptions options, Action func = null) { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); - var args = new CreateGuildChannelParams(name, ChannelType.Category); + var props = new GuildChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Category) + { + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + }; + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, guild, model); } + #endregion + + #region Voice Regions + public static async Task> GetVoiceRegionsAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildVoiceRegionsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + } + #endregion - //Integrations + #region Integrations public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { @@ -202,8 +303,24 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildIntegrationAsync(guild.Id, args, options).ConfigureAwait(false); return RestGuildIntegration.Create(client, guild, model); } + #endregion + + #region Interactions + public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options); + return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray(); + } + public static async Task GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, + RequestOptions options) + { + var model = await client.ApiClient.GetGuildApplicationCommandAsync(guild.Id, id, options); + return RestGuildCommand.Create(client, model, guild.Id); + } + #endregion - //Invites + #region Invites public static async Task> GetInvitesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { @@ -213,31 +330,93 @@ namespace Discord.Rest public static async Task GetVanityInviteAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { - var model = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false); - return RestInviteMetadata.Create(client, guild, null, model); + var vanityModel = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false); + if (vanityModel == null) throw new InvalidOperationException("This guild does not have a vanity URL."); + var inviteModel = await client.ApiClient.GetInviteAsync(vanityModel.Code, options).ConfigureAwait(false); + inviteModel.Uses = vanityModel.Uses; + return RestInviteMetadata.Create(client, guild, null, inviteModel); } + #endregion - //Roles + #region Roles + /// is null. public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, - string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); - var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); - var role = RestRole.Create(client, guild, model); + var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams + { + Color = color?.RawValue ?? Optional.Create(), + Hoist = isHoisted, + Mentionable = isMentionable, + Name = name, + Permissions = permissions?.RawValue.ToString() ?? Optional.Create() + }; + + var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false); + + return RestRole.Create(client, guild, model); + } + #endregion + + #region Users + public static async Task AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); + + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams + { + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; + + var model = await client.ApiClient.AddGuildMemberAsync(guild.Id, userId, apiArgs, options); + + return model is null ? null : RestGuildUser.Create(client, guild, model); + } + + public static async Task AddGuildUserAsync(ulong guildId, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); - await role.ModifyAsync(x => + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams { - x.Name = name; - x.Permissions = (permissions ?? role.Permissions); - x.Color = (color ?? Color.Default); - x.Hoist = isHoisted; - }, options).ConfigureAwait(false); + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; - return role; + await client.ApiClient.AddGuildMemberAsync(guildId, userId, apiArgs, options); } - //Users public static async Task GetUserAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { @@ -255,7 +434,7 @@ namespace Discord.Rest ulong? fromUserId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxMessagesPerBatch, + DiscordConfig.MaxUsersPerBatch, async (info, ct) => { var args = new GetGuildMembersParams @@ -264,12 +443,12 @@ namespace Discord.Rest }; if (info.Position != null) args.AfterUserId = info.Position.Value; - var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options); + var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) return false; info.Position = lastPage.Max(x => x.Id); return true; @@ -279,9 +458,9 @@ namespace Discord.Rest ); } public static async Task PruneUsersAsync(IGuild guild, BaseDiscordClient client, - int days, bool simulate, RequestOptions options) + int days, bool simulate, RequestOptions options, IEnumerable includeRoleIds) { - var args = new GuildPruneParams(days); + var args = new GuildPruneParams(days, includeRoleIds?.ToArray()); GetGuildPruneCountResponse model; if (simulate) model = await client.ApiClient.GetGuildPruneCountAsync(guild.Id, args, options).ConfigureAwait(false); @@ -289,10 +468,22 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); return model.Pruned; } + public static async Task> SearchUsersAsync(IGuild guild, BaseDiscordClient client, + string query, int? limit, RequestOptions options) + { + var apiArgs = new SearchGuildMembersParams + { + Query = query, + Limit = limit ?? Optional.Create() + }; + var models = await client.ApiClient.SearchGuildMembersAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); + } + #endregion - // Audit logs + #region Audit logs public static IAsyncEnumerable> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, - ulong? from, int? limit, RequestOptions options) + ulong? from, int? limit, RequestOptions options, ulong? userId = null, ActionType? actionType = null) { return new PagedAsyncEnumerable( DiscordConfig.MaxAuditLogEntriesPerBatch, @@ -304,6 +495,10 @@ namespace Discord.Rest }; if (info.Position != null) args.BeforeEntryId = info.Position.Value; + if (userId.HasValue) + args.UserId = userId.Value; + if (actionType.HasValue) + args.ActionType = (int)actionType.Value; var model = await client.ApiClient.GetAuditLogsAsync(guild.Id, args, options); return model.Entries.Select((x) => RestAuditLogEntry.Create(client, model, x)).ToImmutableArray(); }, @@ -318,8 +513,9 @@ namespace Discord.Rest count: limit ); } + #endregion - //Webhooks + #region Webhooks public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); @@ -332,14 +528,20 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildWebhooksAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestWebhook.Create(client, guild, x)).ToImmutableArray(); } + #endregion - //Emotes + #region Emotes + public static async Task> GetEmotesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildEmotesAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => x.ToEntity()).ToImmutableArray(); + } public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { - var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options); + var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false); return emote.ToEntity(); } - public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, + public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, RequestOptions options) { var apiargs = new CreateGuildEmoteParams @@ -348,12 +550,13 @@ namespace Discord.Rest Image = image.ToModel() }; if (roles.IsSpecified) - apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray(); + apiargs.RoleIds = roles.Value?.Select(xr => xr.Id).ToArray(); - var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options); + var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } - public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, + /// is null. + public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, RequestOptions options) { if (func == null) throw new ArgumentNullException(paramName: nameof(func)); @@ -366,12 +569,315 @@ namespace Discord.Rest Name = props.Name }; if (props.Roles.IsSpecified) - apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray(); + apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id).ToArray(); - var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options); + var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } - public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); + + public static async Task CreateStickerAsync(BaseDiscordClient client, IGuild guild, string name, string description, IEnumerable tags, + Image image, RequestOptions options = null) + { + Preconditions.NotNull(name, nameof(name)); + Preconditions.NotNull(description, nameof(description)); + + Preconditions.AtLeast(name.Length, 2, nameof(name)); + Preconditions.AtLeast(description.Length, 2, nameof(description)); + + Preconditions.AtMost(name.Length, 30, nameof(name)); + Preconditions.AtMost(description.Length, 100, nameof(name)); + + var apiArgs = new CreateStickerParams() + { + Name = name, + Description = description, + File = image.Stream, + Tags = string.Join(", ", tags) + }; + + return await client.ApiClient.CreateGuildStickerAsync(apiArgs, guild.Id, options).ConfigureAwait(false); + } + + public static async Task CreateStickerAsync(BaseDiscordClient client, IGuild guild, string name, string description, IEnumerable tags, + Stream file, string filename, RequestOptions options = null) + { + Preconditions.NotNull(name, nameof(name)); + Preconditions.NotNull(description, nameof(description)); + Preconditions.NotNull(file, nameof(file)); + Preconditions.NotNull(filename, nameof(filename)); + + Preconditions.AtLeast(name.Length, 2, nameof(name)); + Preconditions.AtLeast(description.Length, 2, nameof(description)); + + Preconditions.AtMost(name.Length, 30, nameof(name)); + Preconditions.AtMost(description.Length, 100, nameof(name)); + + var apiArgs = new CreateStickerParams() + { + Name = name, + Description = description, + File = file, + Tags = string.Join(", ", tags), + FileName = filename + }; + + return await client.ApiClient.CreateGuildStickerAsync(apiArgs, guild.Id, options).ConfigureAwait(false); + } + + public static async Task ModifyStickerAsync(BaseDiscordClient client, ulong guildId, ISticker sticker, Action func, + RequestOptions options = null) + { + if (func == null) + throw new ArgumentNullException(paramName: nameof(func)); + + var props = new StickerProperties(); + func(props); + + var apiArgs = new ModifyStickerParams() + { + Description = props.Description, + Name = props.Name, + Tags = props.Tags.IsSpecified ? + string.Join(", ", props.Tags.Value) : + Optional.Unspecified + }; + + return await client.ApiClient.ModifyStickerAsync(apiArgs, guildId, sticker.Id, options).ConfigureAwait(false); + } + + public static async Task DeleteStickerAsync(BaseDiscordClient client, ulong guildId, ISticker sticker, RequestOptions options = null) + => await client.ApiClient.DeleteStickerAsync(guildId, sticker.Id, options).ConfigureAwait(false); + #endregion + + #region Events + + public static async Task> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, int limit = 100, RequestOptions options = null) + { + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, limit, options).ConfigureAwait(false); + + return models.Select(x => RestUser.Create(client, guildEvent.Guild, x)).ToImmutableArray(); + } + + public static IAsyncEnumerable> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, + ulong? fromUserId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxGuildEventUsersPerBatch, + async (info, ct) => + { + var args = new GetEventUsersParams + { + Limit = info.PageSize, + RelativeDirection = Direction.After, + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, args, options).ConfigureAwait(false); + return models + .Select(x => RestUser.Create(client, guildEvent.Guild, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxGuildEventUsersPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromUserId, + count: limit + ); + } + + public static IAsyncEnumerable> GetEventUsersAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, + ulong? fromUserId, Direction dir, int limit, RequestOptions options = null) + { + if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch) + { + int around = limit / 2; + if (fromUserId.HasValue) + return GetEventUsersAsync(client, guildEvent, fromUserId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself + .Concat(GetEventUsersAsync(client, guildEvent, fromUserId, Direction.After, around, options)); + else //Shouldn't happen since there's no public overload for ulong? and Direction + return GetEventUsersAsync(client, guildEvent, null, Direction.Before, around + 1, options); + } + + return new PagedAsyncEnumerable( + DiscordConfig.MaxGuildEventUsersPerBatch, + async (info, ct) => + { + var args = new GetEventUsersParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + + var models = await client.ApiClient.GetGuildScheduledEventUsersAsync(guildEvent.Id, guildEvent.Guild.Id, args, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + builder.Add(RestUser.Create(client, guildEvent.Guild, model)); + } + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxGuildEventUsersPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.Id); + else + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromUserId, + count: limit + ); + } + + public static async Task ModifyGuildEventAsync(BaseDiscordClient client, Action func, + IGuildScheduledEvent guildEvent, RequestOptions options = null) + { + var args = new GuildScheduledEventsProperties(); + + func(args); + + if (args.Status.IsSpecified) + { + switch (args.Status.Value) + { + case GuildScheduledEventStatus.Active when guildEvent.Status != GuildScheduledEventStatus.Scheduled: + case GuildScheduledEventStatus.Completed when guildEvent.Status != GuildScheduledEventStatus.Active: + case GuildScheduledEventStatus.Cancelled when guildEvent.Status != GuildScheduledEventStatus.Scheduled: + throw new ArgumentException($"Cannot set event to {args.Status.Value} when events status is {guildEvent.Status}"); + } + } + + if (args.Type.IsSpecified) + { + // taken from https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event + switch (args.Type.Value) + { + case GuildScheduledEventType.External: + if (!args.Location.IsSpecified) + throw new ArgumentException("Location must be specified for external events."); + if (!args.EndTime.IsSpecified) + throw new ArgumentException("End time must be specified for external events."); + if (!args.ChannelId.IsSpecified) + throw new ArgumentException("Channel id must be set to null!"); + if (args.ChannelId.Value != null) + throw new ArgumentException("Channel id must be set to null!"); + break; + } + } + + var apiArgs = new ModifyGuildScheduledEventParams() + { + ChannelId = args.ChannelId, + Description = args.Description, + EndTime = args.EndTime, + Name = args.Name, + PrivacyLevel = args.PrivacyLevel, + StartTime = args.StartTime, + Status = args.Status, + Type = args.Type + }; + + if(args.Location.IsSpecified) + { + apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() + { + Location = args.Location, + }; + } + + return await client.ApiClient.ModifyGuildScheduledEventAsync(apiArgs, guildEvent.Id, guildEvent.Guild.Id, options).ConfigureAwait(false); + } + + public static async Task GetGuildEventAsync(BaseDiscordClient client, ulong id, IGuild guild, RequestOptions options = null) + { + var model = await client.ApiClient.GetGuildScheduledEventAsync(id, guild.Id, options).ConfigureAwait(false); + + if (model == null) + return null; + + return RestGuildEvent.Create(client, guild, model); + } + + public static async Task> GetGuildEventsAsync(BaseDiscordClient client, IGuild guild, RequestOptions options = null) + { + var models = await client.ApiClient.ListGuildScheduledEventsAsync(guild.Id, options).ConfigureAwait(false); + + return models.Select(x => RestGuildEvent.Create(client, guild, x)).ToImmutableArray(); + } + + public static async Task CreateGuildEventAsync(BaseDiscordClient client, IGuild guild, + string name, + GuildScheduledEventPrivacyLevel privacyLevel, + DateTimeOffset startTime, + GuildScheduledEventType type, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + { + if(location != null) + { + Preconditions.AtMost(location.Length, 100, nameof(location)); + } + + switch (type) + { + case GuildScheduledEventType.Stage or GuildScheduledEventType.Voice when channelId == null: + throw new ArgumentException($"{nameof(channelId)} must not be null when type is {type}", nameof(channelId)); + case GuildScheduledEventType.External when channelId != null: + throw new ArgumentException($"{nameof(channelId)} must be null when using external event type", nameof(channelId)); + case GuildScheduledEventType.External when location == null: + throw new ArgumentException($"{nameof(location)} must not be null when using external event type", nameof(location)); + case GuildScheduledEventType.External when endTime == null: + throw new ArgumentException($"{nameof(endTime)} must not be null when using external event type", nameof(endTime)); + } + + if (startTime <= DateTimeOffset.Now) + throw new ArgumentOutOfRangeException(nameof(startTime), "The start time for an event cannot be in the past"); + + if (endTime != null && endTime <= startTime) + throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); + + var apiArgs = new CreateGuildScheduledEventParams() + { + ChannelId = channelId ?? Optional.Unspecified, + Description = description ?? Optional.Unspecified, + EndTime = endTime ?? Optional.Unspecified, + Name = name, + PrivacyLevel = privacyLevel, + StartTime = startTime, + Type = type + }; + + if(location != null) + { + apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() + { + Location = location + }; + } + + var model = await client.ApiClient.CreateGuildScheduledEventAsync(apiArgs, guild.Id, options).ConfigureAwait(false); + + return RestGuildEvent.Create(client, guild, client.CurrentUser, model); + } + + public static async Task DeleteEventAsync(BaseDiscordClient client, IGuildScheduledEvent guildEvent, RequestOptions options = null) + { + await client.ApiClient.DeleteGuildScheduledEventAsync(guildEvent.Id, guildEvent.Guild.Id, options).ConfigureAwait(false); + } + + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs index 104bec903..d77d3b626 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -1,12 +1,23 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Ban; namespace Discord.Rest { + /// + /// Represents a REST-based ban object. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestBan : IBan { + #region RestBan + /// + /// Gets the banned user. + /// + /// + /// A generic object that was banned. + /// public RestUser User { get; } + /// public string Reason { get; } internal RestBan(RestUser user, string reason) @@ -19,10 +30,19 @@ namespace Discord.Rest return new RestBan(RestUser.Create(client, model.User), model.Reason); } + /// + /// Gets the name of the banned user. + /// + /// + /// A string containing the name of the user that was banned. + /// public override string ToString() => User.ToString(); private string DebuggerDisplay => $"{User}: {Reason}"; +#endregion - //IBan + #region IBan + /// IUser IBan.User => User; + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index e6819e8a4..daecb1d8c 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -3,47 +3,135 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading.Tasks; -using EmbedModel = Discord.API.GuildEmbed; +using WidgetModel = Discord.API.GuildWidget; using Model = Discord.API.Guild; +using System.IO; namespace Discord.Rest { + /// + /// Represents a REST-based guild/server. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGuild : RestEntity, IGuild, IUpdateable { + #region RestGuild private ImmutableDictionary _roles; private ImmutableArray _emotes; - private ImmutableArray _features; + private ImmutableArray _stickers; + /// public string Name { get; private set; } + /// public int AFKTimeout { get; private set; } - public bool IsEmbeddable { get; private set; } + /// + public bool IsWidgetEnabled { get; private set; } + /// public VerificationLevel VerificationLevel { get; private set; } + /// public MfaLevel MfaLevel { get; private set; } + /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + /// + public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } + /// public ulong? AFKChannelId { get; private set; } - public ulong? EmbedChannelId { get; private set; } + /// + public ulong? WidgetChannelId { get; private set; } + /// public ulong? SystemChannelId { get; private set; } + /// + public ulong? RulesChannelId { get; private set; } + /// + public ulong? PublicUpdatesChannelId { get; private set; } + /// public ulong OwnerId { get; private set; } + /// public string VoiceRegionId { get; private set; } + /// public string IconId { get; private set; } + /// public string SplashId { get; private set; } + /// + public string DiscoverySplashId { get; private set; } internal bool Available { get; private set; } + /// + public ulong? ApplicationId { get; private set; } + /// + public PremiumTier PremiumTier { get; private set; } + /// + public string BannerId { get; private set; } + /// + public string VanityURLCode { get; private set; } + /// + public SystemChannelMessageDeny SystemChannelFlags { get; private set; } + /// + public string Description { get; private set; } + /// + public int PremiumSubscriptionCount { get; private set; } + /// + public string PreferredLocale { get; private set; } + /// + public int? MaxPresences { get; private set; } + /// + public int? MaxMembers { get; private set; } + /// + public int? MaxVideoChannelUsers { get; private set; } + /// + public int? ApproximateMemberCount { get; private set; } + /// + public int? ApproximatePresenceCount { get; private set; } + /// + public int MaxBitrate + { + get + { + return PremiumTier switch + { + PremiumTier.Tier1 => 128000, + PremiumTier.Tier2 => 256000, + PremiumTier.Tier3 => 384000, + _ => 96000, + }; + } + } + /// + public NsfwLevel NsfwLevel { get; private set; } + /// + public bool IsBoostProgressBarEnabled { get; private set; } + /// + public CultureInfo PreferredCulture { get; private set; } + /// + public GuildFeatures Features { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] - public ulong DefaultChannelId => Id; + /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + /// + public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId); + /// + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); + /// + /// Gets the built-in role containing all users in this guild. + /// public RestRole EveryoneRole => GetRole(Id); + + /// + /// Gets a collection of all roles in this guild. + /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + /// public IReadOnlyCollection Emotes => _emotes; - public IReadOnlyCollection Features => _features; + public IReadOnlyCollection Stickers => _stickers; internal RestGuild(BaseDiscordClient client, ulong id) : base(client, id) @@ -58,18 +146,46 @@ namespace Discord.Rest internal void Update(Model model) { AFKChannelId = model.AFKChannelId; - EmbedChannelId = model.EmbedChannelId; + if (model.WidgetChannelId.IsSpecified) + WidgetChannelId = model.WidgetChannelId.Value; SystemChannelId = model.SystemChannelId; + RulesChannelId = model.RulesChannelId; + PublicUpdatesChannelId = model.PublicUpdatesChannelId; AFKTimeout = model.AFKTimeout; - IsEmbeddable = model.EmbedEnabled; + if (model.WidgetEnabled.IsSpecified) + IsWidgetEnabled = model.WidgetEnabled.Value; IconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; VoiceRegionId = model.Region; SplashId = model.Splash; + DiscoverySplashId = model.DiscoverySplash; VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; DefaultMessageNotifications = model.DefaultMessageNotifications; + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; + PremiumTier = model.PremiumTier; + VanityURLCode = model.VanityURLCode; + BannerId = model.Banner; + SystemChannelFlags = model.SystemChannelFlags; + Description = model.Description; + PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); + NsfwLevel = model.NsfwLevel; + if (model.MaxPresences.IsSpecified) + MaxPresences = model.MaxPresences.Value ?? 25000; + if (model.MaxMembers.IsSpecified) + MaxMembers = model.MaxMembers.Value; + if (model.MaxVideoChannelUsers.IsSpecified) + MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value; + PreferredLocale = model.PreferredLocale; + PreferredCulture = new CultureInfo(PreferredLocale); + if (model.ApproximateMemberCount.IsSpecified) + ApproximateMemberCount = model.ApproximateMemberCount.Value; + if (model.ApproximatePresenceCount.IsSpecified) + ApproximatePresenceCount = model.ApproximatePresenceCount.Value; + if (model.IsBoostProgressBarEnabled.IsSpecified) + IsBoostProgressBarEnabled = model.IsBoostProgressBarEnabled.Value; if (model.Emojis != null) { @@ -81,10 +197,7 @@ namespace Discord.Rest else _emotes = ImmutableArray.Create(); - if (model.Features != null) - _features = model.Features.ToImmutableArray(); - else - _features = ImmutableArray.Create(); + Features = model.Features; var roles = ImmutableDictionary.CreateBuilder(); if (model.Roles != null) @@ -94,98 +207,339 @@ namespace Discord.Rest } _roles = roles.ToImmutable(); + if (model.Stickers != null) + { + var stickers = ImmutableArray.CreateBuilder(); + for (int i = 0; i < model.Stickers.Length; i++) + { + var sticker = model.Stickers[i]; + + var entity = CustomSticker.Create(Discord, sticker, this, sticker.User.IsSpecified ? sticker.User.Value.Id : null); + + stickers.Add(entity); + } + + _stickers = stickers.ToImmutable(); + } + else + _stickers = ImmutableArray.Create(); + Available = true; } - internal void Update(EmbedModel model) + internal void Update(WidgetModel model) { - EmbedChannelId = model.ChannelId; - IsEmbeddable = model.Enabled; + WidgetChannelId = model.ChannelId; + IsWidgetEnabled = model.Enabled; } + #endregion - //General + #region General + /// public async Task UpdateAsync(RequestOptions options = null) - => Update(await Discord.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false)); + => Update(await Discord.ApiClient.GetGuildAsync(Id, false, options).ConfigureAwait(false)); + /// + /// Updates this object's properties with its current state. + /// + /// + /// If true, and + /// will be updated as well. + /// + /// The options to be used when sending the request. + /// + /// If is true, and + /// will be updated as well. + /// + public async Task UpdateAsync(bool withCounts, RequestOptions options = null) + => Update(await Discord.ApiClient.GetGuildAsync(Id, withCounts, options).ConfigureAwait(false)); + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); + /// + /// is . public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) + + /// + /// is . + public async Task ModifyWidgetAsync(Action func, RequestOptions options = null) { - var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); + var model = await GuildHelper.ModifyWidgetAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + + /// + /// is . public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); - await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options); + await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options).ConfigureAwait(false); } + /// public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) { var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false); foreach (var model in models) { var role = GetRole(model.Id); - if (role != null) - role.Update(model); + role?.Update(model); } } + /// public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); + #endregion + + #region Interactions + /// + /// Deletes all slash commands in the current guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous delete operation. + /// + public Task DeleteSlashCommandsAsync(RequestOptions options = null) + => InteractionHelper.DeleteAllGuildCommandsAsync(Discord, Id, options); + + /// + /// Gets a collection of slash commands created by the current user in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// slash commands created by the current user. + /// + public Task> GetSlashCommandsAsync(RequestOptions options = null) + => GuildHelper.GetSlashCommandsAsync(this, Discord, options); + + /// + /// Gets a slash command in the current guild. + /// + /// The unique identifier of the slash command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a + /// slash command created by the current user. + /// + public Task GetSlashCommandAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetSlashCommandAsync(this, id, Discord, options); + #endregion - //Bans + #region Bans + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. + /// public Task GetBanAsync(IUser user, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. + /// public Task GetBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, userId, options); + /// public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + /// public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); + /// public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); + /// public Task RemoveBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, userId, options); + #endregion - //Channels + #region Channels + /// + /// Gets a collection of all channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// generic channels found within this guild. + /// public Task> GetChannelsAsync(RequestOptions options = null) => GuildHelper.GetChannelsAsync(this, Discord, options); + + /// + /// Gets a channel in this guild. + /// + /// The snowflake identifier for the channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the generic channel + /// associated with the specified ; if none is found. + /// public Task GetChannelAsync(ulong id, RequestOptions options = null) => GuildHelper.GetChannelAsync(this, Discord, id, options); + + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// associated with the specified ; if none is found. + /// public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) { var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); return channel as RestTextChannel; } + + /// + /// Gets a collection of all text channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// message channels found within this guild. + /// public async Task> GetTextChannelsAsync(RequestOptions options = null) { var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); return channels.OfType().ToImmutableArray(); } + + /// + /// Gets a thread channel in this guild. + /// + /// The snowflake identifier for the thread channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the thread channel associated + /// with the specified ; if none is found. + /// + public async Task GetThreadChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestThreadChannel; + } + + /// + /// Gets a collection of all thread in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// threads found within this guild. + /// + public async Task> GetThreadChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.OfType().ToImmutableArray(); + } + + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel associated + /// with the specified ; if none is found. + /// public async Task GetVoiceChannelAsync(ulong id, RequestOptions options = null) { var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); return channel as RestVoiceChannel; } + + /// + /// Gets a collection of all voice channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice channels found within this guild. + /// public async Task> GetVoiceChannelsAsync(RequestOptions options = null) { var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); return channels.OfType().ToImmutableArray(); } + /// + /// Gets a stage channel in this guild + /// + /// The snowflake identifier for the stage channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the stage channel associated + /// with the specified ; if none is found. + /// + public async Task GetStageChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestStageChannel; + } + + /// + /// Gets a collection of all stage channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// stage channels found within this guild. + /// + public async Task> GetStageChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.OfType().ToImmutableArray(); + } + + /// + /// Gets a collection of all category channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// category channels found within this guild. + /// public async Task> GetCategoryChannelsAsync(RequestOptions options = null) { var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); return channels.OfType().ToImmutableArray(); } + /// + /// Gets the AFK voice channel in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel that the + /// AFK users will be moved to after they have idled for too long; if none is set. + /// public async Task GetAFKChannelAsync(RequestOptions options = null) { var afkId = AFKChannelId; @@ -196,6 +550,15 @@ namespace Discord.Rest } return null; } + + /// + /// Gets the first viewable text channel in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the first viewable text + /// channel in this guild; if none is found. + /// public async Task GetDefaultChannelAsync(RequestOptions options = null) { var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); @@ -205,13 +568,31 @@ namespace Discord.Rest .OrderBy(c => c.Position) .FirstOrDefault(); } - public async Task GetEmbedChannelAsync(RequestOptions options = null) + + /// + /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the widget channel set + /// within the server's widget settings; if none is set. + /// + public async Task GetWidgetChannelAsync(RequestOptions options = null) { - var embedId = EmbedChannelId; - if (embedId.HasValue) - return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); + var widgetChannelId = WidgetChannelId; + if (widgetChannelId.HasValue) + return await GuildHelper.GetChannelAsync(this, Discord, widgetChannelId.Value, options).ConfigureAwait(false); return null; } + + /// + /// Gets the text channel where guild notices such as welcome messages and boost events are posted. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// where guild notices such as welcome messages and boost events are post; if none is found. + /// public async Task GetSystemChannelAsync(RequestOptions options = null) { var systemId = SystemChannelId; @@ -222,90 +603,610 @@ namespace Discord.Rest } return null; } - public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) - => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); - public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) - => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); - public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); - - //Integrations - public Task> GetIntegrationsAsync(RequestOptions options = null) - => GuildHelper.GetIntegrationsAsync(this, Discord, options); - public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) - => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); - //Invites - public Task> GetInvitesAsync(RequestOptions options = null) - => GuildHelper.GetInvitesAsync(this, Discord, options); /// - /// Gets the vanity invite URL of this guild. + /// Gets the text channel where Community guilds can display rules and/or guidelines. /// /// The options to be used when sending the request. /// - /// A partial metadata of the vanity invite found within this guild. + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// where Community guilds can display rules and/or guidelines; if none is set. /// - public Task GetVanityInviteAsync(RequestOptions options = null) - => GuildHelper.GetVanityInviteAsync(this, Discord, options); - - //Roles - public RestRole GetRole(ulong id) + public async Task GetRulesChannelAsync(RequestOptions options = null) { - if (_roles.TryGetValue(id, out RestRole value)) - return value; + var rulesChannelId = RulesChannelId; + if (rulesChannelId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, rulesChannelId.Value, options).ConfigureAwait(false); + return channel as RestTextChannel; + } return null; } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) + /// + /// Gets the text channel where admins and moderators of Community guilds receive notices from Discord. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel where + /// admins and moderators of Community guilds receive notices from Discord; if none is set. + /// + public async Task GetPublicUpdatesChannelAsync(RequestOptions options = null) { - var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); - _roles = _roles.Add(role.Id, role); - return role; + var publicUpdatesChannelId = PublicUpdatesChannelId; + if (publicUpdatesChannelId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, publicUpdatesChannelId.Value, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + return null; } - //Users - public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) - => GuildHelper.GetUsersAsync(this, Discord, null, null, options); - public Task GetUserAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Discord, id, options); - public Task GetCurrentUserAsync(RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id, options); - public Task GetOwnerAsync(RequestOptions options = null) - => GuildHelper.GetUserAsync(this, Discord, OwnerId, options); - - public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) - => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); - - //Audit logs - public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null) - => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); - - //Webhooks - public Task GetWebhookAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetWebhookAsync(this, Discord, id, options); - public Task> GetWebhooksAsync(RequestOptions options = null) - => GuildHelper.GetWebhooksAsync(this, Discord, options); + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// var categories = await guild.GetCategoriesAsync(); + /// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); + /// if (targetCategory == null) return; + /// await Context.Guild.CreateTextChannelAsync(name, x => + /// { + /// x.CategoryId = targetCategory.Id; + /// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}."; + /// }); + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + /// + /// Creates a voice channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// The created voice channel. + /// + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new stage channel in this guild. + /// + /// The new name for the stage channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// stage channel. + /// + public Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func); + /// + /// Creates a category channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// The created category channel. + /// + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => GuildHelper.GetVoiceRegionsAsync(this, Discord, options); + #endregion + + #region Integrations + public Task> GetIntegrationsAsync(RequestOptions options = null) + => GuildHelper.GetIntegrationsAsync(this, Discord, options); + public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) + => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + #endregion + + #region Invites + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// + public Task> GetInvitesAsync(RequestOptions options = null) + => GuildHelper.GetInvitesAsync(this, Discord, options); + /// + /// Gets the vanity invite URL of this guild. + /// + /// The options to be used when sending the request. + /// + /// A partial metadata of the vanity invite found within this guild. + /// + public Task GetVanityInviteAsync(RequestOptions options = null) + => GuildHelper.GetVanityInviteAsync(this, Discord, options); + #endregion + + #region Roles + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; if none is found. + /// + public RestRole GetRole(ulong id) + { + if (_roles.TryGetValue(id, out RestRole value)) + return value; + return null; + } + + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => CreateRoleAsync(name, permissions, color, isHoisted, false, options); + + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// The options to be used when sending the request. + /// Whether the role can be mentioned. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + { + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + _roles = _roles.Add(role.Id, role); + return role; + } + #endregion + + #region Users + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetUsersAsync(this, Discord, null, null, options); + + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// The snowflake identifier of the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild user + /// associated with the specified ; if none is found. + /// + public Task GetUserAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, id, options); + + /// + /// Gets the current user for this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the currently logged-in + /// user within this guild. + /// + public Task GetCurrentUserAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id, options); + + /// + /// Gets the owner of this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. + /// + public Task GetOwnerAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, OwnerId, options); + /// + /// + /// Prunes inactive users. + /// + /// + /// + /// This method removes all users that have not logged on in the provided number of . + /// + /// + /// If is true, this method will only return the number of users that + /// would be removed without kicking the users. + /// + /// + /// The number of days required for the users to be kicked. + /// Whether this prune action is a simulation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous prune operation. The task result contains the number of users to + /// be or has been removed from this guild. + /// + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); + + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users that the name or nickname starts with the provided at . + /// + public Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null) + => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); + #endregion + + #region Audit logs + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The options to be used when sending the request. + /// The audit log entry ID to get entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType); + #endregion + + #region Webhooks + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; if none is found. + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + #endregion + + #region Interactions + /// + /// Gets this guilds slash commands + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of application commands found within the guild. + /// + public async Task> GetApplicationCommandsAsync (RequestOptions options = null) + => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false); + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + public async Task GetApplicationCommandAsync(ulong id, RequestOptions options = null) + => await ClientHelper.GetGuildApplicationCommandAsync(Discord, id, Id, options); + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + public async Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); + + return RestGuildCommand.Create(Discord, model, Id); + } + /// + /// Overwrites the application commands within this guild. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + /// + public async Task> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommandsAsync(Discord, Id, properties, options); + + return models.Select(x => RestGuildCommand.Create(Discord, x, Id)).ToImmutableArray(); + } + + /// + /// Returns the name of the guild. + /// + /// + /// The name of the guild. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + #endregion - //Emotes + #region Emotes + /// + public Task> GetEmotesAsync(RequestOptions options = null) + => GuildHelper.GetEmotesAsync(this, Discord, options); + /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); + /// public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + /// + /// is . public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + /// + /// Moves the user to the voice channel. + /// + /// The user to move. + /// the channel where the user gets moved to. + /// A task that represents the asynchronous operation for moving a user. + public Task MoveAsync(IGuildUser user, IVoiceChannel targetChannel) + => user.ModifyAsync(x => x.Channel = new Optional(targetChannel)); + /// public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + #endregion - //IGuild + #region Stickers + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image of the new emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options = null) + { + var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false); + + return CustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + } + /// + /// Creates a new sticker in this guild + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The path of the file to upload. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public Task CreateStickerAsync(string name, string description, IEnumerable tags, string path, + RequestOptions options = null) + { + var fs = File.OpenRead(path); + return CreateStickerAsync(name, description, tags, fs, Path.GetFileName(fs.Name), options); + } + /// + /// Creates a new sticker in this guild + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The stream containing the file data. + /// The name of the file with the extension, ex: image.png. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, + string filename, RequestOptions options = null) + { + var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, stream, filename, options).ConfigureAwait(false); + + return CustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + } + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the sticker found with the + /// specified ; if none is found. + /// + public async Task GetStickerAsync(ulong id, RequestOptions options = null) + { + var model = await Discord.ApiClient.GetGuildStickerAsync(Id, id, options).ConfigureAwait(false); + + if (model == null) + return null; + + return CustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + } + /// + /// Gets a collection of all stickers within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of stickers found within the guild. + /// + public async Task> GetStickersAsync(RequestOptions options = null) + { + var models = await Discord.ApiClient.ListGuildStickersAsync(Id, options).ConfigureAwait(false); + + if (models.Length == 0) + return null; + + List stickers = new List(); + + foreach(var model in models) + { + var entity = CustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + stickers.Add(entity); + } + + return stickers.ToImmutableArray(); + } + /// + /// Deletes a sticker within this guild. + /// + /// The sticker to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + public Task DeleteStickerAsync(CustomSticker sticker, RequestOptions options = null) + => sticker.DeleteAsync(options); + #endregion + + #region Guild Events + + /// + /// Gets an event within this guild. + /// + /// The snowflake identifier for the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task GetEventAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetGuildEventAsync(Discord, id, this, options); + + /// + /// Gets all active events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task> GetEventsAsync(RequestOptions options = null) + => GuildHelper.GetGuildEventsAsync(Discord, this, options); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + public Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + + #endregion + + #region IGuild + /// bool IGuild.Available => Available; + /// IAudioClient IGuild.AudioClient => null; + /// IRole IGuild.EveryoneRole => EveryoneRole; + /// IReadOnlyCollection IGuild.Roles => Roles; + IReadOnlyCollection IGuild.Stickers => Stickers; + + /// + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + + /// + async Task IGuild.GetEventAsync(ulong id, RequestOptions options) + => await GetEventAsync(id, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetEventsAsync(RequestOptions options) + => await GetEventsAsync(options).ConfigureAwait(false); + + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); /// @@ -315,6 +1216,7 @@ namespace Discord.Rest async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) => await GetBanAsync(userId, options).ConfigureAwait(false); + /// async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -322,6 +1224,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -329,6 +1232,7 @@ namespace Discord.Rest else return null; } + /// async Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -336,6 +1240,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -343,6 +1248,23 @@ namespace Discord.Rest else return null; } + /// + async Task IGuild.GetThreadChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetThreadChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IGuild.GetThreadChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetThreadChannelsAsync(options).ConfigureAwait(false); + else + return null; + } + /// async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -350,6 +1272,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -357,6 +1280,23 @@ namespace Discord.Rest else return null; } + /// + async Task IGuild.GetStageChannelAsync(ulong id, CacheMode mode, RequestOptions options ) + { + if (mode == CacheMode.AllowDownload) + return await GetStageChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IGuild.GetStageChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetStageChannelsAsync(options).ConfigureAwait(false); + else + return null; + } + /// async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -364,6 +1304,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -371,6 +1312,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -378,13 +1320,15 @@ namespace Discord.Rest else return null; } - async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + /// + async Task IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetEmbedChannelAsync(options).ConfigureAwait(false); + return await GetWidgetChannelAsync(options).ConfigureAwait(false); else return null; } + /// async Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -392,29 +1336,75 @@ namespace Discord.Rest else return null; } + /// + async Task IGuild.GetRulesChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetRulesChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetPublicUpdatesChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetPublicUpdatesChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + /// async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); - async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) - => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); + /// + async Task IGuild.CreateStageChannelAsync(string name, Action func, RequestOptions options) + => await CreateStageChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + + /// async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); + /// async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + /// async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); /// async Task IGuild.GetVanityInviteAsync(RequestOptions options) => await GetVanityInviteAsync(options).ConfigureAwait(false); + /// IRole IGuild.GetRole(ulong id) => GetRole(id); + /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + + /// + /// Disconnects the user from its current voice channel + /// + /// The user to disconnect. + /// A task that represents the asynchronous operation for disconnecting a user. + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -422,6 +1412,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -429,6 +1420,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -436,6 +1428,7 @@ namespace Discord.Rest else return null; } + /// async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -443,19 +1436,82 @@ namespace Discord.Rest else return ImmutableArray.Create(); } - Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + /// + /// Downloading users is not supported for a REST-based guild. + Task IGuild.DownloadUsersAsync() => + throw new NotSupportedException(); + /// + async Task> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await SearchUsersAsync(query, limit, options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } - async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options) + async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, + ulong? beforeId, ulong? userId, ActionType? actionType) { if (cacheMode == CacheMode.AllowDownload) - return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + return (await GetAuditLogsAsync(limit, options, beforeId: beforeId, userId: userId, actionType: actionType).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } + /// async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); + /// + async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) + => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) + => await CreateStickerAsync(name, description, tags, image, options); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, string filename, RequestOptions options) + => await CreateStickerAsync(name, description, tags, stream, filename, options); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, string path, RequestOptions options) + => await CreateStickerAsync(name, description, tags, path, options); + /// + async Task IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode != CacheMode.AllowDownload) + return null; + + return await GetStickerAsync(id, options); + } + /// + async Task> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options) + { + if (mode != CacheMode.AllowDownload) + return null; + + return await GetStickersAsync(options); + } + /// + Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) + => sticker.DeleteAsync(); + /// + async Task IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options) + => await CreateApplicationCommandAsync(properties, options); + /// + async Task> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options) + => await BulkOverwriteApplicationCommandsAsync(properties, options); + /// + async Task IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + { + return await GetApplicationCommandAsync(id, options); + } + else + return null; + } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs new file mode 100644 index 000000000..d3ec11fc6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.GuildScheduledEvent; + +namespace Discord.Rest +{ + public class RestGuildEvent : RestEntity, IGuildScheduledEvent + { + /// + public IGuild Guild { get; private set; } + + /// + public ulong? ChannelId { get; private set; } + + /// + public IUser Creator { get; private set; } + + /// + public ulong CreatorId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public DateTimeOffset StartTime { get; private set; } + + /// + public DateTimeOffset? EndTime { get; private set; } + + /// + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } + + /// + public GuildScheduledEventStatus Status { get; private set; } + + /// + public GuildScheduledEventType Type { get; private set; } + + /// + public ulong? EntityId { get; private set; } + + /// + public string Location { get; private set; } + + /// + public int? UserCount { get; private set; } + + internal RestGuildEvent(BaseDiscordClient client, IGuild guild, ulong id) + : base(client, id) + { + Guild = guild; + } + + internal static RestGuildEvent Create(BaseDiscordClient client, IGuild guild, Model model) + { + var entity = new RestGuildEvent(client, guild, model.Id); + entity.Update(model); + return entity; + } + + internal static RestGuildEvent Create(BaseDiscordClient client, IGuild guild, IUser creator, Model model) + { + var entity = new RestGuildEvent(client, guild, model.Id); + entity.Update(model, creator); + return entity; + } + + internal void Update(Model model, IUser creator) + { + Update(model); + Creator = creator; + CreatorId = creator.Id; + } + + internal void Update(Model model) + { + if (model.Creator.IsSpecified) + { + Creator = RestUser.Create(Discord, model.Creator.Value); + } + + CreatorId = model.CreatorId.ToNullable() ?? 0; // should be changed? + ChannelId = model.ChannelId.IsSpecified ? model.ChannelId.Value : null; + Name = model.Name; + Description = model.Description.GetValueOrDefault(); + StartTime = model.ScheduledStartTime; + EndTime = model.ScheduledEndTime; + PrivacyLevel = model.PrivacyLevel; + Status = model.Status; + Type = model.EntityType; + EntityId = model.EntityId; + Location = model.EntityMetadata?.Location.GetValueOrDefault(); + UserCount = model.UserCount.ToNullable(); + } + + /// + public Task StartAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); + + /// + public Task EndAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled + ? GuildScheduledEventStatus.Cancelled + : GuildScheduledEventStatus.Completed); + + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteEventAsync(Discord, this, options); + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); + + #region IGuildScheduledEvent + + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) + => GetUsersAsync(options); + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetUsersAsync(fromUserId, dir, limit, options); + + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs index eadda53f2..9759e64d2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Integration; @@ -10,18 +10,28 @@ namespace Discord.Rest { private long _syncedAtTicks; + /// public string Name { get; private set; } + /// public string Type { get; private set; } + /// public bool IsEnabled { get; private set; } + /// public bool IsSyncing { get; private set; } + /// public ulong ExpireBehavior { get; private set; } + /// public ulong ExpireGracePeriod { get; private set; } + /// public ulong GuildId { get; private set; } + /// public ulong RoleId { get; private set; } public RestUser User { get; private set; } + /// public IntegrationAccount Account { get; private set; } internal IGuild Guild { get; private set; } + /// public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id) @@ -78,6 +88,7 @@ namespace Discord.Rest public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; + /// IGuild IGuildIntegration.Guild { get @@ -87,6 +98,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IUser IGuildIntegration.User => User; } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildWidget.cs similarity index 56% rename from src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs rename to src/Discord.Net.Rest/Entities/Guilds/RestGuildWidget.cs index f26a62d8d..065739c57 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildWidget.cs @@ -1,25 +1,25 @@ -using System.Diagnostics; -using Model = Discord.API.GuildEmbed; +using System.Diagnostics; +using Model = Discord.API.GuildWidget; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct RestGuildEmbed + public struct RestGuildWidget { public bool IsEnabled { get; private set; } public ulong? ChannelId { get; private set; } - internal RestGuildEmbed(bool isEnabled, ulong? channelId) + internal RestGuildWidget(bool isEnabled, ulong? channelId) { ChannelId = channelId; IsEnabled = isEnabled; } - internal static RestGuildEmbed Create(Model model) + internal static RestGuildWidget Create(Model model) { - return new RestGuildEmbed(model.Enabled, model.ChannelId); + return new RestGuildWidget(model.Enabled, model.ChannelId); } - public override string ToString() => ChannelId?.ToString(); + public override string ToString() => ChannelId?.ToString() ?? "Unknown"; private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})"; } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index de5a5f7d9..b75d6288e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -9,12 +9,17 @@ namespace Discord.Rest public class RestUserGuild : RestEntity, IUserGuild { private string _iconId; - + + /// public string Name { get; private set; } + /// public bool IsOwner { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); internal RestUserGuild(BaseDiscordClient discord, ulong id) @@ -40,6 +45,7 @@ namespace Discord.Rest { await Discord.ApiClient.LeaveGuildAsync(Id, options).ConfigureAwait(false); } + /// public async Task DeleteAsync(RequestOptions options = null) { await Discord.ApiClient.DeleteGuildAsync(Id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs index 4e0c3c1ee..a363f051b 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -2,8 +2,11 @@ using Discord.Rest; using System.Diagnostics; using Model = Discord.API.VoiceRegion; -namespace Discord +namespace Discord.Rest { + /// + /// Represents a REST-based voice region. + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class RestVoiceRegion : RestEntity, IVoiceRegion { diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs new file mode 100644 index 000000000..a9efb6de1 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -0,0 +1,345 @@ +using Discord.Net.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based base command interaction. + /// + public class RestCommandBase : RestInteraction + { + /// + /// Gets the name of the invoked command. + /// + public string CommandName + => Data.Name; + + /// + /// Gets the id of the invoked command. + /// + public ulong CommandId + => Data.Id; + + /// + /// The data associated with this interaction. + /// + internal new RestCommandBaseData Data { get; private set; } + + + internal override bool _hasResponded { get; set; } + + private object _lock = new object(); + + internal RestCommandBase(DiscordRestClient client, Model model) + : base(client, model.Id) + { + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestCommandBase(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + + internal override async Task UpdateAsync(DiscordRestClient client, Model model) + { + await base.UpdateAsync(client, model).ConfigureAwait(false); + } + + /// + /// Responds to an Interaction with type . + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid. + /// + /// A string that contains json to write back to the incoming http request. + /// + public override string Respond( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupWithFileAsync( + Stream fileStream, + string fileName, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupWithFileAsync( + string filePath, + string text = null, + string fileName = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); + + fileName ??= Path.GetFileName(filePath); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); + } + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A string that contains json to write back to the incoming http request. + /// + public override string Defer(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs new file mode 100644 index 000000000..4227c802a --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents the base data tied with the interaction. + /// + public class RestCommandBaseData : RestEntity, IApplicationCommandInteractionData where TOption : IApplicationCommandInteractionDataOption + { + /// + public string Name { get; private set; } + + /// + /// Gets a collection of received with this interaction. + /// + public virtual IReadOnlyCollection Options { get; internal set; } + + internal RestResolvableData ResolvableData; + + internal RestCommandBaseData(BaseDiscordClient client, Model model) + : base(client, model.Id) + { + } + + internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + var entity = new RestCommandBaseData(client, model); + await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + return entity; + } + + internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + Name = model.Name; + if (model.Resolved.IsSpecified && ResolvableData == null) + { + ResolvableData = new RestResolvableData(); + await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false); + } + } + + IReadOnlyCollection IApplicationCommandInteractionData.Options + => (IReadOnlyCollection)Options; + } + + /// + /// Represents the base data tied with the interaction. + /// + public class RestCommandBaseData : RestCommandBaseData + { + internal RestCommandBaseData(DiscordRestClient client, Model model) + : base(client, model) { } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs new file mode 100644 index 000000000..710207ef9 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal class RestResolvableData where T : API.IResolvable + { + internal readonly Dictionary GuildMembers + = new Dictionary(); + internal readonly Dictionary Users + = new Dictionary(); + internal readonly Dictionary Channels + = new Dictionary(); + internal readonly Dictionary Roles + = new Dictionary(); + internal readonly Dictionary Messages + = new Dictionary(); + + internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model) + { + var resolved = model.Resolved.Value; + + if (resolved.Users.IsSpecified) + { + foreach (var user in resolved.Users.Value) + { + var restUser = RestUser.Create(discord, user.Value); + + Users.Add(ulong.Parse(user.Key), restUser); + } + } + + if (resolved.Channels.IsSpecified) + { + var channels = await guild.GetChannelsAsync().ConfigureAwait(false); + + foreach (var channelModel in resolved.Channels.Value) + { + var restChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); + + restChannel.Update(channelModel.Value); + + Channels.Add(ulong.Parse(channelModel.Key), restChannel); + } + } + + if (resolved.Members.IsSpecified) + { + foreach (var member in resolved.Members.Value) + { + // pull the adjacent user model + member.Value.User = resolved.Users.Value.FirstOrDefault(x => x.Key == member.Key).Value; + var restMember = RestGuildUser.Create(discord, guild, member.Value); + + GuildMembers.Add(ulong.Parse(member.Key), restMember); + } + } + + if (resolved.Roles.IsSpecified) + { + foreach (var role in resolved.Roles.Value) + { + var restRole = RestRole.Create(discord, guild, role.Value); + + Roles.Add(ulong.Parse(role.Key), restRole); + } + } + + if (resolved.Messages.IsSpecified) + { + foreach (var msg in resolved.Messages.Value) + { + channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value ?? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false)); + + RestUser author; + + if (msg.Value.Author.IsSpecified) + { + author = RestUser.Create(discord, msg.Value.Author.Value); + } + else + { + author = RestGuildUser.Create(discord, guild, msg.Value.Member.Value); + } + + var message = RestMessage.Create(discord, channel, author, msg.Value); + + Messages.Add(message.Id, message); + } + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs new file mode 100644 index 000000000..53055cac3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based message command interaction. + /// + public class RestMessageCommand : RestCommandBase, IMessageCommandInteraction, IDiscordInteraction + { + /// + /// The data associated with this interaction. + /// + public new RestMessageCommandData Data { get; private set; } + + internal RestMessageCommand(DiscordRestClient client, Model model) + : base(client, model) + { + + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestMessageCommand(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + + internal override async Task UpdateAsync(DiscordRestClient client, Model model) + { + await base.UpdateAsync(client, model).ConfigureAwait(false); + + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + } + + //IMessageCommandInteraction + /// + IMessageCommandInteractionData IMessageCommandInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs new file mode 100644 index 000000000..8eadab617 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents the data for a . + /// + public class RestMessageCommandData : RestCommandBaseData, IMessageCommandInteractionData, IDiscordInteractionData + { + /// + /// Gets the message associated with this message command. + /// + public RestMessage Message + => ResolvableData?.Messages.FirstOrDefault().Value; + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal RestMessageCommandData(DiscordRestClient client, Model model) + : base(client, model) { } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + var entity = new RestMessageCommandData(client, model); + await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + return entity; + } + + //IMessageCommandInteractionData + /// + IMessage IMessageCommandInteractionData.Message => Message; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs new file mode 100644 index 000000000..58f1ed375 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based user command. + /// + public class RestUserCommand : RestCommandBase, IUserCommandInteraction, IDiscordInteraction + { + /// + /// Gets the data associated with this interaction. + /// + public new RestUserCommandData Data { get; private set; } + + internal RestUserCommand(DiscordRestClient client, Model model) + : base(client, model) + { + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestUserCommand(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + + internal override async Task UpdateAsync(DiscordRestClient client, Model model) + { + await base.UpdateAsync(client, model).ConfigureAwait(false); + + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + } + + //IUserCommandInteractionData + /// + IUserCommandInteractionData IUserCommandInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs new file mode 100644 index 000000000..7563eecc7 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents the data for a . + /// + public class RestUserCommandData : RestCommandBaseData, IUserCommandInteractionData, IDiscordInteractionData + { + /// + /// Gets the user who this command targets. + /// + public RestUser Member + => (RestUser)ResolvableData.GuildMembers.Values.FirstOrDefault() ?? ResolvableData.Users.Values.FirstOrDefault(); + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal RestUserCommandData(DiscordRestClient client, Model model) + : base(client, model) { } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + var entity = new RestUserCommandData(client, model); + await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + return entity; + } + + //IUserCommandInteractionData + /// + IUser IUserCommandInteractionData.User => Member; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs new file mode 100644 index 000000000..7cfc6a2ec --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -0,0 +1,542 @@ +using Discord.API; +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal static class InteractionHelper + { + public const double ResponseTimeLimit = 3; + public const double ResponseAndFollowupLimit = 15; + + #region InteractionHelper + public static bool CanSendResponse(IDiscordInteraction interaction) + { + return (DateTime.UtcNow - interaction.CreatedAt).TotalSeconds < ResponseTimeLimit; + } + public static bool CanRespondOrFollowup(IDiscordInteraction interaction) + { + return (DateTime.UtcNow - interaction.CreatedAt).TotalMinutes <= ResponseAndFollowupLimit; + } + + public static Task DeleteAllGuildCommandsAsync(BaseDiscordClient client, ulong guildId, RequestOptions options = null) + { + return client.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(guildId, Array.Empty(), options); + } + + public static Task DeleteAllGlobalCommandsAsync(BaseDiscordClient client, RequestOptions options = null) + { + return client.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty(), options); + } + + public static Task SendInteractionResponseAsync(BaseDiscordClient client, InteractionResponse response, + ulong interactionId, string interactionToken, RequestOptions options = null) + { + return client.ApiClient.CreateInteractionResponseAsync(response, interactionId, interactionToken, options); + } + + public static async Task GetOriginalResponseAsync(BaseDiscordClient client, IMessageChannel channel, + IDiscordInteraction interaction, RequestOptions options = null) + { + var model = await client.ApiClient.GetInteractionResponseAsync(interaction.Token, options).ConfigureAwait(false); + return RestInteractionMessage.Create(client, model, interaction.Token, channel); + } + + public static async Task SendFollowupAsync(BaseDiscordClient client, CreateWebhookMessageParams args, + string token, IMessageChannel channel, RequestOptions options = null) + { + var model = await client.ApiClient.CreateInteractionFollowupMessageAsync(args, token, options).ConfigureAwait(false); + + var entity = RestFollowupMessage.Create(client, model, token, channel); + return entity; + } + #endregion + + #region Global commands + public static async Task GetGlobalCommandAsync(BaseDiscordClient client, ulong id, + RequestOptions options = null) + { + var model = await client.ApiClient.GetGlobalApplicationCommandAsync(id, options).ConfigureAwait(false); + + return RestGlobalCommand.Create(client, model); + } + public static Task CreateGlobalCommandAsync(BaseDiscordClient client, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + var args = Activator.CreateInstance(typeof(TArg)); + func((TArg)args); + return CreateGlobalCommandAsync(client, (TArg)args, options); + } + public static async Task CreateGlobalCommandAsync(BaseDiscordClient client, + ApplicationCommandProperties arg, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); + + var model = new CreateApplicationCommandParams + { + Name = arg.Name.Value, + Type = arg.Type, + DefaultPermission = arg.IsDefaultPermission.IsSpecified + ? arg.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + return await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); + } + + public static async Task BulkOverwriteGlobalCommandsAsync(BaseDiscordClient client, + ApplicationCommandProperties[] args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + + var models = new List(); + + foreach (var arg in args) + { + Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); + + var model = new CreateApplicationCommandParams + { + Name = arg.Name.Value, + Type = arg.Type, + DefaultPermission = arg.IsDefaultPermission.IsSpecified + ? arg.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + models.Add(model); + } + + return await client.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(models.ToArray(), options).ConfigureAwait(false); + } + + public static async Task> BulkOverwriteGuildCommandsAsync(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties[] args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + + var models = new List(); + + foreach (var arg in args) + { + Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); + + var model = new CreateApplicationCommandParams + { + Name = arg.Name.Value, + Type = arg.Type, + DefaultPermission = arg.IsDefaultPermission.IsSpecified + ? arg.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + models.Add(model); + } + + return await client.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(guildId, models.ToArray(), options).ConfigureAwait(false); + } + + private static TArg GetApplicationCommandProperties(IApplicationCommand command) + where TArg : ApplicationCommandProperties + { + bool isBaseClass = typeof(TArg) == typeof(ApplicationCommandProperties); + + switch (true) + { + case true when (typeof(TArg) == typeof(SlashCommandProperties) || isBaseClass) && command.Type == ApplicationCommandType.Slash: + return new SlashCommandProperties() as TArg; + case true when (typeof(TArg) == typeof(MessageCommandProperties) || isBaseClass) && command.Type == ApplicationCommandType.Message: + return new MessageCommandProperties() as TArg; + case true when (typeof(TArg) == typeof(UserCommandProperties) || isBaseClass) && command.Type == ApplicationCommandType.User: + return new UserCommandProperties() as TArg; + default: + throw new InvalidOperationException($"Cannot modify application command of type {command.Type} with the parameter type {typeof(TArg).FullName}"); + } + } + + public static Task ModifyGlobalCommandAsync(BaseDiscordClient client, IApplicationCommand command, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + var arg = GetApplicationCommandProperties(command); + func(arg); + return ModifyGlobalCommandAsync(client, command, arg, options); + } + + public static async Task ModifyGlobalCommandAsync(BaseDiscordClient client, IApplicationCommand command, + ApplicationCommandProperties args, RequestOptions options = null) + { + if (args.Name.IsSpecified) + { + Preconditions.AtMost(args.Name.Value.Length, 32, nameof(args.Name)); + Preconditions.AtLeast(args.Name.Value.Length, 1, nameof(args.Name)); + } + + var model = new ModifyApplicationCommandParams + { + Name = args.Name, + DefaultPermission = args.IsDefaultPermission.IsSpecified + ? args.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (args is SlashCommandProperties slashProps) + { + if (slashProps.Description.IsSpecified) + { + Preconditions.AtMost(slashProps.Description.Value.Length, 100, nameof(slashProps.Description)); + Preconditions.AtLeast(slashProps.Description.Value.Length, 1, nameof(slashProps.Description)); + } + + if (slashProps.Options.IsSpecified) + { + if (slashProps.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + model.Description = slashProps.Description; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + return await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); + } + + public static async Task DeleteGlobalCommandAsync(BaseDiscordClient client, IApplicationCommand command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); + + await client.ApiClient.DeleteGlobalApplicationCommandAsync(command.Id, options).ConfigureAwait(false); + } + #endregion + + #region Guild Commands + public static Task CreateGuildCommandAsync(BaseDiscordClient client, ulong guildId, + Action func, RequestOptions options) where TArg : ApplicationCommandProperties + { + var args = Activator.CreateInstance(typeof(TArg)); + func((TArg)args); + return CreateGuildCommandAsync(client, guildId, (TArg)args, options); + } + + public static async Task CreateGuildCommandAsync(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties arg, RequestOptions options = null) + { + var model = new CreateApplicationCommandParams + { + Name = arg.Name.Value, + Type = arg.Type, + DefaultPermission = arg.IsDefaultPermission.IsSpecified + ? arg.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + return await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); + } + + public static Task ModifyGuildCommandAsync(BaseDiscordClient client, IApplicationCommand command, ulong guildId, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + var arg = GetApplicationCommandProperties(command); + func(arg); + return ModifyGuildCommandAsync(client, command, guildId, arg, options); + } + + public static async Task ModifyGuildCommandAsync(BaseDiscordClient client, IApplicationCommand command, ulong guildId, + ApplicationCommandProperties arg, RequestOptions options = null) + { + var model = new ModifyApplicationCommandParams + { + Name = arg.Name, + DefaultPermission = arg.IsDefaultPermission.IsSpecified + ? arg.IsDefaultPermission.Value + : Optional.Unspecified + }; + + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + } + + return await client.ApiClient.ModifyGuildApplicationCommandAsync(model, guildId, command.Id, options).ConfigureAwait(false); + } + + public static async Task DeleteGuildCommandAsync(BaseDiscordClient client, ulong guildId, IApplicationCommand command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); + + await client.ApiClient.DeleteGuildApplicationCommandAsync(guildId, command.Id, options).ConfigureAwait(false); + } + + public static Task DeleteUnknownApplicationCommandAsync(BaseDiscordClient client, ulong? guildId, IApplicationCommand command, RequestOptions options = null) + { + return guildId.HasValue + ? DeleteGuildCommandAsync(client, guildId.Value, command, options) + : DeleteGlobalCommandAsync(client, command, options); + } + #endregion + + #region Responses + public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, + RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(message.Content); + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || message.Embeds.Any(); + bool hasComponents = args.Components.IsSpecified && args.Components.Value != null; + + if (!hasComponents && !hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + var apiArgs = new ModifyInteractionResponseParams + { + Content = args.Content, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Unspecified, + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified + }; + + return await client.ApiClient.ModifyInteractionFollowupMessageAsync(apiArgs, message.Id, message.Token, options).ConfigureAwait(false); + } + public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) + => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); + public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, + RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = !string.IsNullOrEmpty(args.Content.GetValueOrDefault()); + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0; + bool hasComponents = args.Components.IsSpecified && args.Components.Value != null; + + if (!hasComponents && !hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + var apiArgs = new ModifyInteractionResponseParams + { + Content = args.Content, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Flags = args.Flags + }; + + return await client.ApiClient.ModifyInteractionResponseAsync(apiArgs, token, options).ConfigureAwait(false); + } + + public static async Task DeleteInteractionResponseAsync(BaseDiscordClient client, RestInteractionMessage message, RequestOptions options = null) + => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); + + public static Task SendAutocompleteResultAsync(BaseDiscordClient client, IEnumerable result, ulong interactionId, + string interactionToken, RequestOptions options) + { + result ??= Array.Empty(); + + Preconditions.AtMost(result.Count(), 20, nameof(result), "A maximum of 20 choices are allowed!"); + + var apiArgs = new InteractionResponse + { + Type = InteractionResponseType.ApplicationCommandAutocompleteResult, + Data = new InteractionCallbackData + { + Choices = result.Any() + ? result.Select(x => new ApplicationCommandOptionChoice { Name = x.Name, Value = x.Value }).ToArray() + : Array.Empty() + } + }; + + return client.ApiClient.CreateInteractionResponseAsync(apiArgs, interactionId, interactionToken, options); + } + #endregion + + #region Guild permissions + public static async Task> GetGuildCommandPermissionsAsync(BaseDiscordClient client, + ulong guildId, RequestOptions options) + { + var models = await client.ApiClient.GetGuildApplicationCommandPermissionsAsync(guildId, options); + return models.Select(x => + new GuildApplicationCommandPermission(x.Id, x.ApplicationId, guildId, x.Permissions.Select( + y => new ApplicationCommandPermission(y.Id, y.Type, y.Permission)) + .ToArray()) + ).ToArray(); + } + + public static async Task GetGuildCommandPermissionAsync(BaseDiscordClient client, + ulong guildId, ulong commandId, RequestOptions options) + { + try + { + var model = await client.ApiClient.GetGuildApplicationCommandPermissionAsync(guildId, commandId, options); + return new GuildApplicationCommandPermission(model.Id, model.ApplicationId, guildId, model.Permissions.Select( + y => new ApplicationCommandPermission(y.Id, y.Type, y.Permission)).ToArray()); + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.NotFound) + return null; + throw; + } + } + + public static async Task ModifyGuildCommandPermissionsAsync(BaseDiscordClient client, ulong guildId, ulong commandId, + ApplicationCommandPermission[] args, RequestOptions options) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtMost(args.Length, 10, nameof(args)); + Preconditions.AtLeast(args.Length, 0, nameof(args)); + + var permissionsList = new List(); + + foreach (var arg in args) + { + var permissions = new ApplicationCommandPermissions + { + Id = arg.TargetId, + Permission = arg.Permission, + Type = arg.TargetType + }; + + permissionsList.Add(permissions); + } + + var model = new ModifyGuildApplicationCommandPermissionsParams + { + Permissions = permissionsList.ToArray() + }; + + var apiModel = await client.ApiClient.ModifyApplicationCommandPermissionsAsync(model, guildId, commandId, options); + + return new GuildApplicationCommandPermission(apiModel.Id, apiModel.ApplicationId, guildId, apiModel.Permissions.Select( + x => new ApplicationCommandPermission(x.Id, x.Type, x.Permission)).ToArray()); + } + + public static async Task> BatchEditGuildCommandPermissionsAsync(BaseDiscordClient client, ulong guildId, + IDictionary args, RequestOptions options) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.Count, 0, nameof(args)); + + var models = new List(); + + foreach (var arg in args) + { + Preconditions.AtMost(arg.Value.Length, 10, nameof(args)); + + var model = new ModifyGuildApplicationCommandPermissions + { + Id = arg.Key, + Permissions = arg.Value.Select(x => new ApplicationCommandPermissions + { + Id = x.TargetId, + Permission = x.Permission, + Type = x.TargetType + }).ToArray() + }; + + models.Add(model); + } + + var apiModels = await client.ApiClient.BatchModifyApplicationCommandPermissionsAsync(models.ToArray(), guildId, options); + + return apiModels.Select( + x => new GuildApplicationCommandPermission(x.Id, x.ApplicationId, x.GuildId, x.Permissions.Select( + y => new ApplicationCommandPermission(y.Id, y.Type, y.Permission)).ToArray())).ToArray(); + } + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs new file mode 100644 index 000000000..eb47e15aa --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.MessageComponentInteractionData; +using System.IO; +using Discord.Net.Rest; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based message component. + /// + internal class RestMessageComponent : RestInteraction, IComponentInteraction, IDiscordInteraction + { + /// + /// Gets the data received with this interaction, contains the button that was clicked. + /// + public new RestMessageComponentData Data { get; } + + /// + /// Gets the message that contained the trigger for this interaction. + /// + public RestUserMessage Message { get; private set; } + + private object _lock = new object(); + internal override bool _hasResponded { get; set; } = false; + + internal RestMessageComponent(BaseDiscordClient client, Model model) + : base(client, model.Id) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = new RestMessageComponentData(dataModel); + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestMessageComponent(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + internal override async Task UpdateAsync(DiscordRestClient discord, Model model) + { + await base.UpdateAsync(discord, model).ConfigureAwait(false); + + if (model.Message.IsSpecified && model.ChannelId.IsSpecified) + { + if (Message == null) + { + Message = RestUserMessage.Create(Discord, Channel, User, model.Message.Value); + } + } + } + /// + /// Responds to an Interaction with type . + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// A string that contains json to write back to the incoming http request. + /// + public override string Respond( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel(), + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = MessageFlags.Ephemeral; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + + /// + /// Updates the message which this component resides in with the type + /// + /// A delegate containing the properties to modify the message with. + /// The request options for this request. + /// A string that contains json to write back to the incoming http request. + public string Update(Action func, RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed."); + } + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(Message.Content); + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || Message.Embeds.Any(); + + if (!hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue) + { + var allowedMentions = args.AllowedMentions.Value; + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) + && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) + && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Message.Channel, options).ConfigureAwait(false); + } + + /// + public override async Task FollowupWithFileAsync( + Stream fileStream, + string fileName, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); + Preconditions.NotNullOrWhitespace(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Message.Channel, options).ConfigureAwait(false); + } + + /// + public override async Task FollowupWithFileAsync( + string filePath, + string text = null, + string fileName = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNullOrWhitespace(filePath, nameof(filePath), "Path must exist"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Message.Channel, options).ConfigureAwait(false); + } + + /// + /// Defers an interaction and responds with type 5 () + /// + /// to send this message ephemerally, otherwise . + /// The request options for this request. + /// + /// A string that contains json to write back to the incoming http request. + /// + public string DeferLoading(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.Unspecified + }; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + + /// + /// + /// + /// + /// + /// + /// A string that contains json to write back to the incoming http request. + /// + /// + /// + public override string Defer(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredUpdateMessage, + Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.Unspecified + }; + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + return SerializePayload(response); + } + + //IComponentInteraction + /// + IComponentInteractionData IComponentInteraction.Data => Data; + + /// + IUserMessage IComponentInteraction.Message => Message; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs new file mode 100644 index 000000000..e865c208c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.MessageComponentInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents data for a . + /// + public class RestMessageComponentData : IComponentInteractionData, IDiscordInteractionData + { + /// + /// Gets the components Custom Id that was clicked. + /// + public string CustomId { get; } + + /// + /// Gets the type of the component clicked. + /// + public ComponentType Type { get; } + + /// + /// Gets the value(s) of a interaction response. + /// + public IReadOnlyCollection Values { get; } + + internal RestMessageComponentData(Model model) + { + CustomId = model.CustomId; + Type = model.ComponentType; + Values = model.Values.GetValueOrDefault(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs new file mode 100644 index 000000000..c3edaf6ff --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of the . + /// + public abstract class RestApplicationCommand : RestEntity, IApplicationCommand + { + /// + public ulong ApplicationId { get; private set; } + + /// + public ApplicationCommandType Type { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public bool IsDefaultPermission { get; private set; } + + /// + /// The options of this command. + /// + public IReadOnlyCollection Options { get; private set; } + + /// + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(Id); + + internal RestApplicationCommand(BaseDiscordClient client, ulong id) + : base(client, id) { } + + internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, ulong? guildId) + { + return guildId.HasValue + ? RestGuildCommand.Create(client, model, guildId.Value) + : RestGlobalCommand.Create(client, model); + } + + internal virtual void Update(Model model) + { + Type = model.Type; + ApplicationId = model.ApplicationId; + Name = model.Name; + Description = model.Description; + IsDefaultPermission = model.DefaultPermissions.GetValueOrDefault(true); + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() + : ImmutableArray.Create(); + } + + /// + public abstract Task DeleteAsync(RequestOptions options = null); + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + { + return ModifyAsync(func, options); + } + + /// + public abstract Task ModifyAsync(Action func, RequestOptions options = null) + where TArg : ApplicationCommandProperties; + + IReadOnlyCollection IApplicationCommand.Options => Options; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs new file mode 100644 index 000000000..a40491a2c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.ApplicationCommandOptionChoice; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestApplicationCommandChoice : IApplicationCommandOptionChoice + { + /// + public string Name { get; } + + /// + public object Value { get; } + + internal RestApplicationCommandChoice(Model model) + { + Name = model.Name; + Value = model.Value; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs new file mode 100644 index 000000000..d5c261e0b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandOption; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestApplicationCommandOption : IApplicationCommandOption + { + #region RestApplicationCommandOption + /// + public ApplicationCommandOptionType Type { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public bool? IsDefault { get; private set; } + + /// + public bool? IsRequired { get; private set; } + + /// + public double? MinValue { get; private set; } + + /// + public double? MaxValue { get; private set; } + + /// + /// A collection of 's for this command. + /// + public IReadOnlyCollection Choices { get; private set; } + + /// + /// A collection of 's for this command. + /// + public IReadOnlyCollection Options { get; private set; } + + /// + /// The allowed channel types for this option. + /// + public IReadOnlyCollection ChannelTypes { get; private set; } + + internal RestApplicationCommandOption() { } + + internal static RestApplicationCommandOption Create(Model model) + { + var options = new RestApplicationCommandOption(); + options.Update(model); + return options; + } + + internal void Update(Model model) + { + Type = model.Type; + Name = model.Name; + Description = model.Description; + + if (model.Default.IsSpecified) + IsDefault = model.Default.Value; + + if (model.Required.IsSpecified) + IsRequired = model.Required.Value; + + if (model.MinValue.IsSpecified) + MinValue = model.MinValue.Value; + + if (model.MaxValue.IsSpecified) + MaxValue = model.MaxValue.Value; + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(Create).ToImmutableArray() + : ImmutableArray.Create(); + + Choices = model.Choices.IsSpecified + ? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray() + : ImmutableArray.Create(); + + ChannelTypes = model.ChannelTypes.IsSpecified + ? model.ChannelTypes.Value.ToImmutableArray() + : ImmutableArray.Create(); + } + #endregion + + #region IApplicationCommandOption + IReadOnlyCollection IApplicationCommandOption.Options + => Options; + IReadOnlyCollection IApplicationCommandOption.Choices + => Choices; + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs new file mode 100644 index 000000000..c319bcf34 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based global application command. + /// + public class RestGlobalCommand : RestApplicationCommand + { + internal RestGlobalCommand(BaseDiscordClient client, ulong id) + : base(client, id) { } + + internal static RestGlobalCommand Create(BaseDiscordClient client, Model model) + { + var entity = new RestGlobalCommand(client, model.Id); + entity.Update(model); + return entity; + } + + /// + public override async Task DeleteAsync(RequestOptions options = null) + => await InteractionHelper.DeleteGlobalCommandAsync(Discord, this).ConfigureAwait(false); + + /// + /// Modifies this . + /// + /// The delegate containing the properties to modify the command with. + /// The options to be used when sending the request. + /// + /// The modified command. + /// + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var cmd = await InteractionHelper.ModifyGlobalCommandAsync(Discord, this, func, options).ConfigureAwait(false); + Update(cmd); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs new file mode 100644 index 000000000..00804e57e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based guild application command. + /// + public class RestGuildCommand : RestApplicationCommand + { + /// + /// The guild Id where this command originates. + /// + public ulong GuildId { get; private set; } + + internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId) + : base(client, id) + { + GuildId = guildId; + } + + internal static RestGuildCommand Create(BaseDiscordClient client, Model model, ulong guildId) + { + var entity = new RestGuildCommand(client, model.Id, guildId); + entity.Update(model); + return entity; + } + + /// + public override async Task DeleteAsync(RequestOptions options = null) + => await InteractionHelper.DeleteGuildCommandAsync(Discord, GuildId, this).ConfigureAwait(false); + + /// + /// Modifies this . + /// + /// The delegate containing the properties to modify the command with. + /// The options to be used when sending the request. + /// + /// The modified command + /// + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await InteractionHelper.ModifyGuildCommandAsync(Discord, this, GuildId, func, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets this commands permissions inside of the current guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a + /// object defining the permissions of the current slash command. + /// + public Task GetCommandPermission(RequestOptions options = null) + => InteractionHelper.GetGuildCommandPermissionAsync(Discord, GuildId, Id, options); + + /// + /// Modifies the current command permissions for this guild command. + /// + /// The permissions to overwrite. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. The task result contains a + /// object containing the modified permissions. + /// + public Task ModifyCommandPermissions(ApplicationCommandPermission[] permissions, RequestOptions options = null) + => InteractionHelper.ModifyGuildCommandPermissionsAsync(Discord, GuildId, Id, permissions, options); + + /// + /// Gets the guild that this slash command resides in. + /// + /// if you want the approximate member and presence counts for the guild, otherwise . + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a + /// . + /// + public Task GetGuild(bool withCounts = false, RequestOptions options = null) + => ClientHelper.GetGuildAsync(Discord, GuildId, withCounts, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs new file mode 100644 index 000000000..103c43ffb --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Newtonsoft.Json; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based interaction. + /// + public abstract class RestInteraction : RestEntity, IDiscordInteraction + { + /// + public InteractionType Type { get; private set; } + + /// + public IDiscordInteractionData Data { get; private set; } + + /// + public string Token { get; private set; } + + /// + public int Version { get; private set; } + + /// + /// Gets the user who invoked the interaction. + /// + public RestUser User { get; private set; } + + /// + public DateTimeOffset CreatedAt { get; private set; } + + internal abstract bool _hasResponded { get; set; } + + /// + /// if the token is valid for replying to, otherwise . + /// + public bool IsValidToken + => InteractionHelper.CanRespondOrFollowup(this); + + /// + /// Gets the channel that this interaction was executed in. + /// + public IRestMessageChannel Channel { get; private set; } + + /// + /// Gets the guild this interaction was executed in. + /// + public RestGuild Guild { get; private set; } + + internal RestInteraction(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + CreatedAt = discord.UseInteractionSnowflakeDate + ? SnowflakeUtils.FromSnowflake(Id) + : DateTime.UtcNow; + } + + internal static async Task CreateAsync(DiscordRestClient client, Model model) + { + if(model.Type == InteractionType.Ping) + { + return await RestPingInteraction.CreateAsync(client, model); + } + + if (model.Type == InteractionType.ApplicationCommand) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + if (dataModel == null) + return null; + + return dataModel.Type switch + { + ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model).ConfigureAwait(false), + ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model).ConfigureAwait(false), + ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model).ConfigureAwait(false), + _ => null + }; + } + + if (model.Type == InteractionType.MessageComponent) + return await RestMessageComponent.CreateAsync(client, model).ConfigureAwait(false); + + if (model.Type == InteractionType.ApplicationCommandAutocomplete) + return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false); + + return null; + } + + internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model) + { + Data = model.Data.IsSpecified + ? model.Data.Value + : null; + Token = model.Token; + Version = model.Version; + Type = model.Type; + + if(Guild == null && model.GuildId.IsSpecified) + { + Guild = await discord.GetGuildAsync(model.GuildId.Value); + } + + if (User == null) + { + if (model.Member.IsSpecified && model.GuildId.IsSpecified) + { + User = RestGuildUser.Create(Discord, Guild, model.Member.Value); + } + else + { + User = RestUser.Create(Discord, model.User.Value); + } + } + + if(Channel == null && model.ChannelId.IsSpecified) + { + Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + } + } + + internal string SerializePayload(object payload) + { + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + DiscordRestClient.Serializer.Serialize(writer, payload); + + return json.ToString(); + } + + /// + public abstract string Defer(bool ephemeral = false, RequestOptions options = null); + /// + public abstract Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + /// + /// Gets the original response for this interaction. + /// + /// The request options for this request. + /// A that represents the initial response. + public Task GetOriginalResponseAsync(RequestOptions options = null) + => InteractionHelper.GetOriginalResponseAsync(Discord, Channel, this, options); + + /// + /// Edits original response for this interaction. + /// + /// A delegate containing the properties to modify the message with. + /// The request options for this request. + /// A that represents the initial response. + public async Task ModifyOriginalResponseAsync(Action func, RequestOptions options = null) + { + var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options); + return RestInteractionMessage.Create(Discord, model, Token, Channel); + } + /// + public abstract string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public abstract Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public abstract Task FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + #region IDiscordInteraction + /// + Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, RequestOptions options, MessageComponent component, Embed embed) + => Task.FromResult(Respond(text, embeds, isTTS, ephemeral, allowedMentions, options, component, embed)); + + Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options) + => Task.FromResult(Defer(ephemeral, options)); + + /// + async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, + RequestOptions options, MessageComponent component, Embed embed) + => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component, embed).ConfigureAwait(false); + + /// + async Task IDiscordInteraction.GetOriginalResponseAsync(RequestOptions options) + => await GetOriginalResponseAsync(options).ConfigureAwait(false); + + /// + async Task IDiscordInteraction.ModifyOriginalResponseAsync(Action func, RequestOptions options) + => await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false); + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs new file mode 100644 index 000000000..f979a4df2 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based ping interaction. + /// + public class RestPingInteraction : RestInteraction, IDiscordInteraction + { + internal override bool _hasResponded { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + internal RestPingInteraction(BaseDiscordClient client, ulong id) + : base(client, id) + { + } + + internal static new async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestPingInteraction(client, model.Id); + await entity.UpdateAsync(client, model); + return entity; + } + + public string AcknowledgePing() + { + var model = new API.InteractionResponse() + { + Type = InteractionResponseType.Pong + }; + + return SerializePayload(model); + } + + public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException(); + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) => throw new NotSupportedException(); + public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) => throw new NotSupportedException(); + public override Task FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) => throw new NotSupportedException(); + public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs new file mode 100644 index 000000000..3b879cd4e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.AutocompleteInteractionData; +using System.IO; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based autocomplete interaction. + /// + public class RestAutocompleteInteraction : RestInteraction, IAutocompleteInteraction, IDiscordInteraction + { + /// + /// Gets the autocomplete data of this interaction. + /// + public new RestAutocompleteInteractionData Data { get; } + + internal override bool _hasResponded { get; set; } + private object _lock = new object(); + + internal RestAutocompleteInteraction(DiscordRestClient client, Model model) + : base(client, model.Id) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + if (dataModel != null) + Data = new RestAutocompleteInteractionData(dataModel); + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestAutocompleteInteraction(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + + /// + /// Responds to this interaction with a set of choices. + /// + /// + /// The set of choices for the user to pick from. + /// + /// A max of 20 choices are allowed. Passing for this argument will show the executing user that + /// there is no choices for their autocompleted input. + /// + /// + /// The request options for this response. + /// + /// A string that contains json to write back to the incoming http request. + /// + public string Respond(IEnumerable result, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + lock (_lock) + { + if (_hasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + lock (_lock) + { + _hasResponded = true; + } + + var model = new API.InteractionResponse + { + Type = InteractionResponseType.ApplicationCommandAutocompleteResult, + Data = new API.InteractionCallbackData + { + Choices = result.Any() + ? result.Select(x => new API.ApplicationCommandOptionChoice { Name = x.Name, Value = x.Value }).ToArray() + : Array.Empty() + } + }; + + return SerializePayload(model); + } + + /// + /// Responds to this interaction with a set of choices. + /// + /// The request options for this response. + /// + /// The set of choices for the user to pick from. + /// + /// A max of 20 choices are allowed. Passing for this argument will show the executing user that + /// there is no choices for their autocompleted input. + /// + /// + /// + /// A string that contains json to write back to the incoming http request. + /// + public string Respond(RequestOptions options = null, params AutocompleteResult[] result) + => Respond(result, options); + + /// + [Obsolete("Autocomplete interactions cannot be deferred!", true)] + public override string Defer(bool ephemeral = false, RequestOptions options = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have normal responses!", true)] + public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + //IAutocompleteInteraction + /// + IAutocompleteInteractionData IAutocompleteInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteractionData.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteractionData.cs new file mode 100644 index 000000000..135eb88ea --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteractionData.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DataModel = Discord.API.AutocompleteInteractionData; + +namespace Discord.Rest +{ + /// + /// Represents the data for a . + /// + public class RestAutocompleteInteractionData : IAutocompleteInteractionData + { + /// + /// Gets the name of the invoked command. + /// + public string CommandName { get; } + + /// + /// Gets the id of the invoked command. + /// + public ulong CommandId { get; } + + /// + /// Gets the type of the invoked command. + /// + public ApplicationCommandType Type { get; } + + /// + /// Gets the version of the invoked command. + /// + public ulong Version { get; } + + /// + /// Gets the current autocomplete option that is actively being filled out. + /// + public AutocompleteOption Current { get; } + + /// + /// Gets a collection of all the other options the executing users has filled out. + /// + public IReadOnlyCollection Options { get; } + + internal RestAutocompleteInteractionData(DataModel model) + { + var options = model.Options.SelectMany(GetOptions); + + Current = options.FirstOrDefault(x => x.Focused); + Options = options.ToImmutableArray(); + + if (Options.Count == 1 && Current == null) + Current = Options.FirstOrDefault(); + + CommandName = model.Name; + CommandId = model.Id; + Type = model.Type; + Version = model.Version; + } + + private List GetOptions(API.AutocompleteInteractionDataOption model) + { + var options = new List(); + + options.Add(new AutocompleteOption(model.Type, model.Name, model.Value.GetValueOrDefault(null), model.Focused.GetValueOrDefault(false))); + + if (model.Options.IsSpecified) + { + options.AddRange(model.Options.Value.SelectMany(GetOptions)); + } + + return options; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs new file mode 100644 index 000000000..785e39a12 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based slash command. + /// + public class RestSlashCommand : RestCommandBase, ISlashCommandInteraction, IDiscordInteraction + { + /// + /// Gets the data associated with this interaction. + /// + public new RestSlashCommandData Data { get; private set; } + + internal RestSlashCommand(DiscordRestClient client, Model model) + : base(client, model) + { + } + + internal new static async Task CreateAsync(DiscordRestClient client, Model model) + { + var entity = new RestSlashCommand(client, model); + await entity.UpdateAsync(client, model).ConfigureAwait(false); + return entity; + } + + internal override async Task UpdateAsync(DiscordRestClient client, Model model) + { + await base.UpdateAsync(client, model).ConfigureAwait(false); + + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + } + + //ISlashCommandInteraction + /// + IApplicationCommandInteractionData ISlashCommandInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs new file mode 100644 index 000000000..f967cc628 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.Rest +{ + + public class RestSlashCommandData : RestCommandBaseData, IDiscordInteractionData + { + internal RestSlashCommandData(DiscordRestClient client, Model model) + : base(client, model) { } + + internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + var entity = new RestSlashCommandData(client, model); + await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + return entity; + } + internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + { + await base.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new RestSlashCommandDataOption(this, x)).ToImmutableArray() + : ImmutableArray.Create(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandDataOption.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandDataOption.cs new file mode 100644 index 000000000..bb931f68e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandDataOption.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ApplicationCommandInteractionDataOption; + + +namespace Discord.Rest +{ + /// + /// Represents a REST-based option for a slash command. + /// + public class RestSlashCommandDataOption : IApplicationCommandInteractionDataOption + { + #region RestSlashCommandDataOption + /// + public string Name { get; private set; } + + /// + public object Value { get; private set; } + + /// + public ApplicationCommandOptionType Type { get; private set; } + + /// + /// Gets a collection of sub command options received for this sub command group. + /// + public IReadOnlyCollection Options { get; private set; } + + internal RestSlashCommandDataOption() { } + internal RestSlashCommandDataOption(RestSlashCommandData data, Model model) + { + Name = model.Name; + Type = model.Type; + + if (model.Value.IsSpecified) + { + switch (Type) + { + case ApplicationCommandOptionType.User: + case ApplicationCommandOptionType.Role: + case ApplicationCommandOptionType.Channel: + case ApplicationCommandOptionType.Mentionable: + if (ulong.TryParse($"{model.Value.Value}", out var valueId)) + { + switch (Type) + { + case ApplicationCommandOptionType.User: + { + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; + + if (guildUser != null) + Value = guildUser; + else + Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; + } + break; + case ApplicationCommandOptionType.Channel: + Value = data.ResolvableData.Channels.FirstOrDefault(x => x.Key == valueId).Value; + break; + case ApplicationCommandOptionType.Role: + Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; + break; + case ApplicationCommandOptionType.Mentionable: + { + if (data.ResolvableData.GuildMembers.Any(x => x.Key == valueId) || data.ResolvableData.Users.Any(x => x.Key == valueId)) + { + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; + + if (guildUser != null) + Value = guildUser; + else + Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; + } + else if (data.ResolvableData.Roles.Any(x => x.Key == valueId)) + { + Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; + } + } + break; + default: + Value = model.Value.Value; + break; + } + } + break; + case ApplicationCommandOptionType.String: + Value = model.Value.ToString(); + break; + case ApplicationCommandOptionType.Integer: + { + if (model.Value.Value is long val) + Value = val; + else if (long.TryParse(model.Value.Value.ToString(), out long res)) + Value = res; + } + break; + case ApplicationCommandOptionType.Boolean: + { + if (model.Value.Value is bool val) + Value = val; + else if (bool.TryParse(model.Value.Value.ToString(), out bool res)) + Value = res; + } + break; + case ApplicationCommandOptionType.Number: + { + if (model.Value.Value is int val) + Value = val; + else if (double.TryParse(model.Value.Value.ToString(), out double res)) + Value = res; + } + break; + } + } + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new RestSlashCommandDataOption(data, x)).ToImmutableArray() + : ImmutableArray.Create(); + } + #endregion + + #region Converters + public static explicit operator bool(RestSlashCommandDataOption option) + => (bool)option.Value; + public static explicit operator int(RestSlashCommandDataOption option) + => (int)option.Value; + public static explicit operator string(RestSlashCommandDataOption option) + => option.Value.ToString(); + #endregion + + #region IApplicationCommandInteractionDataOption + IReadOnlyCollection IApplicationCommandInteractionDataOption.Options + => Options; + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 050f117fb..95b454c20 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -9,16 +9,30 @@ namespace Discord.Rest public class RestInvite : RestEntity, IInvite, IUpdateable { public ChannelType ChannelType { get; private set; } + /// public string ChannelName { get; private set; } + /// public string GuildName { get; private set; } + /// public int? PresenceCount { get; private set; } + /// public int? MemberCount { get; private set; } + /// public ulong ChannelId { get; private set; } + /// public ulong? GuildId { get; private set; } + /// + public IUser Inviter { get; private set; } + /// + public IUser TargetUser { get; private set; } + /// + public TargetUserType TargetUserType { get; private set; } internal IChannel Channel { get; } internal IGuild Guild { get; } + /// public string Code => Id; + /// public string Url => $"{DiscordConfig.InviteUrl}{Code}"; internal RestInvite(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) @@ -42,19 +56,31 @@ namespace Discord.Rest MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null; PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null; ChannelType = (ChannelType)model.Channel.Type; + Inviter = model.Inviter.IsSpecified ? RestUser.Create(Discord, model.Inviter.Value) : null; + TargetUser = model.TargetUser.IsSpecified ? RestUser.Create(Discord, model.TargetUser.Value) : null; + TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined; } - + + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetInviteAsync(Code, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => InviteHelper.DeleteAsync(this, Discord, options); + /// + /// Gets the URL of the invite. + /// + /// + /// A string that resolves to the Url of the invite. + /// public override string ToString() => Url; private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; - + + /// IGuild IInvite.Guild { get @@ -66,6 +92,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IChannel IInvite.Channel { get diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index c7236be58..a0ed9ec81 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -3,17 +3,21 @@ using Model = Discord.API.InviteMetadata; namespace Discord.Rest { + /// Represents additional information regarding the REST-based invite object. public class RestInviteMetadata : RestInvite, IInviteMetadata { - private long? _createdAtTicks; + private long _createdAtTicks; - public bool IsRevoked { get; private set; } + /// public bool IsTemporary { get; private set; } + /// public int? MaxAge { get; private set; } + /// public int? MaxUses { get; private set; } + /// public int? Uses { get; private set; } - public RestUser Inviter { get; private set; } + /// public DateTimeOffset? CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); internal RestInviteMetadata(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) @@ -29,15 +33,11 @@ namespace Discord.Rest internal void Update(Model model) { base.Update(model); - Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null; - IsRevoked = model.Revoked; IsTemporary = model.Temporary; - MaxAge = model.MaxAge.IsSpecified ? model.MaxAge.Value : (int?)null; - MaxUses = model.MaxUses.IsSpecified ? model.MaxUses.Value : (int?)null; - Uses = model.Uses.IsSpecified ? model.Uses.Value : (int?)null; - _createdAtTicks = model.CreatedAt.IsSpecified ? model.CreatedAt.Value.UtcTicks : (long?)null; + MaxAge = model.MaxAge; + MaxUses = model.MaxUses; + Uses = model.Uses; + _createdAtTicks = model.CreatedAt.UtcTicks; } - - IUser IInviteMetadata.Inviter => Inviter; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index e185234ac..4e4849c51 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -1,20 +1,30 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Attachment; namespace Discord { + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Attachment : IAttachment { + /// public ulong Id { get; } + /// public string Filename { get; } + /// public string Url { get; } + /// public string ProxyUrl { get; } + /// public int Size { get; } + /// public int? Height { get; } + /// public int? Width { get; } + /// + public bool Ephemeral { get; } - internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width) + internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, bool? ephemeral) { Id = id; Filename = filename; @@ -23,14 +33,22 @@ namespace Discord Size = size; Height = height; Width = width; + Ephemeral = ephemeral.GetValueOrDefault(false); } internal static Attachment Create(Model model) { return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null); + model.Width.IsSpecified ? model.Width.Value : (int?)null, + model.Ephemeral.ToNullable()); } + /// + /// Returns the filename of this attachment. + /// + /// + /// A string containing the filename of this attachment. + /// public override string ToString() => Filename; private string DebuggerDisplay => $"{Filename} ({Size} bytes)"; } diff --git a/src/Discord.Net.Rest/Entities/Messages/CustomSticker.cs b/src/Discord.Net.Rest/Entities/Messages/CustomSticker.cs new file mode 100644 index 000000000..6fd0f7700 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/CustomSticker.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Sticker; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based custom sticker within a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CustomSticker : Sticker, ICustomSticker + { + /// + /// Gets the users id who uploaded the sticker. + /// + /// + /// In order to get the author id, the bot needs the MANAGE_EMOJIS_AND_STICKERS permission. + /// + public ulong? AuthorId { get; private set; } + + /// + /// Gets the guild that this custom sticker is in. + /// + /// + /// Note: This property can be if the sticker wasn't fetched from a guild. + /// + public RestGuild Guild { get; private set; } + + private ulong GuildId { get; set; } + + internal CustomSticker(BaseDiscordClient client, ulong id, RestGuild guild, ulong? authorId = null) + : base(client, id) + { + AuthorId = authorId; + Guild = guild; + } + internal CustomSticker(BaseDiscordClient client, ulong id, ulong guildId, ulong? authorId = null) + : base(client, id) + { + AuthorId = authorId; + GuildId = guildId; + } + + internal static CustomSticker Create(BaseDiscordClient client, Model model, RestGuild guild, ulong? authorId = null) + { + var entity = new CustomSticker(client, model.Id, guild, authorId); + entity.Update(model); + return entity; + } + + internal static CustomSticker Create(BaseDiscordClient client, Model model, ulong guildId, ulong? authorId = null) + { + var entity = new CustomSticker(client, model.Id, guildId, authorId); + entity.Update(model); + return entity; + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteStickerAsync(Discord, GuildId, this, options); + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyStickerAsync(Discord, GuildId, this, func, options); + Update(model); + } + + private string DebuggerDisplay => Guild != null ? $"{Name} in {Guild.Name} ({Id})" : $"{Name} ({Id})"; + + IGuild ICustomSticker.Guild => Guild; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 3dc3e74e9..309500c96 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -1,58 +1,170 @@ +using Discord.API; using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Model = Discord.API.Message; +using UserModel = Discord.API.User; namespace Discord.Rest { internal static class MessageHelper { - public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, + /// + /// Regex used to check if some text is formatted as inline code. + /// + private static readonly Regex InlineCodeRegex = new Regex(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + + /// + /// Regex used to check if some text is formatted as a code block. + /// + private static readonly Regex BlockCodeRegex = new Regex(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . + public static Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) - { - if (msg.Author.Id != client.CurrentUser.Id) - throw new InvalidOperationException("Only the author of a message may change it."); + => ModifyAsync(msg.Channel.Id, msg.Id, client, func, options); + public static async Task ModifyAsync(ulong channelId, ulong msgId, BaseDiscordClient client, Action func, + RequestOptions options) + { var args = new MessageProperties(); func(args); - var apiArgs = new API.Rest.ModifyMessageParams + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified && string.IsNullOrEmpty(args.Content.Value); + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0; + bool hasComponents = args.Components.IsSpecified && args.Components.Value != null; + bool hasAttachments = args.Attachments.IsSpecified; + + if (!hasComponents && !hasText && !hasEmbeds && !hasAttachments) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + if (args.AllowedMentions.IsSpecified) + { + AllowedMentions allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(args.Embeds.Value?.Length ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + } + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) { - Content = args.Content, - Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create() - }; - return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + if(!args.Attachments.IsSpecified) + { + var apiArgs = new API.Rest.ModifyMessageParams + { + Content = args.Content, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified, + }; + return await client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options).ConfigureAwait(false); + } + else + { + var apiArgs = new UploadFileParams(args.Attachments.Value.ToArray()) + { + Content = args.Content, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), + MessageComponent = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified + }; + + return await client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options).ConfigureAwait(false); + } } + public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) => DeleteAsync(msg.Channel.Id, msg.Id, client, options); + public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client, RequestOptions options) { await client.ApiClient.DeleteMessageAsync(channelId, msgId, options).ConfigureAwait(false); } + public static async Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.AddReactionAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); + } + public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); + } + + public static async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveReactionAsync(channelId, messageId, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); + } + + public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); } - public static async Task RemoveReactionAsync(IMessage msg, IUser user, IEmote emote, BaseDiscordClient client, RequestOptions options) + public static async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); + await client.ApiClient.RemoveAllReactionsAsync(channelId, messageId, options).ConfigureAwait(false); } public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); + await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); + } + + public static async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); + } + + public static async Task RemoveAllReactionsForEmoteAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); } public static IAsyncEnumerable> GetReactionUsersAsync(IMessage msg, IEmote emote, int? limit, BaseDiscordClient client, RequestOptions options) { Preconditions.NotNull(emote, nameof(emote)); - var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name); + var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name)); return new PagedAsyncEnumerable( DiscordConfig.MaxUserReactionsPerBatch, @@ -71,7 +183,7 @@ namespace Discord.Rest }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) + if (lastPage.Count != DiscordConfig.MaxUserReactionsPerBatch) return false; info.Position = lastPage.Max(x => x.Id); @@ -79,7 +191,21 @@ namespace Discord.Rest }, count: limit ); + } + private static string UrlEncode(string text) + { +#if NET461 + return System.Net.WebUtility.UrlEncode(text); +#else + return System.Web.HttpUtility.UrlEncode(text); +#endif + } + public static string SanitizeMessage(IMessage message) + { + var newContent = MentionUtils.Resolve(message, 0, TagHandling.FullName, TagHandling.FullName, TagHandling.FullName, TagHandling.FullName, TagHandling.FullName); + newContent = Format.StripMarkDown(newContent); + return newContent; } public static async Task PinAsync(IMessage msg, BaseDiscordClient client, @@ -87,6 +213,7 @@ namespace Discord.Rest { await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { @@ -96,14 +223,53 @@ namespace Discord.Rest public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection userMentions) { var tags = ImmutableArray.CreateBuilder(); - int index = 0; + var codeIndex = 0; + + // checks if the tag being parsed is wrapped in code blocks + bool CheckWrappedCode() + { + // util to check if the index of a tag is within the bounds of the codeblock + bool EnclosedInBlock(Match m) + => m.Groups[1].Index < index && index < m.Groups[2].Index; + + // loop through all code blocks that are before the start of the tag + while (codeIndex < index) + { + var blockMatch = BlockCodeRegex.Match(text, codeIndex); + if (blockMatch.Success) + { + if (EnclosedInBlock(blockMatch)) + return true; + // continue if the end of the current code was before the start of the tag + codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length; + if (codeIndex < index) + continue; + return false; + } + var inlineMatch = InlineCodeRegex.Match(text, codeIndex); + if (inlineMatch.Success) + { + if (EnclosedInBlock(inlineMatch)) + return true; + // continue if the end of the current code was before the start of the tag + codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length; + if (codeIndex < index) + continue; + return false; + } + return false; + } + return false; + } + while (true) { index = text.IndexOf('<', index); if (index == -1) break; int endIndex = text.IndexOf('>', index + 1); if (endIndex == -1) break; + if (CheckWrappedCode()) break; string content = text.Substring(index, endIndex - index + 1); if (MentionUtils.TryParseUser(content, out ulong id)) @@ -139,38 +305,41 @@ namespace Discord.Rest tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id, emoji)); else //Bad Tag { - index = index + 1; + index++; continue; } index = endIndex + 1; } index = 0; + codeIndex = 0; while (true) { index = text.IndexOf("@everyone", index); if (index == -1) break; - + if (CheckWrappedCode()) break; var tagIndex = FindIndex(tags, index); if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + tags.Insert(tagIndex.Value, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole)); index++; } index = 0; + codeIndex = 0; while (true) { index = text.IndexOf("@here", index); if (index == -1) break; - + if (CheckWrappedCode()) break; var tagIndex = FindIndex(tags, index); if (tagIndex.HasValue) - tags.Insert(tagIndex.Value, new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + tags.Insert(tagIndex.Value, new Tag(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole)); index++; } return tags.ToImmutable(); } + private static int? FindIndex(IReadOnlyList tags, int index) { int i = 0; @@ -184,6 +353,7 @@ namespace Discord.Rest return null; //Overlaps tag before this return i; } + public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags @@ -191,6 +361,7 @@ namespace Discord.Rest .Select(x => x.Key) .ToImmutableArray(); } + public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArray tags) { return tags @@ -202,7 +373,7 @@ namespace Discord.Rest public static MessageSource GetSource(Model msg) { - if (msg.Type != MessageType.Default) + if (msg.Type != MessageType.Default && msg.Type != MessageType.Reply) return MessageSource.System; else if (msg.WebhookId.IsSpecified) return MessageSource.Webhook; @@ -210,5 +381,24 @@ namespace Discord.Rest return MessageSource.Bot; return MessageSource.User; } + + public static Task CrosspostAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + => CrosspostAsync(msg.Channel.Id, msg.Id, client, options); + + public static async Task CrosspostAsync(ulong channelId, ulong msgId, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.CrosspostAsync(channelId, msgId, options).ConfigureAwait(false); + } + + public static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) + { + IUser author = null; + if (guild != null) + author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; + if (author == null) + author = RestUser.Create(client, guild, model, webhookId); + return author; + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs new file mode 100644 index 000000000..693d36e56 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based follow up message sent by a bot responding to a slash command. + /// + public class RestFollowupMessage : RestUserMessage + { + // Token used to delete/modify this followup message + internal string Token { get; } + + internal RestFollowupMessage(BaseDiscordClient discord, ulong id, IUser author, string token, IMessageChannel channel) + : base(discord, id, channel, author, MessageSource.Bot) + { + Token = token; + } + + internal static RestFollowupMessage Create(BaseDiscordClient discord, Model model, string token, IMessageChannel channel) + { + var entity = new RestFollowupMessage(discord, model.Id, model.Author.IsSpecified ? RestUser.Create(discord, model.Author.Value) : discord.CurrentUser, token, channel); + entity.Update(model); + return entity; + } + + internal new void Update(Model model) + { + base.Update(model); + } + + /// + /// Deletes this object and all of it's children. + /// + /// A task that represents the asynchronous delete operation. + public Task DeleteAsync() + => InteractionHelper.DeleteFollowupMessageAsync(Discord, this); + + /// + /// Modifies this interaction followup message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// The token used to modify/delete this message expired. + /// /// Something went wrong during the request. + public new async Task ModifyAsync(Action func, RequestOptions options = null) + { + try + { + var model = await InteractionHelper.ModifyFollowupMessageAsync(Discord, this, func, options).ConfigureAwait(false); + Update(model); + } + catch (Net.HttpException x) + { + if (x.HttpCode == System.Net.HttpStatusCode.NotFound) + { + throw new InvalidOperationException("The token of this message has expired!", x); + } + + throw; + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs new file mode 100644 index 000000000..26beb03b6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + /// + /// Represents the initial REST-based response to a slash command. + /// + public class RestInteractionMessage : RestUserMessage + { + // Token used to delete/modify this followup message + internal string Token { get; } + + internal RestInteractionMessage(BaseDiscordClient discord, ulong id, IUser author, string token, IMessageChannel channel) + : base(discord, id, channel, author, MessageSource.Bot) + { + Token = token; + } + + internal static RestInteractionMessage Create(BaseDiscordClient discord, Model model, string token, IMessageChannel channel) + { + var entity = new RestInteractionMessage(discord, model.Id, model.Author.IsSpecified ? RestUser.Create(discord, model.Author.Value) : discord.CurrentUser, token, channel); + entity.Update(model); + return entity; + } + + internal new void Update(Model model) + { + base.Update(model); + } + + /// + /// Deletes this object and all of it's children. + /// + /// A task that represents the asynchronous delete operation. + public Task DeleteAsync() + => InteractionHelper.DeleteInteractionResponseAsync(Discord, this); + + /// + /// Modifies this interaction response + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// The token used to modify/delete this message expired. + /// /// Something went wrong during the request. + public new async Task ModifyAsync(Action func, RequestOptions options = null) + { + try + { + var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options).ConfigureAwait(false); + Update(model); + } + catch (Net.HttpException x) + { + if (x.HttpCode == System.Net.HttpStatusCode.NotFound) + { + throw new InvalidOperationException("The token of this message has expired!", x); + } + + throw; + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 590886886..c48a60aac 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -7,28 +8,84 @@ using Model = Discord.API.Message; namespace Discord.Rest { + /// + /// Represents a REST-based message. + /// public abstract class RestMessage : RestEntity, IMessage, IUpdateable { private long _timestampTicks; + private ImmutableArray _reactions = ImmutableArray.Create(); + private ImmutableArray _userMentions = ImmutableArray.Create(); + /// public IMessageChannel Channel { get; } + /// + /// Gets the Author of the message. + /// public IUser Author { get; } + /// public MessageSource Source { get; } + /// public string Content { get; private set; } + /// + public string CleanContent => MessageHelper.SanitizeMessage(this); + + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public virtual bool IsTTS => false; + /// public virtual bool IsPinned => false; + /// + public virtual bool IsSuppressed => false; + /// public virtual DateTimeOffset? EditedTimestamp => null; + /// + public virtual bool MentionedEveryone => false; + + /// + /// Gets a collection of the 's on the message. + /// public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + /// + /// Gets a collection of the 's on the message. + /// public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); - public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection Stickers => ImmutableArray.Create(); + /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + /// + public MessageActivity Activity { get; private set; } + /// + public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } + + /// + /// Gets the interaction this message is a response to. + /// + public MessageInteraction Interaction { get; private set; } + /// + public MessageFlags? Flags { get; private set; } + /// + public MessageType Type { get; private set; } + + /// + public IReadOnlyCollection Components { get; private set; } + /// + /// Gets a collection of the mentioned users in the message. + /// + public IReadOnlyCollection MentionedUsers => _userMentions; internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) @@ -39,34 +96,206 @@ namespace Discord.Rest } internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - if (model.Type == MessageType.Default) + if (model.Type == MessageType.Default || + model.Type == MessageType.Reply || + model.Type == MessageType.ApplicationCommand || + model.Type == MessageType.ThreadStarterMessage) return RestUserMessage.Create(discord, channel, author, model); else return RestSystemMessage.Create(discord, channel, author, model); } internal virtual void Update(Model model) { + Type = model.Type; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; if (model.Content.IsSpecified) Content = model.Content.Value; - } + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.GetValueOrDefault() + }; + } + + if (model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + InternalChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + + if (model.Components.IsSpecified) + { + Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(y => + { + switch (y.Type) + { + case ComponentType.Button: + { + var parsed = (API.ButtonComponent)y; + return new Discord.ButtonComponent( + parsed.Style, + parsed.Label.GetValueOrDefault(), + parsed.Emote.IsSpecified + ? parsed.Emote.Value.Id.HasValue + ? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault()) + : new Emoji(parsed.Emote.Value.Name) + : null, + parsed.CustomId.GetValueOrDefault(), + parsed.Url.GetValueOrDefault(), + parsed.Disabled.GetValueOrDefault()); + } + case ComponentType.SelectMenu: + { + var parsed = (API.SelectMenuComponent)y; + return new SelectMenuComponent( + parsed.CustomId, + parsed.Options.Select(z => new SelectMenuOption( + z.Label, + z.Value, + z.Description.GetValueOrDefault(), + z.Emoji.IsSpecified + ? z.Emoji.Value.Id.HasValue + ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) + : new Emoji(z.Emoji.Value.Name) + : null, + z.Default.ToNullable())).ToList(), + parsed.Placeholder.GetValueOrDefault(), + parsed.MinValues, + parsed.MaxValues, + parsed.Disabled + ); + } + default: + return null; + } + }).ToList())).ToImmutableArray(); + } + else + Components = new List(); + + if (model.Flags.IsSpecified) + Flags = model.Flags.Value; + + if (model.Reactions.IsSpecified) + { + var value = model.Reactions.Value; + if (value.Length > 0) + { + var reactions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + reactions.Add(RestReaction.Create(value[i])); + _reactions = reactions.ToImmutable(); + } + else + _reactions = ImmutableArray.Create(); + } + else + _reactions = ImmutableArray.Create(); + + if (model.Interaction.IsSpecified) + { + Interaction = new MessageInteraction(model.Interaction.Value.Id, + model.Interaction.Value.Type, + model.Interaction.Value.Name, + RestUser.Create(Discord, model.Interaction.Value.User)); + } + + if (model.UserMentions.IsSpecified) + { + var value = model.UserMentions.Value; + if (value.Length > 0) + { + var newMentions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + { + var val = value[i]; + if (val != null) + newMentions.Add(RestUser.Create(Discord, val)); + } + _userMentions = newMentions.ToImmutable(); + } + } + } + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => MessageHelper.DeleteAsync(this, Discord, options); + /// + /// Gets the of the message. + /// + /// + /// A string that is the of the message. + /// public override string ToString() => Content; - MessageType IMessage.Type => MessageType.Default; IUser IMessage.Author => Author; + /// IReadOnlyCollection IMessage.Attachments => Attachments; + /// IReadOnlyCollection IMessage.Embeds => Embeds; + /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + + /// + IReadOnlyCollection IMessage.Components => Components; + + /// + IMessageInteraction IMessage.Interaction => Interaction; + + /// + IReadOnlyCollection IMessage.Stickers => Stickers; + + /// + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); + + /// + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, userId, emote, Discord, options); + /// + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// + public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); + /// + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 6d3f72419..c38efe32d 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -1,11 +1,21 @@ -using Model = Discord.API.Reaction; +using Model = Discord.API.Reaction; namespace Discord.Rest { + /// + /// Represents a REST reaction object. + /// public class RestReaction : IReaction { + /// public IEmote Emote { get; } + /// + /// Gets the number of reactions added. + /// public int Count { get; } + /// + /// Gets whether the reactions is added by the user. + /// public bool Me { get; } internal RestReaction(IEmote emote, int count, bool me) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index b9dda08ae..1c59d4f45 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -1,13 +1,14 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Message; namespace Discord.Rest { + /// + /// Represents a REST-based system message. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestSystemMessage : RestMessage, ISystemMessage { - public MessageType Type { get; private set; } - internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id, channel, author, MessageSource.System) { @@ -21,8 +22,6 @@ namespace Discord.Rest internal override void Update(Model model) { base.Update(model); - - Type = model.Type; } private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 7354cc4af..083a8e72c 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -2,38 +2,56 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Message; namespace Discord.Rest { + /// + /// Represents a REST-based message sent by a user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestUserMessage : RestMessage, IUserMessage { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ImmutableArray _attachments; - private ImmutableArray _embeds; - private ImmutableArray _tags; - private ImmutableArray _reactions; - + private IUserMessage _referencedMessage; + private ImmutableArray _attachments = ImmutableArray.Create(); + private ImmutableArray _embeds = ImmutableArray.Create(); + private ImmutableArray _tags = ImmutableArray.Create(); + private ImmutableArray _roleMentionIds = ImmutableArray.Create(); + private ImmutableArray _stickers = ImmutableArray.Create(); + + /// public override bool IsTTS => _isTTS; + /// public override bool IsPinned => _isPinned; + /// + public override bool IsSuppressed => Flags.HasValue && Flags.Value.HasFlag(MessageFlags.SuppressEmbeds); + /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + /// + public override bool MentionedEveryone => _isMentioningEveryone; + /// public override IReadOnlyCollection Attachments => _attachments; + /// public override IReadOnlyCollection Embeds => _embeds; + /// public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); - public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); - public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + /// + public override IReadOnlyCollection MentionedRoleIds => _roleMentionIds; + /// public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); + /// + public override IReadOnlyCollection Stickers => _stickers; + /// + public IUserMessage ReferencedMessage => _referencedMessage; internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); @@ -52,6 +70,8 @@ namespace Discord.Rest _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; + if (model.RoleMentions.IsSpecified) + _roleMentionIds = model.RoleMentions.Value.ToImmutableArray(); if (model.Attachments.IsSpecified) { @@ -81,77 +101,71 @@ namespace Discord.Rest _embeds = ImmutableArray.Create(); } - ImmutableArray mentions = ImmutableArray.Create(); - if (model.UserMentions.IsSpecified) + var guildId = (Channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + if (model.Content.IsSpecified) { - var value = model.UserMentions.Value; - if (value.Length > 0) - { - var newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - { - var val = value[i]; - if (val.Object != null) - newMentions.Add(RestUser.Create(Discord, val.Object)); - } - mentions = newMentions.ToImmutable(); - } + var text = model.Content.Value; + _tags = MessageHelper.ParseTags(text, null, guild, MentionedUsers); + model.Content = text; + } + + if (model.ReferencedMessage.IsSpecified && model.ReferencedMessage.Value != null) + { + var refMsg = model.ReferencedMessage.Value; + IUser refMsgAuthor = MessageHelper.GetAuthor(Discord, guild, refMsg.Author.Value, refMsg.WebhookId.ToNullable()); + _referencedMessage = RestUserMessage.Create(Discord, Channel, refMsgAuthor, refMsg); } - if (model.Reactions.IsSpecified) + if (model.StickerItems.IsSpecified) { - var value = model.Reactions.Value; + var value = model.StickerItems.Value; if (value.Length > 0) { - var reactions = ImmutableArray.CreateBuilder(value.Length); + var stickers = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) - reactions.Add(RestReaction.Create(value[i])); - _reactions = reactions.ToImmutable(); + stickers.Add(new StickerItem(Discord, value[i])); + _stickers = stickers.ToImmutable(); } else - _reactions = ImmutableArray.Create(); - } - else - _reactions = ImmutableArray.Create(); - - if (model.Content.IsSpecified) - { - var text = model.Content.Value; - var guildId = (Channel as IGuildChannel)?.GuildId; - var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; - _tags = MessageHelper.ParseTags(text, null, guild, mentions); - model.Content = text; + _stickers = ImmutableArray.Create(); } } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public Task AddReactionAsync(IEmote emote, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emote, Discord, options); - public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); - public Task RemoveAllReactionsAsync(RequestOptions options = null) - => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); - - + /// public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); + /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is INewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Sticker.cs b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs new file mode 100644 index 000000000..accdbe66a --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Model = Discord.API.Sticker; + +namespace Discord.Rest +{ + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Sticker : RestEntity, ISticker + { + /// + public ulong PackId { get; protected set; } + /// + public string Name { get; protected set; } + /// + public string Description { get; protected set; } + /// + public IReadOnlyCollection Tags { get; protected set; } + /// + public StickerType Type { get; protected set; } + /// + public bool? IsAvailable { get; protected set; } + /// + public int? SortOrder { get; protected set; } + /// + public StickerFormatType Format { get; protected set; } + + /// + public string GetStickerUrl() + => CDN.GetStickerUrl(Id, Format); + + internal Sticker(BaseDiscordClient client, ulong id) + : base(client, id) { } + internal static Sticker Create(BaseDiscordClient client, Model model) + { + var entity = new Sticker(client, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + PackId = model.PackId; + Name = model.Name; + Description = model.Description; + Tags = model.Tags.IsSpecified ? model.Tags.Value.Split(',').Select(x => x.Trim()).ToArray() : Array.Empty(); + Type = model.Type; + SortOrder = model.SortValue; + IsAvailable = model.Available; + Format = model.FormatType; + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs b/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs new file mode 100644 index 000000000..0ce4f634b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/StickerItem.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Model = Discord.API.StickerItem; + +namespace Discord.Rest +{ + /// + /// Represents a partial sticker received in a message. + /// + public class StickerItem : RestEntity, IStickerItem + { + /// + public string Name { get; } + + /// + public StickerFormatType Format { get; } + + internal StickerItem(BaseDiscordClient client, Model model) + : base(client, model.Id) + { + Name = model.Name; + Format = model.FormatType; + } + + /// + /// Resolves this sticker item by fetching the from the API. + /// + /// + /// A task representing the download operation, the result of the task is a sticker object. + /// + public async Task ResolveStickerAsync() + { + var model = await Discord.ApiClient.GetStickerAsync(Id); + + return model.GuildId.IsSpecified + ? CustomSticker.Create(Discord, model, model.GuildId.Value, model.User.IsSpecified ? model.User.Value.Id : null) + : Sticker.Create(Discord, model); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index 827c33cf7..beec52433 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -1,25 +1,47 @@ -using System; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Application; namespace Discord.Rest { + /// + /// Represents a REST-based entity that contains information about a Discord application created via the developer portal. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestApplication : RestEntity, IApplication { protected string _iconId; - + + /// public string Name { get; private set; } + /// public string Description { get; private set; } - public string[] RPCOrigins { get; private set; } - public ulong Flags { get; private set; } + /// + public IReadOnlyCollection RPCOrigins { get; private set; } + /// + public ApplicationFlags Flags { get; private set; } + /// + public bool IsBotPublic { get; private set; } + /// + public bool BotRequiresCodeGrant { get; private set; } + /// + public ITeam Team { get; private set; } + /// public IUser Owner { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); + public ApplicationInstallParams InstallParams { get; private set; } + + public IReadOnlyCollection Tags { get; private set; } + internal RestApplication(BaseDiscordClient discord, ulong id) : base(discord, id) { @@ -33,16 +55,24 @@ namespace Discord.Rest internal void Update(Model model) { Description = model.Description; - RPCOrigins = model.RPCOrigins; + RPCOrigins = model.RPCOrigins.IsSpecified ? model.RPCOrigins.Value.ToImmutableArray() : ImmutableArray.Empty; Name = model.Name; _iconId = model.Icon; + IsBotPublic = model.IsBotPublic; + BotRequiresCodeGrant = model.BotRequiresCodeGrant; + Tags = model.Tags.GetValueOrDefault(null)?.ToImmutableArray() ?? ImmutableArray.Empty; + var installParams = model.InstallParams.GetValueOrDefault(null); + InstallParams = new ApplicationInstallParams(installParams?.Scopes ?? new string[0], (GuildPermission?)installParams?.Permission ?? null); if (model.Flags.IsSpecified) - Flags = model.Flags.Value; //TODO: Do we still need this? + Flags = model.Flags.Value; if (model.Owner.IsSpecified) Owner = RestUser.Create(Discord, model.Owner.Value); + if (model.Team != null) + Team = RestTeam.Create(Discord, model.Team); } + /// Unable to update this object from a different application token. public async Task UpdateAsync() { var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); @@ -51,6 +81,12 @@ namespace Discord.Rest Update(response); } + /// + /// Gets the name of the application. + /// + /// + /// Name of the application. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; } diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index 486f41b9e..a2ad4fd77 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -1,24 +1,46 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Role; namespace Discord.Rest { + /// + /// Represents a REST-based role. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestRole : RestEntity, IRole { + #region RestRole internal IGuild Guild { get; } + /// public Color Color { get; private set; } + /// public bool IsHoisted { get; private set; } + /// public bool IsManaged { get; private set; } + /// public bool IsMentionable { get; private set; } + /// public string Name { get; private set; } + /// + public string Icon { get; private set; } + /// /> + public Emoji Emoji { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public int Position { get; private set; } + /// + public RoleTags Tags { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets if this role is the @everyone role of the guild or not. + /// public bool IsEveryone => Id == Guild.Id; + /// public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) @@ -41,22 +63,49 @@ namespace Discord.Rest Position = model.Position; Color = new Color(model.Color); Permissions = new GuildPermissions(model.Permissions); + if (model.Tags.IsSpecified) + Tags = model.Tags.Value.ToEntity(); + + if (model.Icon.IsSpecified) + { + Icon = model.Icon.Value; + } + + if (model.Emoji.IsSpecified) + { + Emoji = new Emoji(model.Emoji.Value); + } } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) - { + { var model = await RoleHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); + /// + public string GetIconUrl() + => CDN.GetGuildRoleIconUrl(Id, Icon); + + /// public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + /// + /// Gets the name of the role. + /// + /// + /// A string that is the name of the role. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + #endregion - //IRole + #region IRole + /// IGuild IRole.Guild { get @@ -66,5 +115,6 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index d570f078b..d8552f869 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Model = Discord.API.Role; using BulkParams = Discord.API.Rest.ModifyGuildRolesParams; @@ -7,7 +7,7 @@ namespace Discord.Rest { internal static class RoleHelper { - //General + #region General public static async Task DeleteAsync(IRole role, BaseDiscordClient client, RequestOptions options) { @@ -18,13 +18,20 @@ namespace Discord.Rest { var args = new RoleProperties(); func(args); + + if (args.Icon.IsSpecified) + { + role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons); + } + var apiArgs = new API.Rest.ModifyGuildRoleParams { Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create(), Hoist = args.Hoist, Mentionable = args.Mentionable, Name = args.Name, - Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create() + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create(), + Icon = args.Icon.IsSpecified ? args.Icon.Value.ToModel() : Optional.Unspecified }; var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); @@ -36,5 +43,6 @@ namespace Discord.Rest } return model; } + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs b/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs new file mode 100644 index 000000000..43c9417cc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Team; + +namespace Discord.Rest +{ + public class RestTeam : RestEntity, ITeam + { + /// + public string IconUrl => _iconId != null ? CDN.GetTeamIconUrl(Id, _iconId) : null; + /// + public IReadOnlyList TeamMembers { get; private set; } + /// + public string Name { get; private set; } + /// + public ulong OwnerUserId { get; private set; } + + private string _iconId; + + internal RestTeam(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestTeam Create(BaseDiscordClient discord, Model model) + { + var entity = new RestTeam(discord, model.Id); + entity.Update(model); + return entity; + } + internal virtual void Update(Model model) + { + if (model.Icon.IsSpecified) + _iconId = model.Icon.Value; + Name = model.Name; + OwnerUserId = model.OwnerUserId; + TeamMembers = model.TeamMembers.Select(x => new RestTeamMember(Discord, x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs b/src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs new file mode 100644 index 000000000..322bb6a3f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Teams/RestTeamMember.cs @@ -0,0 +1,30 @@ +using System; +using Model = Discord.API.TeamMember; + +namespace Discord.Rest +{ + public class RestTeamMember : ITeamMember + { + /// + public MembershipState MembershipState { get; } + /// + public string[] Permissions { get; } + /// + public ulong TeamId { get; } + /// + public IUser User { get; } + + internal RestTeamMember(BaseDiscordClient discord, Model model) + { + MembershipState = model.MembershipState switch + { + API.MembershipState.Invited => MembershipState.Invited, + API.MembershipState.Accepted => MembershipState.Accepted, + _ => throw new InvalidOperationException("Invalid membership state"), + }; + Permissions = model.Permissions; + TeamId = model.TeamId; + User = RestUser.Create(discord, model.User); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index b8b83be3e..1afb813c0 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -1,17 +1,22 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using Model = Discord.API.Connection; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestConnection : IConnection { + /// public string Id { get; } + /// public string Type { get; } + /// public string Name { get; } + /// public bool IsRevoked { get; } + /// public IReadOnlyCollection IntegrationIds { get; } internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection integrationIds) @@ -28,6 +33,12 @@ namespace Discord return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray()); } + /// + /// Gets the name of the connection. + /// + /// + /// Name of the connection. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs index 951bd2e7c..40e45b135 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -1,11 +1,16 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using Model = Discord.API.User; namespace Discord.Rest { + /// + /// Represents a REST-based group user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGroupUser : RestUser, IGroupUser { + #region RestGroupUser internal RestGroupUser(BaseDiscordClient discord, ulong id) : base(discord, id) { @@ -16,14 +21,27 @@ namespace Discord.Rest entity.Update(model); return entity; } - - //IVoiceState +#endregion + + #region IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + /// + DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 3c47587cb..2e184d32e 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -8,18 +8,47 @@ using Model = Discord.API.GuildMember; namespace Discord.Rest { + /// + /// Represents a REST-based guild user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGuildUser : RestUser, IGuildUser { + #region RestGuildUser + private long? _premiumSinceTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; + /// public string Nickname { get; private set; } + /// + public string GuildAvatarId { get; private set; } internal IGuild Guild { get; private set; } + /// public bool IsDeafened { get; private set; } + /// public bool IsMuted { get; private set; } - + /// + public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); + /// public ulong GuildId => Guild.Id; + /// + public bool? IsPending { get; private set; } + /// + public int Hierarchy + { + get + { + if (Guild.OwnerId == Id) + return int.MaxValue; + + var orderedRoles = Guild.Roles.OrderByDescending(x => x.Position); + return orderedRoles.Where(x => RoleIds.Contains(x.Id)).Max(x => x.Position); + } + } + + /// + /// Resolving permissions requires the parent guild to be downloaded. public GuildPermissions GuildPermissions { get @@ -29,8 +58,10 @@ namespace Discord.Rest return new GuildPermissions(Permissions.ResolveGuild(Guild, this)); } } + /// public IReadOnlyCollection RoleIds => _roleIds; + /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) @@ -51,12 +82,18 @@ namespace Discord.Rest _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Avatar.IsSpecified) + GuildAvatarId = model.Avatar.Value; if (model.Deaf.IsSpecified) IsDeafened = model.Deaf.Value; if (model.Mute.IsSpecified) IsMuted = model.Mute.Value; if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.Pending.IsSpecified) + IsPending = model.Pending.Value; } private void UpdateRoles(ulong[] roleIds) { @@ -67,11 +104,13 @@ namespace Discord.Rest _roleIds = roles.ToImmutable(); } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetGuildMemberAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var args = await UserHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); @@ -86,28 +125,48 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) UpdateRoles(args.RoleIds.Value.ToArray()); } + /// public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) + => AddRolesAsync(new[] { roleId }, options); + /// public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRolesAsync(new[] { role }, options); + => AddRoleAsync(role.Id, options); + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roleIds, options); /// public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Discord, roles, options); + => AddRolesAsync(roles.Select(x => x.Id), options); + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) + => RemoveRolesAsync(new[] { roleId }, options); /// public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRolesAsync(new[] { role }, options); + => RemoveRoleAsync(role.Id, options); + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roleIds, options); /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + => RemoveRolesAsync(roles.Select(x => x.Id)); + /// + /// Resolving permissions requires the parent guild to be downloaded. public ChannelPermissions GetPermissions(IGuildChannel channel) { var guildPerms = GuildPermissions; return new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, guildPerms.RawValue)); } - //IGuildUser + public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetGuildUserAvatarUrl(Id, GuildId, GuildAvatarId, size, format); +#endregion + + #region IGuildUser + /// IGuild IGuildUser.Guild { get @@ -117,12 +176,23 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + #endregion - //IVoiceState + #region IVoiceState + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + /// + DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs index ab5ec4a3b..b5ef01c53 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -1,16 +1,28 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord.Rest { + /// + /// Represents the logged-in REST-based user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestSelfUser : RestUser, ISelfUser { + /// public string Email { get; private set; } + /// public bool IsVerified { get; private set; } + /// public bool IsMfaEnabled { get; private set; } + /// + public UserProperties Flags { get; private set; } + /// + public PremiumType PremiumType { get; private set; } + /// + public string Locale { get; private set; } internal RestSelfUser(BaseDiscordClient discord, ulong id) : base(discord, id) @@ -22,6 +34,7 @@ namespace Discord.Rest entity.Update(model); return entity; } + /// internal override void Update(Model model) { base.Update(model); @@ -32,8 +45,16 @@ namespace Discord.Rest IsVerified = model.Verified.Value; if (model.MfaEnabled.IsSpecified) IsMfaEnabled = model.MfaEnabled.Value; + if (model.Flags.IsSpecified) + Flags = (UserProperties)model.Flags.Value; + if (model.PremiumType.IsSpecified) + PremiumType = model.PremiumType.Value; + if (model.Locale.IsSpecified) + Locale = model.Locale.Value; } + /// + /// Unable to update this object using a different token. public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetMyUserAsync(options).ConfigureAwait(false); @@ -42,6 +63,8 @@ namespace Discord.Rest Update(model); } + /// + /// Unable to modify this object using a different token. public async Task ModifyAsync(Action func, RequestOptions options = null) { if (Id != Discord.CurrentUser.Id) diff --git a/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs new file mode 100644 index 000000000..82830dafd --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.ThreadMember; + +namespace Discord.Rest +{ + /// + /// Represents a thread user received over the REST api. + /// + public class RestThreadUser : RestEntity + { + /// + /// Gets the this user is in. + /// + public IThreadChannel Thread { get; } + + /// + /// Gets the timestamp for when this user joined this thread. + /// + public DateTimeOffset JoinedAt { get; private set; } + + /// + /// Gets the guild this user is in. + /// + public IGuild Guild { get; } + + internal RestThreadUser(BaseDiscordClient discord, IGuild guild, IThreadChannel channel, ulong id) + : base(discord, id) + { + Guild = guild; + Thread = channel; + } + + internal static RestThreadUser Create(BaseDiscordClient client, IGuild guild, Model model, IThreadChannel channel) + { + var entity = new RestThreadUser(client, guild, channel, model.UserId.Value); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + JoinedAt = model.JoinTimestamp; + } + + /// + /// Gets the guild user for this thread user. + /// + /// + /// A task representing the asynchronous get operation. The task returns a + /// that represents the current thread user. + /// + public Task GetGuildUser() + => Guild.GetUserAsync(Id); + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index c484986b1..70f990fe7 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -1,23 +1,51 @@ using System; +using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Threading.Tasks; using Model = Discord.API.User; +using EventUserModel = Discord.API.GuildScheduledEventUser; +using System.Collections.Generic; namespace Discord.Rest { + /// + /// Represents a REST-based user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestUser : RestEntity, IUser, IUpdateable { + #region RestUser + /// public bool IsBot { get; private set; } + /// public string Username { get; private set; } + /// public ushort DiscriminatorValue { get; private set; } + /// public string AvatarId { get; private set; } + /// + public string BannerId { get; private set; } + /// + public Color? AccentColor { get; private set; } + /// + public UserProperties? PublicFlags { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string Discriminator => DiscriminatorValue.ToString("D4"); + /// public string Mention => MentionUtils.MentionUser(Id); + /// public virtual IActivity Activity => null; + /// public virtual UserStatus Status => UserStatus.Offline; + /// + public virtual IReadOnlyCollection ActiveClients => ImmutableHashSet.Empty; + /// + public virtual IReadOnlyCollection Activities => ImmutableList.Empty; + /// public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) @@ -36,38 +64,79 @@ namespace Discord.Rest entity.Update(model); return entity; } + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, EventUserModel model) + { + if (model.Member.IsSpecified) + { + var member = model.Member.Value; + member.User = model.User; + return RestGuildUser.Create(discord, guild, member); + } + else + return RestUser.Create(discord, model.User); + } + internal virtual void Update(Model model) { if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; + if (model.Banner.IsSpecified) + BannerId = model.Banner.Value; + if (model.AccentColor.IsSpecified) + AccentColor = model.AccentColor.Value; if (model.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); if (model.Bot.IsSpecified) IsBot = model.Bot.Value; if (model.Username.IsSpecified) Username = model.Username.Value; + if (model.PublicFlags.IsSpecified) + PublicFlags = model.PublicFlags.Value; } + /// public virtual async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); Update(model); } - public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + /// + /// Creates a direct message channel to this user. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a rest DM channel where the user is the recipient. + /// + public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + /// + public string GetBannerUrl(ImageFormat format = ImageFormat.Auto, ushort size = 256) + => CDN.GetUserBannerUrl(Id, BannerId, size, format); + + /// public string GetDefaultAvatarUrl() => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + /// + /// Gets the Username#Discriminator of the user. + /// + /// + /// A string that resolves to Username#Discriminator of the user. + /// + public override string ToString() => Format.UsernameAndDiscriminator(this); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; + #endregion - //IUser - async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) - => await GetOrCreateDMChannelAsync(options); + #region IUser + /// + async Task IUser.CreateDMChannelAsync(RequestOptions options) + => await CreateDMChannelAsync(options).ConfigureAwait(false); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index bb44f2777..2cd19da41 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -10,10 +10,16 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestWebhookUser : RestUser, IWebhookUser { + #region RestWebhookUser + /// public ulong WebhookId { get; } internal IGuild Guild { get; } + /// + public DateTimeOffset? PremiumSince { get; private set; } + /// public override bool IsWebhook => true; + /// public ulong GuildId => Guild.Id; internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) @@ -28,8 +34,10 @@ namespace Discord.Rest entity.Update(model); return entity; } - - //IGuildUser +#endregion + + #region IGuildUser + /// IGuild IGuildUser.Guild { get @@ -39,45 +47,77 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// DateTimeOffset? IGuildUser.JoinedAt => null; + /// string IGuildUser.Nickname => null; + /// + string IGuildUser.GuildAvatarId => null; + /// + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; + /// + bool? IGuildUser.IsPending => null; + /// + int IGuildUser.Hierarchy => 0; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(string reason, RequestOptions options) - { + /// + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); - } - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) - { - throw new NotSupportedException("Webhook users cannot be modified."); - } - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) - { + /// + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => + throw new NotSupportedException("Webhook users cannot be modified."); + /// + Task IGuildUser.AddRoleAsync(ulong role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) - { + /// + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) - { + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) - { + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } + /// + Task IGuildUser.RemoveRoleAsync(ulong role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + #endregion - //IVoiceState + #region IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + /// + DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index dfb81ff2c..3a19fcfc1 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -39,7 +39,7 @@ namespace Discord.Rest }; if (args.Channel.IsSpecified) - apiArgs.ChannelId = args.Channel.Value.Id; + apiArgs.ChannelId = args.Channel.Value?.Id; else if (args.ChannelId.IsSpecified) apiArgs.ChannelId = args.ChannelId.Value; @@ -73,16 +73,16 @@ namespace Discord.Rest return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false)); } - public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roleIds, RequestOptions options) { - foreach (var role in roles) - await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options); + foreach (var roleId in roleIds) + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false); } - public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roleIds, RequestOptions options) { - foreach (var role in roles) - await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options); + foreach (var roleId in roleIds) + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs index 47cc50a9c..f40b786cd 100644 --- a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Webhook; @@ -8,17 +8,27 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestWebhook : RestEntity, IWebhook, IUpdateable { + #region RestWebhook internal IGuild Guild { get; private set; } internal ITextChannel Channel { get; private set; } - public ulong ChannelId { get; } + /// public string Token { get; } + /// + public ulong ChannelId { get; private set; } + /// public string Name { get; private set; } + /// public string AvatarId { get; private set; } + /// public ulong? GuildId { get; private set; } + /// public IUser Creator { get; private set; } + /// + public ulong? ApplicationId { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) @@ -49,6 +59,8 @@ namespace Discord.Rest internal void Update(Model model) { + if (ChannelId != model.ChannelId) + ChannelId = model.ChannelId; if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; if (model.Creator.IsSpecified) @@ -57,14 +69,18 @@ namespace Discord.Rest GuildId = model.GuildId.Value; if (model.Name.IsSpecified) Name = model.Name.Value; + + ApplicationId = model.ApplicationId; } + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); @@ -74,18 +90,24 @@ namespace Discord.Rest Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => WebhookHelper.DeleteAsync(this, Discord, options); public override string ToString() => $"Webhook: {Name}:{Id}"; private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + #endregion - //IWebhook + #region IWebhook + /// IGuild IWebhook.Guild => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// ITextChannel IWebhook.Channel => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// Task IWebhook.ModifyAsync(Action func, RequestOptions options) => ModifyAsync(func, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs index 50e9cab78..0b61b6c22 100644 --- a/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs +++ b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -33,6 +33,5 @@ namespace Discord.Rest { await client.ApiClient.DeleteWebhookAsync(webhook.Id, options).ConfigureAwait(false); } - } } diff --git a/src/Discord.Net.Rest/Extensions/ClientExtensions.cs b/src/Discord.Net.Rest/Extensions/ClientExtensions.cs new file mode 100644 index 000000000..647c7d4a1 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/ClientExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public static class ClientExtensions + { + /// + /// Adds a user to the specified guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The Discord client object. + /// The snowflake identifier of the guild. + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + public static Task AddGuildUserAsync(this BaseDiscordClient client, ulong guildId, ulong userId, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(guildId, client, userId, accessToken, func, options); + } +} diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 74b05dacd..9e1e5102f 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -1,15 +1,27 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Discord.Rest { internal static class EntityExtensions { - public static GuildEmote ToEntity(this API.Emoji model) + public static IEmote ToIEmote(this API.Emoji model) { - return new GuildEmote(model.Id.Value, model.Name, model.Animated.GetValueOrDefault(), model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + if (model.Id.HasValue) + return model.ToEntity(); + return new Emoji(model.Name); } + public static GuildEmote ToEntity(this API.Emoji model) + => new GuildEmote(model.Id.Value, + model.Name, + model.Animated.GetValueOrDefault(), + model.Managed, + model.RequireColons, + ImmutableArray.Create(model.Roles), + model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); + public static Embed ToEntity(this API.Embed model) { return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, @@ -22,6 +34,13 @@ namespace Discord.Rest model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); } + public static RoleTags ToEntity(this API.RoleTags model) + { + return new RoleTags( + model.BotId.IsSpecified ? model.BotId.Value : null, + model.IntegrationId.IsSpecified ? model.IntegrationId.Value : null, + model.IsPremiumSubscriber.GetValueOrDefault(false) ?? false); + } public static API.Embed ToModel(this Embed entity) { if (entity == null) return null; @@ -49,6 +68,36 @@ namespace Discord.Rest model.Video = entity.Video.Value.ToModel(); return model; } + + public static API.AllowedMentions ToModel(this AllowedMentions entity) + { + if (entity == null) return null; + return new API.AllowedMentions() + { + Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), + Roles = entity.RoleIds?.ToArray(), + Users = entity.UserIds?.ToArray(), + RepliedUser = entity.MentionRepliedUser ?? Optional.Create(), + }; + } + public static API.MessageReference ToModel(this MessageReference entity) + { + return new API.MessageReference() + { + ChannelId = entity.InternalChannelId, + GuildId = entity.GuildId, + MessageId = entity.MessageId, + }; + } + public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) + { + if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone)) + yield return "everyone"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Roles)) + yield return "roles"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) + yield return "users"; + } public static EmbedAuthor ToEntity(this API.EmbedAuthor model) { return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); diff --git a/src/Discord.Net.Rest/Net/BadSignatureException.cs b/src/Discord.Net.Rest/Net/BadSignatureException.cs new file mode 100644 index 000000000..08672df8e --- /dev/null +++ b/src/Discord.Net.Rest/Net/BadSignatureException.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public class BadSignatureException : Exception + { + internal BadSignatureException() : base("Failed to verify authenticity of message: public key doesnt match signature") + { + + } + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs b/src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs index 3cededb7b..ce2e9b1f7 100644 --- a/src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ArrayConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Collections.Generic; diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index 8a3c1037b..91ba22460 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -10,9 +10,10 @@ namespace Discord.Net.Converters { internal class DiscordContractResolver : DefaultContractResolver { + #region DiscordContractResolver private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); - private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); - + private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); @@ -57,8 +58,9 @@ namespace Discord.Net.Converters else if (genericType == typeof(EntityOrId<>)) return MakeGenericConverter(property, propInfo, typeof(UInt64EntityOrIdConverter<>), type.GenericTypeArguments[0], depth); } + #endregion - //Primitives + #region Primitives bool hasInt53 = propInfo.GetCustomAttribute() != null; if (!hasInt53) { @@ -73,14 +75,22 @@ namespace Discord.Net.Converters } //Enums - if (type == typeof(PermissionTarget)) - return PermissionTargetConverter.Instance; if (type == typeof(UserStatus)) return UserStatusConverter.Instance; + if (type == typeof(EmbedType)) + return EmbedTypeConverter.Instance; //Special if (type == typeof(API.Image)) return ImageConverter.Instance; + if (typeof(IMessageComponent).IsAssignableFrom(type)) + return MessageComponentConverter.Instance; + if (type == typeof(API.Interaction)) + return InteractionConverter.Instance; + if (type == typeof(API.DiscordError)) + return DiscordErrorConverter.Instance; + if (type == typeof(GuildFeatures)) + return GuildFeaturesConverter.Instance; //Entities var typeInfo = type.GetTypeInfo(); @@ -103,5 +113,6 @@ namespace Discord.Net.Converters var innerConverter = GetConverter(property, propInfo, innerType, depth + 1); return genericType.DeclaredConstructors.First().Invoke(new object[] { innerConverter }) as JsonConverter; } + #endregion } } diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordErrorConverter.cs b/src/Discord.Net.Rest/Net/Converters/DiscordErrorConverter.cs new file mode 100644 index 000000000..772ddc6b2 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/DiscordErrorConverter.cs @@ -0,0 +1,88 @@ +using Discord.API; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Net.Converters +{ + internal class DiscordErrorConverter : JsonConverter + { + public static DiscordErrorConverter Instance + => new DiscordErrorConverter(); + + public override bool CanConvert(Type objectType) => objectType == typeof(DiscordError); + + public override bool CanRead => true; + public override bool CanWrite => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var obj = JObject.Load(reader); + var err = new API.DiscordError(); + + + var result = obj.GetValue("errors", StringComparison.OrdinalIgnoreCase); + result?.Parent.Remove(); + + // Populate the remaining properties. + using (var subReader = obj.CreateReader()) + { + serializer.Populate(subReader, err); + } + + if (result != null) + { + var innerReader = result.CreateReader(); + + var errors = ReadErrors(innerReader); + err.Errors = errors.ToArray(); + } + + return err; + } + + private List ReadErrors(JsonReader reader, string path = "") + { + List errs = new List(); + var obj = JObject.Load(reader); + var props = obj.Properties(); + foreach (var prop in props) + { + if (prop.Name == "_errors" && path == "") // root level error + { + errs.Add(new ErrorDetails() + { + Name = Optional.Unspecified, + Errors = prop.Value.ToObject() + }); + } + else if (prop.Name == "_errors") // path errors (not root level) + { + errs.Add(new ErrorDetails() + { + Name = path, + Errors = prop.Value.ToObject() + }); + } + else if(int.TryParse(prop.Name, out var i)) // array value + { + var r = prop.Value.CreateReader(); + errs.AddRange(ReadErrors(r, path + $"[{i}]")); + } + else // property name + { + var r = prop.Value.CreateReader(); + errs.AddRange(ReadErrors(r, path + $"{(path != "" ? "." : "")}{prop.Name[0].ToString().ToUpper() + new string(prop.Name.Skip(1).ToArray())}")); + } + } + + return errs; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs b/src/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs new file mode 100644 index 000000000..cacd2e2e1 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs @@ -0,0 +1,64 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + internal class EmbedTypeConverter : JsonConverter + { + public static readonly EmbedTypeConverter Instance = new EmbedTypeConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return (string)reader.Value switch + { + "rich" => EmbedType.Rich, + "link" => EmbedType.Link, + "video" => EmbedType.Video, + "image" => EmbedType.Image, + "gifv" => EmbedType.Gifv, + "article" => EmbedType.Article, + "tweet" => EmbedType.Tweet, + "html" => EmbedType.Html, + // TODO 2.2 EmbedType.News + _ => EmbedType.Unknown, + }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((EmbedType)value) + { + case EmbedType.Rich: + writer.WriteValue("rich"); + break; + case EmbedType.Link: + writer.WriteValue("link"); + break; + case EmbedType.Video: + writer.WriteValue("video"); + break; + case EmbedType.Image: + writer.WriteValue("image"); + break; + case EmbedType.Gifv: + writer.WriteValue("gifv"); + break; + case EmbedType.Article: + writer.WriteValue("article"); + break; + case EmbedType.Tweet: + writer.WriteValue("tweet"); + break; + case EmbedType.Html: + writer.WriteValue("html"); + break; + default: + throw new JsonSerializationException("Invalid embed type"); + } + } + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs b/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs new file mode 100644 index 000000000..9f82b440b --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/GuildFeaturesConverter.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Net.Converters +{ + internal class GuildFeaturesConverter : JsonConverter + { + public static GuildFeaturesConverter Instance + => new GuildFeaturesConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanWrite => false; + public override bool CanRead => true; + + + private Regex _readRegex = new Regex(@"_(\w)"); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var obj = JToken.Load(reader); + var arr = obj.ToObject(); + + GuildFeature features = GuildFeature.None; + List experimental = new(); + + foreach(var item in arr) + { + var name = _readRegex.Replace(item.ToLower(), (x) => + { + return x.Groups[1].Value.ToUpper(); + }); + + name = name[0].ToString().ToUpper() + new string(name.Skip(1).ToArray()); + + try + { + var result = (GuildFeature)Enum.Parse(typeof(GuildFeature), name); + + features |= result; + } + catch + { + experimental.Add(item); + } + } + + return new GuildFeatures(features, experimental.ToArray()); + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index a5a440d8b..941a35bf1 100644 --- a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Newtonsoft.Json; using Model = Discord.API.Image; @@ -13,6 +13,7 @@ namespace Discord.Net.Converters public override bool CanRead => true; public override bool CanWrite => true; + /// Cannot read from image. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new InvalidOperationException(); @@ -33,12 +34,14 @@ namespace Discord.Net.Converters } else { - var cloneStream = new MemoryStream(); - image.Stream.CopyTo(cloneStream); - bytes = new byte[cloneStream.Length]; - cloneStream.Position = 0; - cloneStream.Read(bytes, 0, bytes.Length); - length = (int)cloneStream.Length; + using (var cloneStream = new MemoryStream()) + { + image.Stream.CopyTo(cloneStream); + bytes = new byte[cloneStream.Length]; + cloneStream.Position = 0; + cloneStream.Read(bytes, 0, bytes.Length); + length = (int)cloneStream.Length; + } } string base64 = Convert.ToBase64String(bytes, 0, length); diff --git a/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs new file mode 100644 index 000000000..f7235841d --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/InteractionConverter.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace Discord.Net.Converters +{ + internal class InteractionConverter : JsonConverter + { + public static InteractionConverter Instance => new InteractionConverter(); + + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanConvert(Type objectType) => true; + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var obj = JObject.Load(reader); + var interaction = new API.Interaction(); + + + // Remove the data property for manual deserialization + var result = obj.GetValue("data", StringComparison.OrdinalIgnoreCase); + result?.Parent.Remove(); + + // Populate the remaining properties. + using (var subReader = obj.CreateReader()) + { + serializer.Populate(subReader, interaction); + } + + // Process the Result property + if (result != null) + { + switch (interaction.Type) + { + case InteractionType.ApplicationCommand: + { + var appCommandData = new API.ApplicationCommandInteractionData(); + serializer.Populate(result.CreateReader(), appCommandData); + interaction.Data = appCommandData; + } + break; + case InteractionType.MessageComponent: + { + var messageComponent = new API.MessageComponentInteractionData(); + serializer.Populate(result.CreateReader(), messageComponent); + interaction.Data = messageComponent; + } + break; + case InteractionType.ApplicationCommandAutocomplete: + { + var autocompleteData = new API.AutocompleteInteractionData(); + serializer.Populate(result.CreateReader(), autocompleteData); + interaction.Data = autocompleteData; + } + break; + } + } + else + interaction.Data = Optional.Unspecified; + + return interaction; + } + + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs new file mode 100644 index 000000000..0bf11a369 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace Discord.Net.Converters +{ + internal class MessageComponentConverter : JsonConverter + { + public static MessageComponentConverter Instance => new MessageComponentConverter(); + + public override bool CanRead => true; + public override bool CanWrite => false; + public override bool CanConvert(Type objectType) => true; + public override void WriteJson(JsonWriter writer, + object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + var messageComponent = default(IMessageComponent); + switch ((ComponentType)jsonObject["type"].Value()) + { + case ComponentType.ActionRow: + messageComponent = new API.ActionRowComponent(); + break; + case ComponentType.Button: + messageComponent = new API.ButtonComponent(); + break; + case ComponentType.SelectMenu: + messageComponent = new API.SelectMenuComponent(); + break; + } + serializer.Populate(jsonObject.CreateReader(), messageComponent); + return messageComponent; + } + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs deleted file mode 100644 index 0ed566a84..000000000 --- a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.Net.Converters -{ - internal class PermissionTargetConverter : JsonConverter - { - public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter(); - - public override bool CanConvert(Type objectType) => true; - 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.Rest/Net/Converters/UInt64EntityOrIdConverter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs index ae8cf2cb2..e55534833 100644 --- a/src/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs @@ -1,6 +1,7 @@ -using Discord.API; +using Discord.API; using Newtonsoft.Json; using System; +using System.Globalization; namespace Discord.Net.Converters { @@ -23,7 +24,7 @@ namespace Discord.Net.Converters { case JsonToken.String: case JsonToken.Integer: - return new EntityOrId(ulong.Parse(reader.ReadAsString())); + return new EntityOrId(ulong.Parse(reader.ReadAsString(), NumberStyles.None, CultureInfo.InvariantCulture)); default: T obj; if (_innerConverter != null) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs index 0b50cb166..876254fb9 100644 --- a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -27,7 +27,7 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - throw new NotImplementedException(); + writer.WriteValue(((DateTimeOffset)value).ToString("O")); } } } diff --git a/src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs b/src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs index c0a287c16..8a13e79a5 100644 --- a/src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace Discord.Net.Converters @@ -13,21 +13,15 @@ namespace Discord.Net.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - switch ((string)reader.Value) + return (string)reader.Value switch { - case "online": - return UserStatus.Online; - case "idle": - return UserStatus.Idle; - case "dnd": - return UserStatus.DoNotDisturb; - case "invisible": - return UserStatus.Invisible; //Should never happen - case "offline": - return UserStatus.Offline; - default: - throw new JsonSerializationException("Unknown user status"); - } + "online" => UserStatus.Online, + "idle" => UserStatus.Idle, + "dnd" => UserStatus.DoNotDisturb, + "invisible" => UserStatus.Invisible,//Should never happen + "offline" => UserStatus.Offline, + _ => throw new JsonSerializationException("Unknown user status"), + }; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 54fe3b681..1db743609 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -27,12 +28,14 @@ namespace Discord.Net.Rest { _baseUrl = baseUrl; +#pragma warning disable IDISP014 _client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, UseProxy = useProxy, }); +#pragma warning restore IDISP014 SetHeader("accept-encoding", "gzip, deflate"); _cancelToken = CancellationToken.None; @@ -82,6 +85,8 @@ namespace Discord.Net.Rest return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } + + /// Unsupported param type. public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); @@ -89,13 +94,15 @@ namespace Discord.Net.Rest { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + MemoryStream memoryStream = null; if (multipartParams != null) { foreach (var p in multipartParams) { switch (p.Value) { - case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } +#pragma warning disable IDISP004 + case string stringValue: { content.Add(new StringContent(stringValue, Encoding.UTF8, "text/plain"), p.Key); continue; } case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } case MultipartFile fileValue: @@ -103,46 +110,62 @@ namespace Discord.Net.Rest var stream = fileValue.Stream; if (!stream.CanSeek) { - var memoryStream = new MemoryStream(); + memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; +#pragma warning disable IDISP001 stream = memoryStream; +#pragma warning restore IDISP001 } - content.Add(new StreamContent(stream), p.Key, fileValue.Filename); + + var streamContent = new StreamContent(stream); + var extension = fileValue.Filename.Split('.').Last(); + + if(fileValue.ContentType != null) + streamContent.Headers.ContentType = new MediaTypeHeaderValue(fileValue.ContentType); + + content.Add(streamContent, p.Key, fileValue.Filename); +#pragma warning restore IDISP004 + continue; } - default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); } } } restRequest.Content = content; - return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + memoryStream?.Dispose(); + return result; } } private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; - HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - - var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken)) + { + cancelToken = cancelTokenSource.Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; - return new RestResponse(response.StatusCode, headers, stream); + return new RestResponse(response.StatusCode, headers, stream); + } } private static readonly HttpMethod Patch = new HttpMethod("PATCH"); private HttpMethod GetMethod(string method) { - switch (method) + return method switch { - 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}"); - } + "DELETE" => HttpMethod.Delete, + "GET" => HttpMethod.Get, + "PATCH" => Patch, + "POST" => HttpMethod.Post, + "PUT" => HttpMethod.Put, + _ => throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"), + }; } } } diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs index e0e776549..67b47096e 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -6,6 +6,7 @@ namespace Discord.Net.Rest { public static readonly RestClientProvider Instance = Create(); + /// The default RestClientProvider is not supported on this platform. public static RestClientProvider Create(bool useProxy = false) { return url => diff --git a/src/Discord.Net.Rest/Net/ED25519/Array16.cs b/src/Discord.Net.Rest/Net/ED25519/Array16.cs new file mode 100644 index 000000000..fca8616c5 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Array16.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Net.ED25519 +{ + // Array16 Salsa20 state + // Array16 SHA-512 block + internal struct Array16 + { + public T x0; + public T x1; + public T x2; + public T x3; + public T x4; + public T x5; + public T x6; + public T x7; + public T x8; + public T x9; + public T x10; + public T x11; + public T x12; + public T x13; + public T x14; + public T x15; + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Array8.cs b/src/Discord.Net.Rest/Net/ED25519/Array8.cs new file mode 100644 index 000000000..b563ac213 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Array8.cs @@ -0,0 +1,18 @@ +using System; + +namespace Discord.Net.ED25519 +{ + // Array8 Poly1305 key + // Array8 SHA-512 state/output + internal struct Array8 + { + public T x0; + public T x1; + public T x2; + public T x3; + public T x4; + public T x5; + public T x6; + public T x7; + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/ByteIntegerConverter.cs b/src/Discord.Net.Rest/Net/ED25519/ByteIntegerConverter.cs new file mode 100644 index 000000000..40c7624ba --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/ByteIntegerConverter.cs @@ -0,0 +1,55 @@ +using System; + +namespace Discord.Net.ED25519 +{ + // Loops? Arrays? Never heard of that stuff + // Library avoids unnecessary heap allocations and unsafe code + // so this ugly code becomes necessary :( + internal static class ByteIntegerConverter + { + public static ulong LoadBigEndian64(byte[] buf, int offset) + { + return + (ulong)(buf[offset + 7]) + | (((ulong)(buf[offset + 6])) << 8) + | (((ulong)(buf[offset + 5])) << 16) + | (((ulong)(buf[offset + 4])) << 24) + | (((ulong)(buf[offset + 3])) << 32) + | (((ulong)(buf[offset + 2])) << 40) + | (((ulong)(buf[offset + 1])) << 48) + | (((ulong)(buf[offset + 0])) << 56); + } + + public static void StoreBigEndian64(byte[] buf, int offset, ulong value) + { + buf[offset + 7] = unchecked((byte)value); + buf[offset + 6] = unchecked((byte)(value >> 8)); + buf[offset + 5] = unchecked((byte)(value >> 16)); + buf[offset + 4] = unchecked((byte)(value >> 24)); + buf[offset + 3] = unchecked((byte)(value >> 32)); + buf[offset + 2] = unchecked((byte)(value >> 40)); + buf[offset + 1] = unchecked((byte)(value >> 48)); + buf[offset + 0] = unchecked((byte)(value >> 56)); + } + + public static void Array16LoadBigEndian64(out Array16 output, byte[] input, int inputOffset) + { + output.x0 = LoadBigEndian64(input, inputOffset + 0); + output.x1 = LoadBigEndian64(input, inputOffset + 8); + output.x2 = LoadBigEndian64(input, inputOffset + 16); + output.x3 = LoadBigEndian64(input, inputOffset + 24); + output.x4 = LoadBigEndian64(input, inputOffset + 32); + output.x5 = LoadBigEndian64(input, inputOffset + 40); + output.x6 = LoadBigEndian64(input, inputOffset + 48); + output.x7 = LoadBigEndian64(input, inputOffset + 56); + output.x8 = LoadBigEndian64(input, inputOffset + 64); + output.x9 = LoadBigEndian64(input, inputOffset + 72); + output.x10 = LoadBigEndian64(input, inputOffset + 80); + output.x11 = LoadBigEndian64(input, inputOffset + 88); + output.x12 = LoadBigEndian64(input, inputOffset + 96); + output.x13 = LoadBigEndian64(input, inputOffset + 104); + output.x14 = LoadBigEndian64(input, inputOffset + 112); + output.x15 = LoadBigEndian64(input, inputOffset + 120); + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs new file mode 100644 index 000000000..cfd64104d --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs @@ -0,0 +1,272 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Discord.Net.ED25519 +{ + internal class CryptoBytes + { + /// + /// Comparison of two arrays. + /// + /// The runtime of this method does not depend on the contents of the arrays. Using constant time + /// prevents timing attacks that allow an attacker to learn if the arrays have a common prefix. + /// + /// It is important to use such a constant time comparison when verifying MACs. + /// + /// Byte array + /// Byte array + /// True if arrays are equal + public static bool ConstantTimeEquals(byte[] x, byte[] y) + { + if (x.Length != y.Length) + return false; + return InternalConstantTimeEquals(x, 0, y, 0, x.Length) != 0; + } + + /// + /// Comparison of two array segments. + /// + /// The runtime of this method does not depend on the contents of the arrays. Using constant time + /// prevents timing attacks that allow an attacker to learn if the arrays have a common prefix. + /// + /// It is important to use such a constant time comparison when verifying MACs. + /// + /// Byte array segment + /// Byte array segment + /// True if contents of x and y are equal + public static bool ConstantTimeEquals(ArraySegment x, ArraySegment y) + { + if (x.Count != y.Count) + return false; + return InternalConstantTimeEquals(x.Array, x.Offset, y.Array, y.Offset, x.Count) != 0; + } + + /// + /// Comparison of two byte sequences. + /// + /// The runtime of this method does not depend on the contents of the arrays. Using constant time + /// prevents timing attacks that allow an attacker to learn if the arrays have a common prefix. + /// + /// It is important to use such a constant time comparison when verifying MACs. + /// + /// Byte array + /// Offset of byte sequence in the x array + /// Byte array + /// Offset of byte sequence in the y array + /// Lengh of byte sequence + /// True if sequences are equal + public static bool ConstantTimeEquals(byte[] x, int xOffset, byte[] y, int yOffset, int length) + { + return InternalConstantTimeEquals(x, xOffset, y, yOffset, length) != 0; + } + + private static uint InternalConstantTimeEquals(byte[] x, int xOffset, byte[] y, int yOffset, int length) + { + int differentbits = 0; + for (int i = 0; i < length; i++) + differentbits |= x[xOffset + i] ^ y[yOffset + i]; + return (1 & (unchecked((uint)differentbits - 1) >> 8)); + } + + /// + /// Overwrites the contents of the array, wiping the previous content. + /// + /// Byte array + public static void Wipe(byte[] data) + { + InternalWipe(data, 0, data.Length); + } + + /// + /// Overwrites the contents of the array, wiping the previous content. + /// + /// Byte array + /// Index of byte sequence + /// Length of byte sequence + public static void Wipe(byte[] data, int offset, int length) + { + InternalWipe(data, offset, length); + } + + /// + /// Overwrites the contents of the array segment, wiping the previous content. + /// + /// Byte array segment + public static void Wipe(ArraySegment data) + { + InternalWipe(data.Array, data.Offset, data.Count); + } + + // Secure wiping is hard + // * the GC can move around and copy memory + // Perhaps this can be avoided by using unmanaged memory or by fixing the position of the array in memory + // * Swap files and error dumps can contain secret information + // It seems possible to lock memory in RAM, no idea about error dumps + // * Compiler could optimize out the wiping if it knows that data won't be read back + // I hope this is enough, suppressing inlining + // but perhaps `RtlSecureZeroMemory` is needed + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(byte[] data, int offset, int count) + { + Array.Clear(data, offset, count); + } + + // shallow wipe of structs + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(ref T data) + where T : struct + { + data = default(T); + } + + /// + /// Constant-time conversion of the bytes array to an upper-case hex string. + /// Please see http://stackoverflow.com/a/14333437/445517 for the detailed explanation + /// + /// Byte array + /// Hex representation of byte array + public static string ToHexStringUpper(byte[] data) + { + if (data == null) + return null; + char[] c = new char[data.Length * 2]; + int b; + for (int i = 0; i < data.Length; i++) + { + b = data[i] >> 4; + c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7)); + b = data[i] & 0xF; + c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7)); + } + return new string(c); + } + + /// + /// Constant-time conversion of the bytes array to an lower-case hex string. + /// Please see http://stackoverflow.com/a/14333437/445517 for the detailed explanation. + /// + /// Byte array + /// Hex representation of byte array + public static string ToHexStringLower(byte[] data) + { + if (data == null) + return null; + char[] c = new char[data.Length * 2]; + int b; + for (int i = 0; i < data.Length; i++) + { + b = data[i] >> 4; + c[i * 2] = (char)(87 + b + (((b - 10) >> 31) & -39)); + b = data[i] & 0xF; + c[i * 2 + 1] = (char)(87 + b + (((b - 10) >> 31) & -39)); + } + return new string(c); + } + + /// + /// Converts the hex string to bytes. Case insensitive. + /// + /// Hex encoded byte sequence + /// Byte array + public static byte[] FromHexString(string hexString) + { + if (hexString == null) + return null; + if (hexString.Length % 2 != 0) + throw new FormatException("The hex string is invalid because it has an odd length"); + var result = new byte[hexString.Length / 2]; + for (int i = 0; i < result.Length; i++) + result[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); + return result; + } + + /// + /// Encodes the bytes with the Base64 encoding. + /// More compact than hex, but it is case-sensitive and uses the special characters `+`, `/` and `=`. + /// + /// Byte array + /// Base 64 encoded data + public static string ToBase64String(byte[] data) + { + if (data == null) + return null; + return Convert.ToBase64String(data); + } + + /// + /// Decodes a Base64 encoded string back to bytes. + /// + /// Base 64 encoded data + /// Byte array + public static byte[] FromBase64String(string base64String) + { + if (base64String == null) + return null; + return Convert.FromBase64String(base64String); + } + + private const string strDigits = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + /// + /// Encode a byte sequence as a base58-encoded string + /// + /// Byte sequence + /// Encoding result + public static string Base58Encode(byte[] input) + { + // Decode byte[] to BigInteger + BigInteger intData = 0; + for (int i = 0; i < input.Length; i++) + { + intData = intData * 256 + input[i]; + } + + // Encode BigInteger to Base58 string + string result = ""; + while (intData > 0) + { + int remainder = (int)(intData % 58); + intData /= 58; + result = strDigits[remainder] + result; + } + + // Append `1` for each leading 0 byte + for (int i = 0; i < input.Length && input[i] == 0; i++) + { + result = '1' + result; + } + return result; + } + + /// + /// // Decode a base58-encoded string into byte array + /// + /// Base58 data string + /// Byte array + public static byte[] Base58Decode(string input) + { + // Decode Base58 string to BigInteger + BigInteger intData = 0; + for (int i = 0; i < input.Length; i++) + { + int digit = strDigits.IndexOf(input[i]); //Slow + if (digit < 0) + throw new FormatException(string.Format("Invalid Base58 character `{0}` at position {1}", input[i], i)); + intData = intData * 58 + digit; + } + + // Encode BigInteger to byte[] + // Leading zero bytes get encoded as leading `1` characters + int leadingZeroCount = input.TakeWhile(c => c == '1').Count(); + var leadingZeros = Enumerable.Repeat((byte)0, leadingZeroCount); + var bytesWithoutLeadingZeros = + intData.ToByteArray() + .Reverse()// to big endian + .SkipWhile(b => b == 0);//strip sign byte + var result = leadingZeros.Concat(bytesWithoutLeadingZeros).ToArray(); + return result; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519.cs new file mode 100644 index 000000000..109620efd --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Net.ED25519 +{ + internal static class Ed25519 + { + /// + /// Public Keys are 32 byte values. All possible values of this size a valid. + /// + public const int PublicKeySize = 32; + /// + /// Signatures are 64 byte values + /// + public const int SignatureSize = 64; + /// + /// Private key seeds are 32 byte arbitrary values. This is the form that should be generated and stored. + /// + public const int PrivateKeySeedSize = 32; + /// + /// A 64 byte expanded form of private key. This form is used internally to improve performance + /// + public const int ExpandedPrivateKeySize = 32 * 2; + + /// + /// Verify Ed25519 signature + /// + /// Signature bytes + /// Message + /// Public key + /// True if signature is valid, false if it's not + public static bool Verify(ArraySegment signature, ArraySegment message, ArraySegment publicKey) + { + if (signature.Count != SignatureSize) + throw new ArgumentException($"Sizeof signature doesnt match defined size of {SignatureSize}"); + + if (publicKey.Count != PublicKeySize) + throw new ArgumentException($"Sizeof public key doesnt match defined size of {PublicKeySize}"); + + return Ed25519Operations.crypto_sign_verify(signature.Array, signature.Offset, message.Array, message.Offset, message.Count, publicKey.Array, publicKey.Offset); + } + + /// + /// Verify Ed25519 signature + /// + /// Signature bytes + /// Message + /// Public key + /// True if signature is valid, false if it's not + public static bool Verify(byte[] signature, byte[] message, byte[] publicKey) + { + Preconditions.NotNull(signature, nameof(signature)); + Preconditions.NotNull(message, nameof(message)); + Preconditions.NotNull(publicKey, nameof(publicKey)); + if (signature.Length != SignatureSize) + throw new ArgumentException($"Sizeof signature doesnt match defined size of {SignatureSize}"); + + if (publicKey.Length != PublicKeySize) + throw new ArgumentException($"Sizeof public key doesnt match defined size of {PublicKeySize}"); + + return Ed25519Operations.crypto_sign_verify(signature, 0, message, 0, message.Length, publicKey, 0); + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Operations.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Operations.cs new file mode 100644 index 000000000..4d5ece1e5 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Operations.cs @@ -0,0 +1,45 @@ +using Discord.Net.ED25519.Ed25519Ref10; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Net.ED25519 +{ + internal class Ed25519Operations + { + public static bool crypto_sign_verify( + byte[] sig, int sigoffset, + byte[] m, int moffset, int mlen, + byte[] pk, int pkoffset) + { + byte[] h; + byte[] checkr = new byte[32]; + GroupElementP3 A; + GroupElementP2 R; + + if ((sig[sigoffset + 63] & 224) != 0) + return false; + if (GroupOperations.ge_frombytes_negate_vartime(out A, pk, pkoffset) != 0) + return false; + + var hasher = new Sha512(); + hasher.Update(sig, sigoffset, 32); + hasher.Update(pk, pkoffset, 32); + hasher.Update(m, moffset, mlen); + h = hasher.Finalize(); + + ScalarOperations.sc_reduce(h); + + var sm32 = new byte[32]; + Array.Copy(sig, sigoffset + 32, sm32, 0, 32); + GroupOperations.ge_double_scalarmult_vartime(out R, h, ref A, sm32); + GroupOperations.ge_tobytes(checkr, 0, ref R); + var result = CryptoBytes.ConstantTimeEquals(checkr, 0, sig, sigoffset, 32); + CryptoBytes.Wipe(h); + CryptoBytes.Wipe(checkr); + return result; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/FieldElement.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/FieldElement.cs new file mode 100644 index 000000000..d612ff5be --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/FieldElement.cs @@ -0,0 +1,23 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal struct FieldElement + { + internal int x0, x1, x2, x3, x4, x5, x6, x7, x8, x9; + + internal FieldElement(params int[] elements) + { + x0 = elements[0]; + x1 = elements[1]; + x2 = elements[2]; + x3 = elements[3]; + x4 = elements[4]; + x5 = elements[5]; + x6 = elements[6]; + x7 = elements[7]; + x8 = elements[8]; + x9 = elements[9]; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/GroupElement.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/GroupElement.cs new file mode 100644 index 000000000..d54b5ada7 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/GroupElement.cs @@ -0,0 +1,63 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + /* + ge means group element. + + Here the group is the set of pairs (x,y) of field elements (see fe.h) + satisfying -x^2 + y^2 = 1 + d x^2y^2 + where d = -121665/121666. + + Representations: + ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge_precomp (Duif): (y+x,y-x,2dxy) + */ + + internal struct GroupElementP2 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + } ; + + internal struct GroupElementP3 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; + + internal struct GroupElementP1P1 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; + + internal struct GroupElementPreComp + { + public FieldElement yplusx; + public FieldElement yminusx; + public FieldElement xy2d; + + public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d) + { + this.yplusx = yplusx; + this.yminusx = yminusx; + this.xy2d = xy2d; + } + } ; + + internal struct GroupElementCached + { + public FieldElement YplusX; + public FieldElement YminusX; + public FieldElement Z; + public FieldElement T2d; + } ; +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base.cs new file mode 100644 index 000000000..2a25504c9 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base.cs @@ -0,0 +1,1355 @@ +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class LookupTables + { + /* base[i][j] = (j+1)*256^i*B */ + //32*8 + internal static GroupElementPreComp[][] Base = new GroupElementPreComp[][] + { + new[]{ + new GroupElementPreComp( + new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ), + new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ), + new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 ) + ), + new GroupElementPreComp( + new FieldElement( -12815894,-12976347,-21581243,11784320,-25355658,-2750717,-11717903,-3814571,-358445,-10211303 ), + new FieldElement( -21703237,6903825,27185491,6451973,-29577724,-9554005,-15616551,11189268,-26829678,-5319081 ), + new FieldElement( 26966642,11152617,32442495,15396054,14353839,-12752335,-3128826,-9541118,-15472047,-4166697 ) + ), + new GroupElementPreComp( + new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ), + new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ), + new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 ) + ), + new GroupElementPreComp( + new FieldElement( -17036878,13921892,10945806,-6033431,27105052,-16084379,-28926210,15006023,3284568,-6276540 ), + new FieldElement( 23599295,-8306047,-11193664,-7687416,13236774,10506355,7464579,9656445,13059162,10374397 ), + new FieldElement( 7798556,16710257,3033922,2874086,28997861,2835604,32406664,-3839045,-641708,-101325 ) + ), + new GroupElementPreComp( + new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ), + new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ), + new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 ) + ), + new GroupElementPreComp( + new FieldElement( -15371964,-12862754,32573250,4720197,-26436522,5875511,-19188627,-15224819,-9818940,-12085777 ), + new FieldElement( -8549212,109983,15149363,2178705,22900618,4543417,3044240,-15689887,1762328,14866737 ), + new FieldElement( -18199695,-15951423,-10473290,1707278,-17185920,3916101,-28236412,3959421,27914454,4383652 ) + ), + new GroupElementPreComp( + new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ), + new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ), + new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 ) + ), + new GroupElementPreComp( + new FieldElement( 14499471,-2729599,-33191113,-4254652,28494862,14271267,30290735,10876454,-33154098,2381726 ), + new FieldElement( -7195431,-2655363,-14730155,462251,-27724326,3941372,-6236617,3696005,-32300832,15351955 ), + new FieldElement( 27431194,8222322,16448760,-3907995,-18707002,11938355,-32961401,-2970515,29551813,10109425 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -13657040,-13155431,-31283750,11777098,21447386,6519384,-2378284,-1627556,10092783,-4764171 ), + new FieldElement( 27939166,14210322,4677035,16277044,-22964462,-12398139,-32508754,12005538,-17810127,12803510 ), + new FieldElement( 17228999,-15661624,-1233527,300140,-1224870,-11714777,30364213,-9038194,18016357,4397660 ) + ), + new GroupElementPreComp( + new FieldElement( -10958843,-7690207,4776341,-14954238,27850028,-15602212,-26619106,14544525,-17477504,982639 ), + new FieldElement( 29253598,15796703,-2863982,-9908884,10057023,3163536,7332899,-4120128,-21047696,9934963 ), + new FieldElement( 5793303,16271923,-24131614,-10116404,29188560,1206517,-14747930,4559895,-30123922,-10897950 ) + ), + new GroupElementPreComp( + new FieldElement( -27643952,-11493006,16282657,-11036493,28414021,-15012264,24191034,4541697,-13338309,5500568 ), + new FieldElement( 12650548,-1497113,9052871,11355358,-17680037,-8400164,-17430592,12264343,10874051,13524335 ), + new FieldElement( 25556948,-3045990,714651,2510400,23394682,-10415330,33119038,5080568,-22528059,5376628 ) + ), + new GroupElementPreComp( + new FieldElement( -26088264,-4011052,-17013699,-3537628,-6726793,1920897,-22321305,-9447443,4535768,1569007 ), + new FieldElement( -2255422,14606630,-21692440,-8039818,28430649,8775819,-30494562,3044290,31848280,12543772 ), + new FieldElement( -22028579,2943893,-31857513,6777306,13784462,-4292203,-27377195,-2062731,7718482,14474653 ) + ), + new GroupElementPreComp( + new FieldElement( 2385315,2454213,-22631320,46603,-4437935,-15680415,656965,-7236665,24316168,-5253567 ), + new FieldElement( 13741529,10911568,-33233417,-8603737,-20177830,-1033297,33040651,-13424532,-20729456,8321686 ), + new FieldElement( 21060490,-2212744,15712757,-4336099,1639040,10656336,23845965,-11874838,-9984458,608372 ) + ), + new GroupElementPreComp( + new FieldElement( -13672732,-15087586,-10889693,-7557059,-6036909,11305547,1123968,-6780577,27229399,23887 ), + new FieldElement( -23244140,-294205,-11744728,14712571,-29465699,-2029617,12797024,-6440308,-1633405,16678954 ), + new FieldElement( -29500620,4770662,-16054387,14001338,7830047,9564805,-1508144,-4795045,-17169265,4904953 ) + ), + new GroupElementPreComp( + new FieldElement( 24059557,14617003,19037157,-15039908,19766093,-14906429,5169211,16191880,2128236,-4326833 ), + new FieldElement( -16981152,4124966,-8540610,-10653797,30336522,-14105247,-29806336,916033,-6882542,-2986532 ), + new FieldElement( -22630907,12419372,-7134229,-7473371,-16478904,16739175,285431,2763829,15736322,4143876 ) + ), + new GroupElementPreComp( + new FieldElement( 2379352,11839345,-4110402,-5988665,11274298,794957,212801,-14594663,23527084,-16458268 ), + new FieldElement( 33431127,-11130478,-17838966,-15626900,8909499,8376530,-32625340,4087881,-15188911,-14416214 ), + new FieldElement( 1767683,7197987,-13205226,-2022635,-13091350,448826,5799055,4357868,-4774191,-16323038 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 6721966,13833823,-23523388,-1551314,26354293,-11863321,23365147,-3949732,7390890,2759800 ), + new FieldElement( 4409041,2052381,23373853,10530217,7676779,-12885954,21302353,-4264057,1244380,-12919645 ), + new FieldElement( -4421239,7169619,4982368,-2957590,30256825,-2777540,14086413,9208236,15886429,16489664 ) + ), + new GroupElementPreComp( + new FieldElement( 1996075,10375649,14346367,13311202,-6874135,-16438411,-13693198,398369,-30606455,-712933 ), + new FieldElement( -25307465,9795880,-2777414,14878809,-33531835,14780363,13348553,12076947,-30836462,5113182 ), + new FieldElement( -17770784,11797796,31950843,13929123,-25888302,12288344,-30341101,-7336386,13847711,5387222 ) + ), + new GroupElementPreComp( + new FieldElement( -18582163,-3416217,17824843,-2340966,22744343,-10442611,8763061,3617786,-19600662,10370991 ), + new FieldElement( 20246567,-14369378,22358229,-543712,18507283,-10413996,14554437,-8746092,32232924,16763880 ), + new FieldElement( 9648505,10094563,26416693,14745928,-30374318,-6472621,11094161,15689506,3140038,-16510092 ) + ), + new GroupElementPreComp( + new FieldElement( -16160072,5472695,31895588,4744994,8823515,10365685,-27224800,9448613,-28774454,366295 ), + new FieldElement( 19153450,11523972,-11096490,-6503142,-24647631,5420647,28344573,8041113,719605,11671788 ), + new FieldElement( 8678025,2694440,-6808014,2517372,4964326,11152271,-15432916,-15266516,27000813,-10195553 ) + ), + new GroupElementPreComp( + new FieldElement( -15157904,7134312,8639287,-2814877,-7235688,10421742,564065,5336097,6750977,-14521026 ), + new FieldElement( 11836410,-3979488,26297894,16080799,23455045,15735944,1695823,-8819122,8169720,16220347 ), + new FieldElement( -18115838,8653647,17578566,-6092619,-8025777,-16012763,-11144307,-2627664,-5990708,-14166033 ) + ), + new GroupElementPreComp( + new FieldElement( -23308498,-10968312,15213228,-10081214,-30853605,-11050004,27884329,2847284,2655861,1738395 ), + new FieldElement( -27537433,-14253021,-25336301,-8002780,-9370762,8129821,21651608,-3239336,-19087449,-11005278 ), + new FieldElement( 1533110,3437855,23735889,459276,29970501,11335377,26030092,5821408,10478196,8544890 ) + ), + new GroupElementPreComp( + new FieldElement( 32173121,-16129311,24896207,3921497,22579056,-3410854,19270449,12217473,17789017,-3395995 ), + new FieldElement( -30552961,-2228401,-15578829,-10147201,13243889,517024,15479401,-3853233,30460520,1052596 ), + new FieldElement( -11614875,13323618,32618793,8175907,-15230173,12596687,27491595,-4612359,3179268,-9478891 ) + ), + new GroupElementPreComp( + new FieldElement( 31947069,-14366651,-4640583,-15339921,-15125977,-6039709,-14756777,-16411740,19072640,-9511060 ), + new FieldElement( 11685058,11822410,3158003,-13952594,33402194,-4165066,5977896,-5215017,473099,5040608 ), + new FieldElement( -20290863,8198642,-27410132,11602123,1290375,-2799760,28326862,1721092,-19558642,-3131606 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 7881532,10687937,7578723,7738378,-18951012,-2553952,21820786,8076149,-27868496,11538389 ), + new FieldElement( -19935666,3899861,18283497,-6801568,-15728660,-11249211,8754525,7446702,-5676054,5797016 ), + new FieldElement( -11295600,-3793569,-15782110,-7964573,12708869,-8456199,2014099,-9050574,-2369172,-5877341 ) + ), + new GroupElementPreComp( + new FieldElement( -22472376,-11568741,-27682020,1146375,18956691,16640559,1192730,-3714199,15123619,10811505 ), + new FieldElement( 14352098,-3419715,-18942044,10822655,32750596,4699007,-70363,15776356,-28886779,-11974553 ), + new FieldElement( -28241164,-8072475,-4978962,-5315317,29416931,1847569,-20654173,-16484855,4714547,-9600655 ) + ), + new GroupElementPreComp( + new FieldElement( 15200332,8368572,19679101,15970074,-31872674,1959451,24611599,-4543832,-11745876,12340220 ), + new FieldElement( 12876937,-10480056,33134381,6590940,-6307776,14872440,9613953,8241152,15370987,9608631 ), + new FieldElement( -4143277,-12014408,8446281,-391603,4407738,13629032,-7724868,15866074,-28210621,-8814099 ) + ), + new GroupElementPreComp( + new FieldElement( 26660628,-15677655,8393734,358047,-7401291,992988,-23904233,858697,20571223,8420556 ), + new FieldElement( 14620715,13067227,-15447274,8264467,14106269,15080814,33531827,12516406,-21574435,-12476749 ), + new FieldElement( 236881,10476226,57258,-14677024,6472998,2466984,17258519,7256740,8791136,15069930 ) + ), + new GroupElementPreComp( + new FieldElement( 1276410,-9371918,22949635,-16322807,-23493039,-5702186,14711875,4874229,-30663140,-2331391 ), + new FieldElement( 5855666,4990204,-13711848,7294284,-7804282,1924647,-1423175,-7912378,-33069337,9234253 ), + new FieldElement( 20590503,-9018988,31529744,-7352666,-2706834,10650548,31559055,-11609587,18979186,13396066 ) + ), + new GroupElementPreComp( + new FieldElement( 24474287,4968103,22267082,4407354,24063882,-8325180,-18816887,13594782,33514650,7021958 ), + new FieldElement( -11566906,-6565505,-21365085,15928892,-26158305,4315421,-25948728,-3916677,-21480480,12868082 ), + new FieldElement( -28635013,13504661,19988037,-2132761,21078225,6443208,-21446107,2244500,-12455797,-8089383 ) + ), + new GroupElementPreComp( + new FieldElement( -30595528,13793479,-5852820,319136,-25723172,-6263899,33086546,8957937,-15233648,5540521 ), + new FieldElement( -11630176,-11503902,-8119500,-7643073,2620056,1022908,-23710744,-1568984,-16128528,-14962807 ), + new FieldElement( 23152971,775386,27395463,14006635,-9701118,4649512,1689819,892185,-11513277,-15205948 ) + ), + new GroupElementPreComp( + new FieldElement( 9770129,9586738,26496094,4324120,1556511,-3550024,27453819,4763127,-19179614,5867134 ), + new FieldElement( -32765025,1927590,31726409,-4753295,23962434,-16019500,27846559,5931263,-29749703,-16108455 ), + new FieldElement( 27461885,-2977536,22380810,1815854,-23033753,-3031938,7283490,-15148073,-19526700,7734629 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -8010264,-9590817,-11120403,6196038,29344158,-13430885,7585295,-3176626,18549497,15302069 ), + new FieldElement( -32658337,-6171222,-7672793,-11051681,6258878,13504381,10458790,-6418461,-8872242,8424746 ), + new FieldElement( 24687205,8613276,-30667046,-3233545,1863892,-1830544,19206234,7134917,-11284482,-828919 ) + ), + new GroupElementPreComp( + new FieldElement( 11334899,-9218022,8025293,12707519,17523892,-10476071,10243738,-14685461,-5066034,16498837 ), + new FieldElement( 8911542,6887158,-9584260,-6958590,11145641,-9543680,17303925,-14124238,6536641,10543906 ), + new FieldElement( -28946384,15479763,-17466835,568876,-1497683,11223454,-2669190,-16625574,-27235709,8876771 ) + ), + new GroupElementPreComp( + new FieldElement( -25742899,-12566864,-15649966,-846607,-33026686,-796288,-33481822,15824474,-604426,-9039817 ), + new FieldElement( 10330056,70051,7957388,-9002667,9764902,15609756,27698697,-4890037,1657394,3084098 ), + new FieldElement( 10477963,-7470260,12119566,-13250805,29016247,-5365589,31280319,14396151,-30233575,15272409 ) + ), + new GroupElementPreComp( + new FieldElement( -12288309,3169463,28813183,16658753,25116432,-5630466,-25173957,-12636138,-25014757,1950504 ), + new FieldElement( -26180358,9489187,11053416,-14746161,-31053720,5825630,-8384306,-8767532,15341279,8373727 ), + new FieldElement( 28685821,7759505,-14378516,-12002860,-31971820,4079242,298136,-10232602,-2878207,15190420 ) + ), + new GroupElementPreComp( + new FieldElement( -32932876,13806336,-14337485,-15794431,-24004620,10940928,8669718,2742393,-26033313,-6875003 ), + new FieldElement( -1580388,-11729417,-25979658,-11445023,-17411874,-10912854,9291594,-16247779,-12154742,6048605 ), + new FieldElement( -30305315,14843444,1539301,11864366,20201677,1900163,13934231,5128323,11213262,9168384 ) + ), + new GroupElementPreComp( + new FieldElement( -26280513,11007847,19408960,-940758,-18592965,-4328580,-5088060,-11105150,20470157,-16398701 ), + new FieldElement( -23136053,9282192,14855179,-15390078,-7362815,-14408560,-22783952,14461608,14042978,5230683 ), + new FieldElement( 29969567,-2741594,-16711867,-8552442,9175486,-2468974,21556951,3506042,-5933891,-12449708 ) + ), + new GroupElementPreComp( + new FieldElement( -3144746,8744661,19704003,4581278,-20430686,6830683,-21284170,8971513,-28539189,15326563 ), + new FieldElement( -19464629,10110288,-17262528,-3503892,-23500387,1355669,-15523050,15300988,-20514118,9168260 ), + new FieldElement( -5353335,4488613,-23803248,16314347,7780487,-15638939,-28948358,9601605,33087103,-9011387 ) + ), + new GroupElementPreComp( + new FieldElement( -19443170,-15512900,-20797467,-12445323,-29824447,10229461,-27444329,-15000531,-5996870,15664672 ), + new FieldElement( 23294591,-16632613,-22650781,-8470978,27844204,11461195,13099750,-2460356,18151676,13417686 ), + new FieldElement( -24722913,-4176517,-31150679,5988919,-26858785,6685065,1661597,-12551441,15271676,-15452665 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 11433042,-13228665,8239631,-5279517,-1985436,-725718,-18698764,2167544,-6921301,-13440182 ), + new FieldElement( -31436171,15575146,30436815,12192228,-22463353,9395379,-9917708,-8638997,12215110,12028277 ), + new FieldElement( 14098400,6555944,23007258,5757252,-15427832,-12950502,30123440,4617780,-16900089,-655628 ) + ), + new GroupElementPreComp( + new FieldElement( -4026201,-15240835,11893168,13718664,-14809462,1847385,-15819999,10154009,23973261,-12684474 ), + new FieldElement( -26531820,-3695990,-1908898,2534301,-31870557,-16550355,18341390,-11419951,32013174,-10103539 ), + new FieldElement( -25479301,10876443,-11771086,-14625140,-12369567,1838104,21911214,6354752,4425632,-837822 ) + ), + new GroupElementPreComp( + new FieldElement( -10433389,-14612966,22229858,-3091047,-13191166,776729,-17415375,-12020462,4725005,14044970 ), + new FieldElement( 19268650,-7304421,1555349,8692754,-21474059,-9910664,6347390,-1411784,-19522291,-16109756 ), + new FieldElement( -24864089,12986008,-10898878,-5558584,-11312371,-148526,19541418,8180106,9282262,10282508 ) + ), + new GroupElementPreComp( + new FieldElement( -26205082,4428547,-8661196,-13194263,4098402,-14165257,15522535,8372215,5542595,-10702683 ), + new FieldElement( -10562541,14895633,26814552,-16673850,-17480754,-2489360,-2781891,6993761,-18093885,10114655 ), + new FieldElement( -20107055,-929418,31422704,10427861,-7110749,6150669,-29091755,-11529146,25953725,-106158 ) + ), + new GroupElementPreComp( + new FieldElement( -4234397,-8039292,-9119125,3046000,2101609,-12607294,19390020,6094296,-3315279,12831125 ), + new FieldElement( -15998678,7578152,5310217,14408357,-33548620,-224739,31575954,6326196,7381791,-2421839 ), + new FieldElement( -20902779,3296811,24736065,-16328389,18374254,7318640,6295303,8082724,-15362489,12339664 ) + ), + new GroupElementPreComp( + new FieldElement( 27724736,2291157,6088201,-14184798,1792727,5857634,13848414,15768922,25091167,14856294 ), + new FieldElement( -18866652,8331043,24373479,8541013,-701998,-9269457,12927300,-12695493,-22182473,-9012899 ), + new FieldElement( -11423429,-5421590,11632845,3405020,30536730,-11674039,-27260765,13866390,30146206,9142070 ) + ), + new GroupElementPreComp( + new FieldElement( 3924129,-15307516,-13817122,-10054960,12291820,-668366,-27702774,9326384,-8237858,4171294 ), + new FieldElement( -15921940,16037937,6713787,16606682,-21612135,2790944,26396185,3731949,345228,-5462949 ), + new FieldElement( -21327538,13448259,25284571,1143661,20614966,-8849387,2031539,-12391231,-16253183,-13582083 ) + ), + new GroupElementPreComp( + new FieldElement( 31016211,-16722429,26371392,-14451233,-5027349,14854137,17477601,3842657,28012650,-16405420 ), + new FieldElement( -5075835,9368966,-8562079,-4600902,-15249953,6970560,-9189873,16292057,-8867157,3507940 ), + new FieldElement( 29439664,3537914,23333589,6997794,-17555561,-11018068,-15209202,-15051267,-9164929,6580396 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -12185861,-7679788,16438269,10826160,-8696817,-6235611,17860444,-9273846,-2095802,9304567 ), + new FieldElement( 20714564,-4336911,29088195,7406487,11426967,-5095705,14792667,-14608617,5289421,-477127 ), + new FieldElement( -16665533,-10650790,-6160345,-13305760,9192020,-1802462,17271490,12349094,26939669,-3752294 ) + ), + new GroupElementPreComp( + new FieldElement( -12889898,9373458,31595848,16374215,21471720,13221525,-27283495,-12348559,-3698806,117887 ), + new FieldElement( 22263325,-6560050,3984570,-11174646,-15114008,-566785,28311253,5358056,-23319780,541964 ), + new FieldElement( 16259219,3261970,2309254,-15534474,-16885711,-4581916,24134070,-16705829,-13337066,-13552195 ) + ), + new GroupElementPreComp( + new FieldElement( 9378160,-13140186,-22845982,-12745264,28198281,-7244098,-2399684,-717351,690426,14876244 ), + new FieldElement( 24977353,-314384,-8223969,-13465086,28432343,-1176353,-13068804,-12297348,-22380984,6618999 ), + new FieldElement( -1538174,11685646,12944378,13682314,-24389511,-14413193,8044829,-13817328,32239829,-5652762 ) + ), + new GroupElementPreComp( + new FieldElement( -18603066,4762990,-926250,8885304,-28412480,-3187315,9781647,-10350059,32779359,5095274 ), + new FieldElement( -33008130,-5214506,-32264887,-3685216,9460461,-9327423,-24601656,14506724,21639561,-2630236 ), + new FieldElement( -16400943,-13112215,25239338,15531969,3987758,-4499318,-1289502,-6863535,17874574,558605 ) + ), + new GroupElementPreComp( + new FieldElement( -13600129,10240081,9171883,16131053,-20869254,9599700,33499487,5080151,2085892,5119761 ), + new FieldElement( -22205145,-2519528,-16381601,414691,-25019550,2170430,30634760,-8363614,-31999993,-5759884 ), + new FieldElement( -6845704,15791202,8550074,-1312654,29928809,-12092256,27534430,-7192145,-22351378,12961482 ) + ), + new GroupElementPreComp( + new FieldElement( -24492060,-9570771,10368194,11582341,-23397293,-2245287,16533930,8206996,-30194652,-5159638 ), + new FieldElement( -11121496,-3382234,2307366,6362031,-135455,8868177,-16835630,7031275,7589640,8945490 ), + new FieldElement( -32152748,8917967,6661220,-11677616,-1192060,-15793393,7251489,-11182180,24099109,-14456170 ) + ), + new GroupElementPreComp( + new FieldElement( 5019558,-7907470,4244127,-14714356,-26933272,6453165,-19118182,-13289025,-6231896,-10280736 ), + new FieldElement( 10853594,10721687,26480089,5861829,-22995819,1972175,-1866647,-10557898,-3363451,-6441124 ), + new FieldElement( -17002408,5906790,221599,-6563147,7828208,-13248918,24362661,-2008168,-13866408,7421392 ) + ), + new GroupElementPreComp( + new FieldElement( 8139927,-6546497,32257646,-5890546,30375719,1886181,-21175108,15441252,28826358,-4123029 ), + new FieldElement( 6267086,9695052,7709135,-16603597,-32869068,-1886135,14795160,-7840124,13746021,-1742048 ), + new FieldElement( 28584902,7787108,-6732942,-15050729,22846041,-7571236,-3181936,-363524,4771362,-8419958 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 24949256,6376279,-27466481,-8174608,-18646154,-9930606,33543569,-12141695,3569627,11342593 ), + new FieldElement( 26514989,4740088,27912651,3697550,19331575,-11472339,6809886,4608608,7325975,-14801071 ), + new FieldElement( -11618399,-14554430,-24321212,7655128,-1369274,5214312,-27400540,10258390,-17646694,-8186692 ) + ), + new GroupElementPreComp( + new FieldElement( 11431204,15823007,26570245,14329124,18029990,4796082,-31446179,15580664,9280358,-3973687 ), + new FieldElement( -160783,-10326257,-22855316,-4304997,-20861367,-13621002,-32810901,-11181622,-15545091,4387441 ), + new FieldElement( -20799378,12194512,3937617,-5805892,-27154820,9340370,-24513992,8548137,20617071,-7482001 ) + ), + new GroupElementPreComp( + new FieldElement( -938825,-3930586,-8714311,16124718,24603125,-6225393,-13775352,-11875822,24345683,10325460 ), + new FieldElement( -19855277,-1568885,-22202708,8714034,14007766,6928528,16318175,-1010689,4766743,3552007 ), + new FieldElement( -21751364,-16730916,1351763,-803421,-4009670,3950935,3217514,14481909,10988822,-3994762 ) + ), + new GroupElementPreComp( + new FieldElement( 15564307,-14311570,3101243,5684148,30446780,-8051356,12677127,-6505343,-8295852,13296005 ), + new FieldElement( -9442290,6624296,-30298964,-11913677,-4670981,-2057379,31521204,9614054,-30000824,12074674 ), + new FieldElement( 4771191,-135239,14290749,-13089852,27992298,14998318,-1413936,-1556716,29832613,-16391035 ) + ), + new GroupElementPreComp( + new FieldElement( 7064884,-7541174,-19161962,-5067537,-18891269,-2912736,25825242,5293297,-27122660,13101590 ), + new FieldElement( -2298563,2439670,-7466610,1719965,-27267541,-16328445,32512469,-5317593,-30356070,-4190957 ), + new FieldElement( -30006540,10162316,-33180176,3981723,-16482138,-13070044,14413974,9515896,19568978,9628812 ) + ), + new GroupElementPreComp( + new FieldElement( 33053803,199357,15894591,1583059,27380243,-4580435,-17838894,-6106839,-6291786,3437740 ), + new FieldElement( -18978877,3884493,19469877,12726490,15913552,13614290,-22961733,70104,7463304,4176122 ), + new FieldElement( -27124001,10659917,11482427,-16070381,12771467,-6635117,-32719404,-5322751,24216882,5944158 ) + ), + new GroupElementPreComp( + new FieldElement( 8894125,7450974,-2664149,-9765752,-28080517,-12389115,19345746,14680796,11632993,5847885 ), + new FieldElement( 26942781,-2315317,9129564,-4906607,26024105,11769399,-11518837,6367194,-9727230,4782140 ), + new FieldElement( 19916461,-4828410,-22910704,-11414391,25606324,-5972441,33253853,8220911,6358847,-1873857 ) + ), + new GroupElementPreComp( + new FieldElement( 801428,-2081702,16569428,11065167,29875704,96627,7908388,-4480480,-13538503,1387155 ), + new FieldElement( 19646058,5720633,-11416706,12814209,11607948,12749789,14147075,15156355,-21866831,11835260 ), + new FieldElement( 19299512,1155910,28703737,14890794,2925026,7269399,26121523,15467869,-26560550,5052483 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -3017432,10058206,1980837,3964243,22160966,12322533,-6431123,-12618185,12228557,-7003677 ), + new FieldElement( 32944382,14922211,-22844894,5188528,21913450,-8719943,4001465,13238564,-6114803,8653815 ), + new FieldElement( 22865569,-4652735,27603668,-12545395,14348958,8234005,24808405,5719875,28483275,2841751 ) + ), + new GroupElementPreComp( + new FieldElement( -16420968,-1113305,-327719,-12107856,21886282,-15552774,-1887966,-315658,19932058,-12739203 ), + new FieldElement( -11656086,10087521,-8864888,-5536143,-19278573,-3055912,3999228,13239134,-4777469,-13910208 ), + new FieldElement( 1382174,-11694719,17266790,9194690,-13324356,9720081,20403944,11284705,-14013818,3093230 ) + ), + new GroupElementPreComp( + new FieldElement( 16650921,-11037932,-1064178,1570629,-8329746,7352753,-302424,16271225,-24049421,-6691850 ), + new FieldElement( -21911077,-5927941,-4611316,-5560156,-31744103,-10785293,24123614,15193618,-21652117,-16739389 ), + new FieldElement( -9935934,-4289447,-25279823,4372842,2087473,10399484,31870908,14690798,17361620,11864968 ) + ), + new GroupElementPreComp( + new FieldElement( -11307610,6210372,13206574,5806320,-29017692,-13967200,-12331205,-7486601,-25578460,-16240689 ), + new FieldElement( 14668462,-12270235,26039039,15305210,25515617,4542480,10453892,6577524,9145645,-6443880 ), + new FieldElement( 5974874,3053895,-9433049,-10385191,-31865124,3225009,-7972642,3936128,-5652273,-3050304 ) + ), + new GroupElementPreComp( + new FieldElement( 30625386,-4729400,-25555961,-12792866,-20484575,7695099,17097188,-16303496,-27999779,1803632 ), + new FieldElement( -3553091,9865099,-5228566,4272701,-5673832,-16689700,14911344,12196514,-21405489,7047412 ), + new FieldElement( 20093277,9920966,-11138194,-5343857,13161587,12044805,-32856851,4124601,-32343828,-10257566 ) + ), + new GroupElementPreComp( + new FieldElement( -20788824,14084654,-13531713,7842147,19119038,-13822605,4752377,-8714640,-21679658,2288038 ), + new FieldElement( -26819236,-3283715,29965059,3039786,-14473765,2540457,29457502,14625692,-24819617,12570232 ), + new FieldElement( -1063558,-11551823,16920318,12494842,1278292,-5869109,-21159943,-3498680,-11974704,4724943 ) + ), + new GroupElementPreComp( + new FieldElement( 17960970,-11775534,-4140968,-9702530,-8876562,-1410617,-12907383,-8659932,-29576300,1903856 ), + new FieldElement( 23134274,-14279132,-10681997,-1611936,20684485,15770816,-12989750,3190296,26955097,14109738 ), + new FieldElement( 15308788,5320727,-30113809,-14318877,22902008,7767164,29425325,-11277562,31960942,11934971 ) + ), + new GroupElementPreComp( + new FieldElement( -27395711,8435796,4109644,12222639,-24627868,14818669,20638173,4875028,10491392,1379718 ), + new FieldElement( -13159415,9197841,3875503,-8936108,-1383712,-5879801,33518459,16176658,21432314,12180697 ), + new FieldElement( -11787308,11500838,13787581,-13832590,-22430679,10140205,1465425,12689540,-10301319,-13872883 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 5414091,-15386041,-21007664,9643570,12834970,1186149,-2622916,-1342231,26128231,6032912 ), + new FieldElement( -26337395,-13766162,32496025,-13653919,17847801,-12669156,3604025,8316894,-25875034,-10437358 ), + new FieldElement( 3296484,6223048,24680646,-12246460,-23052020,5903205,-8862297,-4639164,12376617,3188849 ) + ), + new GroupElementPreComp( + new FieldElement( 29190488,-14659046,27549113,-1183516,3520066,-10697301,32049515,-7309113,-16109234,-9852307 ), + new FieldElement( -14744486,-9309156,735818,-598978,-20407687,-5057904,25246078,-15795669,18640741,-960977 ), + new FieldElement( -6928835,-16430795,10361374,5642961,4910474,12345252,-31638386,-494430,10530747,1053335 ) + ), + new GroupElementPreComp( + new FieldElement( -29265967,-14186805,-13538216,-12117373,-19457059,-10655384,-31462369,-2948985,24018831,15026644 ), + new FieldElement( -22592535,-3145277,-2289276,5953843,-13440189,9425631,25310643,13003497,-2314791,-15145616 ), + new FieldElement( -27419985,-603321,-8043984,-1669117,-26092265,13987819,-27297622,187899,-23166419,-2531735 ) + ), + new GroupElementPreComp( + new FieldElement( -21744398,-13810475,1844840,5021428,-10434399,-15911473,9716667,16266922,-5070217,726099 ), + new FieldElement( 29370922,-6053998,7334071,-15342259,9385287,2247707,-13661962,-4839461,30007388,-15823341 ), + new FieldElement( -936379,16086691,23751945,-543318,-1167538,-5189036,9137109,730663,9835848,4555336 ) + ), + new GroupElementPreComp( + new FieldElement( -23376435,1410446,-22253753,-12899614,30867635,15826977,17693930,544696,-11985298,12422646 ), + new FieldElement( 31117226,-12215734,-13502838,6561947,-9876867,-12757670,-5118685,-4096706,29120153,13924425 ), + new FieldElement( -17400879,-14233209,19675799,-2734756,-11006962,-5858820,-9383939,-11317700,7240931,-237388 ) + ), + new GroupElementPreComp( + new FieldElement( -31361739,-11346780,-15007447,-5856218,-22453340,-12152771,1222336,4389483,3293637,-15551743 ), + new FieldElement( -16684801,-14444245,11038544,11054958,-13801175,-3338533,-24319580,7733547,12796905,-6335822 ), + new FieldElement( -8759414,-10817836,-25418864,10783769,-30615557,-9746811,-28253339,3647836,3222231,-11160462 ) + ), + new GroupElementPreComp( + new FieldElement( 18606113,1693100,-25448386,-15170272,4112353,10045021,23603893,-2048234,-7550776,2484985 ), + new FieldElement( 9255317,-3131197,-12156162,-1004256,13098013,-9214866,16377220,-2102812,-19802075,-3034702 ), + new FieldElement( -22729289,7496160,-5742199,11329249,19991973,-3347502,-31718148,9936966,-30097688,-10618797 ) + ), + new GroupElementPreComp( + new FieldElement( 21878590,-5001297,4338336,13643897,-3036865,13160960,19708896,5415497,-7360503,-4109293 ), + new FieldElement( 27736861,10103576,12500508,8502413,-3413016,-9633558,10436918,-1550276,-23659143,-8132100 ), + new FieldElement( 19492550,-12104365,-29681976,-852630,-3208171,12403437,30066266,8367329,13243957,8709688 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 12015105,2801261,28198131,10151021,24818120,-4743133,-11194191,-5645734,5150968,7274186 ), + new FieldElement( 2831366,-12492146,1478975,6122054,23825128,-12733586,31097299,6083058,31021603,-9793610 ), + new FieldElement( -2529932,-2229646,445613,10720828,-13849527,-11505937,-23507731,16354465,15067285,-14147707 ) + ), + new GroupElementPreComp( + new FieldElement( 7840942,14037873,-33364863,15934016,-728213,-3642706,21403988,1057586,-19379462,-12403220 ), + new FieldElement( 915865,-16469274,15608285,-8789130,-24357026,6060030,-17371319,8410997,-7220461,16527025 ), + new FieldElement( 32922597,-556987,20336074,-16184568,10903705,-5384487,16957574,52992,23834301,6588044 ) + ), + new GroupElementPreComp( + new FieldElement( 32752030,11232950,3381995,-8714866,22652988,-10744103,17159699,16689107,-20314580,-1305992 ), + new FieldElement( -4689649,9166776,-25710296,-10847306,11576752,12733943,7924251,-2752281,1976123,-7249027 ), + new FieldElement( 21251222,16309901,-2983015,-6783122,30810597,12967303,156041,-3371252,12331345,-8237197 ) + ), + new GroupElementPreComp( + new FieldElement( 8651614,-4477032,-16085636,-4996994,13002507,2950805,29054427,-5106970,10008136,-4667901 ), + new FieldElement( 31486080,15114593,-14261250,12951354,14369431,-7387845,16347321,-13662089,8684155,-10532952 ), + new FieldElement( 19443825,11385320,24468943,-9659068,-23919258,2187569,-26263207,-6086921,31316348,14219878 ) + ), + new GroupElementPreComp( + new FieldElement( -28594490,1193785,32245219,11392485,31092169,15722801,27146014,6992409,29126555,9207390 ), + new FieldElement( 32382935,1110093,18477781,11028262,-27411763,-7548111,-4980517,10843782,-7957600,-14435730 ), + new FieldElement( 2814918,7836403,27519878,-7868156,-20894015,-11553689,-21494559,8550130,28346258,1994730 ) + ), + new GroupElementPreComp( + new FieldElement( -19578299,8085545,-14000519,-3948622,2785838,-16231307,-19516951,7174894,22628102,8115180 ), + new FieldElement( -30405132,955511,-11133838,-15078069,-32447087,-13278079,-25651578,3317160,-9943017,930272 ), + new FieldElement( -15303681,-6833769,28856490,1357446,23421993,1057177,24091212,-1388970,-22765376,-10650715 ) + ), + new GroupElementPreComp( + new FieldElement( -22751231,-5303997,-12907607,-12768866,-15811511,-7797053,-14839018,-16554220,-1867018,8398970 ), + new FieldElement( -31969310,2106403,-4736360,1362501,12813763,16200670,22981545,-6291273,18009408,-15772772 ), + new FieldElement( -17220923,-9545221,-27784654,14166835,29815394,7444469,29551787,-3727419,19288549,1325865 ) + ), + new GroupElementPreComp( + new FieldElement( 15100157,-15835752,-23923978,-1005098,-26450192,15509408,12376730,-3479146,33166107,-8042750 ), + new FieldElement( 20909231,13023121,-9209752,16251778,-5778415,-8094914,12412151,10018715,2213263,-13878373 ), + new FieldElement( 32529814,-11074689,30361439,-16689753,-9135940,1513226,22922121,6382134,-5766928,8371348 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 9923462,11271500,12616794,3544722,-29998368,-1721626,12891687,-8193132,-26442943,10486144 ), + new FieldElement( -22597207,-7012665,8587003,-8257861,4084309,-12970062,361726,2610596,-23921530,-11455195 ), + new FieldElement( 5408411,-1136691,-4969122,10561668,24145918,14240566,31319731,-4235541,19985175,-3436086 ) + ), + new GroupElementPreComp( + new FieldElement( -13994457,16616821,14549246,3341099,32155958,13648976,-17577068,8849297,65030,8370684 ), + new FieldElement( -8320926,-12049626,31204563,5839400,-20627288,-1057277,-19442942,6922164,12743482,-9800518 ), + new FieldElement( -2361371,12678785,28815050,4759974,-23893047,4884717,23783145,11038569,18800704,255233 ) + ), + new GroupElementPreComp( + new FieldElement( -5269658,-1773886,13957886,7990715,23132995,728773,13393847,9066957,19258688,-14753793 ), + new FieldElement( -2936654,-10827535,-10432089,14516793,-3640786,4372541,-31934921,2209390,-1524053,2055794 ), + new FieldElement( 580882,16705327,5468415,-2683018,-30926419,-14696000,-7203346,-8994389,-30021019,7394435 ) + ), + new GroupElementPreComp( + new FieldElement( 23838809,1822728,-15738443,15242727,8318092,-3733104,-21672180,-3492205,-4821741,14799921 ), + new FieldElement( 13345610,9759151,3371034,-16137791,16353039,8577942,31129804,13496856,-9056018,7402518 ), + new FieldElement( 2286874,-4435931,-20042458,-2008336,-13696227,5038122,11006906,-15760352,8205061,1607563 ) + ), + new GroupElementPreComp( + new FieldElement( 14414086,-8002132,3331830,-3208217,22249151,-5594188,18364661,-2906958,30019587,-9029278 ), + new FieldElement( -27688051,1585953,-10775053,931069,-29120221,-11002319,-14410829,12029093,9944378,8024 ), + new FieldElement( 4368715,-3709630,29874200,-15022983,-20230386,-11410704,-16114594,-999085,-8142388,5640030 ) + ), + new GroupElementPreComp( + new FieldElement( 10299610,13746483,11661824,16234854,7630238,5998374,9809887,-16694564,15219798,-14327783 ), + new FieldElement( 27425505,-5719081,3055006,10660664,23458024,595578,-15398605,-1173195,-18342183,9742717 ), + new FieldElement( 6744077,2427284,26042789,2720740,-847906,1118974,32324614,7406442,12420155,1994844 ) + ), + new GroupElementPreComp( + new FieldElement( 14012521,-5024720,-18384453,-9578469,-26485342,-3936439,-13033478,-10909803,24319929,-6446333 ), + new FieldElement( 16412690,-4507367,10772641,15929391,-17068788,-4658621,10555945,-10484049,-30102368,-4739048 ), + new FieldElement( 22397382,-7767684,-9293161,-12792868,17166287,-9755136,-27333065,6199366,21880021,-12250760 ) + ), + new GroupElementPreComp( + new FieldElement( -4283307,5368523,-31117018,8163389,-30323063,3209128,16557151,8890729,8840445,4957760 ), + new FieldElement( -15447727,709327,-6919446,-10870178,-29777922,6522332,-21720181,12130072,-14796503,5005757 ), + new FieldElement( -2114751,-14308128,23019042,15765735,-25269683,6002752,10183197,-13239326,-16395286,-2176112 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -19025756,1632005,13466291,-7995100,-23640451,16573537,-32013908,-3057104,22208662,2000468 ), + new FieldElement( 3065073,-1412761,-25598674,-361432,-17683065,-5703415,-8164212,11248527,-3691214,-7414184 ), + new FieldElement( 10379208,-6045554,8877319,1473647,-29291284,-12507580,16690915,2553332,-3132688,16400289 ) + ), + new GroupElementPreComp( + new FieldElement( 15716668,1254266,-18472690,7446274,-8448918,6344164,-22097271,-7285580,26894937,9132066 ), + new FieldElement( 24158887,12938817,11085297,-8177598,-28063478,-4457083,-30576463,64452,-6817084,-2692882 ), + new FieldElement( 13488534,7794716,22236231,5989356,25426474,-12578208,2350710,-3418511,-4688006,2364226 ) + ), + new GroupElementPreComp( + new FieldElement( 16335052,9132434,25640582,6678888,1725628,8517937,-11807024,-11697457,15445875,-7798101 ), + new FieldElement( 29004207,-7867081,28661402,-640412,-12794003,-7943086,31863255,-4135540,-278050,-15759279 ), + new FieldElement( -6122061,-14866665,-28614905,14569919,-10857999,-3591829,10343412,-6976290,-29828287,-10815811 ) + ), + new GroupElementPreComp( + new FieldElement( 27081650,3463984,14099042,-4517604,1616303,-6205604,29542636,15372179,17293797,960709 ), + new FieldElement( 20263915,11434237,-5765435,11236810,13505955,-10857102,-16111345,6493122,-19384511,7639714 ), + new FieldElement( -2830798,-14839232,25403038,-8215196,-8317012,-16173699,18006287,-16043750,29994677,-15808121 ) + ), + new GroupElementPreComp( + new FieldElement( 9769828,5202651,-24157398,-13631392,-28051003,-11561624,-24613141,-13860782,-31184575,709464 ), + new FieldElement( 12286395,13076066,-21775189,-1176622,-25003198,4057652,-32018128,-8890874,16102007,13205847 ), + new FieldElement( 13733362,5599946,10557076,3195751,-5557991,8536970,-25540170,8525972,10151379,10394400 ) + ), + new GroupElementPreComp( + new FieldElement( 4024660,-16137551,22436262,12276534,-9099015,-2686099,19698229,11743039,-33302334,8934414 ), + new FieldElement( -15879800,-4525240,-8580747,-2934061,14634845,-698278,-9449077,3137094,-11536886,11721158 ), + new FieldElement( 17555939,-5013938,8268606,2331751,-22738815,9761013,9319229,8835153,-9205489,-1280045 ) + ), + new GroupElementPreComp( + new FieldElement( -461409,-7830014,20614118,16688288,-7514766,-4807119,22300304,505429,6108462,-6183415 ), + new FieldElement( -5070281,12367917,-30663534,3234473,32617080,-8422642,29880583,-13483331,-26898490,-7867459 ), + new FieldElement( -31975283,5726539,26934134,10237677,-3173717,-605053,24199304,3795095,7592688,-14992079 ) + ), + new GroupElementPreComp( + new FieldElement( 21594432,-14964228,17466408,-4077222,32537084,2739898,6407723,12018833,-28256052,4298412 ), + new FieldElement( -20650503,-11961496,-27236275,570498,3767144,-1717540,13891942,-1569194,13717174,10805743 ), + new FieldElement( -14676630,-15644296,15287174,11927123,24177847,-8175568,-796431,14860609,-26938930,-5863836 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 12962541,5311799,-10060768,11658280,18855286,-7954201,13286263,-12808704,-4381056,9882022 ), + new FieldElement( 18512079,11319350,-20123124,15090309,18818594,5271736,-22727904,3666879,-23967430,-3299429 ), + new FieldElement( -6789020,-3146043,16192429,13241070,15898607,-14206114,-10084880,-6661110,-2403099,5276065 ) + ), + new GroupElementPreComp( + new FieldElement( 30169808,-5317648,26306206,-11750859,27814964,7069267,7152851,3684982,1449224,13082861 ), + new FieldElement( 10342826,3098505,2119311,193222,25702612,12233820,23697382,15056736,-21016438,-8202000 ), + new FieldElement( -33150110,3261608,22745853,7948688,19370557,-15177665,-26171976,6482814,-10300080,-11060101 ) + ), + new GroupElementPreComp( + new FieldElement( 32869458,-5408545,25609743,15678670,-10687769,-15471071,26112421,2521008,-22664288,6904815 ), + new FieldElement( 29506923,4457497,3377935,-9796444,-30510046,12935080,1561737,3841096,-29003639,-6657642 ), + new FieldElement( 10340844,-6630377,-18656632,-2278430,12621151,-13339055,30878497,-11824370,-25584551,5181966 ) + ), + new GroupElementPreComp( + new FieldElement( 25940115,-12658025,17324188,-10307374,-8671468,15029094,24396252,-16450922,-2322852,-12388574 ), + new FieldElement( -21765684,9916823,-1300409,4079498,-1028346,11909559,1782390,12641087,20603771,-6561742 ), + new FieldElement( -18882287,-11673380,24849422,11501709,13161720,-4768874,1925523,11914390,4662781,7820689 ) + ), + new GroupElementPreComp( + new FieldElement( 12241050,-425982,8132691,9393934,32846760,-1599620,29749456,12172924,16136752,15264020 ), + new FieldElement( -10349955,-14680563,-8211979,2330220,-17662549,-14545780,10658213,6671822,19012087,3772772 ), + new FieldElement( 3753511,-3421066,10617074,2028709,14841030,-6721664,28718732,-15762884,20527771,12988982 ) + ), + new GroupElementPreComp( + new FieldElement( -14822485,-5797269,-3707987,12689773,-898983,-10914866,-24183046,-10564943,3299665,-12424953 ), + new FieldElement( -16777703,-15253301,-9642417,4978983,3308785,8755439,6943197,6461331,-25583147,8991218 ), + new FieldElement( -17226263,1816362,-1673288,-6086439,31783888,-8175991,-32948145,7417950,-30242287,1507265 ) + ), + new GroupElementPreComp( + new FieldElement( 29692663,6829891,-10498800,4334896,20945975,-11906496,-28887608,8209391,14606362,-10647073 ), + new FieldElement( -3481570,8707081,32188102,5672294,22096700,1711240,-33020695,9761487,4170404,-2085325 ), + new FieldElement( -11587470,14855945,-4127778,-1531857,-26649089,15084046,22186522,16002000,-14276837,-8400798 ) + ), + new GroupElementPreComp( + new FieldElement( -4811456,13761029,-31703877,-2483919,-3312471,7869047,-7113572,-9620092,13240845,10965870 ), + new FieldElement( -7742563,-8256762,-14768334,-13656260,-23232383,12387166,4498947,14147411,29514390,4302863 ), + new FieldElement( -13413405,-12407859,20757302,-13801832,14785143,8976368,-5061276,-2144373,17846988,-13971927 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -2244452,-754728,-4597030,-1066309,-6247172,1455299,-21647728,-9214789,-5222701,12650267 ), + new FieldElement( -9906797,-16070310,21134160,12198166,-27064575,708126,387813,13770293,-19134326,10958663 ), + new FieldElement( 22470984,12369526,23446014,-5441109,-21520802,-9698723,-11772496,-11574455,-25083830,4271862 ) + ), + new GroupElementPreComp( + new FieldElement( -25169565,-10053642,-19909332,15361595,-5984358,2159192,75375,-4278529,-32526221,8469673 ), + new FieldElement( 15854970,4148314,-8893890,7259002,11666551,13824734,-30531198,2697372,24154791,-9460943 ), + new FieldElement( 15446137,-15806644,29759747,14019369,30811221,-9610191,-31582008,12840104,24913809,9815020 ) + ), + new GroupElementPreComp( + new FieldElement( -4709286,-5614269,-31841498,-12288893,-14443537,10799414,-9103676,13438769,18735128,9466238 ), + new FieldElement( 11933045,9281483,5081055,-5183824,-2628162,-4905629,-7727821,-10896103,-22728655,16199064 ), + new FieldElement( 14576810,379472,-26786533,-8317236,-29426508,-10812974,-102766,1876699,30801119,2164795 ) + ), + new GroupElementPreComp( + new FieldElement( 15995086,3199873,13672555,13712240,-19378835,-4647646,-13081610,-15496269,-13492807,1268052 ), + new FieldElement( -10290614,-3659039,-3286592,10948818,23037027,3794475,-3470338,-12600221,-17055369,3565904 ), + new FieldElement( 29210088,-9419337,-5919792,-4952785,10834811,-13327726,-16512102,-10820713,-27162222,-14030531 ) + ), + new GroupElementPreComp( + new FieldElement( -13161890,15508588,16663704,-8156150,-28349942,9019123,-29183421,-3769423,2244111,-14001979 ), + new FieldElement( -5152875,-3800936,-9306475,-6071583,16243069,14684434,-25673088,-16180800,13491506,4641841 ), + new FieldElement( 10813417,643330,-19188515,-728916,30292062,-16600078,27548447,-7721242,14476989,-12767431 ) + ), + new GroupElementPreComp( + new FieldElement( 10292079,9984945,6481436,8279905,-7251514,7032743,27282937,-1644259,-27912810,12651324 ), + new FieldElement( -31185513,-813383,22271204,11835308,10201545,15351028,17099662,3988035,21721536,-3148940 ), + new FieldElement( 10202177,-6545839,-31373232,-9574638,-32150642,-8119683,-12906320,3852694,13216206,14842320 ) + ), + new GroupElementPreComp( + new FieldElement( -15815640,-10601066,-6538952,-7258995,-6984659,-6581778,-31500847,13765824,-27434397,9900184 ), + new FieldElement( 14465505,-13833331,-32133984,-14738873,-27443187,12990492,33046193,15796406,-7051866,-8040114 ), + new FieldElement( 30924417,-8279620,6359016,-12816335,16508377,9071735,-25488601,15413635,9524356,-7018878 ) + ), + new GroupElementPreComp( + new FieldElement( 12274201,-13175547,32627641,-1785326,6736625,13267305,5237659,-5109483,15663516,4035784 ), + new FieldElement( -2951309,8903985,17349946,601635,-16432815,-4612556,-13732739,-15889334,-22258478,4659091 ), + new FieldElement( -16916263,-4952973,-30393711,-15158821,20774812,15897498,5736189,15026997,-2178256,-13455585 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -8858980,-2219056,28571666,-10155518,-474467,-10105698,-3801496,278095,23440562,-290208 ), + new FieldElement( 10226241,-5928702,15139956,120818,-14867693,5218603,32937275,11551483,-16571960,-7442864 ), + new FieldElement( 17932739,-12437276,-24039557,10749060,11316803,7535897,22503767,5561594,-3646624,3898661 ) + ), + new GroupElementPreComp( + new FieldElement( 7749907,-969567,-16339731,-16464,-25018111,15122143,-1573531,7152530,21831162,1245233 ), + new FieldElement( 26958459,-14658026,4314586,8346991,-5677764,11960072,-32589295,-620035,-30402091,-16716212 ), + new FieldElement( -12165896,9166947,33491384,13673479,29787085,13096535,6280834,14587357,-22338025,13987525 ) + ), + new GroupElementPreComp( + new FieldElement( -24349909,7778775,21116000,15572597,-4833266,-5357778,-4300898,-5124639,-7469781,-2858068 ), + new FieldElement( 9681908,-6737123,-31951644,13591838,-6883821,386950,31622781,6439245,-14581012,4091397 ), + new FieldElement( -8426427,1470727,-28109679,-1596990,3978627,-5123623,-19622683,12092163,29077877,-14741988 ) + ), + new GroupElementPreComp( + new FieldElement( 5269168,-6859726,-13230211,-8020715,25932563,1763552,-5606110,-5505881,-20017847,2357889 ), + new FieldElement( 32264008,-15407652,-5387735,-1160093,-2091322,-3946900,23104804,-12869908,5727338,189038 ), + new FieldElement( 14609123,-8954470,-6000566,-16622781,-14577387,-7743898,-26745169,10942115,-25888931,-14884697 ) + ), + new GroupElementPreComp( + new FieldElement( 20513500,5557931,-15604613,7829531,26413943,-2019404,-21378968,7471781,13913677,-5137875 ), + new FieldElement( -25574376,11967826,29233242,12948236,-6754465,4713227,-8940970,14059180,12878652,8511905 ), + new FieldElement( -25656801,3393631,-2955415,-7075526,-2250709,9366908,-30223418,6812974,5568676,-3127656 ) + ), + new GroupElementPreComp( + new FieldElement( 11630004,12144454,2116339,13606037,27378885,15676917,-17408753,-13504373,-14395196,8070818 ), + new FieldElement( 27117696,-10007378,-31282771,-5570088,1127282,12772488,-29845906,10483306,-11552749,-1028714 ), + new FieldElement( 10637467,-5688064,5674781,1072708,-26343588,-6982302,-1683975,9177853,-27493162,15431203 ) + ), + new GroupElementPreComp( + new FieldElement( 20525145,10892566,-12742472,12779443,-29493034,16150075,-28240519,14943142,-15056790,-7935931 ), + new FieldElement( -30024462,5626926,-551567,-9981087,753598,11981191,25244767,-3239766,-3356550,9594024 ), + new FieldElement( -23752644,2636870,-5163910,-10103818,585134,7877383,11345683,-6492290,13352335,-10977084 ) + ), + new GroupElementPreComp( + new FieldElement( -1931799,-5407458,3304649,-12884869,17015806,-4877091,-29783850,-7752482,-13215537,-319204 ), + new FieldElement( 20239939,6607058,6203985,3483793,-18386976,-779229,-20723742,15077870,-22750759,14523817 ), + new FieldElement( 27406042,-6041657,27423596,-4497394,4996214,10002360,-28842031,-4545494,-30172742,-4805667 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 11374242,12660715,17861383,-12540833,10935568,1099227,-13886076,-9091740,-27727044,11358504 ), + new FieldElement( -12730809,10311867,1510375,10778093,-2119455,-9145702,32676003,11149336,-26123651,4985768 ), + new FieldElement( -19096303,341147,-6197485,-239033,15756973,-8796662,-983043,13794114,-19414307,-15621255 ) + ), + new GroupElementPreComp( + new FieldElement( 6490081,11940286,25495923,-7726360,8668373,-8751316,3367603,6970005,-1691065,-9004790 ), + new FieldElement( 1656497,13457317,15370807,6364910,13605745,8362338,-19174622,-5475723,-16796596,-5031438 ), + new FieldElement( -22273315,-13524424,-64685,-4334223,-18605636,-10921968,-20571065,-7007978,-99853,-10237333 ) + ), + new GroupElementPreComp( + new FieldElement( 17747465,10039260,19368299,-4050591,-20630635,-16041286,31992683,-15857976,-29260363,-5511971 ), + new FieldElement( 31932027,-4986141,-19612382,16366580,22023614,88450,11371999,-3744247,4882242,-10626905 ), + new FieldElement( 29796507,37186,19818052,10115756,-11829032,3352736,18551198,3272828,-5190932,-4162409 ) + ), + new GroupElementPreComp( + new FieldElement( 12501286,4044383,-8612957,-13392385,-32430052,5136599,-19230378,-3529697,330070,-3659409 ), + new FieldElement( 6384877,2899513,17807477,7663917,-2358888,12363165,25366522,-8573892,-271295,12071499 ), + new FieldElement( -8365515,-4042521,25133448,-4517355,-6211027,2265927,-32769618,1936675,-5159697,3829363 ) + ), + new GroupElementPreComp( + new FieldElement( 28425966,-5835433,-577090,-4697198,-14217555,6870930,7921550,-6567787,26333140,14267664 ), + new FieldElement( -11067219,11871231,27385719,-10559544,-4585914,-11189312,10004786,-8709488,-21761224,8930324 ), + new FieldElement( -21197785,-16396035,25654216,-1725397,12282012,11008919,1541940,4757911,-26491501,-16408940 ) + ), + new GroupElementPreComp( + new FieldElement( 13537262,-7759490,-20604840,10961927,-5922820,-13218065,-13156584,6217254,-15943699,13814990 ), + new FieldElement( -17422573,15157790,18705543,29619,24409717,-260476,27361681,9257833,-1956526,-1776914 ), + new FieldElement( -25045300,-10191966,15366585,15166509,-13105086,8423556,-29171540,12361135,-18685978,4578290 ) + ), + new GroupElementPreComp( + new FieldElement( 24579768,3711570,1342322,-11180126,-27005135,14124956,-22544529,14074919,21964432,8235257 ), + new FieldElement( -6528613,-2411497,9442966,-5925588,12025640,-1487420,-2981514,-1669206,13006806,2355433 ), + new FieldElement( -16304899,-13605259,-6632427,-5142349,16974359,-10911083,27202044,1719366,1141648,-12796236 ) + ), + new GroupElementPreComp( + new FieldElement( -12863944,-13219986,-8318266,-11018091,-6810145,-4843894,13475066,-3133972,32674895,13715045 ), + new FieldElement( 11423335,-5468059,32344216,8962751,24989809,9241752,-13265253,16086212,-28740881,-15642093 ), + new FieldElement( -1409668,12530728,-6368726,10847387,19531186,-14132160,-11709148,7791794,-27245943,4383347 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -28970898,5271447,-1266009,-9736989,-12455236,16732599,-4862407,-4906449,27193557,6245191 ), + new FieldElement( -15193956,5362278,-1783893,2695834,4960227,12840725,23061898,3260492,22510453,8577507 ), + new FieldElement( -12632451,11257346,-32692994,13548177,-721004,10879011,31168030,13952092,-29571492,-3635906 ) + ), + new GroupElementPreComp( + new FieldElement( 3877321,-9572739,32416692,5405324,-11004407,-13656635,3759769,11935320,5611860,8164018 ), + new FieldElement( -16275802,14667797,15906460,12155291,-22111149,-9039718,32003002,-8832289,5773085,-8422109 ), + new FieldElement( -23788118,-8254300,1950875,8937633,18686727,16459170,-905725,12376320,31632953,190926 ) + ), + new GroupElementPreComp( + new FieldElement( -24593607,-16138885,-8423991,13378746,14162407,6901328,-8288749,4508564,-25341555,-3627528 ), + new FieldElement( 8884438,-5884009,6023974,10104341,-6881569,-4941533,18722941,-14786005,-1672488,827625 ), + new FieldElement( -32720583,-16289296,-32503547,7101210,13354605,2659080,-1800575,-14108036,-24878478,1541286 ) + ), + new GroupElementPreComp( + new FieldElement( 2901347,-1117687,3880376,-10059388,-17620940,-3612781,-21802117,-3567481,20456845,-1885033 ), + new FieldElement( 27019610,12299467,-13658288,-1603234,-12861660,-4861471,-19540150,-5016058,29439641,15138866 ), + new FieldElement( 21536104,-6626420,-32447818,-10690208,-22408077,5175814,-5420040,-16361163,7779328,109896 ) + ), + new GroupElementPreComp( + new FieldElement( 30279744,14648750,-8044871,6425558,13639621,-743509,28698390,12180118,23177719,-554075 ), + new FieldElement( 26572847,3405927,-31701700,12890905,-19265668,5335866,-6493768,2378492,4439158,-13279347 ), + new FieldElement( -22716706,3489070,-9225266,-332753,18875722,-1140095,14819434,-12731527,-17717757,-5461437 ) + ), + new GroupElementPreComp( + new FieldElement( -5056483,16566551,15953661,3767752,-10436499,15627060,-820954,2177225,8550082,-15114165 ), + new FieldElement( -18473302,16596775,-381660,15663611,22860960,15585581,-27844109,-3582739,-23260460,-8428588 ), + new FieldElement( -32480551,15707275,-8205912,-5652081,29464558,2713815,-22725137,15860482,-21902570,1494193 ) + ), + new GroupElementPreComp( + new FieldElement( -19562091,-14087393,-25583872,-9299552,13127842,759709,21923482,16529112,8742704,12967017 ), + new FieldElement( -28464899,1553205,32536856,-10473729,-24691605,-406174,-8914625,-2933896,-29903758,15553883 ), + new FieldElement( 21877909,3230008,9881174,10539357,-4797115,2841332,11543572,14513274,19375923,-12647961 ) + ), + new GroupElementPreComp( + new FieldElement( 8832269,-14495485,13253511,5137575,5037871,4078777,24880818,-6222716,2862653,9455043 ), + new FieldElement( 29306751,5123106,20245049,-14149889,9592566,8447059,-2077124,-2990080,15511449,4789663 ), + new FieldElement( -20679756,7004547,8824831,-9434977,-4045704,-3750736,-5754762,108893,23513200,16652362 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -33256173,4144782,-4476029,-6579123,10770039,-7155542,-6650416,-12936300,-18319198,10212860 ), + new FieldElement( 2756081,8598110,7383731,-6859892,22312759,-1105012,21179801,2600940,-9988298,-12506466 ), + new FieldElement( -24645692,13317462,-30449259,-15653928,21365574,-10869657,11344424,864440,-2499677,-16710063 ) + ), + new GroupElementPreComp( + new FieldElement( -26432803,6148329,-17184412,-14474154,18782929,-275997,-22561534,211300,2719757,4940997 ), + new FieldElement( -1323882,3911313,-6948744,14759765,-30027150,7851207,21690126,8518463,26699843,5276295 ), + new FieldElement( -13149873,-6429067,9396249,365013,24703301,-10488939,1321586,149635,-15452774,7159369 ) + ), + new GroupElementPreComp( + new FieldElement( 9987780,-3404759,17507962,9505530,9731535,-2165514,22356009,8312176,22477218,-8403385 ), + new FieldElement( 18155857,-16504990,19744716,9006923,15154154,-10538976,24256460,-4864995,-22548173,9334109 ), + new FieldElement( 2986088,-4911893,10776628,-3473844,10620590,-7083203,-21413845,14253545,-22587149,536906 ) + ), + new GroupElementPreComp( + new FieldElement( 4377756,8115836,24567078,15495314,11625074,13064599,7390551,10589625,10838060,-15420424 ), + new FieldElement( -19342404,867880,9277171,-3218459,-14431572,-1986443,19295826,-15796950,6378260,699185 ), + new FieldElement( 7895026,4057113,-7081772,-13077756,-17886831,-323126,-716039,15693155,-5045064,-13373962 ) + ), + new GroupElementPreComp( + new FieldElement( -7737563,-5869402,-14566319,-7406919,11385654,13201616,31730678,-10962840,-3918636,-9669325 ), + new FieldElement( 10188286,-15770834,-7336361,13427543,22223443,14896287,30743455,7116568,-21786507,5427593 ), + new FieldElement( 696102,13206899,27047647,-10632082,15285305,-9853179,10798490,-4578720,19236243,12477404 ) + ), + new GroupElementPreComp( + new FieldElement( -11229439,11243796,-17054270,-8040865,-788228,-8167967,-3897669,11180504,-23169516,7733644 ), + new FieldElement( 17800790,-14036179,-27000429,-11766671,23887827,3149671,23466177,-10538171,10322027,15313801 ), + new FieldElement( 26246234,11968874,32263343,-5468728,6830755,-13323031,-15794704,-101982,-24449242,10890804 ) + ), + new GroupElementPreComp( + new FieldElement( -31365647,10271363,-12660625,-6267268,16690207,-13062544,-14982212,16484931,25180797,-5334884 ), + new FieldElement( -586574,10376444,-32586414,-11286356,19801893,10997610,2276632,9482883,316878,13820577 ), + new FieldElement( -9882808,-4510367,-2115506,16457136,-11100081,11674996,30756178,-7515054,30696930,-3712849 ) + ), + new GroupElementPreComp( + new FieldElement( 32988917,-9603412,12499366,7910787,-10617257,-11931514,-7342816,-9985397,-32349517,7392473 ), + new FieldElement( -8855661,15927861,9866406,-3649411,-2396914,-16655781,-30409476,-9134995,25112947,-2926644 ), + new FieldElement( -2504044,-436966,25621774,-5678772,15085042,-5479877,-24884878,-13526194,5537438,-13914319 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -11225584,2320285,-9584280,10149187,-33444663,5808648,-14876251,-1729667,31234590,6090599 ), + new FieldElement( -9633316,116426,26083934,2897444,-6364437,-2688086,609721,15878753,-6970405,-9034768 ), + new FieldElement( -27757857,247744,-15194774,-9002551,23288161,-10011936,-23869595,6503646,20650474,1804084 ) + ), + new GroupElementPreComp( + new FieldElement( -27589786,15456424,8972517,8469608,15640622,4439847,3121995,-10329713,27842616,-202328 ), + new FieldElement( -15306973,2839644,22530074,10026331,4602058,5048462,28248656,5031932,-11375082,12714369 ), + new FieldElement( 20807691,-7270825,29286141,11421711,-27876523,-13868230,-21227475,1035546,-19733229,12796920 ) + ), + new GroupElementPreComp( + new FieldElement( 12076899,-14301286,-8785001,-11848922,-25012791,16400684,-17591495,-12899438,3480665,-15182815 ), + new FieldElement( -32361549,5457597,28548107,7833186,7303070,-11953545,-24363064,-15921875,-33374054,2771025 ), + new FieldElement( -21389266,421932,26597266,6860826,22486084,-6737172,-17137485,-4210226,-24552282,15673397 ) + ), + new GroupElementPreComp( + new FieldElement( -20184622,2338216,19788685,-9620956,-4001265,-8740893,-20271184,4733254,3727144,-12934448 ), + new FieldElement( 6120119,814863,-11794402,-622716,6812205,-15747771,2019594,7975683,31123697,-10958981 ), + new FieldElement( 30069250,-11435332,30434654,2958439,18399564,-976289,12296869,9204260,-16432438,9648165 ) + ), + new GroupElementPreComp( + new FieldElement( 32705432,-1550977,30705658,7451065,-11805606,9631813,3305266,5248604,-26008332,-11377501 ), + new FieldElement( 17219865,2375039,-31570947,-5575615,-19459679,9219903,294711,15298639,2662509,-16297073 ), + new FieldElement( -1172927,-7558695,-4366770,-4287744,-21346413,-8434326,32087529,-1222777,32247248,-14389861 ) + ), + new GroupElementPreComp( + new FieldElement( 14312628,1221556,17395390,-8700143,-4945741,-8684635,-28197744,-9637817,-16027623,-13378845 ), + new FieldElement( -1428825,-9678990,-9235681,6549687,-7383069,-468664,23046502,9803137,17597934,2346211 ), + new FieldElement( 18510800,15337574,26171504,981392,-22241552,7827556,-23491134,-11323352,3059833,-11782870 ) + ), + new GroupElementPreComp( + new FieldElement( 10141598,6082907,17829293,-1947643,9830092,13613136,-25556636,-5544586,-33502212,3592096 ), + new FieldElement( 33114168,-15889352,-26525686,-13343397,33076705,8716171,1151462,1521897,-982665,-6837803 ), + new FieldElement( -32939165,-4255815,23947181,-324178,-33072974,-12305637,-16637686,3891704,26353178,693168 ) + ), + new GroupElementPreComp( + new FieldElement( 30374239,1595580,-16884039,13186931,4600344,406904,9585294,-400668,31375464,14369965 ), + new FieldElement( -14370654,-7772529,1510301,6434173,-18784789,-6262728,32732230,-13108839,17901441,16011505 ), + new FieldElement( 18171223,-11934626,-12500402,15197122,-11038147,-15230035,-19172240,-16046376,8764035,12309598 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 5975908,-5243188,-19459362,-9681747,-11541277,14015782,-23665757,1228319,17544096,-10593782 ), + new FieldElement( 5811932,-1715293,3442887,-2269310,-18367348,-8359541,-18044043,-15410127,-5565381,12348900 ), + new FieldElement( -31399660,11407555,25755363,6891399,-3256938,14872274,-24849353,8141295,-10632534,-585479 ) + ), + new GroupElementPreComp( + new FieldElement( -12675304,694026,-5076145,13300344,14015258,-14451394,-9698672,-11329050,30944593,1130208 ), + new FieldElement( 8247766,-6710942,-26562381,-7709309,-14401939,-14648910,4652152,2488540,23550156,-271232 ), + new FieldElement( 17294316,-3788438,7026748,15626851,22990044,113481,2267737,-5908146,-408818,-137719 ) + ), + new GroupElementPreComp( + new FieldElement( 16091085,-16253926,18599252,7340678,2137637,-1221657,-3364161,14550936,3260525,-7166271 ), + new FieldElement( -4910104,-13332887,18550887,10864893,-16459325,-7291596,-23028869,-13204905,-12748722,2701326 ), + new FieldElement( -8574695,16099415,4629974,-16340524,-20786213,-6005432,-10018363,9276971,11329923,1862132 ) + ), + new GroupElementPreComp( + new FieldElement( 14763076,-15903608,-30918270,3689867,3511892,10313526,-21951088,12219231,-9037963,-940300 ), + new FieldElement( 8894987,-3446094,6150753,3013931,301220,15693451,-31981216,-2909717,-15438168,11595570 ), + new FieldElement( 15214962,3537601,-26238722,-14058872,4418657,-15230761,13947276,10730794,-13489462,-4363670 ) + ), + new GroupElementPreComp( + new FieldElement( -2538306,7682793,32759013,263109,-29984731,-7955452,-22332124,-10188635,977108,699994 ), + new FieldElement( -12466472,4195084,-9211532,550904,-15565337,12917920,19118110,-439841,-30534533,-14337913 ), + new FieldElement( 31788461,-14507657,4799989,7372237,8808585,-14747943,9408237,-10051775,12493932,-5409317 ) + ), + new GroupElementPreComp( + new FieldElement( -25680606,5260744,-19235809,-6284470,-3695942,16566087,27218280,2607121,29375955,6024730 ), + new FieldElement( 842132,-2794693,-4763381,-8722815,26332018,-12405641,11831880,6985184,-9940361,2854096 ), + new FieldElement( -4847262,-7969331,2516242,-5847713,9695691,-7221186,16512645,960770,12121869,16648078 ) + ), + new GroupElementPreComp( + new FieldElement( -15218652,14667096,-13336229,2013717,30598287,-464137,-31504922,-7882064,20237806,2838411 ), + new FieldElement( -19288047,4453152,15298546,-16178388,22115043,-15972604,12544294,-13470457,1068881,-12499905 ), + new FieldElement( -9558883,-16518835,33238498,13506958,30505848,-1114596,-8486907,-2630053,12521378,4845654 ) + ), + new GroupElementPreComp( + new FieldElement( -28198521,10744108,-2958380,10199664,7759311,-13088600,3409348,-873400,-6482306,-12885870 ), + new FieldElement( -23561822,6230156,-20382013,10655314,-24040585,-11621172,10477734,-1240216,-3113227,13974498 ), + new FieldElement( 12966261,15550616,-32038948,-1615346,21025980,-629444,5642325,7188737,18895762,12629579 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 14741879,-14946887,22177208,-11721237,1279741,8058600,11758140,789443,32195181,3895677 ), + new FieldElement( 10758205,15755439,-4509950,9243698,-4879422,6879879,-2204575,-3566119,-8982069,4429647 ), + new FieldElement( -2453894,15725973,-20436342,-10410672,-5803908,-11040220,-7135870,-11642895,18047436,-15281743 ) + ), + new GroupElementPreComp( + new FieldElement( -25173001,-11307165,29759956,11776784,-22262383,-15820455,10993114,-12850837,-17620701,-9408468 ), + new FieldElement( 21987233,700364,-24505048,14972008,-7774265,-5718395,32155026,2581431,-29958985,8773375 ), + new FieldElement( -25568350,454463,-13211935,16126715,25240068,8594567,20656846,12017935,-7874389,-13920155 ) + ), + new GroupElementPreComp( + new FieldElement( 6028182,6263078,-31011806,-11301710,-818919,2461772,-31841174,-5468042,-1721788,-2776725 ), + new FieldElement( -12278994,16624277,987579,-5922598,32908203,1248608,7719845,-4166698,28408820,6816612 ), + new FieldElement( -10358094,-8237829,19549651,-12169222,22082623,16147817,20613181,13982702,-10339570,5067943 ) + ), + new GroupElementPreComp( + new FieldElement( -30505967,-3821767,12074681,13582412,-19877972,2443951,-19719286,12746132,5331210,-10105944 ), + new FieldElement( 30528811,3601899,-1957090,4619785,-27361822,-15436388,24180793,-12570394,27679908,-1648928 ), + new FieldElement( 9402404,-13957065,32834043,10838634,-26580150,-13237195,26653274,-8685565,22611444,-12715406 ) + ), + new GroupElementPreComp( + new FieldElement( 22190590,1118029,22736441,15130463,-30460692,-5991321,19189625,-4648942,4854859,6622139 ), + new FieldElement( -8310738,-2953450,-8262579,-3388049,-10401731,-271929,13424426,-3567227,26404409,13001963 ), + new FieldElement( -31241838,-15415700,-2994250,8939346,11562230,-12840670,-26064365,-11621720,-15405155,11020693 ) + ), + new GroupElementPreComp( + new FieldElement( 1866042,-7949489,-7898649,-10301010,12483315,13477547,3175636,-12424163,28761762,1406734 ), + new FieldElement( -448555,-1777666,13018551,3194501,-9580420,-11161737,24760585,-4347088,25577411,-13378680 ), + new FieldElement( -24290378,4759345,-690653,-1852816,2066747,10693769,-29595790,9884936,-9368926,4745410 ) + ), + new GroupElementPreComp( + new FieldElement( -9141284,6049714,-19531061,-4341411,-31260798,9944276,-15462008,-11311852,10931924,-11931931 ), + new FieldElement( -16561513,14112680,-8012645,4817318,-8040464,-11414606,-22853429,10856641,-20470770,13434654 ), + new FieldElement( 22759489,-10073434,-16766264,-1871422,13637442,-10168091,1765144,-12654326,28445307,-5364710 ) + ), + new GroupElementPreComp( + new FieldElement( 29875063,12493613,2795536,-3786330,1710620,15181182,-10195717,-8788675,9074234,1167180 ), + new FieldElement( -26205683,11014233,-9842651,-2635485,-26908120,7532294,-18716888,-9535498,3843903,9367684 ), + new FieldElement( -10969595,-6403711,9591134,9582310,11349256,108879,16235123,8601684,-139197,4242895 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 22092954,-13191123,-2042793,-11968512,32186753,-11517388,-6574341,2470660,-27417366,16625501 ), + new FieldElement( -11057722,3042016,13770083,-9257922,584236,-544855,-7770857,2602725,-27351616,14247413 ), + new FieldElement( 6314175,-10264892,-32772502,15957557,-10157730,168750,-8618807,14290061,27108877,-1180880 ) + ), + new GroupElementPreComp( + new FieldElement( -8586597,-7170966,13241782,10960156,-32991015,-13794596,33547976,-11058889,-27148451,981874 ), + new FieldElement( 22833440,9293594,-32649448,-13618667,-9136966,14756819,-22928859,-13970780,-10479804,-16197962 ), + new FieldElement( -7768587,3326786,-28111797,10783824,19178761,14905060,22680049,13906969,-15933690,3797899 ) + ), + new GroupElementPreComp( + new FieldElement( 21721356,-4212746,-12206123,9310182,-3882239,-13653110,23740224,-2709232,20491983,-8042152 ), + new FieldElement( 9209270,-15135055,-13256557,-6167798,-731016,15289673,25947805,15286587,30997318,-6703063 ), + new FieldElement( 7392032,16618386,23946583,-8039892,-13265164,-1533858,-14197445,-2321576,17649998,-250080 ) + ), + new GroupElementPreComp( + new FieldElement( -9301088,-14193827,30609526,-3049543,-25175069,-1283752,-15241566,-9525724,-2233253,7662146 ), + new FieldElement( -17558673,1763594,-33114336,15908610,-30040870,-12174295,7335080,-8472199,-3174674,3440183 ), + new FieldElement( -19889700,-5977008,-24111293,-9688870,10799743,-16571957,40450,-4431835,4862400,1133 ) + ), + new GroupElementPreComp( + new FieldElement( -32856209,-7873957,-5422389,14860950,-16319031,7956142,7258061,311861,-30594991,-7379421 ), + new FieldElement( -3773428,-1565936,28985340,7499440,24445838,9325937,29727763,16527196,18278453,15405622 ), + new FieldElement( -4381906,8508652,-19898366,-3674424,-5984453,15149970,-13313598,843523,-21875062,13626197 ) + ), + new GroupElementPreComp( + new FieldElement( 2281448,-13487055,-10915418,-2609910,1879358,16164207,-10783882,3953792,13340839,15928663 ), + new FieldElement( 31727126,-7179855,-18437503,-8283652,2875793,-16390330,-25269894,-7014826,-23452306,5964753 ), + new FieldElement( 4100420,-5959452,-17179337,6017714,-18705837,12227141,-26684835,11344144,2538215,-7570755 ) + ), + new GroupElementPreComp( + new FieldElement( -9433605,6123113,11159803,-2156608,30016280,14966241,-20474983,1485421,-629256,-15958862 ), + new FieldElement( -26804558,4260919,11851389,9658551,-32017107,16367492,-20205425,-13191288,11659922,-11115118 ), + new FieldElement( 26180396,10015009,-30844224,-8581293,5418197,9480663,2231568,-10170080,33100372,-1306171 ) + ), + new GroupElementPreComp( + new FieldElement( 15121113,-5201871,-10389905,15427821,-27509937,-15992507,21670947,4486675,-5931810,-14466380 ), + new FieldElement( 16166486,-9483733,-11104130,6023908,-31926798,-1364923,2340060,-16254968,-10735770,-10039824 ), + new FieldElement( 28042865,-3557089,-12126526,12259706,-3717498,-6945899,6766453,-8689599,18036436,5803270 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -817581,6763912,11803561,1585585,10958447,-2671165,23855391,4598332,-6159431,-14117438 ), + new FieldElement( -31031306,-14256194,17332029,-2383520,31312682,-5967183,696309,50292,-20095739,11763584 ), + new FieldElement( -594563,-2514283,-32234153,12643980,12650761,14811489,665117,-12613632,-19773211,-10713562 ) + ), + new GroupElementPreComp( + new FieldElement( 30464590,-11262872,-4127476,-12734478,19835327,-7105613,-24396175,2075773,-17020157,992471 ), + new FieldElement( 18357185,-6994433,7766382,16342475,-29324918,411174,14578841,8080033,-11574335,-10601610 ), + new FieldElement( 19598397,10334610,12555054,2555664,18821899,-10339780,21873263,16014234,26224780,16452269 ) + ), + new GroupElementPreComp( + new FieldElement( -30223925,5145196,5944548,16385966,3976735,2009897,-11377804,-7618186,-20533829,3698650 ), + new FieldElement( 14187449,3448569,-10636236,-10810935,-22663880,-3433596,7268410,-10890444,27394301,12015369 ), + new FieldElement( 19695761,16087646,28032085,12999827,6817792,11427614,20244189,-1312777,-13259127,-3402461 ) + ), + new GroupElementPreComp( + new FieldElement( 30860103,12735208,-1888245,-4699734,-16974906,2256940,-8166013,12298312,-8550524,-10393462 ), + new FieldElement( -5719826,-11245325,-1910649,15569035,26642876,-7587760,-5789354,-15118654,-4976164,12651793 ), + new FieldElement( -2848395,9953421,11531313,-5282879,26895123,-12697089,-13118820,-16517902,9768698,-2533218 ) + ), + new GroupElementPreComp( + new FieldElement( -24719459,1894651,-287698,-4704085,15348719,-8156530,32767513,12765450,4940095,10678226 ), + new FieldElement( 18860224,15980149,-18987240,-1562570,-26233012,-11071856,-7843882,13944024,-24372348,16582019 ), + new FieldElement( -15504260,4970268,-29893044,4175593,-20993212,-2199756,-11704054,15444560,-11003761,7989037 ) + ), + new GroupElementPreComp( + new FieldElement( 31490452,5568061,-2412803,2182383,-32336847,4531686,-32078269,6200206,-19686113,-14800171 ), + new FieldElement( -17308668,-15879940,-31522777,-2831,-32887382,16375549,8680158,-16371713,28550068,-6857132 ), + new FieldElement( -28126887,-5688091,16837845,-1820458,-6850681,12700016,-30039981,4364038,1155602,5988841 ) + ), + new GroupElementPreComp( + new FieldElement( 21890435,-13272907,-12624011,12154349,-7831873,15300496,23148983,-4470481,24618407,8283181 ), + new FieldElement( -33136107,-10512751,9975416,6841041,-31559793,16356536,3070187,-7025928,1466169,10740210 ), + new FieldElement( -1509399,-15488185,-13503385,-10655916,32799044,909394,-13938903,-5779719,-32164649,-15327040 ) + ), + new GroupElementPreComp( + new FieldElement( 3960823,-14267803,-28026090,-15918051,-19404858,13146868,15567327,951507,-3260321,-573935 ), + new FieldElement( 24740841,5052253,-30094131,8961361,25877428,6165135,-24368180,14397372,-7380369,-6144105 ), + new FieldElement( -28888365,3510803,-28103278,-1158478,-11238128,-10631454,-15441463,-14453128,-1625486,-6494814 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 793299,-9230478,8836302,-6235707,-27360908,-2369593,33152843,-4885251,-9906200,-621852 ), + new FieldElement( 5666233,525582,20782575,-8038419,-24538499,14657740,16099374,1468826,-6171428,-15186581 ), + new FieldElement( -4859255,-3779343,-2917758,-6748019,7778750,11688288,-30404353,-9871238,-1558923,-9863646 ) + ), + new GroupElementPreComp( + new FieldElement( 10896332,-7719704,824275,472601,-19460308,3009587,25248958,14783338,-30581476,-15757844 ), + new FieldElement( 10566929,12612572,-31944212,11118703,-12633376,12362879,21752402,8822496,24003793,14264025 ), + new FieldElement( 27713862,-7355973,-11008240,9227530,27050101,2504721,23886875,-13117525,13958495,-5732453 ) + ), + new GroupElementPreComp( + new FieldElement( -23481610,4867226,-27247128,3900521,29838369,-8212291,-31889399,-10041781,7340521,-15410068 ), + new FieldElement( 4646514,-8011124,-22766023,-11532654,23184553,8566613,31366726,-1381061,-15066784,-10375192 ), + new FieldElement( -17270517,12723032,-16993061,14878794,21619651,-6197576,27584817,3093888,-8843694,3849921 ) + ), + new GroupElementPreComp( + new FieldElement( -9064912,2103172,25561640,-15125738,-5239824,9582958,32477045,-9017955,5002294,-15550259 ), + new FieldElement( -12057553,-11177906,21115585,-13365155,8808712,-12030708,16489530,13378448,-25845716,12741426 ), + new FieldElement( -5946367,10645103,-30911586,15390284,-3286982,-7118677,24306472,15852464,28834118,-7646072 ) + ), + new GroupElementPreComp( + new FieldElement( -17335748,-9107057,-24531279,9434953,-8472084,-583362,-13090771,455841,20461858,5491305 ), + new FieldElement( 13669248,-16095482,-12481974,-10203039,-14569770,-11893198,-24995986,11293807,-28588204,-9421832 ), + new FieldElement( 28497928,6272777,-33022994,14470570,8906179,-1225630,18504674,-14165166,29867745,-8795943 ) + ), + new GroupElementPreComp( + new FieldElement( -16207023,13517196,-27799630,-13697798,24009064,-6373891,-6367600,-13175392,22853429,-4012011 ), + new FieldElement( 24191378,16712145,-13931797,15217831,14542237,1646131,18603514,-11037887,12876623,-2112447 ), + new FieldElement( 17902668,4518229,-411702,-2829247,26878217,5258055,-12860753,608397,16031844,3723494 ) + ), + new GroupElementPreComp( + new FieldElement( -28632773,12763728,-20446446,7577504,33001348,-13017745,17558842,-7872890,23896954,-4314245 ), + new FieldElement( -20005381,-12011952,31520464,605201,2543521,5991821,-2945064,7229064,-9919646,-8826859 ), + new FieldElement( 28816045,298879,-28165016,-15920938,19000928,-1665890,-12680833,-2949325,-18051778,-2082915 ) + ), + new GroupElementPreComp( + new FieldElement( 16000882,-344896,3493092,-11447198,-29504595,-13159789,12577740,16041268,-19715240,7847707 ), + new FieldElement( 10151868,10572098,27312476,7922682,14825339,4723128,-32855931,-6519018,-10020567,3852848 ), + new FieldElement( -11430470,15697596,-21121557,-4420647,5386314,15063598,16514493,-15932110,29330899,-15076224 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -25499735,-4378794,-15222908,-6901211,16615731,2051784,3303702,15490,-27548796,12314391 ), + new FieldElement( 15683520,-6003043,18109120,-9980648,15337968,-5997823,-16717435,15921866,16103996,-3731215 ), + new FieldElement( -23169824,-10781249,13588192,-1628807,-3798557,-1074929,-19273607,5402699,-29815713,-9841101 ) + ), + new GroupElementPreComp( + new FieldElement( 23190676,2384583,-32714340,3462154,-29903655,-1529132,-11266856,8911517,-25205859,2739713 ), + new FieldElement( 21374101,-3554250,-33524649,9874411,15377179,11831242,-33529904,6134907,4931255,11987849 ), + new FieldElement( -7732,-2978858,-16223486,7277597,105524,-322051,-31480539,13861388,-30076310,10117930 ) + ), + new GroupElementPreComp( + new FieldElement( -29501170,-10744872,-26163768,13051539,-25625564,5089643,-6325503,6704079,12890019,15728940 ), + new FieldElement( -21972360,-11771379,-951059,-4418840,14704840,2695116,903376,-10428139,12885167,8311031 ), + new FieldElement( -17516482,5352194,10384213,-13811658,7506451,13453191,26423267,4384730,1888765,-5435404 ) + ), + new GroupElementPreComp( + new FieldElement( -25817338,-3107312,-13494599,-3182506,30896459,-13921729,-32251644,-12707869,-19464434,-3340243 ), + new FieldElement( -23607977,-2665774,-526091,4651136,5765089,4618330,6092245,14845197,17151279,-9854116 ), + new FieldElement( -24830458,-12733720,-15165978,10367250,-29530908,-265356,22825805,-7087279,-16866484,16176525 ) + ), + new GroupElementPreComp( + new FieldElement( -23583256,6564961,20063689,3798228,-4740178,7359225,2006182,-10363426,-28746253,-10197509 ), + new FieldElement( -10626600,-4486402,-13320562,-5125317,3432136,-6393229,23632037,-1940610,32808310,1099883 ), + new FieldElement( 15030977,5768825,-27451236,-2887299,-6427378,-15361371,-15277896,-6809350,2051441,-15225865 ) + ), + new GroupElementPreComp( + new FieldElement( -3362323,-7239372,7517890,9824992,23555850,295369,5148398,-14154188,-22686354,16633660 ), + new FieldElement( 4577086,-16752288,13249841,-15304328,19958763,-14537274,18559670,-10759549,8402478,-9864273 ), + new FieldElement( -28406330,-1051581,-26790155,-907698,-17212414,-11030789,9453451,-14980072,17983010,9967138 ) + ), + new GroupElementPreComp( + new FieldElement( -25762494,6524722,26585488,9969270,24709298,1220360,-1677990,7806337,17507396,3651560 ), + new FieldElement( -10420457,-4118111,14584639,15971087,-15768321,8861010,26556809,-5574557,-18553322,-11357135 ), + new FieldElement( 2839101,14284142,4029895,3472686,14402957,12689363,-26642121,8459447,-5605463,-7621941 ) + ), + new GroupElementPreComp( + new FieldElement( -4839289,-3535444,9744961,2871048,25113978,3187018,-25110813,-849066,17258084,-7977739 ), + new FieldElement( 18164541,-10595176,-17154882,-1542417,19237078,-9745295,23357533,-15217008,26908270,12150756 ), + new FieldElement( -30264870,-7647865,5112249,-7036672,-1499807,-6974257,43168,-5537701,-32302074,16215819 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -6898905,9824394,-12304779,-4401089,-31397141,-6276835,32574489,12532905,-7503072,-8675347 ), + new FieldElement( -27343522,-16515468,-27151524,-10722951,946346,16291093,254968,7168080,21676107,-1943028 ), + new FieldElement( 21260961,-8424752,-16831886,-11920822,-23677961,3968121,-3651949,-6215466,-3556191,-7913075 ) + ), + new GroupElementPreComp( + new FieldElement( 16544754,13250366,-16804428,15546242,-4583003,12757258,-2462308,-8680336,-18907032,-9662799 ), + new FieldElement( -2415239,-15577728,18312303,4964443,-15272530,-12653564,26820651,16690659,25459437,-4564609 ), + new FieldElement( -25144690,11425020,28423002,-11020557,-6144921,-15826224,9142795,-2391602,-6432418,-1644817 ) + ), + new GroupElementPreComp( + new FieldElement( -23104652,6253476,16964147,-3768872,-25113972,-12296437,-27457225,-16344658,6335692,7249989 ), + new FieldElement( -30333227,13979675,7503222,-12368314,-11956721,-4621693,-30272269,2682242,25993170,-12478523 ), + new FieldElement( 4364628,5930691,32304656,-10044554,-8054781,15091131,22857016,-10598955,31820368,15075278 ) + ), + new GroupElementPreComp( + new FieldElement( 31879134,-8918693,17258761,90626,-8041836,-4917709,24162788,-9650886,-17970238,12833045 ), + new FieldElement( 19073683,14851414,-24403169,-11860168,7625278,11091125,-19619190,2074449,-9413939,14905377 ), + new FieldElement( 24483667,-11935567,-2518866,-11547418,-1553130,15355506,-25282080,9253129,27628530,-7555480 ) + ), + new GroupElementPreComp( + new FieldElement( 17597607,8340603,19355617,552187,26198470,-3176583,4593324,-9157582,-14110875,15297016 ), + new FieldElement( 510886,14337390,-31785257,16638632,6328095,2713355,-20217417,-11864220,8683221,2921426 ), + new FieldElement( 18606791,11874196,27155355,-5281482,-24031742,6265446,-25178240,-1278924,4674690,13890525 ) + ), + new GroupElementPreComp( + new FieldElement( 13609624,13069022,-27372361,-13055908,24360586,9592974,14977157,9835105,4389687,288396 ), + new FieldElement( 9922506,-519394,13613107,5883594,-18758345,-434263,-12304062,8317628,23388070,16052080 ), + new FieldElement( 12720016,11937594,-31970060,-5028689,26900120,8561328,-20155687,-11632979,-14754271,-10812892 ) + ), + new GroupElementPreComp( + new FieldElement( 15961858,14150409,26716931,-665832,-22794328,13603569,11829573,7467844,-28822128,929275 ), + new FieldElement( 11038231,-11582396,-27310482,-7316562,-10498527,-16307831,-23479533,-9371869,-21393143,2465074 ), + new FieldElement( 20017163,-4323226,27915242,1529148,12396362,15675764,13817261,-9658066,2463391,-4622140 ) + ), + new GroupElementPreComp( + new FieldElement( -16358878,-12663911,-12065183,4996454,-1256422,1073572,9583558,12851107,4003896,12673717 ), + new FieldElement( -1731589,-15155870,-3262930,16143082,19294135,13385325,14741514,-9103726,7903886,2348101 ), + new FieldElement( 24536016,-16515207,12715592,-3862155,1511293,10047386,-3842346,-7129159,-28377538,10048127 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -12622226,-6204820,30718825,2591312,-10617028,12192840,18873298,-7297090,-32297756,15221632 ), + new FieldElement( -26478122,-11103864,11546244,-1852483,9180880,7656409,-21343950,2095755,29769758,6593415 ), + new FieldElement( -31994208,-2907461,4176912,3264766,12538965,-868111,26312345,-6118678,30958054,8292160 ) + ), + new GroupElementPreComp( + new FieldElement( 31429822,-13959116,29173532,15632448,12174511,-2760094,32808831,3977186,26143136,-3148876 ), + new FieldElement( 22648901,1402143,-22799984,13746059,7936347,365344,-8668633,-1674433,-3758243,-2304625 ), + new FieldElement( -15491917,8012313,-2514730,-12702462,-23965846,-10254029,-1612713,-1535569,-16664475,8194478 ) + ), + new GroupElementPreComp( + new FieldElement( 27338066,-7507420,-7414224,10140405,-19026427,-6589889,27277191,8855376,28572286,3005164 ), + new FieldElement( 26287124,4821776,25476601,-4145903,-3764513,-15788984,-18008582,1182479,-26094821,-13079595 ), + new FieldElement( -7171154,3178080,23970071,6201893,-17195577,-4489192,-21876275,-13982627,32208683,-1198248 ) + ), + new GroupElementPreComp( + new FieldElement( -16657702,2817643,-10286362,14811298,6024667,13349505,-27315504,-10497842,-27672585,-11539858 ), + new FieldElement( 15941029,-9405932,-21367050,8062055,31876073,-238629,-15278393,-1444429,15397331,-4130193 ), + new FieldElement( 8934485,-13485467,-23286397,-13423241,-32446090,14047986,31170398,-1441021,-27505566,15087184 ) + ), + new GroupElementPreComp( + new FieldElement( -18357243,-2156491,24524913,-16677868,15520427,-6360776,-15502406,11461896,16788528,-5868942 ), + new FieldElement( -1947386,16013773,21750665,3714552,-17401782,-16055433,-3770287,-10323320,31322514,-11615635 ), + new FieldElement( 21426655,-5650218,-13648287,-5347537,-28812189,-4920970,-18275391,-14621414,13040862,-12112948 ) + ), + new GroupElementPreComp( + new FieldElement( 11293895,12478086,-27136401,15083750,-29307421,14748872,14555558,-13417103,1613711,4896935 ), + new FieldElement( -25894883,15323294,-8489791,-8057900,25967126,-13425460,2825960,-4897045,-23971776,-11267415 ), + new FieldElement( -15924766,-5229880,-17443532,6410664,3622847,10243618,20615400,12405433,-23753030,-8436416 ) + ), + new GroupElementPreComp( + new FieldElement( -7091295,12556208,-20191352,9025187,-17072479,4333801,4378436,2432030,23097949,-566018 ), + new FieldElement( 4565804,-16025654,20084412,-7842817,1724999,189254,24767264,10103221,-18512313,2424778 ), + new FieldElement( 366633,-11976806,8173090,-6890119,30788634,5745705,-7168678,1344109,-3642553,12412659 ) + ), + new GroupElementPreComp( + new FieldElement( -24001791,7690286,14929416,-168257,-32210835,-13412986,24162697,-15326504,-3141501,11179385 ), + new FieldElement( 18289522,-14724954,8056945,16430056,-21729724,7842514,-6001441,-1486897,-18684645,-11443503 ), + new FieldElement( 476239,6601091,-6152790,-9723375,17503545,-4863900,27672959,13403813,11052904,5219329 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 20678546,-8375738,-32671898,8849123,-5009758,14574752,31186971,-3973730,9014762,-8579056 ), + new FieldElement( -13644050,-10350239,-15962508,5075808,-1514661,-11534600,-33102500,9160280,8473550,-3256838 ), + new FieldElement( 24900749,14435722,17209120,-15292541,-22592275,9878983,-7689309,-16335821,-24568481,11788948 ) + ), + new GroupElementPreComp( + new FieldElement( -3118155,-11395194,-13802089,14797441,9652448,-6845904,-20037437,10410733,-24568470,-1458691 ), + new FieldElement( -15659161,16736706,-22467150,10215878,-9097177,7563911,11871841,-12505194,-18513325,8464118 ), + new FieldElement( -23400612,8348507,-14585951,-861714,-3950205,-6373419,14325289,8628612,33313881,-8370517 ) + ), + new GroupElementPreComp( + new FieldElement( -20186973,-4967935,22367356,5271547,-1097117,-4788838,-24805667,-10236854,-8940735,-5818269 ), + new FieldElement( -6948785,-1795212,-32625683,-16021179,32635414,-7374245,15989197,-12838188,28358192,-4253904 ), + new FieldElement( -23561781,-2799059,-32351682,-1661963,-9147719,10429267,-16637684,4072016,-5351664,5596589 ) + ), + new GroupElementPreComp( + new FieldElement( -28236598,-3390048,12312896,6213178,3117142,16078565,29266239,2557221,1768301,15373193 ), + new FieldElement( -7243358,-3246960,-4593467,-7553353,-127927,-912245,-1090902,-4504991,-24660491,3442910 ), + new FieldElement( -30210571,5124043,14181784,8197961,18964734,-11939093,22597931,7176455,-18585478,13365930 ) + ), + new GroupElementPreComp( + new FieldElement( -7877390,-1499958,8324673,4690079,6261860,890446,24538107,-8570186,-9689599,-3031667 ), + new FieldElement( 25008904,-10771599,-4305031,-9638010,16265036,15721635,683793,-11823784,15723479,-15163481 ), + new FieldElement( -9660625,12374379,-27006999,-7026148,-7724114,-12314514,11879682,5400171,519526,-1235876 ) + ), + new GroupElementPreComp( + new FieldElement( 22258397,-16332233,-7869817,14613016,-22520255,-2950923,-20353881,7315967,16648397,7605640 ), + new FieldElement( -8081308,-8464597,-8223311,9719710,19259459,-15348212,23994942,-5281555,-9468848,4763278 ), + new FieldElement( -21699244,9220969,-15730624,1084137,-25476107,-2852390,31088447,-7764523,-11356529,728112 ) + ), + new GroupElementPreComp( + new FieldElement( 26047220,-11751471,-6900323,-16521798,24092068,9158119,-4273545,-12555558,-29365436,-5498272 ), + new FieldElement( 17510331,-322857,5854289,8403524,17133918,-3112612,-28111007,12327945,10750447,10014012 ), + new FieldElement( -10312768,3936952,9156313,-8897683,16498692,-994647,-27481051,-666732,3424691,7540221 ) + ), + new GroupElementPreComp( + new FieldElement( 30322361,-6964110,11361005,-4143317,7433304,4989748,-7071422,-16317219,-9244265,15258046 ), + new FieldElement( 13054562,-2779497,19155474,469045,-12482797,4566042,5631406,2711395,1062915,-5136345 ), + new FieldElement( -19240248,-11254599,-29509029,-7499965,-5835763,13005411,-6066489,12194497,32960380,1459310 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 19852034,7027924,23669353,10020366,8586503,-6657907,394197,-6101885,18638003,-11174937 ), + new FieldElement( 31395534,15098109,26581030,8030562,-16527914,-5007134,9012486,-7584354,-6643087,-5442636 ), + new FieldElement( -9192165,-2347377,-1997099,4529534,25766844,607986,-13222,9677543,-32294889,-6456008 ) + ), + new GroupElementPreComp( + new FieldElement( -2444496,-149937,29348902,8186665,1873760,12489863,-30934579,-7839692,-7852844,-8138429 ), + new FieldElement( -15236356,-15433509,7766470,746860,26346930,-10221762,-27333451,10754588,-9431476,5203576 ), + new FieldElement( 31834314,14135496,-770007,5159118,20917671,-16768096,-7467973,-7337524,31809243,7347066 ) + ), + new GroupElementPreComp( + new FieldElement( -9606723,-11874240,20414459,13033986,13716524,-11691881,19797970,-12211255,15192876,-2087490 ), + new FieldElement( -12663563,-2181719,1168162,-3804809,26747877,-14138091,10609330,12694420,33473243,-13382104 ), + new FieldElement( 33184999,11180355,15832085,-11385430,-1633671,225884,15089336,-11023903,-6135662,14480053 ) + ), + new GroupElementPreComp( + new FieldElement( 31308717,-5619998,31030840,-1897099,15674547,-6582883,5496208,13685227,27595050,8737275 ), + new FieldElement( -20318852,-15150239,10933843,-16178022,8335352,-7546022,-31008351,-12610604,26498114,66511 ), + new FieldElement( 22644454,-8761729,-16671776,4884562,-3105614,-13559366,30540766,-4286747,-13327787,-7515095 ) + ), + new GroupElementPreComp( + new FieldElement( -28017847,9834845,18617207,-2681312,-3401956,-13307506,8205540,13585437,-17127465,15115439 ), + new FieldElement( 23711543,-672915,31206561,-8362711,6164647,-9709987,-33535882,-1426096,8236921,16492939 ), + new FieldElement( -23910559,-13515526,-26299483,-4503841,25005590,-7687270,19574902,10071562,6708380,-6222424 ) + ), + new GroupElementPreComp( + new FieldElement( 2101391,-4930054,19702731,2367575,-15427167,1047675,5301017,9328700,29955601,-11678310 ), + new FieldElement( 3096359,9271816,-21620864,-15521844,-14847996,-7592937,-25892142,-12635595,-9917575,6216608 ), + new FieldElement( -32615849,338663,-25195611,2510422,-29213566,-13820213,24822830,-6146567,-26767480,7525079 ) + ), + new GroupElementPreComp( + new FieldElement( -23066649,-13985623,16133487,-7896178,-3389565,778788,-910336,-2782495,-19386633,11994101 ), + new FieldElement( 21691500,-13624626,-641331,-14367021,3285881,-3483596,-25064666,9718258,-7477437,13381418 ), + new FieldElement( 18445390,-4202236,14979846,11622458,-1727110,-3582980,23111648,-6375247,28535282,15779576 ) + ), + new GroupElementPreComp( + new FieldElement( 30098053,3089662,-9234387,16662135,-21306940,11308411,-14068454,12021730,9955285,-16303356 ), + new FieldElement( 9734894,-14576830,-7473633,-9138735,2060392,11313496,-18426029,9924399,20194861,13380996 ), + new FieldElement( -26378102,-7965207,-22167821,15789297,-18055342,-6168792,-1984914,15707771,26342023,10146099 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( -26016874,-219943,21339191,-41388,19745256,-2878700,-29637280,2227040,21612326,-545728 ), + new FieldElement( -13077387,1184228,23562814,-5970442,-20351244,-6348714,25764461,12243797,-20856566,11649658 ), + new FieldElement( -10031494,11262626,27384172,2271902,26947504,-15997771,39944,6114064,33514190,2333242 ) + ), + new GroupElementPreComp( + new FieldElement( -21433588,-12421821,8119782,7219913,-21830522,-9016134,-6679750,-12670638,24350578,-13450001 ), + new FieldElement( -4116307,-11271533,-23886186,4843615,-30088339,690623,-31536088,-10406836,8317860,12352766 ), + new FieldElement( 18200138,-14475911,-33087759,-2696619,-23702521,-9102511,-23552096,-2287550,20712163,6719373 ) + ), + new GroupElementPreComp( + new FieldElement( 26656208,6075253,-7858556,1886072,-28344043,4262326,11117530,-3763210,26224235,-3297458 ), + new FieldElement( -17168938,-14854097,-3395676,-16369877,-19954045,14050420,21728352,9493610,18620611,-16428628 ), + new FieldElement( -13323321,13325349,11432106,5964811,18609221,6062965,-5269471,-9725556,-30701573,-16479657 ) + ), + new GroupElementPreComp( + new FieldElement( -23860538,-11233159,26961357,1640861,-32413112,-16737940,12248509,-5240639,13735342,1934062 ), + new FieldElement( 25089769,6742589,17081145,-13406266,21909293,-16067981,-15136294,-3765346,-21277997,5473616 ), + new FieldElement( 31883677,-7961101,1083432,-11572403,22828471,13290673,-7125085,12469656,29111212,-5451014 ) + ), + new GroupElementPreComp( + new FieldElement( 24244947,-15050407,-26262976,2791540,-14997599,16666678,24367466,6388839,-10295587,452383 ), + new FieldElement( -25640782,-3417841,5217916,16224624,19987036,-4082269,-24236251,-5915248,15766062,8407814 ), + new FieldElement( -20406999,13990231,15495425,16395525,5377168,15166495,-8917023,-4388953,-8067909,2276718 ) + ), + new GroupElementPreComp( + new FieldElement( 30157918,12924066,-17712050,9245753,19895028,3368142,-23827587,5096219,22740376,-7303417 ), + new FieldElement( 2041139,-14256350,7783687,13876377,-25946985,-13352459,24051124,13742383,-15637599,13295222 ), + new FieldElement( 33338237,-8505733,12532113,7977527,9106186,-1715251,-17720195,-4612972,-4451357,-14669444 ) + ), + new GroupElementPreComp( + new FieldElement( -20045281,5454097,-14346548,6447146,28862071,1883651,-2469266,-4141880,7770569,9620597 ), + new FieldElement( 23208068,7979712,33071466,8149229,1758231,-10834995,30945528,-1694323,-33502340,-14767970 ), + new FieldElement( 1439958,-16270480,-1079989,-793782,4625402,10647766,-5043801,1220118,30494170,-11440799 ) + ), + new GroupElementPreComp( + new FieldElement( -5037580,-13028295,-2970559,-3061767,15640974,-6701666,-26739026,926050,-1684339,-13333647 ), + new FieldElement( 13908495,-3549272,30919928,-6273825,-21521863,7989039,9021034,9078865,3353509,4033511 ), + new FieldElement( -29663431,-15113610,32259991,-344482,24295849,-12912123,23161163,8839127,27485041,7356032 ) + ), + }, + new[]{ + new GroupElementPreComp( + new FieldElement( 9661027,705443,11980065,-5370154,-1628543,14661173,-6346142,2625015,28431036,-16771834 ), + new FieldElement( -23839233,-8311415,-25945511,7480958,-17681669,-8354183,-22545972,14150565,15970762,4099461 ), + new FieldElement( 29262576,16756590,26350592,-8793563,8529671,-11208050,13617293,-9937143,11465739,8317062 ) + ), + new GroupElementPreComp( + new FieldElement( -25493081,-6962928,32500200,-9419051,-23038724,-2302222,14898637,3848455,20969334,-5157516 ), + new FieldElement( -20384450,-14347713,-18336405,13884722,-33039454,2842114,-21610826,-3649888,11177095,14989547 ), + new FieldElement( -24496721,-11716016,16959896,2278463,12066309,10137771,13515641,2581286,-28487508,9930240 ) + ), + new GroupElementPreComp( + new FieldElement( -17751622,-2097826,16544300,-13009300,-15914807,-14949081,18345767,-13403753,16291481,-5314038 ), + new FieldElement( -33229194,2553288,32678213,9875984,8534129,6889387,-9676774,6957617,4368891,9788741 ), + new FieldElement( 16660756,7281060,-10830758,12911820,20108584,-8101676,-21722536,-8613148,16250552,-11111103 ) + ), + new GroupElementPreComp( + new FieldElement( -19765507,2390526,-16551031,14161980,1905286,6414907,4689584,10604807,-30190403,4782747 ), + new FieldElement( -1354539,14736941,-7367442,-13292886,7710542,-14155590,-9981571,4383045,22546403,437323 ), + new FieldElement( 31665577,-12180464,-16186830,1491339,-18368625,3294682,27343084,2786261,-30633590,-14097016 ) + ), + new GroupElementPreComp( + new FieldElement( -14467279,-683715,-33374107,7448552,19294360,14334329,-19690631,2355319,-19284671,-6114373 ), + new FieldElement( 15121312,-15796162,6377020,-6031361,-10798111,-12957845,18952177,15496498,-29380133,11754228 ), + new FieldElement( -2637277,-13483075,8488727,-14303896,12728761,-1622493,7141596,11724556,22761615,-10134141 ) + ), + new GroupElementPreComp( + new FieldElement( 16918416,11729663,-18083579,3022987,-31015732,-13339659,-28741185,-12227393,32851222,11717399 ), + new FieldElement( 11166634,7338049,-6722523,4531520,-29468672,-7302055,31474879,3483633,-1193175,-4030831 ), + new FieldElement( -185635,9921305,31456609,-13536438,-12013818,13348923,33142652,6546660,-19985279,-3948376 ) + ), + new GroupElementPreComp( + new FieldElement( -32460596,11266712,-11197107,-7899103,31703694,3855903,-8537131,-12833048,-30772034,-15486313 ), + new FieldElement( -18006477,12709068,3991746,-6479188,-21491523,-10550425,-31135347,-16049879,10928917,3011958 ), + new FieldElement( -6957757,-15594337,31696059,334240,29576716,14796075,-30831056,-12805180,18008031,10258577 ) + ), + new GroupElementPreComp( + new FieldElement( -22448644,15655569,7018479,-4410003,-30314266,-1201591,-1853465,1367120,25127874,6671743 ), + new FieldElement( 29701166,-14373934,-10878120,9279288,-17568,13127210,21382910,11042292,25838796,4642684 ), + new FieldElement( -20430234,14955537,-24126347,8124619,-5369288,-5990470,30468147,-13900640,18423289,4177476 ) + ) + } + }; + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base2.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base2.cs new file mode 100644 index 000000000..c86de62a1 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/base2.cs @@ -0,0 +1,50 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static readonly GroupElementPreComp[] Base2 = new GroupElementPreComp[]{ + new GroupElementPreComp( + new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ), + new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ), + new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 ) + ), + new GroupElementPreComp( + new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ), + new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ), + new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 ) + ), + new GroupElementPreComp( + new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ), + new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ), + new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 ) + ), + new GroupElementPreComp( + new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ), + new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ), + new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 ) + ), + new GroupElementPreComp( + new FieldElement( -22518993,-6692182,14201702,-8745502,-23510406,8844726,18474211,-1361450,-13062696,13821877 ), + new FieldElement( -6455177,-7839871,3374702,-4740862,-27098617,-10571707,31655028,-7212327,18853322,-14220951 ), + new FieldElement( 4566830,-12963868,-28974889,-12240689,-7602672,-2830569,-8514358,-10431137,2207753,-3209784 ) + ), + new GroupElementPreComp( + new FieldElement( -25154831,-4185821,29681144,7868801,-6854661,-9423865,-12437364,-663000,-31111463,-16132436 ), + new FieldElement( 25576264,-2703214,7349804,-11814844,16472782,9300885,3844789,15725684,171356,6466918 ), + new FieldElement( 23103977,13316479,9739013,-16149481,817875,-15038942,8965339,-14088058,-30714912,16193877 ) + ), + new GroupElementPreComp( + new FieldElement( -33521811,3180713,-2394130,14003687,-16903474,-16270840,17238398,4729455,-18074513,9256800 ), + new FieldElement( -25182317,-4174131,32336398,5036987,-21236817,11360617,22616405,9761698,-19827198,630305 ), + new FieldElement( -13720693,2639453,-24237460,-7406481,9494427,-5774029,-6554551,-15960994,-2449256,-14291300 ) + ), + new GroupElementPreComp( + new FieldElement( -3151181,-5046075,9282714,6866145,-31907062,-863023,-18940575,15033784,25105118,-7894876 ), + new FieldElement( -24326370,15950226,-31801215,-14592823,-11662737,-5090925,1573892,-2625887,2198790,-15804619 ), + new FieldElement( -3099351,10324967,-2241613,7453183,-5446979,-2735503,-13812022,-16236442,-32461234,-12290683 ) + ) + }; + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d.cs new file mode 100644 index 000000000..b5a957307 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static FieldElement d = new FieldElement(-10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116); + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d2.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d2.cs new file mode 100644 index 000000000..5c6bb61e9 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/d2.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static FieldElement d2 = new FieldElement(-21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199); + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_0.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_0.cs new file mode 100644 index 000000000..632c1b942 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_0.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + public static void fe_0(out FieldElement h) + { + h = default(FieldElement); + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_1.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_1.cs new file mode 100644 index 000000000..dfed794b5 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_1.cs @@ -0,0 +1,13 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + public static void fe_1(out FieldElement h) + { + h = default(FieldElement); + h.x0 = 1; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_add.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_add.cs new file mode 100644 index 000000000..7eb6b9ff1 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_add.cs @@ -0,0 +1,64 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + h = f + g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + //void fe_add(fe h,const fe f,const fe g) + internal static void fe_add(out FieldElement h, ref FieldElement f, ref FieldElement g) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + int g0 = g.x0; + int g1 = g.x1; + int g2 = g.x2; + int g3 = g.x3; + int g4 = g.x4; + int g5 = g.x5; + int g6 = g.x6; + int g7 = g.x7; + int g8 = g.x8; + int g9 = g.x9; + int h0 = f0 + g0; + int h1 = f1 + g1; + int h2 = f2 + g2; + int h3 = f3 + g3; + int h4 = f4 + g4; + int h5 = f5 + g5; + int h6 = f6 + g6; + int h7 = f7 + g7; + int h8 = f8 + g8; + int h9 = f9 + g9; + + h.x0 = h0; + h.x1 = h1; + h.x2 = h2; + h.x3 = h3; + h.x4 = h4; + h.x5 = h5; + h.x6 = h6; + h.x7 = h7; + h.x8 = h8; + h.x9 = h9; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cmov.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cmov.cs new file mode 100644 index 000000000..765650694 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cmov.cs @@ -0,0 +1,71 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. + */ + + //void fe_cmov(fe f,const fe g,unsigned int b) + internal static void fe_cmov(ref FieldElement f, ref FieldElement g, int b) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + int g0 = g.x0; + int g1 = g.x1; + int g2 = g.x2; + int g3 = g.x3; + int g4 = g.x4; + int g5 = g.x5; + int g6 = g.x6; + int g7 = g.x7; + int g8 = g.x8; + int g9 = g.x9; + int x0 = f0 ^ g0; + int x1 = f1 ^ g1; + int x2 = f2 ^ g2; + int x3 = f3 ^ g3; + int x4 = f4 ^ g4; + int x5 = f5 ^ g5; + int x6 = f6 ^ g6; + int x7 = f7 ^ g7; + int x8 = f8 ^ g8; + int x9 = f9 ^ g9; + + b = -b; + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + f.x0 = f0 ^ x0; + f.x1 = f1 ^ x1; + f.x2 = f2 ^ x2; + f.x3 = f3 ^ x3; + f.x4 = f4 ^ x4; + f.x5 = f5 ^ x5; + f.x6 = f6 ^ x6; + f.x7 = f7 ^ x7; + f.x8 = f8 ^ x8; + f.x9 = f9 ^ x9; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cswap.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cswap.cs new file mode 100644 index 000000000..50815dbfa --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_cswap.cs @@ -0,0 +1,79 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + Replace (f,g) with (g,f) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. + */ + public static void fe_cswap(ref FieldElement f, ref FieldElement g, uint b) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + int g0 = g.x0; + int g1 = g.x1; + int g2 = g.x2; + int g3 = g.x3; + int g4 = g.x4; + int g5 = g.x5; + int g6 = g.x6; + int g7 = g.x7; + int g8 = g.x8; + int g9 = g.x9; + int x0 = f0 ^ g0; + int x1 = f1 ^ g1; + int x2 = f2 ^ g2; + int x3 = f3 ^ g3; + int x4 = f4 ^ g4; + int x5 = f5 ^ g5; + int x6 = f6 ^ g6; + int x7 = f7 ^ g7; + int x8 = f8 ^ g8; + int x9 = f9 ^ g9; + + int negb = unchecked((int)-b); + x0 &= negb; + x1 &= negb; + x2 &= negb; + x3 &= negb; + x4 &= negb; + x5 &= negb; + x6 &= negb; + x7 &= negb; + x8 &= negb; + x9 &= negb; + f.x0 = f0 ^ x0; + f.x1 = f1 ^ x1; + f.x2 = f2 ^ x2; + f.x3 = f3 ^ x3; + f.x4 = f4 ^ x4; + f.x5 = f5 ^ x5; + f.x6 = f6 ^ x6; + f.x7 = f7 ^ x7; + f.x8 = f8 ^ x8; + f.x9 = f9 ^ x9; + g.x0 = g0 ^ x0; + g.x1 = g1 ^ x1; + g.x2 = g2 ^ x2; + g.x3 = g3 ^ x3; + g.x4 = g4 ^ x4; + g.x5 = g5 ^ x5; + g.x6 = g6 ^ x6; + g.x7 = g7 ^ x7; + g.x8 = g8 ^ x8; + g.x9 = g9 ^ x9; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_frombytes.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_frombytes.cs new file mode 100644 index 000000000..3689ff952 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_frombytes.cs @@ -0,0 +1,102 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + private static long load_3(byte[] data, int offset) + { + uint result; + result = data[offset + 0]; + result |= (uint)data[offset + 1] << 8; + result |= (uint)data[offset + 2] << 16; + return (long)(ulong)result; + } + + private static long load_4(byte[] data, int offset) + { + uint result; + result = data[offset + 0]; + result |= (uint)data[offset + 1] << 8; + result |= (uint)data[offset + 2] << 16; + result |= (uint)data[offset + 3] << 24; + return (long)(ulong)result; + } + + // Ignores top bit of h. + internal static void fe_frombytes(out FieldElement h, byte[] data, int offset) + { + var h0 = load_4(data, offset); + var h1 = load_3(data, offset + 4) << 6; + var h2 = load_3(data, offset + 7) << 5; + var h3 = load_3(data, offset + 10) << 3; + var h4 = load_3(data, offset + 13) << 2; + var h5 = load_4(data, offset + 16); + var h6 = load_3(data, offset + 20) << 7; + var h7 = load_3(data, offset + 23) << 5; + var h8 = load_3(data, offset + 26) << 4; + var h9 = (load_3(data, offset + 29) & 8388607) << 2; + + var carry9 = (h9 + (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + var carry1 = (h1 + (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry3 = (h3 + (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry5 = (h5 + (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry7 = (h7 + (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + var carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + var carry2 = (h2 + (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry6 = (h6 + (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry8 = (h8 + (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + + // does NOT ignore top bit + internal static void fe_frombytes2(out FieldElement h, byte[] data, int offset) + { + var h0 = load_4(data, offset); + var h1 = load_3(data, offset + 4) << 6; + var h2 = load_3(data, offset + 7) << 5; + var h3 = load_3(data, offset + 10) << 3; + var h4 = load_3(data, offset + 13) << 2; + var h5 = load_4(data, offset + 16); + var h6 = load_3(data, offset + 20) << 7; + var h7 = load_3(data, offset + 23) << 5; + var h8 = load_3(data, offset + 26) << 4; + var h9 = load_3(data, offset + 29) << 2; + + var carry9 = (h9 + (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + var carry1 = (h1 + (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry3 = (h3 + (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry5 = (h5 + (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry7 = (h7 + (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + var carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + var carry2 = (h2 + (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry6 = (h6 + (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry8 = (h8 + (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_invert.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_invert.cs new file mode 100644 index 000000000..943133e07 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_invert.cs @@ -0,0 +1,128 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + internal static void fe_invert(out FieldElement result, ref FieldElement z) + { + FieldElement t0, t1, t2, t3; + int i; + + /* qhasm: z2 = z1^2^1 */ + /* asm 1: fe_sq(>z2=fe#1,z2=fe#1,>z2=fe#1); */ + /* asm 2: fe_sq(>z2=t0,z2=t0,>z2=t0); */ + fe_sq(out t0, ref z); //for (i = 1; i < 1; ++i) fe_sq(out t0, ref t0); + + /* qhasm: z8 = z2^2^2 */ + /* asm 1: fe_sq(>z8=fe#2,z8=fe#2,>z8=fe#2); */ + /* asm 2: fe_sq(>z8=t1,z8=t1,>z8=t1); */ + fe_sq(out t1, ref t0); for (i = 1; i < 2; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z9 = z1*z8 */ + /* asm 1: fe_mul(>z9=fe#2,z9=t1,z11=fe#1,z11=t0,z22=fe#3,z22=fe#3,>z22=fe#3); */ + /* asm 2: fe_sq(>z22=t2,z22=t2,>z22=t2); */ + fe_sq(out t2, ref t0); //for (i = 1; i < 1; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_5_0 = z9*z22 */ + /* asm 1: fe_mul(>z_5_0=fe#2,z_5_0=t1,z_10_5=fe#3,z_10_5=fe#3,>z_10_5=fe#3); */ + /* asm 2: fe_sq(>z_10_5=t2,z_10_5=t2,>z_10_5=t2); */ + fe_sq(out t2, ref t1); for (i = 1; i < 5; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_10_0 = z_10_5*z_5_0 */ + /* asm 1: fe_mul(>z_10_0=fe#2,z_10_0=t1,z_20_10=fe#3,z_20_10=fe#3,>z_20_10=fe#3); */ + /* asm 2: fe_sq(>z_20_10=t2,z_20_10=t2,>z_20_10=t2); */ + fe_sq(out t2, ref t1); for (i = 1; i < 10; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_20_0 = z_20_10*z_10_0 */ + /* asm 1: fe_mul(>z_20_0=fe#3,z_20_0=t2,z_40_20=fe#4,z_40_20=fe#4,>z_40_20=fe#4); */ + /* asm 2: fe_sq(>z_40_20=t3,z_40_20=t3,>z_40_20=t3); */ + fe_sq(out t3, ref t2); for (i = 1; i < 20; ++i) fe_sq(out t3, ref t3); + + /* qhasm: z_40_0 = z_40_20*z_20_0 */ + /* asm 1: fe_mul(>z_40_0=fe#3,z_40_0=t2,z_50_10=fe#3,z_50_10=fe#3,>z_50_10=fe#3); */ + /* asm 2: fe_sq(>z_50_10=t2,z_50_10=t2,>z_50_10=t2); */ + fe_sq(out t2, ref t2); for (i = 1; i < 10; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_50_0 = z_50_10*z_10_0 */ + /* asm 1: fe_mul(>z_50_0=fe#2,z_50_0=t1,z_100_50=fe#3,z_100_50=fe#3,>z_100_50=fe#3); */ + /* asm 2: fe_sq(>z_100_50=t2,z_100_50=t2,>z_100_50=t2); */ + fe_sq(out t2, ref t1); for (i = 1; i < 50; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_100_0 = z_100_50*z_50_0 */ + /* asm 1: fe_mul(>z_100_0=fe#3,z_100_0=t2,z_200_100=fe#4,z_200_100=fe#4,>z_200_100=fe#4); */ + /* asm 2: fe_sq(>z_200_100=t3,z_200_100=t3,>z_200_100=t3); */ + fe_sq(out t3, ref t2); for (i = 1; i < 100; ++i) fe_sq(out t3, ref t3); + + /* qhasm: z_200_0 = z_200_100*z_100_0 */ + /* asm 1: fe_mul(>z_200_0=fe#3,z_200_0=t2,z_250_50=fe#3,z_250_50=fe#3,>z_250_50=fe#3); */ + /* asm 2: fe_sq(>z_250_50=t2,z_250_50=t2,>z_250_50=t2); */ + fe_sq(out t2, ref t2); for (i = 1; i < 50; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_250_0 = z_250_50*z_50_0 */ + /* asm 1: fe_mul(>z_250_0=fe#2,z_250_0=t1,z_255_5=fe#2,z_255_5=fe#2,>z_255_5=fe#2); */ + /* asm 2: fe_sq(>z_255_5=t1,z_255_5=t1,>z_255_5=t1); */ + fe_sq(out t1, ref t1); for (i = 1; i < 5; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_255_21 = z_255_5*z11 */ + /* asm 1: fe_mul(>z_255_21=fe#12,z_255_21=out,> 31) ^ 1); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul.cs new file mode 100644 index 000000000..4774cd5d5 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul.cs @@ -0,0 +1,263 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + h = f * g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + + /* + Notes on implementation strategy: + + Using schoolbook multiplication. + Karatsuba would save a little in some cost models. + + Most multiplications by 2 and 19 are 32-bit precomputations; + cheaper than 64-bit postcomputations. + + There is one remaining multiplication by 19 in the carry chain; + one *19 precomputation can be merged into this, + but the resulting data flow is considerably less clean. + + There are 12 carries below. + 10 of them are 2-way parallelizable and vectorizable. + Can get away with 11 carries, but then data flow is much deeper. + + With tighter constraints on inputs can squeeze carries into int32. + */ + + internal static void fe_mul(out FieldElement h, ref FieldElement f, ref FieldElement g) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + int g0 = g.x0; + int g1 = g.x1; + int g2 = g.x2; + int g3 = g.x3; + int g4 = g.x4; + int g5 = g.x5; + int g6 = g.x6; + int g7 = g.x7; + int g8 = g.x8; + int g9 = g.x9; + + int g1_19 = 19 * g1; /* 1.959375*2^29 */ + int g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ + int g3_19 = 19 * g3; + int g4_19 = 19 * g4; + int g5_19 = 19 * g5; + int g6_19 = 19 * g6; + int g7_19 = 19 * g7; + int g8_19 = 19 * g8; + int g9_19 = 19 * g9; + + int f1_2 = 2 * f1; + int f3_2 = 2 * f3; + int f5_2 = 2 * f5; + int f7_2 = 2 * f7; + int f9_2 = 2 * f9; + + long f0g0 = f0 * (long)g0; + long f0g1 = f0 * (long)g1; + long f0g2 = f0 * (long)g2; + long f0g3 = f0 * (long)g3; + long f0g4 = f0 * (long)g4; + long f0g5 = f0 * (long)g5; + long f0g6 = f0 * (long)g6; + long f0g7 = f0 * (long)g7; + long f0g8 = f0 * (long)g8; + long f0g9 = f0 * (long)g9; + long f1g0 = f1 * (long)g0; + long f1g1_2 = f1_2 * (long)g1; + long f1g2 = f1 * (long)g2; + long f1g3_2 = f1_2 * (long)g3; + long f1g4 = f1 * (long)g4; + long f1g5_2 = f1_2 * (long)g5; + long f1g6 = f1 * (long)g6; + long f1g7_2 = f1_2 * (long)g7; + long f1g8 = f1 * (long)g8; + long f1g9_38 = f1_2 * (long)g9_19; + long f2g0 = f2 * (long)g0; + long f2g1 = f2 * (long)g1; + long f2g2 = f2 * (long)g2; + long f2g3 = f2 * (long)g3; + long f2g4 = f2 * (long)g4; + long f2g5 = f2 * (long)g5; + long f2g6 = f2 * (long)g6; + long f2g7 = f2 * (long)g7; + long f2g8_19 = f2 * (long)g8_19; + long f2g9_19 = f2 * (long)g9_19; + long f3g0 = f3 * (long)g0; + long f3g1_2 = f3_2 * (long)g1; + long f3g2 = f3 * (long)g2; + long f3g3_2 = f3_2 * (long)g3; + long f3g4 = f3 * (long)g4; + long f3g5_2 = f3_2 * (long)g5; + long f3g6 = f3 * (long)g6; + long f3g7_38 = f3_2 * (long)g7_19; + long f3g8_19 = f3 * (long)g8_19; + long f3g9_38 = f3_2 * (long)g9_19; + long f4g0 = f4 * (long)g0; + long f4g1 = f4 * (long)g1; + long f4g2 = f4 * (long)g2; + long f4g3 = f4 * (long)g3; + long f4g4 = f4 * (long)g4; + long f4g5 = f4 * (long)g5; + long f4g6_19 = f4 * (long)g6_19; + long f4g7_19 = f4 * (long)g7_19; + long f4g8_19 = f4 * (long)g8_19; + long f4g9_19 = f4 * (long)g9_19; + long f5g0 = f5 * (long)g0; + long f5g1_2 = f5_2 * (long)g1; + long f5g2 = f5 * (long)g2; + long f5g3_2 = f5_2 * (long)g3; + long f5g4 = f5 * (long)g4; + long f5g5_38 = f5_2 * (long)g5_19; + long f5g6_19 = f5 * (long)g6_19; + long f5g7_38 = f5_2 * (long)g7_19; + long f5g8_19 = f5 * (long)g8_19; + long f5g9_38 = f5_2 * (long)g9_19; + long f6g0 = f6 * (long)g0; + long f6g1 = f6 * (long)g1; + long f6g2 = f6 * (long)g2; + long f6g3 = f6 * (long)g3; + long f6g4_19 = f6 * (long)g4_19; + long f6g5_19 = f6 * (long)g5_19; + long f6g6_19 = f6 * (long)g6_19; + long f6g7_19 = f6 * (long)g7_19; + long f6g8_19 = f6 * (long)g8_19; + long f6g9_19 = f6 * (long)g9_19; + long f7g0 = f7 * (long)g0; + long f7g1_2 = f7_2 * (long)g1; + long f7g2 = f7 * (long)g2; + long f7g3_38 = f7_2 * (long)g3_19; + long f7g4_19 = f7 * (long)g4_19; + long f7g5_38 = f7_2 * (long)g5_19; + long f7g6_19 = f7 * (long)g6_19; + long f7g7_38 = f7_2 * (long)g7_19; + long f7g8_19 = f7 * (long)g8_19; + long f7g9_38 = f7_2 * (long)g9_19; + long f8g0 = f8 * (long)g0; + long f8g1 = f8 * (long)g1; + long f8g2_19 = f8 * (long)g2_19; + long f8g3_19 = f8 * (long)g3_19; + long f8g4_19 = f8 * (long)g4_19; + long f8g5_19 = f8 * (long)g5_19; + long f8g6_19 = f8 * (long)g6_19; + long f8g7_19 = f8 * (long)g7_19; + long f8g8_19 = f8 * (long)g8_19; + long f8g9_19 = f8 * (long)g9_19; + long f9g0 = f9 * (long)g0; + long f9g1_38 = f9_2 * (long)g1_19; + long f9g2_19 = f9 * (long)g2_19; + long f9g3_38 = f9_2 * (long)g3_19; + long f9g4_19 = f9 * (long)g4_19; + long f9g5_38 = f9_2 * (long)g5_19; + long f9g6_19 = f9 * (long)g6_19; + long f9g7_38 = f9_2 * (long)g7_19; + long f9g8_19 = f9 * (long)g8_19; + long f9g9_38 = f9_2 * (long)g9_19; + + long h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + long h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19; + long h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38; + long h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19; + long h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38; + long h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19; + long h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38; + long h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19; + long h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38; + long h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0; + + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + /* + |h0| <= (1.65*1.65*2^52*(1+19+19+19+19)+1.65*1.65*2^50*(38+38+38+38+38)) + i.e. |h0| <= 1.4*2^60; narrower ranges for h2, h4, h6, h8 + |h1| <= (1.65*1.65*2^51*(1+1+19+19+19+19+19+19+19+19)) + i.e. |h1| <= 1.7*2^59; narrower ranges for h3, h5, h7, h9 + */ + + carry0 = (h0 + (long)(1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + carry4 = (h4 + (long)(1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.71*2^59 */ + /* |h5| <= 1.71*2^59 */ + + carry1 = (h1 + (long)(1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + carry5 = (h5 + (long)(1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.41*2^60 */ + /* |h6| <= 1.41*2^60 */ + + carry2 = (h2 + (long)(1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + carry6 = (h6 + (long)(1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.71*2^59 */ + /* |h7| <= 1.71*2^59 */ + + carry3 = (h3 + (long)(1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + carry7 = (h7 + (long)(1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.72*2^34 */ + /* |h8| <= 1.41*2^60 */ + + carry4 = (h4 + (long)(1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + carry8 = (h8 + (long)(1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.71*2^59 */ + + carry9 = (h9 + (long)(1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.1*2^39 */ + + carry0 = (h0 + (long)(1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul121666.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul121666.cs new file mode 100644 index 000000000..2bbd3f688 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_mul121666.cs @@ -0,0 +1,67 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + + /* + h = f * 121666 + Can overlap h with f. + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + + Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + */ + + public static void fe_mul121666(out FieldElement h, ref FieldElement f) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + + var h0 = f0 * 121666L; + var h1 = f1 * 121666L; + var h2 = f2 * 121666L; + var h3 = f3 * 121666L; + var h4 = f4 * 121666L; + var h5 = f5 * 121666L; + var h6 = f6 * 121666L; + var h7 = f7 * 121666L; + var h8 = f8 * 121666L; + var h9 = f9 * 121666L; + + var carry9 = (h9 + (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + var carry1 = (h1 + (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry3 = (h3 + (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry5 = (h5 + (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry7 = (h7 + (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + var carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + var carry2 = (h2 + (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry6 = (h6 + (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry8 = (h8 + (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_neg.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_neg.cs new file mode 100644 index 000000000..9b3d18139 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_neg.cs @@ -0,0 +1,51 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + h = -f + + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + + Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + */ + internal static void fe_neg(out FieldElement h, ref FieldElement f) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + int h0 = -f0; + int h1 = -f1; + int h2 = -f2; + int h3 = -f3; + int h4 = -f4; + int h5 = -f5; + int h6 = -f6; + int h7 = -f7; + int h8 = -f8; + int h9 = -f9; + + h.x0 = h0; + h.x1 = h1; + h.x2 = h2; + h.x3 = h3; + h.x4 = h4; + h.x5 = h5; + h.x6 = h6; + h.x7 = h7; + h.x8 = h8; + h.x9 = h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_pow22523.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_pow22523.cs new file mode 100644 index 000000000..63bb33b59 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_pow22523.cs @@ -0,0 +1,125 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + internal static void fe_pow22523(out FieldElement result, ref FieldElement z) + { + FieldElement t0, t1, t2; + int i; + + /* qhasm: z2 = z1^2^1 */ + /* asm 1: fe_sq(>z2=fe#1,z2=fe#1,>z2=fe#1); */ + /* asm 2: fe_sq(>z2=t0,z2=t0,>z2=t0); */ + fe_sq(out t0, ref z); //for (i = 1; i < 1; ++i) fe_sq(out t0, ref t0); + + /* qhasm: z8 = z2^2^2 */ + /* asm 1: fe_sq(>z8=fe#2,z8=fe#2,>z8=fe#2); */ + /* asm 2: fe_sq(>z8=t1,z8=t1,>z8=t1); */ + fe_sq(out t1, ref t0); for (i = 1; i < 2; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z9 = z1*z8 */ + /* asm 1: fe_mul(>z9=fe#2,z9=t1,z11=fe#1,z11=t0,z22=fe#1,z22=fe#1,>z22=fe#1); */ + /* asm 2: fe_sq(>z22=t0,z22=t0,>z22=t0); */ + fe_sq(out t0, ref t0); //for (i = 1; i < 1; ++i) fe_sq(out t0, ref t0); + + /* qhasm: z_5_0 = z9*z22 */ + /* asm 1: fe_mul(>z_5_0=fe#1,z_5_0=t0,z_10_5=fe#2,z_10_5=fe#2,>z_10_5=fe#2); */ + /* asm 2: fe_sq(>z_10_5=t1,z_10_5=t1,>z_10_5=t1); */ + fe_sq(out t1, ref t0); for (i = 1; i < 5; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_10_0 = z_10_5*z_5_0 */ + /* asm 1: fe_mul(>z_10_0=fe#1,z_10_0=t0,z_20_10=fe#2,z_20_10=fe#2,>z_20_10=fe#2); */ + /* asm 2: fe_sq(>z_20_10=t1,z_20_10=t1,>z_20_10=t1); */ + fe_sq(out t1, ref t0); for (i = 1; i < 10; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_20_0 = z_20_10*z_10_0 */ + /* asm 1: fe_mul(>z_20_0=fe#2,z_20_0=t1,z_40_20=fe#3,z_40_20=fe#3,>z_40_20=fe#3); */ + /* asm 2: fe_sq(>z_40_20=t2,z_40_20=t2,>z_40_20=t2); */ + fe_sq(out t2, ref t1); for (i = 1; i < 20; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_40_0 = z_40_20*z_20_0 */ + /* asm 1: fe_mul(>z_40_0=fe#2,z_40_0=t1,z_50_10=fe#2,z_50_10=fe#2,>z_50_10=fe#2); */ + /* asm 2: fe_sq(>z_50_10=t1,z_50_10=t1,>z_50_10=t1); */ + fe_sq(out t1, ref t1); for (i = 1; i < 10; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_50_0 = z_50_10*z_10_0 */ + /* asm 1: fe_mul(>z_50_0=fe#1,z_50_0=t0,z_100_50=fe#2,z_100_50=fe#2,>z_100_50=fe#2); */ + /* asm 2: fe_sq(>z_100_50=t1,z_100_50=t1,>z_100_50=t1); */ + fe_sq(out t1, ref t0); for (i = 1; i < 50; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_100_0 = z_100_50*z_50_0 */ + /* asm 1: fe_mul(>z_100_0=fe#2,z_100_0=t1,z_200_100=fe#3,z_200_100=fe#3,>z_200_100=fe#3); */ + /* asm 2: fe_sq(>z_200_100=t2,z_200_100=t2,>z_200_100=t2); */ + fe_sq(out t2, ref t1); for (i = 1; i < 100; ++i) fe_sq(out t2, ref t2); + + /* qhasm: z_200_0 = z_200_100*z_100_0 */ + /* asm 1: fe_mul(>z_200_0=fe#2,z_200_0=t1,z_250_50=fe#2,z_250_50=fe#2,>z_250_50=fe#2); */ + /* asm 2: fe_sq(>z_250_50=t1,z_250_50=t1,>z_250_50=t1); */ + fe_sq(out t1, ref t1); for (i = 1; i < 50; ++i) fe_sq(out t1, ref t1); + + /* qhasm: z_250_0 = z_250_50*z_50_0 */ + /* asm 1: fe_mul(>z_250_0=fe#1,z_250_0=t0,z_252_2=fe#1,z_252_2=fe#1,>z_252_2=fe#1); */ + /* asm 2: fe_sq(>z_252_2=t0,z_252_2=t0,>z_252_2=t0); */ + fe_sq(out t0, ref t0); for (i = 1; i < 2; ++i) fe_sq(out t0, ref t0); + + /* qhasm: z_252_3 = z_252_2*z1 */ + /* asm 1: fe_mul(>z_252_3=fe#12,z_252_3=out,> 26; h1 += carry0; h0 -= carry0 << 26; + var carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry1 = (h1 + (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry5 = (h5 + (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry2 = (h2 + (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry6 = (h6 + (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry3 = (h3 + (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry7 = (h7 + (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + + var carry8 = (h8 + (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + var carry9 = (h9 + (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + + carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sq2.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sq2.cs new file mode 100644 index 000000000..d1c2ee33d --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sq2.cs @@ -0,0 +1,154 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* +h = 2 * f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + + /* + See fe_mul.c for discussion of implementation strategy. + */ + internal static void fe_sq2(out FieldElement h, ref FieldElement f) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + + int f0_2 = 2 * f0; + int f1_2 = 2 * f1; + int f2_2 = 2 * f2; + int f3_2 = 2 * f3; + int f4_2 = 2 * f4; + int f5_2 = 2 * f5; + int f6_2 = 2 * f6; + int f7_2 = 2 * f7; + int f5_38 = 38 * f5; /* 1.959375*2^30 */ + int f6_19 = 19 * f6; /* 1.959375*2^30 */ + int f7_38 = 38 * f7; /* 1.959375*2^30 */ + int f8_19 = 19 * f8; /* 1.959375*2^30 */ + int f9_38 = 38 * f9; /* 1.959375*2^30 */ + + var f0f0 = f0 * (long)f0; + var f0f1_2 = f0_2 * (long)f1; + var f0f2_2 = f0_2 * (long)f2; + var f0f3_2 = f0_2 * (long)f3; + var f0f4_2 = f0_2 * (long)f4; + var f0f5_2 = f0_2 * (long)f5; + var f0f6_2 = f0_2 * (long)f6; + var f0f7_2 = f0_2 * (long)f7; + var f0f8_2 = f0_2 * (long)f8; + var f0f9_2 = f0_2 * (long)f9; + var f1f1_2 = f1_2 * (long)f1; + var f1f2_2 = f1_2 * (long)f2; + var f1f3_4 = f1_2 * (long)f3_2; + var f1f4_2 = f1_2 * (long)f4; + var f1f5_4 = f1_2 * (long)f5_2; + var f1f6_2 = f1_2 * (long)f6; + var f1f7_4 = f1_2 * (long)f7_2; + var f1f8_2 = f1_2 * (long)f8; + var f1f9_76 = f1_2 * (long)f9_38; + var f2f2 = f2 * (long)f2; + var f2f3_2 = f2_2 * (long)f3; + var f2f4_2 = f2_2 * (long)f4; + var f2f5_2 = f2_2 * (long)f5; + var f2f6_2 = f2_2 * (long)f6; + var f2f7_2 = f2_2 * (long)f7; + var f2f8_38 = f2_2 * (long)f8_19; + var f2f9_38 = f2 * (long)f9_38; + var f3f3_2 = f3_2 * (long)f3; + var f3f4_2 = f3_2 * (long)f4; + var f3f5_4 = f3_2 * (long)f5_2; + var f3f6_2 = f3_2 * (long)f6; + var f3f7_76 = f3_2 * (long)f7_38; + var f3f8_38 = f3_2 * (long)f8_19; + var f3f9_76 = f3_2 * (long)f9_38; + var f4f4 = f4 * (long)f4; + var f4f5_2 = f4_2 * (long)f5; + var f4f6_38 = f4_2 * (long)f6_19; + var f4f7_38 = f4 * (long)f7_38; + var f4f8_38 = f4_2 * (long)f8_19; + var f4f9_38 = f4 * (long)f9_38; + var f5f5_38 = f5 * (long)f5_38; + var f5f6_38 = f5_2 * (long)f6_19; + var f5f7_76 = f5_2 * (long)f7_38; + var f5f8_38 = f5_2 * (long)f8_19; + var f5f9_76 = f5_2 * (long)f9_38; + var f6f6_19 = f6 * (long)f6_19; + var f6f7_38 = f6 * (long)f7_38; + var f6f8_38 = f6_2 * (long)f8_19; + var f6f9_38 = f6 * (long)f9_38; + var f7f7_38 = f7 * (long)f7_38; + var f7f8_38 = f7_2 * (long)f8_19; + var f7f9_76 = f7_2 * (long)f9_38; + var f8f8_19 = f8 * (long)f8_19; + var f8f9_38 = f8 * (long)f9_38; + var f9f9_38 = f9 * (long)f9_38; + + var h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + var h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + var h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + var h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + var h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + var h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + var h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + var h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + var h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + var h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + + var carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + var carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry1 = (h1 + (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry5 = (h5 + (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry2 = (h2 + (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry6 = (h6 + (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry3 = (h3 + (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry7 = (h7 + (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry4 = (h4 + (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + + var carry8 = (h8 + (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + var carry9 = (h9 + (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + + carry0 = (h0 + (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + + h.x0 = (int)h0; + h.x1 = (int)h1; + h.x2 = (int)h2; + h.x3 = (int)h3; + h.x4 = (int)h4; + h.x5 = (int)h5; + h.x6 = (int)h6; + h.x7 = (int)h7; + h.x8 = (int)h8; + h.x9 = (int)h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sub.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sub.cs new file mode 100644 index 000000000..f76e6d752 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_sub.cs @@ -0,0 +1,66 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + h = f - g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + + internal static void fe_sub(out FieldElement h, ref FieldElement f, ref FieldElement g) + { + int f0 = f.x0; + int f1 = f.x1; + int f2 = f.x2; + int f3 = f.x3; + int f4 = f.x4; + int f5 = f.x5; + int f6 = f.x6; + int f7 = f.x7; + int f8 = f.x8; + int f9 = f.x9; + + int g0 = g.x0; + int g1 = g.x1; + int g2 = g.x2; + int g3 = g.x3; + int g4 = g.x4; + int g5 = g.x5; + int g6 = g.x6; + int g7 = g.x7; + int g8 = g.x8; + int g9 = g.x9; + + int h0 = f0 - g0; + int h1 = f1 - g1; + int h2 = f2 - g2; + int h3 = f3 - g3; + int h4 = f4 - g4; + int h5 = f5 - g5; + int h6 = f6 - g6; + int h7 = f7 - g7; + int h8 = f8 - g8; + int h9 = f9 - g9; + + h.x0 = h0; + h.x1 = h1; + h.x2 = h2; + h.x3 = h3; + h.x4 = h4; + h.x5 = h5; + h.x6 = h6; + h.x7 = h7; + h.x8 = h8; + h.x9 = h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_tobytes.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_tobytes.cs new file mode 100644 index 000000000..601f88f28 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/fe_tobytes.cs @@ -0,0 +1,145 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class FieldOperations + { + /* + Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + + Write p=2^255-19; q=floor(h/p). + Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + + Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 0); + s[offset + 1] = (byte) (h0 >> 8); + s[offset + 2] = (byte) (h0 >> 16); + s[offset + 3] = (byte) ((h0 >> 24) | (h1 << 2)); + s[offset + 4] = (byte) (h1 >> 6); + s[offset + 5] = (byte) (h1 >> 14); + s[offset + 6] = (byte) ((h1 >> 22) | (h2 << 3)); + s[offset + 7] = (byte) (h2 >> 5); + s[offset + 8] = (byte) (h2 >> 13); + s[offset + 9] = (byte) ((h2 >> 21) | (h3 << 5)); + s[offset + 10] = (byte) (h3 >> 3); + s[offset + 11] = (byte) (h3 >> 11); + s[offset + 12] = (byte) ((h3 >> 19) | (h4 << 6)); + s[offset + 13] = (byte) (h4 >> 2); + s[offset + 14] = (byte) (h4 >> 10); + s[offset + 15] = (byte) (h4 >> 18); + s[offset + 16] = (byte) (h5 >> 0); + s[offset + 17] = (byte) (h5 >> 8); + s[offset + 18] = (byte) (h5 >> 16); + s[offset + 19] = (byte) ((h5 >> 24) | (h6 << 1)); + s[offset + 20] = (byte) (h6 >> 7); + s[offset + 21] = (byte) (h6 >> 15); + s[offset + 22] = (byte) ((h6 >> 23) | (h7 << 3)); + s[offset + 23] = (byte) (h7 >> 5); + s[offset + 24] = (byte) (h7 >> 13); + s[offset + 25] = (byte) ((h7 >> 21) | (h8 << 4)); + s[offset + 26] = (byte) (h8 >> 4); + s[offset + 27] = (byte) (h8 >> 12); + s[offset + 28] = (byte) ((h8 >> 20) | (h9 << 6)); + s[offset + 29] = (byte) (h9 >> 2); + s[offset + 30] = (byte) (h9 >> 10); + s[offset + 31] = (byte) (h9 >> 18); + } + } + + internal static void fe_reduce(out FieldElement hr, ref FieldElement h) + { + int h0 = h.x0; + int h1 = h.x1; + int h2 = h.x2; + int h3 = h.x3; + int h4 = h.x4; + int h5 = h.x5; + int h6 = h.x6; + int h7 = h.x7; + int h8 = h.x8; + int h9 = h.x9; + + int q; + + q = (19 * h9 + (1 << 24)) >> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + + var carry0 = h0 >> 26; h1 += carry0; h0 -= carry0 << 26; + var carry1 = h1 >> 25; h2 += carry1; h1 -= carry1 << 25; + var carry2 = h2 >> 26; h3 += carry2; h2 -= carry2 << 26; + var carry3 = h3 >> 25; h4 += carry3; h3 -= carry3 << 25; + var carry4 = h4 >> 26; h5 += carry4; h4 -= carry4 << 26; + var carry5 = h5 >> 25; h6 += carry5; h5 -= carry5 << 25; + var carry6 = h6 >> 26; h7 += carry6; h6 -= carry6 << 26; + var carry7 = h7 >> 25; h8 += carry7; h7 -= carry7 << 25; + var carry8 = h8 >> 26; h9 += carry8; h8 -= carry8 << 26; + var carry9 = h9 >> 25; h9 -= carry9 << 25; + /* h10 = carry9 */ + + hr.x0 = h0; + hr.x1 = h1; + hr.x2 = h2; + hr.x3 = h3; + hr.x4 = h4; + hr.x5 = h5; + hr.x6 = h6; + hr.x7 = h7; + hr.x8 = h8; + hr.x9 = h9; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_add.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_add.cs new file mode 100644 index 000000000..de8e08f12 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_add.cs @@ -0,0 +1,73 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class GroupOperations + { + /* + r = p + q + */ + + internal static void ge_add(out GroupElementP1P1 r, ref GroupElementP3 p, ref GroupElementCached q) + { + FieldElement t0; + + /* qhasm: YpX1 = Y1+X1 */ + /* asm 1: fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,ZZ=fe#1,ZZ=r.X,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,> 3] >> (i & 7))); + + for (int i = 0; i < 256; ++i) + { + if (r[i] != 0) + { + for (int b = 1; b <= 6 && (i + b) < 256; ++b) + { + if (r[i + b] != 0) + { + if (r[i] + (r[i + b] << b) <= 15) + { + r[i] += (sbyte)(r[i + b] << b); r[i + b] = 0; + } + else if (r[i] - (r[i + b] << b) >= -15) + { + r[i] -= (sbyte)(r[i + b] << b); + for (int k = i + b; k < 256; ++k) + { + if (r[k] == 0) + { + r[k] = 1; + break; + } + r[k] = 0; + } + } + else + break; + } + } + } + } + } + + /* + r = a * A + b * B + where a = a[0]+256*a[1]+...+256^31 a[31]. + and b = b[0]+256*b[1]+...+256^31 b[31]. + B is the Ed25519 base point (x,4/5) with x positive. + */ + + public static void ge_double_scalarmult_vartime(out GroupElementP2 r, byte[] a, ref GroupElementP3 A, byte[] b) + { + GroupElementPreComp[] Bi = LookupTables.Base2; + // todo: Perhaps remove these allocations? + sbyte[] aslide = new sbyte[256]; + sbyte[] bslide = new sbyte[256]; + GroupElementCached[] Ai = new GroupElementCached[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + GroupElementP1P1 t; + GroupElementP3 u; + GroupElementP3 A2; + int i; + + slide(aslide, a); + slide(bslide, b); + + ge_p3_to_cached(out Ai[0], ref A); + ge_p3_dbl(out t, ref A); ge_p1p1_to_p3(out A2, ref t); + ge_add(out t, ref A2, ref Ai[0]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[1], ref u); + ge_add(out t, ref A2, ref Ai[1]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[2], ref u); + ge_add(out t, ref A2, ref Ai[2]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[3], ref u); + ge_add(out t, ref A2, ref Ai[3]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[4], ref u); + ge_add(out t, ref A2, ref Ai[4]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[5], ref u); + ge_add(out t, ref A2, ref Ai[5]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[6], ref u); + ge_add(out t, ref A2, ref Ai[6]); ge_p1p1_to_p3(out u, ref t); ge_p3_to_cached(out Ai[7], ref u); + + ge_p2_0(out r); + + for (i = 255; i >= 0; --i) + { + if ((aslide[i] != 0) || (bslide[i] != 0)) break; + } + + for (; i >= 0; --i) + { + ge_p2_dbl(out t, ref r); + + if (aslide[i] > 0) + { + ge_p1p1_to_p3(out u, ref t); + ge_add(out t, ref u, ref Ai[aslide[i] / 2]); + } + else if (aslide[i] < 0) + { + ge_p1p1_to_p3(out u, ref t); + ge_sub(out t, ref u, ref Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) + { + ge_p1p1_to_p3(out u, ref t); + ge_madd(out t, ref u, ref Bi[bslide[i] / 2]); + } + else if (bslide[i] < 0) + { + ge_p1p1_to_p3(out u, ref t); + ge_msub(out t, ref u, ref Bi[(-bslide[i]) / 2]); + } + + ge_p1p1_to_p2(out r, ref t); + } + } + + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_frombytes.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_frombytes.cs new file mode 100644 index 000000000..2e7abe9d4 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_frombytes.cs @@ -0,0 +1,50 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class GroupOperations + { + public static int ge_frombytes_negate_vartime(out GroupElementP3 h, byte[] data, int offset) + { + FieldElement u, v, v3, vxx, check; + + FieldOperations.fe_frombytes(out h.Y, data, offset); + FieldOperations.fe_1(out h.Z); + FieldOperations.fe_sq(out u, ref h.Y); + FieldOperations.fe_mul(out v, ref u, ref LookupTables.d); + FieldOperations.fe_sub(out u, ref u, ref h.Z); /* u = y^2-1 */ + FieldOperations.fe_add(out v, ref v, ref h.Z); /* v = dy^2+1 */ + + FieldOperations.fe_sq(out v3, ref v); + FieldOperations.fe_mul(out v3, ref v3, ref v); /* v3 = v^3 */ + FieldOperations.fe_sq(out h.X, ref v3); + FieldOperations.fe_mul(out h.X, ref h.X, ref v); + FieldOperations.fe_mul(out h.X, ref h.X, ref u); /* x = uv^7 */ + + FieldOperations.fe_pow22523(out h.X, ref h.X); /* x = (uv^7)^((q-5)/8) */ + FieldOperations.fe_mul(out h.X, ref h.X, ref v3); + FieldOperations.fe_mul(out h.X, ref h.X, ref u); /* x = uv^3(uv^7)^((q-5)/8) */ + + FieldOperations.fe_sq(out vxx, ref h.X); + FieldOperations.fe_mul(out vxx, ref vxx, ref v); + FieldOperations.fe_sub(out check, ref vxx, ref u); /* vx^2-u */ + if (FieldOperations.fe_isnonzero(ref check) != 0) + { + FieldOperations.fe_add(out check, ref vxx, ref u); /* vx^2+u */ + if (FieldOperations.fe_isnonzero(ref check) != 0) + { + h = default(GroupElementP3); + return -1; + } + FieldOperations.fe_mul(out h.X, ref h.X, ref LookupTables.sqrtm1); + } + + if (FieldOperations.fe_isnegative(ref h.X) == (data[offset + 31] >> 7)) + FieldOperations.fe_neg(out h.X, ref h.X); + + FieldOperations.fe_mul(out h.T, ref h.X, ref h.Y); + return 0; + } + + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_madd.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_madd.cs new file mode 100644 index 000000000..547e17d86 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_madd.cs @@ -0,0 +1,69 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class GroupOperations + { + /* + r = p + q + */ + public static void ge_madd(out GroupElementP1P1 r, ref GroupElementP3 p, ref GroupElementPreComp q) + { + FieldElement t0; + + /* qhasm: YpX1 = Y1+X1 */ + /* asm 1: fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,XX=fe#1,XX=r.X,YY=fe#3,YY=r.Z,B=fe#4,B=r.T,A=fe#2,A=r.Y,AA=fe#5,AA=t0,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,X3=fe#1,X3=r.X,T3=fe#4,T3=r.T,>= 31; /* 1: yes; 0: no */ + return (byte)y; + } + + static byte negative(sbyte b) + { + var x = unchecked((ulong)b); /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + x >>= 63; /* 1: yes; 0: no */ + return (byte)x; + } + + static void cmov(ref GroupElementPreComp t, ref GroupElementPreComp u, byte b) + { + FieldOperations.fe_cmov(ref t.yplusx, ref u.yplusx, b); + FieldOperations.fe_cmov(ref t.yminusx, ref u.yminusx, b); + FieldOperations.fe_cmov(ref t.xy2d, ref u.xy2d, b); + } + + static void select(out GroupElementPreComp t, int pos, sbyte b) + { + GroupElementPreComp minust; + var bnegative = negative(b); + var babs = (byte)(b - (((-bnegative) & b) << 1)); + + ge_precomp_0(out t); + var table = LookupTables.Base[pos]; + cmov(ref t, ref table[0], equal(babs, 1)); + cmov(ref t, ref table[1], equal(babs, 2)); + cmov(ref t, ref table[2], equal(babs, 3)); + cmov(ref t, ref table[3], equal(babs, 4)); + cmov(ref t, ref table[4], equal(babs, 5)); + cmov(ref t, ref table[5], equal(babs, 6)); + cmov(ref t, ref table[6], equal(babs, 7)); + cmov(ref t, ref table[7], equal(babs, 8)); + minust.yplusx = t.yminusx; + minust.yminusx = t.yplusx; + FieldOperations.fe_neg(out minust.xy2d, ref t.xy2d); + cmov(ref t, ref minust, bnegative); + } + + /* + h = a * B + where a = a[0]+256*a[1]+...+256^31 a[31] + B is the Ed25519 base point (x,4/5) with x positive. + + Preconditions: + a[31] <= 127 + */ + + public static void ge_scalarmult_base(out GroupElementP3 h, byte[] a, int offset) + { + // todo: Perhaps remove this allocation + var e = new sbyte[64]; + sbyte carry; + + GroupElementP1P1 r; + GroupElementP2 s; + GroupElementPreComp t; + + for (int i = 0; i < 32; ++i) + { + e[2 * i + 0] = (sbyte)((a[offset + i] >> 0) & 15); + e[2 * i + 1] = (sbyte)((a[offset + i] >> 4) & 15); + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (int i = 0; i < 63; ++i) + { + e[i] += carry; + carry = (sbyte)(e[i] + 8); + carry >>= 4; + e[i] -= (sbyte)(carry << 4); + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge_p3_0(out h); + for (int i = 1; i < 64; i += 2) + { + select(out t, i / 2, e[i]); + ge_madd(out r, ref h, ref t); ge_p1p1_to_p3(out h, ref r); + } + + ge_p3_dbl(out r, ref h); ge_p1p1_to_p2(out s, ref r); + ge_p2_dbl(out r, ref s); ge_p1p1_to_p2(out s, ref r); + ge_p2_dbl(out r, ref s); ge_p1p1_to_p2(out s, ref r); + ge_p2_dbl(out r, ref s); ge_p1p1_to_p3(out h, ref r); + + for (int i = 0; i < 64; i += 2) + { + select(out t, i / 2, e[i]); + ge_madd(out r, ref h, ref t); ge_p1p1_to_p3(out h, ref r); + } + } + + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_sub.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_sub.cs new file mode 100644 index 000000000..c0b9ba5a2 --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/ge_sub.cs @@ -0,0 +1,74 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class GroupOperations + { + /* + r = p - q + */ + + public static void ge_sub(out GroupElementP1P1 r, ref GroupElementP3 p, ref GroupElementCached q) + { + FieldElement t0; + + /* qhasm: YpX1 = Y1+X1 */ + /* asm 1: fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,ZZ=fe#1,ZZ=r.X,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,> 5); + long a2 = 2097151 & (load_3(a, 5) >> 2); + long a3 = 2097151 & (load_4(a, 7) >> 7); + long a4 = 2097151 & (load_4(a, 10) >> 4); + long a5 = 2097151 & (load_3(a, 13) >> 1); + long a6 = 2097151 & (load_4(a, 15) >> 6); + long a7 = 2097151 & (load_3(a, 18) >> 3); + long a8 = 2097151 & load_3(a, 21); + long a9 = 2097151 & (load_4(a, 23) >> 5); + long a10 = 2097151 & (load_3(a, 26) >> 2); + long a11 = (load_4(a, 28) >> 7); + long b0 = 2097151 & load_3(b, 0); + long b1 = 2097151 & (load_4(b, 2) >> 5); + long b2 = 2097151 & (load_3(b, 5) >> 2); + long b3 = 2097151 & (load_4(b, 7) >> 7); + long b4 = 2097151 & (load_4(b, 10) >> 4); + long b5 = 2097151 & (load_3(b, 13) >> 1); + long b6 = 2097151 & (load_4(b, 15) >> 6); + long b7 = 2097151 & (load_3(b, 18) >> 3); + long b8 = 2097151 & load_3(b, 21); + long b9 = 2097151 & (load_4(b, 23) >> 5); + long b10 = 2097151 & (load_3(b, 26) >> 2); + long b11 = (load_4(b, 28) >> 7); + long c0 = 2097151 & load_3(c, 0); + long c1 = 2097151 & (load_4(c, 2) >> 5); + long c2 = 2097151 & (load_3(c, 5) >> 2); + long c3 = 2097151 & (load_4(c, 7) >> 7); + long c4 = 2097151 & (load_4(c, 10) >> 4); + long c5 = 2097151 & (load_3(c, 13) >> 1); + long c6 = 2097151 & (load_4(c, 15) >> 6); + long c7 = 2097151 & (load_3(c, 18) >> 3); + long c8 = 2097151 & load_3(c, 21); + long c9 = 2097151 & (load_4(c, 23) >> 5); + long c10 = 2097151 & (load_3(c, 26) >> 2); + long c11 = (load_4(c, 28) >> 7); + long s0; + long s1; + long s2; + long s3; + long s4; + long s5; + long s6; + long s7; + long s8; + long s9; + long s10; + long s11; + long s12; + long s13; + long s14; + long s15; + long s16; + long s17; + long s18; + long s19; + long s20; + long s21; + long s22; + long s23; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + long carry17; + long carry18; + long carry19; + long carry20; + long carry21; + long carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; s19 += carry18; s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; s21 += carry20; s20 -= carry20 << 21; + carry22 = (s22 + (1 << 20)) >> 21; s23 += carry22; s22 -= carry22 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; s18 += carry17; s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; s20 += carry19; s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; s22 += carry21; s21 -= carry21 << 21; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + + unchecked + { + s[0] = (byte)(s0 >> 0); + s[1] = (byte)(s0 >> 8); + s[2] = (byte)((s0 >> 16) | (s1 << 5)); + s[3] = (byte)(s1 >> 3); + s[4] = (byte)(s1 >> 11); + s[5] = (byte)((s1 >> 19) | (s2 << 2)); + s[6] = (byte)(s2 >> 6); + s[7] = (byte)((s2 >> 14) | (s3 << 7)); + s[8] = (byte)(s3 >> 1); + s[9] = (byte)(s3 >> 9); + s[10] = (byte)((s3 >> 17) | (s4 << 4)); + s[11] = (byte)(s4 >> 4); + s[12] = (byte)(s4 >> 12); + s[13] = (byte)((s4 >> 20) | (s5 << 1)); + s[14] = (byte)(s5 >> 7); + s[15] = (byte)((s5 >> 15) | (s6 << 6)); + s[16] = (byte)(s6 >> 2); + s[17] = (byte)(s6 >> 10); + s[18] = (byte)((s6 >> 18) | (s7 << 3)); + s[19] = (byte)(s7 >> 5); + s[20] = (byte)(s7 >> 13); + s[21] = (byte)(s8 >> 0); + s[22] = (byte)(s8 >> 8); + s[23] = (byte)((s8 >> 16) | (s9 << 5)); + s[24] = (byte)(s9 >> 3); + s[25] = (byte)(s9 >> 11); + s[26] = (byte)((s9 >> 19) | (s10 << 2)); + s[27] = (byte)(s10 >> 6); + s[28] = (byte)((s10 >> 14) | (s11 << 7)); + s[29] = (byte)(s11 >> 1); + s[30] = (byte)(s11 >> 9); + s[31] = (byte)(s11 >> 17); + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/sc_reduce.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/sc_reduce.cs new file mode 100644 index 000000000..d3554455f --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/sc_reduce.cs @@ -0,0 +1,264 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + internal static partial class ScalarOperations + { + /* + Input: + s[0]+256*s[1]+...+256^63*s[63] = s + + Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. + */ + + public static void sc_reduce(byte[] s) + { + long s0 = 2097151 & load_3(s, 0); + long s1 = 2097151 & (load_4(s, 2) >> 5); + long s2 = 2097151 & (load_3(s, 5) >> 2); + long s3 = 2097151 & (load_4(s, 7) >> 7); + long s4 = 2097151 & (load_4(s, 10) >> 4); + long s5 = 2097151 & (load_3(s, 13) >> 1); + long s6 = 2097151 & (load_4(s, 15) >> 6); + long s7 = 2097151 & (load_3(s, 18) >> 3); + long s8 = 2097151 & load_3(s, 21); + long s9 = 2097151 & (load_4(s, 23) >> 5); + long s10 = 2097151 & (load_3(s, 26) >> 2); + long s11 = 2097151 & (load_4(s, 28) >> 7); + long s12 = 2097151 & (load_4(s, 31) >> 4); + long s13 = 2097151 & (load_3(s, 34) >> 1); + long s14 = 2097151 & (load_4(s, 36) >> 6); + long s15 = 2097151 & (load_3(s, 39) >> 3); + long s16 = 2097151 & load_3(s, 42); + long s17 = 2097151 & (load_4(s, 44) >> 5); + long s18 = 2097151 & (load_3(s, 47) >> 2); + long s19 = 2097151 & (load_4(s, 49) >> 7); + long s20 = 2097151 & (load_4(s, 52) >> 4); + long s21 = 2097151 & (load_3(s, 55) >> 1); + long s22 = 2097151 & (load_4(s, 57) >> 6); + long s23 = (load_4(s, 60) >> 3); + + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; + carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; + carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; + carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; + carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; + carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; + carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; + carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; + carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; + carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; + carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; + + unchecked + { + s[0] = (byte)(s0 >> 0); + s[1] = (byte)(s0 >> 8); + s[2] = (byte)((s0 >> 16) | (s1 << 5)); + s[3] = (byte)(s1 >> 3); + s[4] = (byte)(s1 >> 11); + s[5] = (byte)((s1 >> 19) | (s2 << 2)); + s[6] = (byte)(s2 >> 6); + s[7] = (byte)((s2 >> 14) | (s3 << 7)); + s[8] = (byte)(s3 >> 1); + s[9] = (byte)(s3 >> 9); + s[10] = (byte)((s3 >> 17) | (s4 << 4)); + s[11] = (byte)(s4 >> 4); + s[12] = (byte)(s4 >> 12); + s[13] = (byte)((s4 >> 20) | (s5 << 1)); + s[14] = (byte)(s5 >> 7); + s[15] = (byte)((s5 >> 15) | (s6 << 6)); + s[16] = (byte)(s6 >> 2); + s[17] = (byte)(s6 >> 10); + s[18] = (byte)((s6 >> 18) | (s7 << 3)); + s[19] = (byte)(s7 >> 5); + s[20] = (byte)(s7 >> 13); + s[21] = (byte)(s8 >> 0); + s[22] = (byte)(s8 >> 8); + s[23] = (byte)((s8 >> 16) | (s9 << 5)); + s[24] = (byte)(s9 >> 3); + s[25] = (byte)(s9 >> 11); + s[26] = (byte)((s9 >> 19) | (s10 << 2)); + s[27] = (byte)(s10 >> 6); + s[28] = (byte)((s10 >> 14) | (s11 << 7)); + s[29] = (byte)(s11 >> 1); + s[30] = (byte)(s11 >> 9); + s[31] = (byte)(s11 >> 17); + } + } + + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/scalarmult.cs b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/scalarmult.cs new file mode 100644 index 000000000..3a7d8feea --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Ed25519Ref10/scalarmult.cs @@ -0,0 +1,153 @@ +using System; + +namespace Discord.Net.ED25519.Ed25519Ref10 +{ + public static class MontgomeryOperations + { + public static void scalarmult( + byte[] q, int qoffset, + byte[] n, int noffset, + byte[] p, int poffset) + { + FieldElement p0, q0; + FieldOperations.fe_frombytes2(out p0, p, poffset); + scalarmult(out q0, n, noffset, ref p0); + FieldOperations.fe_tobytes(q, qoffset, ref q0); + } + + internal static void scalarmult( + out FieldElement q, + byte[] n, int noffset, + ref FieldElement p) + { + byte[] e = new byte[32];//ToDo: remove allocation + FieldElement x1, x2, x3; + FieldElement z2, z3; + FieldElement tmp0, tmp1; + + for (int i = 0; i < 32; ++i) + e[i] = n[noffset + i]; + ScalarOperations.sc_clamp(e, 0); + x1 = p; + FieldOperations.fe_1(out x2); + FieldOperations.fe_0(out z2); + x3 = x1; + FieldOperations.fe_1(out z3); + + uint swap = 0; + for (int pos = 254; pos >= 0; --pos) + { + uint b = (uint)(e[pos / 8] >> (pos & 7)); + b &= 1; + swap ^= b; + FieldOperations.fe_cswap(ref x2, ref x3, swap); + FieldOperations.fe_cswap(ref z2, ref z3, swap); + swap = b; + + /* qhasm: enter ladder */ + + /* qhasm: D = X3-Z3 */ + /* asm 1: fe_sub(>D=fe#5,D=tmp0,B=fe#6,B=tmp1,A=fe#1,A=x2,C=fe#2,C=z2,DA=fe#4,DA=z3,CB=fe#2,CB=z2,BB=fe#5,BB=tmp0,AA=fe#6,AA=tmp1,t0=fe#3,t0=x3,t1=fe#2,t1=z2,X4=fe#1,X4=x2,E=fe#6,E=tmp1,t2=fe#2,t2=z2,t3=fe#4,t3=z3,X5=fe#3,X5=x3,t4=fe#5,t4=tmp0,Z5=fe#4,x1,Z5=z3,x1,Z4=fe#2,Z4=z2, _state; + private readonly byte[] _buffer; + private ulong _totalBytes; + public const int BlockSize = 128; + private static readonly byte[] _padding = new byte[] { 0x80 }; + + /// + /// Allocation and initialization of the new SHA-512 object. + /// + public Sha512() + { + _buffer = new byte[BlockSize];//todo: remove allocation + Init(); + } + + /// + /// Performs an initialization of internal SHA-512 state. + /// + public void Init() + { + Sha512Internal.Sha512Init(out _state); + _totalBytes = 0; + } + + /// + /// Updates internal state with data from the provided array segment. + /// + /// Array segment + public void Update(ArraySegment data) + { + Update(data.Array, data.Offset, data.Count); + } + + /// + /// Updates internal state with data from the provided array. + /// + /// Array of bytes + /// Offset of byte sequence + /// Sequence length + public void Update(byte[] data, int index, int length) + { + + Array16 block; + int bytesInBuffer = (int)_totalBytes & (BlockSize - 1); + _totalBytes += (uint)length; + + if (_totalBytes >= ulong.MaxValue / 8) + throw new InvalidOperationException("Too much data"); + // Fill existing buffer + if (bytesInBuffer != 0) + { + var toCopy = Math.Min(BlockSize - bytesInBuffer, length); + Buffer.BlockCopy(data, index, _buffer, bytesInBuffer, toCopy); + index += toCopy; + length -= toCopy; + bytesInBuffer += toCopy; + if (bytesInBuffer == BlockSize) + { + ByteIntegerConverter.Array16LoadBigEndian64(out block, _buffer, 0); + Sha512Internal.Core(out _state, ref _state, ref block); + CryptoBytes.InternalWipe(_buffer, 0, _buffer.Length); + bytesInBuffer = 0; + } + } + // Hash complete blocks without copying + while (length >= BlockSize) + { + ByteIntegerConverter.Array16LoadBigEndian64(out block, data, index); + Sha512Internal.Core(out _state, ref _state, ref block); + index += BlockSize; + length -= BlockSize; + } + // Copy remainder into buffer + if (length > 0) + { + Buffer.BlockCopy(data, index, _buffer, bytesInBuffer, length); + } + } + + /// + /// Finalizes SHA-512 hashing + /// + /// Output buffer + public void Finalize(ArraySegment output) + { + Preconditions.NotNull(output.Array, nameof(output)); + if (output.Count != 64) + throw new ArgumentException("Output should be 64 in length"); + + Update(_padding, 0, _padding.Length); + Array16 block; + ByteIntegerConverter.Array16LoadBigEndian64(out block, _buffer, 0); + CryptoBytes.InternalWipe(_buffer, 0, _buffer.Length); + int bytesInBuffer = (int)_totalBytes & (BlockSize - 1); + if (bytesInBuffer > BlockSize - 16) + { + Sha512Internal.Core(out _state, ref _state, ref block); + block = default(Array16); + } + block.x15 = (_totalBytes - 1) * 8; + Sha512Internal.Core(out _state, ref _state, ref block); + + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 0, _state.x0); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 8, _state.x1); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 16, _state.x2); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 24, _state.x3); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 32, _state.x4); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 40, _state.x5); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 48, _state.x6); + ByteIntegerConverter.StoreBigEndian64(output.Array, output.Offset + 56, _state.x7); + _state = default(Array8); + } + + /// + /// Finalizes SHA-512 hashing. + /// + /// Hash bytes + public byte[] Finalize() + { + var result = new byte[64]; + Finalize(new ArraySegment(result)); + return result; + } + + /// + /// Calculates SHA-512 hash value for the given bytes array. + /// + /// Data bytes array + /// Hash bytes + public static byte[] Hash(byte[] data) + { + return Hash(data, 0, data.Length); + } + + /// + /// Calculates SHA-512 hash value for the given bytes array. + /// + /// Data bytes array + /// Offset of byte sequence + /// Sequence length + /// Hash bytes + public static byte[] Hash(byte[] data, int index, int length) + { + var hasher = new Sha512(); + hasher.Update(data, index, length); + return hasher.Finalize(); + } + } +} diff --git a/src/Discord.Net.Rest/Net/ED25519/Sha512Internal.cs b/src/Discord.Net.Rest/Net/ED25519/Sha512Internal.cs new file mode 100644 index 000000000..df8842d8d --- /dev/null +++ b/src/Discord.Net.Rest/Net/ED25519/Sha512Internal.cs @@ -0,0 +1,447 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Net.ED25519 +{ + internal static class Sha512Internal + { + private static readonly ulong[] K = new ulong[] + { + 0x428a2f98d728ae22,0x7137449123ef65cd,0xb5c0fbcfec4d3b2f,0xe9b5dba58189dbbc, + 0x3956c25bf348b538,0x59f111f1b605d019,0x923f82a4af194f9b,0xab1c5ed5da6d8118, + 0xd807aa98a3030242,0x12835b0145706fbe,0x243185be4ee4b28c,0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f,0x80deb1fe3b1696b1,0x9bdc06a725c71235,0xc19bf174cf692694, + 0xe49b69c19ef14ad2,0xefbe4786384f25e3,0x0fc19dc68b8cd5b5,0x240ca1cc77ac9c65, + 0x2de92c6f592b0275,0x4a7484aa6ea6e483,0x5cb0a9dcbd41fbd4,0x76f988da831153b5, + 0x983e5152ee66dfab,0xa831c66d2db43210,0xb00327c898fb213f,0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2,0xd5a79147930aa725,0x06ca6351e003826f,0x142929670a0e6e70, + 0x27b70a8546d22ffc,0x2e1b21385c26c926,0x4d2c6dfc5ac42aed,0x53380d139d95b3df, + 0x650a73548baf63de,0x766a0abb3c77b2a8,0x81c2c92e47edaee6,0x92722c851482353b, + 0xa2bfe8a14cf10364,0xa81a664bbc423001,0xc24b8b70d0f89791,0xc76c51a30654be30, + 0xd192e819d6ef5218,0xd69906245565a910,0xf40e35855771202a,0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8,0x1e376c085141ab53,0x2748774cdf8eeb99,0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63,0x4ed8aa4ae3418acb,0x5b9cca4f7763e373,0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc,0x78a5636f43172f60,0x84c87814a1f0ab72,0x8cc702081a6439ec, + 0x90befffa23631e28,0xa4506cebde82bde9,0xbef9a3f7b2c67915,0xc67178f2e372532b, + 0xca273eceea26619c,0xd186b8c721c0c207,0xeada7dd6cde0eb1e,0xf57d4f7fee6ed178, + 0x06f067aa72176fba,0x0a637dc5a2c898a6,0x113f9804bef90dae,0x1b710b35131c471b, + 0x28db77f523047d84,0x32caab7b40c72493,0x3c9ebe0a15c9bebc,0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6,0x597f299cfc657e2a,0x5fcb6fab3ad6faec,0x6c44198c4a475817 + }; + + internal static void Sha512Init(out Array8 state) + { + state.x0 = 0x6a09e667f3bcc908; + state.x1 = 0xbb67ae8584caa73b; + state.x2 = 0x3c6ef372fe94f82b; + state.x3 = 0xa54ff53a5f1d36f1; + state.x4 = 0x510e527fade682d1; + state.x5 = 0x9b05688c2b3e6c1f; + state.x6 = 0x1f83d9abfb41bd6b; + state.x7 = 0x5be0cd19137e2179; + } + + internal static void Core(out Array8 outputState, ref Array8 inputState, ref Array16 input) + { + unchecked + { + var a = inputState.x0; + var b = inputState.x1; + var c = inputState.x2; + var d = inputState.x3; + var e = inputState.x4; + var f = inputState.x5; + var g = inputState.x6; + var h = inputState.x7; + + var w0 = input.x0; + var w1 = input.x1; + var w2 = input.x2; + var w3 = input.x3; + var w4 = input.x4; + var w5 = input.x5; + var w6 = input.x6; + var w7 = input.x7; + var w8 = input.x8; + var w9 = input.x9; + var w10 = input.x10; + var w11 = input.x11; + var w12 = input.x12; + var w13 = input.x13; + var w14 = input.x14; + var w15 = input.x15; + + int t = 0; + while (true) + { + ulong t1, t2; + + {//0 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w0; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//1 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w1; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//2 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w2; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//3 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w3; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//4 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w4; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//5 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w5; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//6 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w6; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//7 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w7; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//8 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w8; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//9 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w9; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//10 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w10; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//11 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w11; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//12 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w12; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//13 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w13; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//14 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w14; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + {//15 + t1 = h + + ((e >> 14) ^ (e << (64 - 14)) ^ (e >> 18) ^ (e << (64 - 18)) ^ (e >> 41) ^ (e << (64 - 41))) + + //Sigma1(e) + ((e & f) ^ (~e & g)) + //Ch(e,f,g) + K[t] + w15; + t2 = ((a >> 28) ^ (a << (64 - 28)) ^ (a >> 34) ^ (a << (64 - 34)) ^ (a >> 39) ^ (a << (64 - 39))) + + //Sigma0(a) + ((a & b) ^ (a & c) ^ (b & c)); //Maj(a,b,c) + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + t++; + } + if (t == 80) + break; + + w0 += ((w14 >> 19) ^ (w14 << (64 - 19)) ^ (w14 >> 61) ^ (w14 << (64 - 61)) ^ (w14 >> 6)) + + w9 + + ((w1 >> 1) ^ (w1 << (64 - 1)) ^ (w1 >> 8) ^ (w1 << (64 - 8)) ^ (w1 >> 7)); + w1 += ((w15 >> 19) ^ (w15 << (64 - 19)) ^ (w15 >> 61) ^ (w15 << (64 - 61)) ^ (w15 >> 6)) + + w10 + + ((w2 >> 1) ^ (w2 << (64 - 1)) ^ (w2 >> 8) ^ (w2 << (64 - 8)) ^ (w2 >> 7)); + w2 += ((w0 >> 19) ^ (w0 << (64 - 19)) ^ (w0 >> 61) ^ (w0 << (64 - 61)) ^ (w0 >> 6)) + + w11 + + ((w3 >> 1) ^ (w3 << (64 - 1)) ^ (w3 >> 8) ^ (w3 << (64 - 8)) ^ (w3 >> 7)); + w3 += ((w1 >> 19) ^ (w1 << (64 - 19)) ^ (w1 >> 61) ^ (w1 << (64 - 61)) ^ (w1 >> 6)) + + w12 + + ((w4 >> 1) ^ (w4 << (64 - 1)) ^ (w4 >> 8) ^ (w4 << (64 - 8)) ^ (w4 >> 7)); + w4 += ((w2 >> 19) ^ (w2 << (64 - 19)) ^ (w2 >> 61) ^ (w2 << (64 - 61)) ^ (w2 >> 6)) + + w13 + + ((w5 >> 1) ^ (w5 << (64 - 1)) ^ (w5 >> 8) ^ (w5 << (64 - 8)) ^ (w5 >> 7)); + w5 += ((w3 >> 19) ^ (w3 << (64 - 19)) ^ (w3 >> 61) ^ (w3 << (64 - 61)) ^ (w3 >> 6)) + + w14 + + ((w6 >> 1) ^ (w6 << (64 - 1)) ^ (w6 >> 8) ^ (w6 << (64 - 8)) ^ (w6 >> 7)); + w6 += ((w4 >> 19) ^ (w4 << (64 - 19)) ^ (w4 >> 61) ^ (w4 << (64 - 61)) ^ (w4 >> 6)) + + w15 + + ((w7 >> 1) ^ (w7 << (64 - 1)) ^ (w7 >> 8) ^ (w7 << (64 - 8)) ^ (w7 >> 7)); + w7 += ((w5 >> 19) ^ (w5 << (64 - 19)) ^ (w5 >> 61) ^ (w5 << (64 - 61)) ^ (w5 >> 6)) + + w0 + + ((w8 >> 1) ^ (w8 << (64 - 1)) ^ (w8 >> 8) ^ (w8 << (64 - 8)) ^ (w8 >> 7)); + w8 += ((w6 >> 19) ^ (w6 << (64 - 19)) ^ (w6 >> 61) ^ (w6 << (64 - 61)) ^ (w6 >> 6)) + + w1 + + ((w9 >> 1) ^ (w9 << (64 - 1)) ^ (w9 >> 8) ^ (w9 << (64 - 8)) ^ (w9 >> 7)); + w9 += ((w7 >> 19) ^ (w7 << (64 - 19)) ^ (w7 >> 61) ^ (w7 << (64 - 61)) ^ (w7 >> 6)) + + w2 + + ((w10 >> 1) ^ (w10 << (64 - 1)) ^ (w10 >> 8) ^ (w10 << (64 - 8)) ^ (w10 >> 7)); + w10 += ((w8 >> 19) ^ (w8 << (64 - 19)) ^ (w8 >> 61) ^ (w8 << (64 - 61)) ^ (w8 >> 6)) + + w3 + + ((w11 >> 1) ^ (w11 << (64 - 1)) ^ (w11 >> 8) ^ (w11 << (64 - 8)) ^ (w11 >> 7)); + w11 += ((w9 >> 19) ^ (w9 << (64 - 19)) ^ (w9 >> 61) ^ (w9 << (64 - 61)) ^ (w9 >> 6)) + + w4 + + ((w12 >> 1) ^ (w12 << (64 - 1)) ^ (w12 >> 8) ^ (w12 << (64 - 8)) ^ (w12 >> 7)); + w12 += ((w10 >> 19) ^ (w10 << (64 - 19)) ^ (w10 >> 61) ^ (w10 << (64 - 61)) ^ (w10 >> 6)) + + w5 + + ((w13 >> 1) ^ (w13 << (64 - 1)) ^ (w13 >> 8) ^ (w13 << (64 - 8)) ^ (w13 >> 7)); + w13 += ((w11 >> 19) ^ (w11 << (64 - 19)) ^ (w11 >> 61) ^ (w11 << (64 - 61)) ^ (w11 >> 6)) + + w6 + + ((w14 >> 1) ^ (w14 << (64 - 1)) ^ (w14 >> 8) ^ (w14 << (64 - 8)) ^ (w14 >> 7)); + w14 += ((w12 >> 19) ^ (w12 << (64 - 19)) ^ (w12 >> 61) ^ (w12 << (64 - 61)) ^ (w12 >> 6)) + + w7 + + ((w15 >> 1) ^ (w15 << (64 - 1)) ^ (w15 >> 8) ^ (w15 << (64 - 8)) ^ (w15 >> 7)); + w15 += ((w13 >> 19) ^ (w13 << (64 - 19)) ^ (w13 >> 61) ^ (w13 << (64 - 61)) ^ (w13 >> 6)) + + w8 + + ((w0 >> 1) ^ (w0 << (64 - 1)) ^ (w0 >> 8) ^ (w0 << (64 - 8)) ^ (w0 >> 7)); + } + + outputState.x0 = inputState.x0 + a; + outputState.x1 = inputState.x1 + b; + outputState.x2 = inputState.x2 + c; + outputState.x3 = inputState.x3 + d; + outputState.x4 = inputState.x4 + e; + outputState.x5 = inputState.x5 + f; + outputState.x6 = inputState.x6 + g; + outputState.x7 = inputState.x7 + h; + } + } + } +} diff --git a/src/Discord.Net.Rest/Net/Queue/ClientBucket.cs b/src/Discord.Net.Rest/Net/Queue/ClientBucket.cs index cd9d8aa54..e726a08cf 100644 --- a/src/Discord.Net.Rest/Net/Queue/ClientBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/ClientBucket.cs @@ -10,14 +10,14 @@ namespace Discord.Net.Queue internal struct ClientBucket { private static readonly ImmutableDictionary DefsByType; - private static readonly ImmutableDictionary DefsById; + private static readonly ImmutableDictionary DefsById; static ClientBucket() { var buckets = new[] { - new ClientBucket(ClientBucketType.Unbucketed, "", 10, 10), - new ClientBucket(ClientBucketType.SendEdit, "", 10, 10) + new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "", null), 10, 10), + new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "", null), 10, 10) }; var builder = ImmutableDictionary.CreateBuilder(); @@ -25,21 +25,21 @@ namespace Discord.Net.Queue builder.Add(bucket.Type, bucket); DefsByType = builder.ToImmutable(); - var builder2 = ImmutableDictionary.CreateBuilder(); + var builder2 = ImmutableDictionary.CreateBuilder(); foreach (var bucket in buckets) builder2.Add(bucket.Id, bucket); DefsById = builder2.ToImmutable(); } public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; - public static ClientBucket Get(string id) => DefsById[id]; + public static ClientBucket Get(BucketId id) => DefsById[id]; public ClientBucketType Type { get; } - public string Id { get; } + public BucketId Id { get; } public int WindowCount { get; } public int WindowSeconds { get; } - public ClientBucket(ClientBucketType type, string id, int count, int seconds) + public ClientBucket(ClientBucketType type, BucketId id, int count, int seconds) { Type = type; Id = id; diff --git a/src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs b/src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs new file mode 100644 index 000000000..aa849018a --- /dev/null +++ b/src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs @@ -0,0 +1,53 @@ +using System.Collections.Immutable; + +namespace Discord.Net.Queue +{ + public enum GatewayBucketType + { + Unbucketed = 0, + Identify = 1, + PresenceUpdate = 2, + } + internal struct GatewayBucket + { + private static readonly ImmutableDictionary DefsByType; + private static readonly ImmutableDictionary DefsById; + + static GatewayBucket() + { + var buckets = new[] + { + // Limit is 120/60s, but 3 will be reserved for heartbeats (2 for possible heartbeats in the same timeframe and a possible failure) + new GatewayBucket(GatewayBucketType.Unbucketed, BucketId.Create(null, "", null), 117, 60), + new GatewayBucket(GatewayBucketType.Identify, BucketId.Create(null, "", null), 1, 5), + new GatewayBucket(GatewayBucketType.PresenceUpdate, BucketId.Create(null, "", null), 5, 60), + }; + + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder.Add(bucket.Type, bucket); + DefsByType = builder.ToImmutable(); + + var builder2 = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder2.Add(bucket.Id, bucket); + DefsById = builder2.ToImmutable(); + } + + public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; + public static GatewayBucket Get(BucketId id) => DefsById[id]; + + public GatewayBucketType Type { get; } + public BucketId Id { get; } + public int WindowCount { get; set; } + public int WindowSeconds { get; set; } + + public GatewayBucket(GatewayBucketType type, BucketId id, int count, int seconds) + { + Type = type; + Id = id; + WindowCount = count; + WindowSeconds = seconds; + } + } +} diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 40b91e98e..2bf8e20b0 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -12,28 +12,29 @@ namespace Discord.Net.Queue { internal class RequestQueue : IDisposable { - public event Func RateLimitTriggered; + public event Func RateLimitTriggered; - private readonly ConcurrentDictionary _buckets; + private readonly ConcurrentDictionary _buckets; private readonly SemaphoreSlim _tokenLock; - private readonly CancellationTokenSource _cancelToken; //Dispose token + private readonly CancellationTokenSource _cancelTokenSource; //Dispose token private CancellationTokenSource _clearToken; private CancellationToken _parentToken; + private CancellationTokenSource _requestCancelTokenSource; private CancellationToken _requestCancelToken; //Parent token + Clear token private DateTimeOffset _waitUntil; private Task _cleanupTask; - + public RequestQueue() { _tokenLock = new SemaphoreSlim(1, 1); _clearToken = new CancellationTokenSource(); - _cancelToken = new CancellationTokenSource(); + _cancelTokenSource = new CancellationTokenSource(); _requestCancelToken = CancellationToken.None; _parentToken = CancellationToken.None; - - _buckets = new ConcurrentDictionary(); + + _buckets = new ConcurrentDictionary(); _cleanupTask = RunCleanup(); } @@ -44,7 +45,9 @@ namespace Discord.Net.Queue try { _parentToken = cancelToken; - _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token); + _requestCancelToken = _requestCancelTokenSource.Token; } finally { _tokenLock.Release(); } } @@ -54,9 +57,14 @@ namespace Discord.Net.Queue try { _clearToken?.Cancel(); + _clearToken?.Dispose(); _clearToken = new CancellationTokenSource(); if (_parentToken != null) - _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; + { + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; + } else _requestCancelToken = _clearToken.Token; } @@ -65,19 +73,34 @@ namespace Discord.Net.Queue public async Task SendAsync(RestRequest request) { + CancellationTokenSource createdTokenSource = null; if (request.Options.CancelToken.CanBeCanceled) - request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token; + { + createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); + request.Options.CancelToken = createdTokenSource.Token; + } else request.Options.CancelToken = _requestCancelToken; - var bucket = GetOrCreateBucket(request.Options.BucketId, request); - return await bucket.SendAsync(request).ConfigureAwait(false); + var bucket = GetOrCreateBucket(request.Options, request); + var result = await bucket.SendAsync(request).ConfigureAwait(false); + createdTokenSource?.Dispose(); + return result; } public async Task SendAsync(WebSocketRequest request) { - //TODO: Re-impl websocket buckets - request.CancelToken = _requestCancelToken; - await request.SendAsync().ConfigureAwait(false); + CancellationTokenSource createdTokenSource = null; + if (request.Options.CancelToken.CanBeCanceled) + { + createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); + request.Options.CancelToken = createdTokenSource.Token; + } + else + request.Options.CancelToken = _requestCancelToken; + + var bucket = GetOrCreateBucket(request.Options, request); + await bucket.SendAsync(request).ConfigureAwait(false); + createdTokenSource?.Dispose(); } internal async Task EnterGlobalAsync(int id, RestRequest request) @@ -95,29 +118,73 @@ namespace Discord.Net.Queue { _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); } + internal async Task EnterGlobalAsync(int id, WebSocketRequest request) + { + //If this is a global request (unbucketed), it'll be dealt in EnterAsync + var requestBucket = GatewayBucket.Get(request.Options.BucketId); + if (requestBucket.Type == GatewayBucketType.Unbucketed) + return; + + //It's not a global request, so need to remove one from global (per-session) + var globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed); + var options = RequestOptions.CreateOrClone(request.Options); + options.BucketId = globalBucketType.Id; + var globalRequest = new WebSocketRequest(null, null, false, false, options); + var globalBucket = GetOrCreateBucket(options, globalRequest); + await globalBucket.TriggerAsync(id, globalRequest); + } - private RequestBucket GetOrCreateBucket(string id, RestRequest request) + private RequestBucket GetOrCreateBucket(RequestOptions options, IRequest request) + { + var bucketId = options.BucketId; + object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x)); + if (obj is BucketId hashBucket) + { + options.BucketId = hashBucket; + return (RequestBucket)_buckets.GetOrAdd(hashBucket, x => new RequestBucket(this, request, x)); + } + return (RequestBucket)obj; + } + internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info, string endpoint) + { + await RateLimitTriggered(bucketId, info, endpoint).ConfigureAwait(false); + } + internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash) { - return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); + if (!id.IsHashBucket) + { + var bucket = BucketId.Create(discordHash, id); + var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]); + _buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket); + return (hashReqQueue, bucket); + } + return (null, null); } - internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info) + + public void ClearGatewayBuckets() { - await RateLimitTriggered(bucketId, info).ConfigureAwait(false); + foreach (var gwBucket in (GatewayBucketType[])Enum.GetValues(typeof(GatewayBucketType))) + _buckets.TryRemove(GatewayBucket.Get(gwBucket).Id, out _); } private async Task RunCleanup() { try { - while (!_cancelToken.IsCancellationRequested) + while (!_cancelTokenSource.IsCancellationRequested) { var now = DateTimeOffset.UtcNow; - foreach (var bucket in _buckets.Select(x => x.Value)) + foreach (var bucket in _buckets.Where(x => x.Value is RequestBucket).Select(x => (RequestBucket)x.Value)) { if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) + { + if (bucket.Id.IsHashBucket) + foreach (var redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value)) + _buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket _buckets.TryRemove(bucket.Id, out _); + } } - await Task.Delay(60000, _cancelToken.Token); //Runs each minute + await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute } } catch (OperationCanceledException) { } @@ -126,7 +193,10 @@ namespace Discord.Net.Queue public void Dispose() { - _cancelToken.Dispose(); + _cancelTokenSource?.Dispose(); + _tokenLock?.Dispose(); + _clearToken?.Dispose(); + _requestCancelTokenSource?.Dispose(); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index c4f5996c5..d9f4ba57a 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -1,3 +1,4 @@ +using Discord.API; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -5,6 +6,7 @@ using System; using System.Diagnostics; #endif using System.IO; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -13,16 +15,19 @@ namespace Discord.Net.Queue { internal class RequestBucket { + private const int MinimumSleepTimeMs = 750; + private readonly object _lock; private readonly RequestQueue _queue; private int _semaphore; private DateTimeOffset? _resetTick; + private RequestBucket _redirectBucket; - public string Id { get; private set; } + public BucketId Id { get; private set; } public int WindowCount { get; private set; } public DateTimeOffset LastAttemptAt { get; private set; } - public RequestBucket(RequestQueue queue, RestRequest request, string id) + public RequestBucket(RequestQueue queue, IRequest request, BucketId id) { _queue = queue; Id = id; @@ -31,13 +36,15 @@ namespace Discord.Net.Queue if (request.Options.IsClientBucket) WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; + else if (request.Options.IsGatewayBucket) + WindowCount = GatewayBucket.Get(request.Options.BucketId).WindowCount; else WindowCount = 1; //Only allow one request until we get a header back _semaphore = WindowCount; _resetTick = null; LastAttemptAt = DateTimeOffset.UtcNow; } - + static int nextId = 0; public async Task SendAsync(RestRequest request) { @@ -50,6 +57,8 @@ namespace Discord.Net.Queue { await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false); + if (_redirectBucket != null) + return await _redirectBucket.SendAsync(request); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sending..."); @@ -58,7 +67,9 @@ namespace Discord.Net.Queue try { var response = await request.SendAsync().ConfigureAwait(false); - info = new RateLimitInfo(response.Headers); + info = new RateLimitInfo(response.Headers, request.Endpoint); + + request.Options.ExecuteRatelimitCallback(info); if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) { @@ -79,7 +90,7 @@ namespace Discord.Net.Queue #endif UpdateRateLimit(id, request, info, true); } - await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); + await _queue.RaiseRateLimitTriggered(Id, info, $"{request.Method} {request.Endpoint}").ConfigureAwait(false); continue; //Retry case HttpStatusCode.BadGateway: //502 #if DEBUG_LIMITS @@ -90,8 +101,7 @@ namespace Discord.Net.Queue continue; //Retry default: - int? code = null; - string reason = null; + API.DiscordError error = null; if (response.Stream != null) { try @@ -99,14 +109,14 @@ namespace Discord.Net.Queue using (var reader = new StreamReader(response.Stream)) using (var jsonReader = new JsonTextReader(reader)) { - var json = JToken.Load(jsonReader); - try { code = json.Value("code"); } catch { }; - try { reason = json.Value("message"); } catch { }; + error = Discord.Rest.DiscordRestClient.Serializer.Deserialize(jsonReader); } } catch { } } - throw new HttpException(response.StatusCode, request, code, reason); + throw new HttpException(response.StatusCode, request, error?.Code, error.Message, error.Errors.IsSpecified + ? error.Errors.Value.Select(x => new DiscordJsonError(x.Name.GetValueOrDefault("root"), x.Errors.Select(y => new DiscordError(y.Code, y.Message)).ToArray())).ToArray() + : null); } } else @@ -126,7 +136,7 @@ namespace Discord.Net.Queue if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) throw; - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); continue; //Retry } /*catch (Exception) @@ -149,8 +159,68 @@ namespace Discord.Net.Queue } } } + public async Task SendAsync(WebSocketRequest request) + { + int id = Interlocked.Increment(ref nextId); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Start"); +#endif + LastAttemptAt = DateTimeOffset.UtcNow; + while (true) + { + await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); + await EnterAsync(id, request).ConfigureAwait(false); + +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Sending..."); +#endif + try + { + await request.SendAsync().ConfigureAwait(false); + return; + } + catch (TimeoutException) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Timeout"); +#endif + if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) + throw; + + await Task.Delay(500).ConfigureAwait(false); + continue; //Retry + } + /*catch (Exception) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Error"); +#endif + if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) + throw; + + await Task.Delay(500); + continue; //Retry + }*/ + finally + { + UpdateRateLimit(id, request, default(RateLimitInfo), false); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Stop"); +#endif + } + } + } - private async Task EnterAsync(int id, RestRequest request) + internal async Task TriggerAsync(int id, IRequest request) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Trigger Bucket"); +#endif + await EnterAsync(id, request).ConfigureAwait(false); + UpdateRateLimit(id, request, default(RateLimitInfo), false); + } + + private async Task EnterAsync(int id, IRequest request) { int windowCount; DateTimeOffset? resetAt; @@ -158,6 +228,9 @@ namespace Discord.Net.Queue while (true) { + if (_redirectBucket != null) + break; + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) { if (!isRateLimited) @@ -173,20 +246,45 @@ namespace Discord.Net.Queue } DateTimeOffset? timeoutAt = request.TimeoutAt; - if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0) + int semaphore = Interlocked.Decrement(ref _semaphore); + if (windowCount > 0 && semaphore < 0) { if (!isRateLimited) { + bool ignoreRatelimit = false; isRateLimited = true; - await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); + switch (request) + { + case RestRequest restRequest: + await _queue.RaiseRateLimitTriggered(Id, null, $"{restRequest.Method} {restRequest.Endpoint}").ConfigureAwait(false); + break; + case WebSocketRequest webSocketRequest: + if (webSocketRequest.IgnoreLimit) + { + ignoreRatelimit = true; + break; + } + await _queue.RaiseRateLimitTriggered(Id, null, Id.Endpoint).ConfigureAwait(false); + break; + default: + throw new InvalidOperationException("Unknown request type"); + } + if (ignoreRatelimit) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Ignoring ratelimit"); +#endif + break; + } } ThrowRetryLimit(request); - if (resetAt.HasValue) + if (resetAt.HasValue && resetAt > DateTimeOffset.UtcNow) { if (resetAt > timeoutAt) ThrowRetryLimit(request); + int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); @@ -196,41 +294,72 @@ namespace Discord.Net.Queue } else { - if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) + if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < MinimumSleepTimeMs) ThrowRetryLimit(request); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); + Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)"); #endif - await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); + await Task.Delay(MinimumSleepTimeMs, request.Options.CancelToken).ConfigureAwait(false); } continue; } #if DEBUG_LIMITS else - Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); + Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)"); #endif break; } } - private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) + private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429, bool redirected = false) { if (WindowCount == 0) return; lock (_lock) { + if (redirected) + { + Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Decrease Semaphore"); +#endif + } bool hasQueuedReset = _resetTick != null; + + if (info.Bucket != null && !redirected) + { + (RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket); + if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null)) + { + if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue + { + Id = hashBucket.Item2; +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})"); +#endif + } + else + { + _redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything + _redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}"); +#endif + return; + } + } + } + if (info.Limit.HasValue && WindowCount != info.Limit.Value) { WindowCount = info.Limit.Value; - _semaphore = info.Remaining.Value; + _semaphore = is429 ? 0 : info.Remaining.Value; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}"); #endif } - var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); DateTimeOffset? resetTick = null; //Using X-RateLimit-Remaining causes a race condition @@ -242,30 +371,60 @@ namespace Discord.Net.Queue if (info.RetryAfter.HasValue) { //RetryAfter is more accurate than Reset, where available - resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value); + resetTick = DateTimeOffset.UtcNow.AddSeconds(info.RetryAfter.Value); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); +#endif + } + else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue && !request.Options.UseSystemClock.Value)) + { + resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)"); #endif } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); + + /* millisecond precision makes this unnecessary, retaining in case of regression + if (request.Options.IsReactionBucket) + resetTick = DateTimeOffset.Now.AddMilliseconds(250); + */ + int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); #endif } - else if (request.Options.IsClientBucket && request.Options.BucketId != null) + else if (request.Options.IsClientBucket && Id != null) + { + resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)"); +#endif + } + else if (request.Options.IsGatewayBucket && request.Options.BucketId != null) { - resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); + resetTick = DateTimeOffset.UtcNow.AddSeconds(GatewayBucket.Get(request.Options.BucketId).WindowSeconds); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Gateway Bucket ({GatewayBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); +#endif + if (!hasQueuedReset) + { + _resetTick = resetTick; + LastAttemptAt = resetTick.Value; #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); + Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif + var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); + } + return; } if (resetTick == null) { - WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) + WindowCount = 0; //No rate limit info, disable limits on this bucket #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Disabled Semaphore"); #endif @@ -275,19 +434,19 @@ namespace Discord.Net.Queue if (!hasQueuedReset || resetTick > _resetTick) { _resetTick = resetTick; - LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset + LastAttemptAt = resetTick.Value; //Make sure we don't destroy this until after its been reset #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif if (!hasQueuedReset) { - var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds)); + var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); } } } } - private async Task QueueReset(int id, int millis) + private async Task QueueReset(int id, int millis, IRequest request) { while (true) { @@ -296,7 +455,7 @@ namespace Discord.Net.Queue lock (_lock) { millis = (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds); - if (millis <= 0) //Make sure we havent gotten a more accurate reset time + if (millis <= 0) //Make sure we haven't gotten a more accurate reset time { #if DEBUG_LIMITS Debug.WriteLine($"[{id}] * Reset *"); @@ -309,7 +468,7 @@ namespace Discord.Net.Queue } } - private void ThrowRetryLimit(RestRequest request) + private void ThrowRetryLimit(IRequest request) { if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) throw new RateLimitedException(request); diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs index 81eb40b31..ebebd7bef 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs @@ -9,22 +9,22 @@ namespace Discord.Net.Queue public class WebSocketRequest : IRequest { public IWebSocketClient Client { get; } - public string BucketId { get; } public byte[] Data { get; } public bool IsText { get; } + public bool IgnoreLimit { get; } public DateTimeOffset? TimeoutAt { get; } public TaskCompletionSource Promise { get; } public RequestOptions Options { get; } public CancellationToken CancelToken { get; internal set; } - public WebSocketRequest(IWebSocketClient client, string bucketId, byte[] data, bool isText, RequestOptions options) + public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, bool ignoreLimit, RequestOptions options) { Preconditions.NotNull(options, nameof(options)); Client = client; - BucketId = bucketId; Data = data; IsText = isText; + IgnoreLimit = ignoreLimit; Options = options; TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; Promise = new TaskCompletionSource(); diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index d31cc5cdd..c08f30c7b 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -1,31 +1,60 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace Discord.Net { - internal struct RateLimitInfo + /// + /// Represents a REST-Based ratelimit info. + /// + public struct RateLimitInfo : IRateLimitInfo { + /// public bool IsGlobal { get; } + + /// public int? Limit { get; } + + /// public int? Remaining { get; } + + /// public int? RetryAfter { get; } + + /// public DateTimeOffset? Reset { get; } + + /// + public TimeSpan? ResetAfter { get; } + + /// + public string Bucket { get; } + + /// public TimeSpan? Lag { get; } - internal RateLimitInfo(Dictionary headers) + /// + public string Endpoint { get; } + + internal RateLimitInfo(Dictionary headers, string endpoint) { + Endpoint = endpoint; + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) && bool.TryParse(temp, out var isGlobal) && isGlobal; Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && - int.TryParse(temp, out var limit) ? limit : (int?)null; + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var limit) ? limit : (int?)null; Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && - int.TryParse(temp, out var remaining) ? remaining : (int?)null; + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var remaining) ? remaining : (int?)null; Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) && - int.TryParse(temp, out var reset) ? DateTimeOffset.FromUnixTimeSeconds(reset) : (DateTimeOffset?)null; + double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; RetryAfter = headers.TryGetValue("Retry-After", out temp) && - int.TryParse(temp, out var retryAfter) ? retryAfter : (int?)null; + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; + ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && + double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var resetAfter) ? TimeSpan.FromSeconds(resetAfter) : (TimeSpan?)null; + Bucket = headers.TryGetValue("X-RateLimit-Bucket", out temp) ? temp : null; Lag = headers.TryGetValue("Date", out temp) && - DateTimeOffset.TryParse(temp, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; + DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } } } diff --git a/src/Discord.Net.Rest/Utils/HexConverter.cs b/src/Discord.Net.Rest/Utils/HexConverter.cs new file mode 100644 index 000000000..ebd959dcb --- /dev/null +++ b/src/Discord.Net.Rest/Utils/HexConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal class HexConverter + { + public static byte[] HexToByteArray(string hex) + { + if (hex.Length % 2 == 1) + throw new Exception("The binary key cannot have an odd number of digits"); + + byte[] arr = new byte[hex.Length >> 1]; + + for (int i = 0; i < hex.Length >> 1; ++i) + { + arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1]))); + } + + return arr; + } + private static int GetHexVal(char hex) + { + int val = (int)hex; + //For uppercase A-F letters: + //return val - (val < 58 ? 48 : 55); + //For lowercase a-f letters: + //return val - (val < 58 ? 48 : 87); + //Or the two combined, but a bit slower: + return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); + } + } +} diff --git a/src/Discord.Net.Rest/Utils/TypingNotifier.cs b/src/Discord.Net.Rest/Utils/TypingNotifier.cs index b4bd2f44b..745dbd36d 100644 --- a/src/Discord.Net.Rest/Utils/TypingNotifier.cs +++ b/src/Discord.Net.Rest/Utils/TypingNotifier.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -6,21 +6,19 @@ namespace Discord.Rest { internal class TypingNotifier : IDisposable { - private readonly BaseDiscordClient _client; private readonly CancellationTokenSource _cancelToken; private readonly IMessageChannel _channel; private readonly RequestOptions _options; - public TypingNotifier(BaseDiscordClient discord, IMessageChannel channel, RequestOptions options) + public TypingNotifier(IMessageChannel channel, RequestOptions options) { - _client = discord; _cancelToken = new CancellationTokenSource(); _channel = channel; _options = options; - var _ = Run(); + _ = RunAsync(); } - private async Task Run() + private async Task RunAsync() { try { @@ -31,7 +29,11 @@ namespace Discord.Rest { await _channel.TriggerTypingAsync(_options).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } + await Task.Delay(9500, token).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs new file mode 100644 index 000000000..91dcbde11 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class ApplicationCommandCreatedUpdatedEvent : ApplicationCommand + { + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs index 910f6d909..04ee38c0b 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -8,18 +7,29 @@ namespace Discord.API.Gateway { [JsonProperty("unavailable")] public bool? Unavailable { get; set; } + [JsonProperty("member_count")] public int MemberCount { get; set; } + [JsonProperty("large")] public bool Large { get; set; } [JsonProperty("presences")] public Presence[] Presences { get; set; } + [JsonProperty("members")] public GuildMember[] Members { get; set; } + [JsonProperty("channels")] public Channel[] Channels { get; set; } + [JsonProperty("joined_at")] public DateTimeOffset JoinedAt { get; set; } + + [JsonProperty("threads")] + public new Channel[] Threads { get; set; } + + [JsonProperty("guild_scheduled_events")] + public GuildScheduledEvent[] GuildScheduledEvents { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs b/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs index 13a2bb462..6f8bf48d4 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Gateway { internal enum GatewayOpCode : byte @@ -10,7 +9,7 @@ namespace Discord.API.Gateway /// C→S - Used to associate a connection with a token and specify configuration. Identify = 2, /// C→S - Used to update client's status and current game id. - StatusUpdate = 3, + PresenceUpdate = 3, /// C→S - Used to join a particular voice channel. VoiceStateUpdate = 4, /// C→S - Used to ensure the guild's voice server is alive. diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs index 59a3304dd..a8a72e791 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs index 715341dc5..33c10e648 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs new file mode 100644 index 000000000..cb6fc5f40 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildJoinRequestDeleteEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs index 350652faf..dd42978fc 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs index 501408a7f..ec7df8fd3 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs index a234d6da5..0f6fa6f37 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs @@ -1,10 +1,13 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; +using System; namespace Discord.API.Gateway { internal class GuildMemberUpdateEvent : GuildMember { + [JsonProperty("joined_at")] + public new DateTimeOffset? JoinedAt { get; set; } + [JsonProperty("guild_id")] public ulong GuildId { get; set; } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs index e401d7fa1..26114bf54 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs index 3409b1c91..3b02164d5 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs index dbdaeff67..d9bdb9892 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs index b04ecb182..bb6a39620 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildScheduledEventUserAddRemoveEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildScheduledEventUserAddRemoveEvent.cs new file mode 100644 index 000000000..3fc959125 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildScheduledEventUserAddRemoveEvent.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class GuildScheduledEventUserAddRemoveEvent + { + [JsonProperty("guild_scheduled_event_id")] + public ulong EventId { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildStickerUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildStickerUpdateEvent.cs new file mode 100644 index 000000000..f0ecd3a4f --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildStickerUpdateEvent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildStickerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("stickers")] + public Sticker[] Stickers { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs index 6b2e6c02f..ba4c1ca60 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs index e1ed9463c..a53a96fd8 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs index af16f22f5..96c7cb32f 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; @@ -15,5 +14,9 @@ namespace Discord.API.Gateway public int LargeThreshold { get; set; } [JsonProperty("shard")] public Optional ShardingParams { get; set; } + [JsonProperty("presence")] + public Optional Presence { get; set; } + [JsonProperty("intents")] + public Optional Intents { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs new file mode 100644 index 000000000..e2ddd8816 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Gateway +{ + internal class InviteCreateEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("inviter")] + public Optional Inviter { get; set; } + [JsonProperty("max_age")] + public int MaxAge { get; set; } + [JsonProperty("max_uses")] + public int MaxUses { get; set; } + [JsonProperty("target_user")] + public Optional TargetUser { get; set; } + [JsonProperty("target_user_type")] + public Optional TargetUserType { get; set; } + [JsonProperty("temporary")] + public bool Temporary { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs new file mode 100644 index 000000000..1613cdfa6 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs @@ -0,0 +1,32 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class InviteCreatedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("code")] + public string InviteCode { get; set; } + [JsonProperty("timestamp")] + public Optional RawTimestamp { get; set; } + [JsonProperty("guild_id")] + public ulong? GuildID { get; set; } + [JsonProperty("inviter")] + public Optional Inviter { get; set; } + [JsonProperty("max_age")] + public int RawAge { get; set; } + [JsonProperty("max_uses")] + public int MaxUsers { get; set; } + [JsonProperty("temporary")] + public bool TempInvite { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs new file mode 100644 index 000000000..54bc75595 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class InviteDeleteEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs new file mode 100644 index 000000000..6bdd337f5 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class InviteDeletedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("guild_id")] + public Optional GuildID { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs index aba4a2f1c..c503e636d 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; @@ -9,6 +8,6 @@ namespace Discord.API.Gateway [JsonProperty("channel_id")] public ulong ChannelId { get; set; } [JsonProperty("ids")] - public IEnumerable Ids { get; set; } + public ulong[] Ids { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs index 62de456e2..0d17cbff8 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API.Gateway { @@ -10,7 +10,11 @@ namespace Discord.API.Gateway public ulong MessageId { get; set; } [JsonProperty("channel_id")] public ulong ChannelId { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } [JsonProperty("emoji")] public Emoji Emoji { get; set; } + [JsonProperty("member")] + public Optional Member { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs index ab92d8c36..5cd75dbee 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs index 336ffd029..778b5708c 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs new file mode 100644 index 000000000..7f804d3f5 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class RemoveAllReactionsForEmoteEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs index 6a8d283ed..f7a63e330 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; diff --git a/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs index ffb46327b..826e8fadd 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs index d1347beae..870ae7366 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs index fc0964c17..cbde225d2 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs @@ -1,18 +1,18 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway { [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - internal class StatusUpdateParams + internal class PresenceUpdateParams + { [JsonProperty("status")] public UserStatus Status { get; set; } - [JsonProperty("since"), Int53] + [JsonProperty("since", NullValueHandling = NullValueHandling.Include), Int53] public long? IdleSince { get; set; } [JsonProperty("afk")] public bool IsAFK { get; set; } - [JsonProperty("game")] - public Game Game { get; set; } + [JsonProperty("activities")] + public object[] Activities { get; set; } // TODO, change to interface later } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs new file mode 100644 index 000000000..5084f6c95 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class ThreadListSyncEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_ids")] + public Optional ChannelIds { get; set; } + + [JsonProperty("threads")] + public Channel[] Threads { get; set; } + + [JsonProperty("members")] + public ThreadMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/ThreadMembersUpdate.cs b/src/Discord.Net.WebSocket/API/Gateway/ThreadMembersUpdate.cs new file mode 100644 index 000000000..83d2c0edd --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/ThreadMembersUpdate.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class ThreadMembersUpdated + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("member_count")] + public int MemberCount { get; set; } + + [JsonProperty("added_members")] + public Optional AddedMembers { get; set; } + + [JsonProperty("removed_member_ids")] + public Optional RemovedMemberIds { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs index 5ceae4b7a..729ea176f 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs index 29167c1cc..8df3f0108 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs index 521160126..ad21b14f1 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs index e5c7afe41..c1e6d5385 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Gateway diff --git a/src/Discord.Net.WebSocket/API/SocketFrame.cs b/src/Discord.Net.WebSocket/API/SocketFrame.cs index fae741432..11c82ec44 100644 --- a/src/Discord.Net.WebSocket/API/SocketFrame.cs +++ b/src/Discord.Net.WebSocket/API/SocketFrame.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs index d446867e1..508b70d70 100644 --- a/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs index 7188cd8f7..fb910573a 100644 --- a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -15,7 +14,7 @@ namespace Discord.API.Voice [JsonProperty("modes")] public string[] Modes { get; set; } [JsonProperty("heartbeat_interval")] - [Obsolete("This field is errorneous and should not be used", true)] + [Obsolete("This field is erroneous and should not be used", true)] public int HeartbeatInterval { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs b/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs index 8c577e5b5..2e9bd157a 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs index 45befadcf..043b9fe86 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs index 0272a8f53..c1746e9ce 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs index abdf90667..e03bfc751 100644 --- a/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs +++ b/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs b/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs index 6f4719e7e..5e69a0370 100644 --- a/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs +++ b/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Voice diff --git a/src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs b/src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs new file mode 100644 index 000000000..e35227050 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Voice +{ + /// + /// Represents generic op codes for voice disconnect. + /// + public enum VoiceCloseCode + { + /// + /// You sent an invalid opcode. + /// + UnknownOpcode = 4001, + /// + /// You sent an invalid payload in your identifying to the Gateway. + /// + DecodeFailure = 4002, + /// + /// You sent a payload before identifying with the Gateway. + /// + NotAuthenticated = 4003, + /// + /// The token you sent in your identify payload is incorrect. + /// + AuthenticationFailed = 4004, + /// + /// You sent more than one identify payload. Stahp. + /// + AlreadyAuthenticated = 4005, + /// + /// Your session is no longer valid. + /// + SessionNolongerValid = 4006, + /// + /// Your session has timed out. + /// + SessionTimeout = 4009, + /// + /// We can't find the server you're trying to connect to. + /// + ServerNotFound = 4011, + /// + /// We didn't recognize the protocol you sent. + /// + UnknownProtocol = 4012, + /// + /// Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect. + /// + Disconnected = 4014, + /// + /// The server crashed. Our bad! Try resuming. + /// + VoiceServerCrashed = 4015, + /// + /// We didn't recognize your encryption. + /// + UnknownEncryptionMode = 4016, + } +} diff --git a/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs b/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs index 67afe6173..94006505a 100644 --- a/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs +++ b/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 namespace Discord.API.Voice { internal enum VoiceOpCode : byte diff --git a/src/Discord.Net.WebSocket/AssemblyInfo.cs b/src/Discord.Net.WebSocket/AssemblyInfo.cs index ca3e05e2f..442ec7dd8 100644 --- a/src/Discord.Net.WebSocket/AssemblyInfo.cs +++ b/src/Discord.Net.WebSocket/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Relay")] -[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 9f197b5c5..3549fb106 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -71,7 +71,7 @@ namespace Discord.Audio ApiClient.ReceivedPacket += ProcessPacketAsync; _stateLock = new SemaphoreSlim(1, 1); - _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, + _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); @@ -79,7 +79,7 @@ namespace Discord.Audio _keepaliveTimes = new ConcurrentQueue>(); _ssrcMap = new ConcurrentDictionary(); _streams = new ConcurrentDictionary(); - + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { @@ -91,7 +91,7 @@ namespace Discord.Audio UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } - internal async Task StartAsync(string url, ulong userId, string sessionId, string token) + internal async Task StartAsync(string url, ulong userId, string sessionId, string token) { _url = url; _userId = userId; @@ -99,8 +99,14 @@ namespace Discord.Audio _token = token; await _connection.StartAsync().ConfigureAwait(false); } + + public IReadOnlyDictionary GetStreams() + { + return _streams.ToDictionary(pair => pair.Key, pair => pair.Value.Reader); + } + public async Task StopAsync() - { + { await _connection.StopAsync().ConfigureAwait(false); } @@ -225,11 +231,11 @@ namespace Discord.Audio if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); - + ApiClient.SetUdpEndpoint(data.Ip, data.Port); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); - + _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); } break; @@ -305,9 +311,9 @@ namespace Discord.Audio catch (Exception ex) { await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); - return; + return; } - + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } @@ -317,7 +323,7 @@ namespace Discord.Audio { await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); - ulong value = + ulong value = ((ulong)packet[0] >> 0) | ((ulong)packet[1] >> 8) | ((ulong)packet[2] >> 16) | @@ -341,7 +347,7 @@ namespace Discord.Audio } } else - { + { if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) { await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); @@ -379,7 +385,7 @@ namespace Discord.Audio private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { - //TODO: Clean this up when Discord's session patch is live + // TODO: Clean this up when Discord's session patch is live try { await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); @@ -388,7 +394,7 @@ namespace Discord.Audio var now = Environment.TickCount; //Did server respond to our last heartbeat? - if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && + if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && ConnectionState == ConnectionState.Connected) { _connection.Error(new Exception("Server missed last heartbeat")); @@ -420,7 +426,6 @@ namespace Discord.Audio } private async Task RunKeepaliveAsync(int intervalMillis, CancellationToken cancelToken) { - var packet = new byte[8]; try { await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); @@ -438,7 +443,7 @@ namespace Discord.Audio { await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); } - + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); @@ -468,6 +473,7 @@ namespace Discord.Audio { StopAsync().GetAwaiter().GetResult(); ApiClient.Dispose(); + _stateLock?.Dispose(); } } /// diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 47a7e2809..25afde784 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -27,7 +27,7 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; - private readonly CancellationTokenSource _cancelTokenSource; + private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource; private readonly CancellationToken _cancelToken; private readonly Task _task; private readonly ConcurrentQueue _queuedFrames; @@ -49,21 +49,30 @@ namespace Discord.Audio.Streams _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; + _disposeTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_disposeTokenSource.Token, cancelToken); + _cancelToken = _cancelTokenSource.Token; _queuedFrames = new ConcurrentQueue(); _bufferPool = new ConcurrentQueue(); for (int i = 0; i < _queueLength; i++) - _bufferPool.Enqueue(new byte[maxFrameSize]); + _bufferPool.Enqueue(new byte[maxFrameSize]); _queueLock = new SemaphoreSlim(_queueLength, _queueLength); _silenceFrames = MaxSilenceFrames; _task = Run(); } + protected override void Dispose(bool disposing) { if (disposing) - _cancelTokenSource.Cancel(); + { + _disposeTokenSource?.Cancel(); + _disposeTokenSource?.Dispose(); + _cancelTokenSource?.Cancel(); + _cancelTokenSource?.Dispose(); + _queueLock?.Dispose(); + _next.Dispose(); + } base.Dispose(disposing); } @@ -116,7 +125,7 @@ namespace Discord.Audio.Streams timestamp += OpusEncoder.FrameSamplesPerChannel; } #if DEBUG - var _ = _logger?.DebugAsync("Buffer underrun"); + var _ = _logger?.DebugAsync("Buffer under run"); #endif } } @@ -131,8 +140,12 @@ namespace Discord.Audio.Streams public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) { + CancellationTokenSource writeCancelToken = null; if (cancelToken.CanBeCanceled) - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; + { + writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken); + cancelToken = writeCancelToken.Token; + } else cancelToken = _cancelToken; @@ -142,6 +155,9 @@ namespace Discord.Audio.Streams #if DEBUG var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock #endif +#pragma warning disable IDISP016 + writeCancelToken?.Dispose(); +#pragma warning restore IDISP016 return; } Buffer.BlockCopy(data, offset, buffer, 0, count); @@ -153,6 +169,7 @@ namespace Discord.Audio.Streams #endif _isPreloaded = true; } + writeCancelToken?.Dispose(); } public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index b9d6157ea..6233c47b5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -96,7 +96,17 @@ namespace Discord.Audio.Streams protected override void Dispose(bool isDisposing) { - _isDisposed = true; + if (!_isDisposed) + { + if (isDisposing) + { + _signal?.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(isDisposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 58c4f4c70..ad1c285e8 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -22,19 +22,22 @@ namespace Discord.Audio.Streams _decoder = new OpusDecoder(); } + /// Header received with no payload. public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) - throw new InvalidOperationException("Header received with no payload"); + throw new InvalidOperationException("Header received with no payload."); _hasHeader = true; _nextMissed = missed; _next.WriteHeader(seq, timestamp, missed); } + + /// Received payload without an RTP header. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { if (!_hasHeader) - throw new InvalidOperationException("Received payload without an RTP header"); + throw new InvalidOperationException("Received payload without an RTP header."); _hasHeader = false; if (!_nextMissed) @@ -65,10 +68,12 @@ namespace Discord.Audio.Streams protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (disposing) + { _decoder.Dispose(); + _next.Dispose(); + } + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index f5883ad4b..05d12b490 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -25,7 +25,7 @@ namespace Discord.Audio.Streams public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { - //Assume threadsafe + //Assume thread-safe while (count > 0) { if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) @@ -89,10 +89,12 @@ namespace Discord.Audio.Streams protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (disposing) + { _encoder.Dispose(); + _next.Dispose(); + } + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 2cedea114..1002502b6 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -20,6 +21,8 @@ namespace Discord.Audio.Streams _nonce = new byte[24]; } + /// The token has had cancellation requested. + /// The associated has been disposed. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); @@ -73,5 +76,12 @@ namespace Discord.Audio.Streams (buffer[extensionOffset + 3]); return extensionOffset + 4 + (extensionLength * 4); } + + protected override void Dispose(bool disposing) + { + if (disposing) + _next.Dispose(); + base.Dispose(disposing); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index ce407eada..7ecb56bee 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -69,5 +69,12 @@ namespace Discord.Audio.Streams { await _next.ClearAsync(cancelToken).ConfigureAwait(false); } + + protected override void Dispose(bool disposing) + { + if (disposing) + _next.Dispose(); + base.Dispose(disposing); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs index 9ed849a5e..40cd6864e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -4,7 +4,9 @@ using System.Threading.Tasks; namespace Discord.Audio.Streams { - /// Decrypts an RTP frame using libsodium + /// + /// Decrypts an RTP frame using libsodium. + /// public class SodiumDecryptStream : AudioOutStream { private readonly AudioClient _client; @@ -42,5 +44,12 @@ namespace Discord.Audio.Streams { await _next.ClearAsync(cancelToken).ConfigureAwait(false); } + + protected override void Dispose(bool disposing) + { + if (disposing) + _next.Dispose(); + base.Dispose(disposing); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index bacc9be47..fa1d34de5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -4,7 +4,9 @@ using System.Threading.Tasks; namespace Discord.Audio.Streams { - /// Encrypts an RTP frame using libsodium + /// + /// Encrypts an RTP frame using libsodium. + /// public class SodiumEncryptStream : AudioOutStream { private readonly AudioClient _client; @@ -20,21 +22,25 @@ namespace Discord.Audio.Streams _client = (AudioClient)client; _nonce = new byte[24]; } - + + /// Header received with no payload. public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) - throw new InvalidOperationException("Header received with no payload"); + throw new InvalidOperationException("Header received with no payload."); _nextSeq = seq; _nextTimestamp = timestamp; _hasHeader = true; } + /// Received payload without an RTP header. + /// The token has had cancellation requested. + /// The associated has been disposed. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); if (!_hasHeader) - throw new InvalidOperationException("Received payload without an RTP header"); + throw new InvalidOperationException("Received payload without an RTP header."); _hasHeader = false; if (_client.SecretKey == null) @@ -54,5 +60,12 @@ namespace Discord.Audio.Streams { await _next.ClearAsync(cancelToken).ConfigureAwait(false); } + + protected override void Dispose(bool disposing) + { + if (disposing) + _next.Dispose(); + base.Dispose(disposing); + } } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index c236b1045..91fb24021 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -1,166 +1,466 @@ +using Discord.Rest; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord.WebSocket { public partial class BaseSocketClient { - //Channels + #region Channels /// Fired when a channel is created. - public event Func ChannelCreated + /// + /// + /// This event is fired when a generic channel has been created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The newly created channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// + /// + /// + /// + public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } remove { _channelCreatedEvent.Remove(value); } } internal readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); /// Fired when a channel is destroyed. - public event Func ChannelDestroyed { + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The destroyed channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// + /// + /// + /// + public event Func ChannelDestroyed + { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } internal readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); /// Fired when a channel is updated. - public event Func ChannelUpdated { + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// and accept 2 as its parameters. + /// + /// + /// The original (prior to update) channel is passed into the first , while + /// the updated channel is passed into the second. The given channel type may include, but not limited + /// to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); see the derived classes of + /// for more details. + /// + /// + /// + /// + /// + public event Func ChannelUpdated + { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } - } + } internal readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + #endregion - //Messages + #region Messages /// Fired when a message is received. - public event Func MessageReceived { + /// + /// + /// This event is fired when a message is received. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The message that is sent to the client is passed into the event handler parameter as + /// . This message may be a system message (i.e. + /// ) or a user message (i.e. . See the + /// derived classes of for more details. + /// + /// + /// + /// The example below checks if the newly received message contains the target user. + /// + /// + public event Func MessageReceived + { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } internal readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); /// Fired when a message is deleted. - public event Func, ISocketMessageChannel, Task> MessageDeleted { + /// + /// + /// This event is fired when a message is deleted. The event handler must return a + /// and accept a and + /// as its parameters. + /// + /// + /// + /// It is not possible to retrieve the message via + /// ; the message cannot be retrieved by Discord + /// after the message has been deleted. + /// + /// If caching is enabled via , the + /// entity will contain the deleted message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the removed message will be passed into the + /// parameter. + /// + /// + /// + /// + /// + + public event Func, Cacheable, Task> MessageDeleted + { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + internal readonly AsyncEvent, Cacheable, Task>> _messageDeletedEvent = new AsyncEvent, Cacheable, Task>>(); + /// Fired when multiple messages are bulk deleted. + /// + /// + /// The event will not be fired for individual messages contained in this event. + /// + /// + /// This event is fired when multiple messages are bulk deleted. The event handler must return a + /// and accept an and + /// as its parameters. + /// + /// + /// + /// It is not possible to retrieve the message via + /// ; the message cannot be retrieved by Discord + /// after the message has been deleted. + /// + /// If caching is enabled via , the + /// entity will contain the deleted message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the removed message will be passed into the + /// parameter. + /// + /// + public event Func>, Cacheable, Task> MessagesBulkDeleted + { + add { _messagesBulkDeletedEvent.Add(value); } + remove { _messagesBulkDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent>, Cacheable, Task>> _messagesBulkDeletedEvent = new AsyncEvent>, Cacheable, Task>>(); /// Fired when a message is updated. - public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated { + /// + /// + /// This event is fired when a message is updated. The event handler must return a + /// and accept a , , + /// and as its parameters. + /// + /// + /// If caching is enabled via , the + /// entity will contain the original message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The updated message will be passed into the parameter. + /// + /// + /// The source channel of the updated message will be passed into the + /// parameter. + /// + /// + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated + { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } internal readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); /// Fired when a reaction is added to a message. - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded { + /// + /// + /// This event is fired when a reaction is added to a user message. The event handler must return a + /// and accept a , an + /// , and a as its parameter. + /// + /// + /// If caching is enabled via , the + /// entity will contain the original message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the reaction addition will be passed into the + /// parameter. + /// + /// + /// The reaction that was added will be passed into the parameter. + /// + /// + /// When fetching the reaction from this event, a user may not be provided under + /// . Please see the documentation of the property for more + /// information. + /// + /// + /// + /// + /// + public event Func, Cacheable, SocketReaction, Task> ReactionAdded + { add { _reactionAddedEvent.Add(value); } remove { _reactionAddedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + internal readonly AsyncEvent, Cacheable, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, Cacheable, SocketReaction, Task>>(); /// Fired when a reaction is removed from a message. - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved { + public event Func, Cacheable, SocketReaction, Task> ReactionRemoved + { add { _reactionRemovedEvent.Add(value); } remove { _reactionRemovedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + internal readonly AsyncEvent, Cacheable, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, Cacheable, SocketReaction, Task>>(); /// Fired when all reactions to a message are cleared. - public event Func, ISocketMessageChannel, Task> ReactionsCleared { + public event Func, Cacheable, Task> ReactionsCleared + { add { _reactionsClearedEvent.Add(value); } remove { _reactionsClearedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + internal readonly AsyncEvent, Cacheable, Task>> _reactionsClearedEvent = new AsyncEvent, Cacheable, Task>>(); + /// + /// Fired when all reactions to a message with a specific emote are removed. + /// + /// + /// + /// This event is fired when all reactions to a message with a specific emote are removed. + /// The event handler must return a and accept a and + /// a as its parameters. + /// + /// + /// The channel where this message was sent will be passed into the parameter. + /// + /// + /// The emoji that all reactions had and were removed will be passed into the parameter. + /// + /// + public event Func, Cacheable, IEmote, Task> ReactionsRemovedForEmote + { + add { _reactionsRemovedForEmoteEvent.Add(value); } + remove { _reactionsRemovedForEmoteEvent.Remove(value); } + } + internal readonly AsyncEvent, Cacheable, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent, Cacheable, IEmote, Task>>(); + #endregion - //Roles + #region Roles /// Fired when a role is created. - public event Func RoleCreated { + public event Func RoleCreated + { add { _roleCreatedEvent.Add(value); } remove { _roleCreatedEvent.Remove(value); } } internal readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); /// Fired when a role is deleted. - public event Func RoleDeleted { + public event Func RoleDeleted + { add { _roleDeletedEvent.Add(value); } remove { _roleDeletedEvent.Remove(value); } } internal readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); /// Fired when a role is updated. - public event Func RoleUpdated { + public event Func RoleUpdated + { add { _roleUpdatedEvent.Add(value); } remove { _roleUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + #endregion - //Guilds + #region Guilds /// Fired when the connected account joins a guild. - public event Func JoinedGuild { + public event Func JoinedGuild + { add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } internal readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); /// Fired when the connected account leaves a guild. - public event Func LeftGuild { + public event Func LeftGuild + { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } internal readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); /// Fired when a guild becomes available. - public event Func GuildAvailable { + public event Func GuildAvailable + { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } internal readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); /// Fired when a guild becomes unavailable. - public event Func GuildUnavailable { + public event Func GuildUnavailable + { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } internal readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); /// Fired when offline guild members are downloaded. - public event Func GuildMembersDownloaded { + public event Func GuildMembersDownloaded + { add { _guildMembersDownloadedEvent.Add(value); } remove { _guildMembersDownloadedEvent.Remove(value); } } internal readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); /// Fired when a guild is updated. - public event Func GuildUpdated { + public event Func GuildUpdated + { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + /// Fired when a user leaves without agreeing to the member screening + public event Func, SocketGuild, Task> GuildJoinRequestDeleted + { + add { _guildJoinRequestDeletedEvent.Add(value); } + remove { _guildJoinRequestDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent, SocketGuild, Task>> _guildJoinRequestDeletedEvent = new AsyncEvent, SocketGuild, Task>>(); + #endregion + + #region Guild Events + + /// + /// Fired when a guild event is created. + /// + public event Func GuildScheduledEventCreated + { + add { _guildScheduledEventCreated.Add(value); } + remove { _guildScheduledEventCreated.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCreated = new AsyncEvent>(); + + /// + /// Fired when a guild event is updated. + /// + public event Func, SocketGuildEvent, Task> GuildScheduledEventUpdated + { + add { _guildScheduledEventUpdated.Add(value); } + remove { _guildScheduledEventUpdated.Remove(value); } + } + internal readonly AsyncEvent, SocketGuildEvent, Task>> _guildScheduledEventUpdated = new AsyncEvent, SocketGuildEvent, Task>>(); - //Users + + /// + /// Fired when a guild event is cancelled. + /// + public event Func GuildScheduledEventCancelled + { + add { _guildScheduledEventCancelled.Add(value); } + remove { _guildScheduledEventCancelled.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCancelled = new AsyncEvent>(); + + /// + /// Fired when a guild event is completed. + /// + public event Func GuildScheduledEventCompleted + { + add { _guildScheduledEventCompleted.Add(value); } + remove { _guildScheduledEventCompleted.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventCompleted = new AsyncEvent>(); + + /// + /// Fired when a guild event is started. + /// + public event Func GuildScheduledEventStarted + { + add { _guildScheduledEventStarted.Add(value); } + remove { _guildScheduledEventStarted.Remove(value); } + } + internal readonly AsyncEvent> _guildScheduledEventStarted = new AsyncEvent>(); + + public event Func, SocketGuildEvent, Task> GuildScheduledEventUserAdd + { + add { _guildScheduledEventUserAdd.Add(value); } + remove { _guildScheduledEventUserAdd.Remove(value); } + } + internal readonly AsyncEvent, SocketGuildEvent, Task>> _guildScheduledEventUserAdd = new AsyncEvent, SocketGuildEvent, Task>>(); + + public event Func, SocketGuildEvent, Task> GuildScheduledEventUserRemove + { + add { _guildScheduledEventUserRemove.Add(value); } + remove { _guildScheduledEventUserRemove.Remove(value); } + } + internal readonly AsyncEvent, SocketGuildEvent, Task>> _guildScheduledEventUserRemove = new AsyncEvent, SocketGuildEvent, Task>>(); + + + #endregion + + #region Users /// Fired when a user joins a guild. - public event Func UserJoined { + public event Func UserJoined + { add { _userJoinedEvent.Add(value); } remove { _userJoinedEvent.Remove(value); } } internal readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); /// Fired when a user leaves a guild. - public event Func UserLeft { + public event Func UserLeft + { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); /// Fired when a user is banned from a guild. - public event Func UserBanned { + public event Func UserBanned + { add { _userBannedEvent.Add(value); } remove { _userBannedEvent.Remove(value); } } internal readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); /// Fired when a user is unbanned from a guild. - public event Func UserUnbanned { + public event Func UserUnbanned + { add { _userUnbannedEvent.Add(value); } remove { _userUnbannedEvent.Remove(value); } } internal readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); /// Fired when a user is updated. - public event Func UserUpdated { + public event Func UserUpdated + { add { _userUpdatedEvent.Add(value); } remove { _userUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); /// Fired when a guild member is updated, or a member presence is updated. - public event Func GuildMemberUpdated { + public event Func, SocketGuildUser, Task> GuildMemberUpdated + { add { _guildMemberUpdatedEvent.Add(value); } remove { _guildMemberUpdatedEvent.Remove(value); } } - internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); + internal readonly AsyncEvent, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent, SocketGuildUser, Task>>(); /// Fired when a user joins, leaves, or moves voice channels. - public event Func UserVoiceStateUpdated { + public event Func UserVoiceStateUpdated + { add { _userVoiceStateUpdatedEvent.Add(value); } remove { _userVoiceStateUpdatedEvent.Remove(value); } } @@ -168,33 +468,379 @@ namespace Discord.WebSocket /// Fired when the bot connects to a Discord voice server. public event Func VoiceServerUpdated { - add { _voiceServerUpdatedEvent.Add(value); } + add { _voiceServerUpdatedEvent.Add(value); } remove { _voiceServerUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _voiceServerUpdatedEvent = new AsyncEvent>(); /// Fired when the connected account is updated. - public event Func CurrentUserUpdated { + public event Func CurrentUserUpdated + { add { _selfUpdatedEvent.Add(value); } remove { _selfUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); /// Fired when a user starts typing. - public event Func UserIsTyping { + public event Func, Cacheable, Task> UserIsTyping + { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } - internal readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + internal readonly AsyncEvent, Cacheable, Task>> _userIsTypingEvent = new AsyncEvent, Cacheable, Task>>(); /// Fired when a user joins a group channel. - public event Func RecipientAdded { + public event Func RecipientAdded + { add { _recipientAddedEvent.Add(value); } remove { _recipientAddedEvent.Remove(value); } } internal readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); /// Fired when a user is removed from a group channel. - public event Func RecipientRemoved { + public event Func RecipientRemoved + { add { _recipientRemovedEvent.Add(value); } remove { _recipientRemovedEvent.Remove(value); } } internal readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + #endregion + + #region Presence + + /// Fired when a users presence is updated. + public event Func PresenceUpdated + { + add { _presenceUpdated.Add(value); } + remove { _presenceUpdated.Remove(value); } + } + internal readonly AsyncEvent> _presenceUpdated = new AsyncEvent>(); + + #endregion + + #region Invites + /// + /// Fired when an invite is created. + /// + /// + /// + /// This event is fired when an invite is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The invite created will be passed into the parameter. + /// + /// + public event Func InviteCreated + { + add { _inviteCreatedEvent.Add(value); } + remove { _inviteCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _inviteCreatedEvent = new AsyncEvent>(); + /// + /// Fired when an invite is deleted. + /// + /// + /// + /// This event is fired when an invite is deleted. The event handler must return + /// a and accept a and + /// as its parameter. + /// + /// + /// The channel where this invite was created will be passed into the parameter. + /// + /// + /// The code of the deleted invite will be passed into the parameter. + /// + /// + public event Func InviteDeleted + { + add { _inviteDeletedEvent.Add(value); } + remove { _inviteDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent> _inviteDeletedEvent = new AsyncEvent>(); + #endregion + + #region Interactions + /// + /// Fired when an Interaction is created. This event covers all types of interactions including but not limited to: buttons, select menus, slash commands, autocompletes. + /// + /// + /// + /// This event is fired when an interaction is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The interaction created will be passed into the parameter. + /// + /// + public event Func InteractionCreated + { + add { _interactionCreatedEvent.Add(value); } + remove { _interactionCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _interactionCreatedEvent = new AsyncEvent>(); + + /// + /// Fired when a button is clicked and its interaction is received. + /// + public event Func ButtonExecuted + { + add => _buttonExecuted.Add(value); + remove => _buttonExecuted.Remove(value); + } + internal readonly AsyncEvent> _buttonExecuted = new AsyncEvent>(); + + /// + /// Fired when a select menu is used and its interaction is received. + /// + public event Func SelectMenuExecuted + { + add => _selectMenuExecuted.Add(value); + remove => _selectMenuExecuted.Remove(value); + } + internal readonly AsyncEvent> _selectMenuExecuted = new AsyncEvent>(); + /// + /// Fired when a slash command is used and its interaction is received. + /// + public event Func SlashCommandExecuted + { + add => _slashCommandExecuted.Add(value); + remove => _slashCommandExecuted.Remove(value); + } + internal readonly AsyncEvent> _slashCommandExecuted = new AsyncEvent>(); + + /// + /// Fired when a user command is used and its interaction is received. + /// + public event Func UserCommandExecuted + { + add => _userCommandExecuted.Add(value); + remove => _userCommandExecuted.Remove(value); + } + internal readonly AsyncEvent> _userCommandExecuted = new AsyncEvent>(); + + /// + /// Fired when a message command is used and its interaction is received. + /// + public event Func MessageCommandExecuted + { + add => _messageCommandExecuted.Add(value); + remove => _messageCommandExecuted.Remove(value); + } + internal readonly AsyncEvent> _messageCommandExecuted = new AsyncEvent>(); + /// + /// Fired when an autocomplete is used and its interaction is received. + /// + public event Func AutocompleteExecuted + { + add => _autocompleteExecuted.Add(value); + remove => _autocompleteExecuted.Remove(value); + } + internal readonly AsyncEvent> _autocompleteExecuted = new AsyncEvent>(); + + /// + /// Fired when a guild application command is created. + /// + /// + /// + /// This event is fired when an application command is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandCreated + { + add { _applicationCommandCreated.Add(value); } + remove { _applicationCommandCreated.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandCreated = new AsyncEvent>(); + + /// + /// Fired when a guild application command is updated. + /// + /// + /// + /// This event is fired when an application command is updated. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandUpdated + { + add { _applicationCommandUpdated.Add(value); } + remove { _applicationCommandUpdated.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandUpdated = new AsyncEvent>(); + + /// + /// Fired when a guild application command is deleted. + /// + /// + /// + /// This event is fired when an application command is deleted. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The command that was deleted will be passed into the parameter. + /// + /// + /// This event is an undocumented discord event and may break at any time, its not recommended to rely on this event + /// + /// + public event Func ApplicationCommandDeleted + { + add { _applicationCommandDeleted.Add(value); } + remove { _applicationCommandDeleted.Remove(value); } + } + internal readonly AsyncEvent> _applicationCommandDeleted = new AsyncEvent>(); + + /// + /// Fired when a thread is created within a guild, or when the current user is added to a thread. + /// + public event Func ThreadCreated + { + add { _threadCreated.Add(value); } + remove { _threadCreated.Remove(value); } + } + internal readonly AsyncEvent> _threadCreated = new AsyncEvent>(); + + /// + /// Fired when a thread is updated within a guild. + /// + public event Func, SocketThreadChannel, Task> ThreadUpdated + { + add { _threadUpdated.Add(value); } + remove { _threadUpdated.Remove(value); } + } + + internal readonly AsyncEvent, SocketThreadChannel, Task>> _threadUpdated = new(); + + /// + /// Fired when a thread is deleted. + /// + public event Func, Task> ThreadDeleted + { + add { _threadDeleted.Add(value); } + remove { _threadDeleted.Remove(value); } + } + internal readonly AsyncEvent, Task>> _threadDeleted = new AsyncEvent, Task>>(); + + /// + /// Fired when a user joins a thread + /// + public event Func ThreadMemberJoined + { + add { _threadMemberJoined.Add(value); } + remove { _threadMemberJoined.Remove(value); } + } + internal readonly AsyncEvent> _threadMemberJoined = new AsyncEvent>(); + + /// + /// Fired when a user leaves a thread + /// + public event Func ThreadMemberLeft + { + add { _threadMemberLeft.Add(value); } + remove { _threadMemberLeft.Remove(value); } + } + internal readonly AsyncEvent> _threadMemberLeft = new AsyncEvent>(); + + /// + /// Fired when a stage is started. + /// + public event Func StageStarted + { + add { _stageStarted.Add(value); } + remove { _stageStarted.Remove(value); } + } + internal readonly AsyncEvent> _stageStarted = new AsyncEvent>(); + + /// + /// Fired when a stage ends. + /// + public event Func StageEnded + { + add { _stageEnded.Add(value); } + remove { _stageEnded.Remove(value); } + } + internal readonly AsyncEvent> _stageEnded = new AsyncEvent>(); + + /// + /// Fired when a stage is updated. + /// + public event Func StageUpdated + { + add { _stageUpdated.Add(value); } + remove { _stageUpdated.Remove(value); } + } + internal readonly AsyncEvent> _stageUpdated = new AsyncEvent>(); + + /// + /// Fired when a user requests to speak within a stage channel. + /// + public event Func RequestToSpeak + { + add { _requestToSpeak.Add(value); } + remove { _requestToSpeak.Remove(value); } + } + internal readonly AsyncEvent> _requestToSpeak = new AsyncEvent>(); + + /// + /// Fired when a speaker is added in a stage channel. + /// + public event Func SpeakerAdded + { + add { _speakerAdded.Add(value); } + remove { _speakerAdded.Remove(value); } + } + internal readonly AsyncEvent> _speakerAdded = new AsyncEvent>(); + + /// + /// Fired when a speaker is removed from a stage channel. + /// + public event Func SpeakerRemoved + { + add { _speakerRemoved.Add(value); } + remove { _speakerRemoved.Remove(value); } + } + internal readonly AsyncEvent> _speakerRemoved = new AsyncEvent>(); + + /// + /// Fired when a sticker in a guild is created. + /// + public event Func GuildStickerCreated + { + add { _guildStickerCreated.Add(value); } + remove { _guildStickerCreated.Remove(value); } + } + internal readonly AsyncEvent> _guildStickerCreated = new AsyncEvent>(); + + /// + /// Fired when a sticker in a guild is updated. + /// + public event Func GuildStickerUpdated + { + add { _guildStickerUpdated.Add(value); } + remove { _guildStickerUpdated.Remove(value); } + } + internal readonly AsyncEvent> _guildStickerUpdated = new AsyncEvent>(); + + /// + /// Fired when a sticker in a guild is deleted. + /// + public event Func GuildStickerDeleted + { + add { _guildStickerDeleted.Add(value); } + remove { _guildStickerDeleted.Remove(value); } + } + internal readonly AsyncEvent> _guildStickerDeleted = new AsyncEvent>(); + #endregion } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index a7d590b42..9e25ab382 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -6,89 +7,332 @@ using Discord.Rest; namespace Discord.WebSocket { + /// + /// Represents the base of a WebSocket-based Discord client. + /// public abstract partial class BaseSocketClient : BaseDiscordClient, IDiscordClient { - protected readonly DiscordSocketConfig _baseconfig; + #region BaseSocketClient + protected readonly DiscordSocketConfig BaseConfig; - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// + /// + /// An that represents the round-trip latency to the WebSocket server. Please + /// note that this value does not represent a "true" latency for operations such as sending a message. + /// public abstract int Latency { get; protected set; } - public abstract UserStatus Status { get; protected set; } + /// + /// Gets the status for the logged-in user. + /// + /// + /// A status object that represents the user's online presence status. + /// + public abstract UserStatus Status { get; protected set; } + /// + /// Gets the activity for the logged-in user. + /// + /// + /// An activity object that represents the user's current activity. + /// public abstract IActivity Activity { get; protected set; } + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public abstract DiscordSocketRestClient Rest { get; } + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + /// + /// Gets a collection of default stickers. + /// + public abstract IReadOnlyCollection> DefaultStickerPacks { get; } + /// + /// Gets the current logged-in user. + /// + public virtual new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + /// + /// Gets a collection of guilds that the user is currently in. + /// + /// + /// A read-only collection of guilds that the current user is in. + /// public abstract IReadOnlyCollection Guilds { get; } + /// + /// Gets a collection of private channels opened in this session. + /// + /// + /// This method will retrieve all private channels (including direct-message, group channel and such) that + /// are currently opened in this session. + /// + /// This method will not return previously opened private channels outside of the current session! If + /// you have just started the client, this may return an empty collection. + /// + /// + /// + /// A read-only collection of private channels that the user currently partakes in. + /// public abstract IReadOnlyCollection PrivateChannels { get; } - public abstract IReadOnlyCollection VoiceRegions { get; } internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) - : base(config, client) => _baseconfig = config; + : base(config, client) => BaseConfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); + => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, + useSystemClock: config.UseSystemClock); - /// + /// + /// Gets a Discord application information for the logged-in user. + /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application + /// information. + /// public abstract Task GetApplicationInfoAsync(RequestOptions options = null); - /// + /// + /// Gets a generic user. + /// + /// The user snowflake ID. + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return null due to Discord not sending offline users in large guilds + /// (i.e. guild with 100+ members) actively. To download users on startup and to see more information + /// about this subject, see . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do + /// not have access to, consider using the REST implementation of + /// . + /// + /// + /// + /// A generic WebSocket-based user; null when the user cannot be found. + /// public abstract SocketUser GetUser(ulong id); - /// + + /// + /// Gets a user. + /// + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return null due to Discord not sending offline users in large guilds + /// (i.e. guild with 100+ members) actively. To download users on startup and to see more information + /// about this subject, see . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do + /// not have access to, consider using the REST implementation of + /// . + /// + /// + /// The name of the user. + /// The discriminator value of the user. + /// + /// A generic WebSocket-based user; null when the user cannot be found. + /// public abstract SocketUser GetUser(string username, string discriminator); - /// + /// + /// Gets a channel. + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// + /// A generic WebSocket-based channel object (voice, text, category, etc.) associated with the identifier; + /// null when the channel cannot be found. + /// public abstract SocketChannel GetChannel(ulong id); - /// + /// + /// Gets a guild. + /// + /// The guild snowflake identifier. + /// + /// A WebSocket-based guild associated with the snowflake identifier; null when the guild cannot be + /// found. + /// public abstract SocketGuild GetGuild(ulong id); - /// - public abstract RestVoiceRegion GetVoiceRegion(string id); + /// + /// Gets all voice regions. + /// + /// The options to be used when sending the request. + /// + /// A task that contains a read-only collection of REST-based voice regions. + /// + public abstract ValueTask> GetVoiceRegionsAsync(RequestOptions options = null); + /// + /// Gets a voice region. + /// + /// The identifier of the voice region (e.g. eu-central ). + /// The options to be used when sending the request. + /// + /// A task that contains a REST-based voice region associated with the identifier; null if the + /// voice region is not found. + /// + public abstract ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null); /// public abstract Task StartAsync(); /// public abstract Task StopAsync(); + /// + /// Sets the current status of the user (e.g. Online, Do not Disturb). + /// + /// The new status to be set. + /// + /// A task that represents the asynchronous set operation. + /// public abstract Task SetStatusAsync(UserStatus status); + /// + /// Sets the game of the user. + /// + /// The name of the game. + /// If streaming, the URL of the stream. Must be a valid Twitch URL. + /// The type of the game. + /// + /// + /// Bot accounts cannot set as their activity + /// type and it will have no effect. + /// + /// + /// + /// A task that represents the asynchronous set operation. + /// public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); + /// + /// Sets the of the logged-in user. + /// + /// + /// This method sets the of the user. + /// + /// Discord will only accept setting of name and the type of activity. + /// + /// + /// Bot accounts cannot set as their activity + /// type and it will have no effect. + /// + /// + /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC + /// clients only. + /// + /// + /// The activity to be set. + /// + /// A task that represents the asynchronous set operation. + /// public abstract Task SetActivityAsync(IActivity activity); - public abstract Task DownloadUsersAsync(IEnumerable guilds); + /// + /// Attempts to download users into the user cache for the selected guilds. + /// + /// The guilds to download the members from. + /// + /// A task that represents the asynchronous download operation. + /// + public abstract Task DownloadUsersAsync(IEnumerable guilds); - /// + /// + /// Creates a guild for the logged-in user who is in less than 10 active guilds. + /// + /// + /// This method creates a new guild on behalf of the logged-in user. + /// + /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. + /// + /// + /// The name of the new guild. + /// The voice region to create the guild with. + /// The icon of the guild. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created guild. + /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default); - /// + /// + /// Gets the connections that the user has set up. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of connections. + /// public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); - /// + /// + /// Gets an invite. + /// + /// The invitation identifier. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the invite information. + /// public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); - - // IDiscordClient + /// + /// Gets a sticker. + /// + /// Whether or not to allow downloading from the api. + /// The id of the sticker to get. + /// The options to be used when sending the request. + /// + /// A if found, otherwise . + /// + public abstract Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); +#endregion + + #region IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); + /// + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + { + return await GetVoiceRegionAsync(id).ConfigureAwait(false); + } + /// + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + { + return await GetVoiceRegionsAsync().ConfigureAwait(false); + } + #endregion } } diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index a119e5e28..7129feb48 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -16,12 +16,14 @@ namespace Discord.WebSocket private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; private readonly ConcurrentHashSet _groupChannels; + private readonly ConcurrentDictionary _commands; internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal IReadOnlyCollection GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal IReadOnlyCollection Commands => _commands.ToReadOnlyCollection(); internal IReadOnlyCollection PrivateChannels => _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( @@ -37,6 +39,7 @@ namespace Discord.WebSocket _guilds = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); + _commands = new ConcurrentDictionary(); } internal SocketChannel GetChannel(ulong id) @@ -74,7 +77,7 @@ namespace Discord.WebSocket case SocketDMChannel dmChannel: _dmChannels.TryRemove(dmChannel.Recipient.Id, out _); break; - case SocketGroupChannel groupChannel: + case SocketGroupChannel _: _groupChannels.TryRemove(id); break; } @@ -82,6 +85,20 @@ namespace Discord.WebSocket } return null; } + internal void PurgeAllChannels() + { + foreach (var guild in _guilds.Values) + guild.PurgeChannelCache(this); + + PurgeDMChannels(); + } + internal void PurgeDMChannels() + { + foreach (var channel in _dmChannels.Values) + _channels.TryRemove(channel.Id, out _); + + _dmChannels.Clear(); + } internal SocketGuild GetGuild(ulong id) { @@ -96,7 +113,11 @@ namespace Discord.WebSocket internal SocketGuild RemoveGuild(ulong id) { if (_guilds.TryRemove(id, out SocketGuild guild)) + { + guild.PurgeChannelCache(this); + guild.PurgeGuildUserCache(); return guild; + } return null; } @@ -116,5 +137,38 @@ namespace Discord.WebSocket return user; return null; } + internal void PurgeUsers() + { + foreach (var guild in _guilds.Values) + guild.PurgeGuildUserCache(); + } + + internal SocketApplicationCommand GetCommand(ulong id) + { + if (_commands.TryGetValue(id, out SocketApplicationCommand command)) + return command; + return null; + } + internal void AddCommand(SocketApplicationCommand command) + { + _commands[command.Id] = command; + } + internal SocketApplicationCommand GetOrAddCommand(ulong id, Func commandFactory) + { + return _commands.GetOrAdd(id, commandFactory); + } + internal SocketApplicationCommand RemoveCommand(ulong id) + { + if (_commands.TryRemove(id, out SocketApplicationCommand command)) + return command; + return null; + } + internal void PurgeCommands(Func precondition) + { + var ids = _commands.Where(x => precondition(x.Value)).Select(x => x.Key); + + foreach (var id in ids) + _commands.TryRemove(id, out var _); + } } } diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index a29c9bb70..905cd01a1 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -1,9 +1,12 @@ -using Discord.WebSocket; +using Discord.WebSocket; namespace Discord.Commands { + /// The sharded variant of , which may contain the client, user, guild, channel, and message. public class ShardedCommandContext : SocketCommandContext, ICommandContext { + #region ShardedCommandContext + /// Gets the that the command is executed with. public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) @@ -12,10 +15,14 @@ namespace Discord.Commands Client = client; } + /// Gets the shard ID of the command context. private static int GetShardId(DiscordShardedClient client, IGuild guild) => guild == null ? 0 : client.GetShardIdFor(guild); +#endregion - //ICommandContext + #region ICommandContext + /// IDiscordClient ICommandContext.Client => Client; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index c8b0747e7..d7180873b 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -1,17 +1,44 @@ -using Discord.WebSocket; +using Discord.WebSocket; namespace Discord.Commands { + /// + /// Represents a WebSocket-based context of a command. This may include the client, guild, channel, user, and message. + /// public class SocketCommandContext : ICommandContext { + #region SocketCommandContext + /// + /// Gets the that the command is executed with. + /// public DiscordSocketClient Client { get; } + /// + /// Gets the that the command is executed in. + /// public SocketGuild Guild { get; } + /// + /// Gets the that the command is executed in. + /// public ISocketMessageChannel Channel { get; } + /// + /// Gets the who executed the command. + /// public SocketUser User { get; } + /// + /// Gets the that the command is interpreted from. + /// public SocketUserMessage Message { get; } + /// + /// Indicates whether the channel that the command is executed in is a private channel. + /// public bool IsPrivate => Channel is IPrivateChannel; + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) { Client = client; @@ -20,12 +47,19 @@ namespace Discord.Commands User = msg.Author; Message = msg; } +#endregion - //ICommandContext + #region ICommandContext + /// IDiscordClient ICommandContext.Client => Client; + /// IGuild ICommandContext.Guild => Guild; + /// IMessageChannel ICommandContext.Channel => Channel; + /// IUser ICommandContext.User => User; + /// IUserMessage ICommandContext.Message => Message; + #endregion } } diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index decae4163..f304d4ea8 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -6,7 +6,7 @@ using Discord.Net; namespace Discord { - internal class ConnectionManager + internal class ConnectionManager : IDisposable { public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); @@ -23,10 +23,12 @@ namespace Discord private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; private Task _task; + private bool _isDisposed; + public ConnectionState State { get; private set; } public CancellationToken CancelToken { get; private set; } - internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, + internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) { _stateLock = stateLock; @@ -42,6 +44,8 @@ namespace Discord var ex2 = ex as WebSocketClosedException; if (ex2?.CloseCode == 4006) CriticalError(new Exception("WebSocket session expired", ex)); + else if (ex2?.CloseCode == 4014) + CriticalError(new Exception("WebSocket connection was closed", ex)); else Error(new Exception("WebSocket connection was closed", ex)); } @@ -53,8 +57,12 @@ namespace Discord public virtual async Task StartAsync() { + if (State != ConnectionState.Disconnected) + throw new InvalidOperationException("Cannot start an already running client."); + await AcquireConnectionLock().ConfigureAwait(false); var reconnectCancelToken = new CancellationTokenSource(); + _reconnectCancelToken?.Dispose(); _reconnectCancelToken = reconnectCancelToken; _task = Task.Run(async () => { @@ -67,16 +75,11 @@ namespace Discord try { await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); - nextReconnectDelay = 1000; //Reset delay + nextReconnectDelay = 1000; //Reset delay await _connectionPromise.Task.ConfigureAwait(false); } - catch (OperationCanceledException ex) - { - Cancel(); //In case this exception didn't come from another Error call - await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); - } - catch (Exception ex) - { + catch (Exception ex) + { Error(ex); //In case this exception didn't come from another Error call if (!reconnectCancelToken.IsCancellationRequested) { @@ -103,16 +106,16 @@ namespace Discord finally { _stateLock.Release(); } }); } - public virtual async Task StopAsync() + public virtual Task StopAsync() { Cancel(); - var task = _task; - if (task != null) - await task.ConfigureAwait(false); + return Task.CompletedTask; } private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) { + _connectionCancelToken?.Dispose(); + _combinedCancelToken?.Dispose(); _connectionCancelToken = new CancellationTokenSource(); _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); CancelToken = _combinedCancelToken.Token; @@ -120,7 +123,7 @@ namespace Discord _connectionPromise = new TaskCompletionSource(); State = ConnectionState.Connecting; await _logger.InfoAsync("Connecting").ConfigureAwait(false); - + try { var readyPromise = new TaskCompletionSource(); @@ -159,9 +162,9 @@ namespace Discord await _onDisconnecting(ex).ConfigureAwait(false); - await _logger.InfoAsync("Disconnected").ConfigureAwait(false); - State = ConnectionState.Disconnected; await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); + State = ConnectionState.Disconnected; + await _logger.InfoAsync("Disconnected").ConfigureAwait(false); } public async Task CompleteAsync() @@ -206,5 +209,25 @@ namespace Discord break; } } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _combinedCancelToken?.Dispose(); + _reconnectCancelToken?.Dispose(); + _connectionCancelToken?.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index ddd3b7954..4121e7d00 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,18 +1,16 @@ - + + Discord.Net.WebSocket Discord.WebSocket A core Discord.Net library containing the WebSocket client and models. - net46;netstandard1.3;netstandard2.0 - netstandard1.3;netstandard2.0 + net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 true - - - - + \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs index c9e679669..50230572c 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.WebSocket { public partial class DiscordShardedClient { - //General + #region General /// Fired when a shard is connected to the Discord gateway. public event Func ShardConnected { @@ -34,5 +34,6 @@ namespace Discord.WebSocket remove { _shardLatencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _shardLatencyUpdatedEvent = new AsyncEvent>(); + #endregion } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 881ce3909..1e71ce853 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -6,38 +6,72 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using System.Threading; +using System.Collections.Immutable; namespace Discord.WebSocket { public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient { + #region DiscordShardedClient private readonly DiscordSocketConfig _baseConfig; - private readonly SemaphoreSlim _connectionGroupLock; private readonly Dictionary _shardIdsToIndex; private readonly bool _automaticShards; private int[] _shardIds; private DiscordSocketClient[] _shards; + private ImmutableArray> _defaultStickers; private int _totalShards; - - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + private SemaphoreSlim[] _identifySemaphores; + private object _semaphoreResetLock; + private Task _semaphoreResetTask; + + private bool _isDisposed; + + /// public override int Latency { get => GetLatency(); protected set { } } + /// public override UserStatus Status { get => _shards[0].Status; protected set { } } + /// public override IActivity Activity { get => _shards[0].Activity; protected set { } } - internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + internal new DiscordSocketApiClient ApiClient + { + get + { + if (base.ApiClient.CurrentUserId == null) + base.ApiClient.CurrentUserId = CurrentUser?.Id; + + return base.ApiClient; + } + } + /// + public override IReadOnlyCollection> DefaultStickerPacks + => _defaultStickers.ToReadOnlyCollection(); + + /// public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(GetGuildCount); + /// public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount); public IReadOnlyCollection Shards => _shards; - public override IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; - /// Creates a new REST/WebSocket discord client. + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public override DiscordSocketRestClient Rest => _shards?[0].Rest; + + public override SocketSelfUser CurrentUser { get => _shards?.FirstOrDefault(x => x.CurrentUser != null)?.CurrentUser; protected set => throw new InvalidOperationException(); } + + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } - /// Creates a new REST/WebSocket discord client. +#pragma warning restore IDISP004 + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) : base(config, client) { @@ -46,10 +80,11 @@ namespace Discord.WebSocket if (ids != null && config.TotalShards == null) throw new ArgumentException($"Custom ids are not supported when {nameof(config.TotalShards)} is not specified."); + _semaphoreResetLock = new object(); _shardIdsToIndex = new Dictionary(); config.DisplayInitialLog = false; _baseConfig = config; - _connectionGroupLock = new SemaphoreSlim(1, 1); + _defaultStickers = ImmutableArray.Create>(); if (config.TotalShards == null) _automaticShards = true; @@ -58,12 +93,15 @@ namespace Discord.WebSocket _totalShards = config.TotalShards.Value; _shardIds = ids ?? Enumerable.Range(0, _totalShards).ToArray(); _shards = new DiscordSocketClient[_shardIds.Length]; + _identifySemaphores = new SemaphoreSlim[config.IdentifyMaxConcurrency]; + for (int i = 0; i < config.IdentifyMaxConcurrency; i++) + _identifySemaphores[i] = new SemaphoreSlim(1, 1); for (int i = 0; i < _shardIds.Length; i++) { _shardIdsToIndex.Add(_shardIds[i], i); var newConfig = config.Clone(); newConfig.ShardId = _shardIds[i]; - _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + _shards[i] = new DiscordSocketClient(newConfig, this, i != 0 ? _shards[0] : null); RegisterEvents(_shards[i], i == 0); } } @@ -71,32 +109,68 @@ namespace Discord.WebSocket private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); + internal async Task AcquireIdentifyLockAsync(int shardId, CancellationToken token) + { + int semaphoreIdx = shardId % _baseConfig.IdentifyMaxConcurrency; + await _identifySemaphores[semaphoreIdx].WaitAsync(token).ConfigureAwait(false); + } + + internal void ReleaseIdentifyLock() + { + lock (_semaphoreResetLock) + { + if (_semaphoreResetTask == null) + _semaphoreResetTask = ResetSemaphoresAsync(); + } + } + + private async Task ResetSemaphoresAsync() + { + await Task.Delay(5000).ConfigureAwait(false); + lock (_semaphoreResetLock) + { + foreach (var semaphore in _identifySemaphores) + if (semaphore.CurrentCount == 0) + semaphore.Release(); + _semaphoreResetTask = null; + } + } + internal override async Task OnLoginAsync(TokenType tokenType, string token) { if (_automaticShards) { - var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false); - _shardIds = Enumerable.Range(0, shardCount).ToArray(); + var botGateway = await GetBotGatewayAsync().ConfigureAwait(false); + _shardIds = Enumerable.Range(0, botGateway.Shards).ToArray(); _totalShards = _shardIds.Length; _shards = new DiscordSocketClient[_shardIds.Length]; + int maxConcurrency = botGateway.SessionStartLimit.MaxConcurrency; + _baseConfig.IdentifyMaxConcurrency = maxConcurrency; + _identifySemaphores = new SemaphoreSlim[maxConcurrency]; + for (int i = 0; i < maxConcurrency; i++) + _identifySemaphores[i] = new SemaphoreSlim(1, 1); for (int i = 0; i < _shardIds.Length; i++) { _shardIdsToIndex.Add(_shardIds[i], i); var newConfig = _baseConfig.Clone(); newConfig.ShardId = _shardIds[i]; newConfig.TotalShards = _totalShards; - _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + _shards[i] = new DiscordSocketClient(newConfig, this, i != 0 ? _shards[0] : null); RegisterEvents(_shards[i], i == 0); } } - //Assume threadsafe: already in a connection lock + //Assume thread safe: already in a connection lock for (int i = 0; i < _shards.Length; i++) - await _shards[i].LoginAsync(tokenType, token, false); + await _shards[i].LoginAsync(tokenType, token); + + if(_defaultStickers.Length == 0 && _baseConfig.AlwaysDownloadDefaultStickers) + await DownloadDefaultStickersAsync().ConfigureAwait(false); + } internal override async Task OnLogoutAsync() { - //Assume threadsafe: already in a connection lock + //Assume thread safe: already in a connection lock if (_shards != null) { for (int i = 0; i < _shards.Length; i++) @@ -114,10 +188,10 @@ namespace Discord.WebSocket } /// - public override async Task StartAsync() + public override async Task StartAsync() => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); /// - public override async Task StopAsync() + public override async Task StopAsync() => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); public DiscordSocketClient GetShard(int id) @@ -140,7 +214,7 @@ namespace Discord.WebSocket => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); /// - public override SocketGuild GetGuild(ulong id) + public override SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); /// @@ -168,7 +242,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].PrivateChannels.Count; return result; - } + } private IEnumerable GetGuilds() { @@ -184,7 +258,68 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].Guilds.Count; return result; - } + } + /// + public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var sticker = _defaultStickers.FirstOrDefault(x => x.Stickers.Any(y => y.Id == id))?.Stickers.FirstOrDefault(x => x.Id == id); + + if (sticker != null) + return sticker; + + foreach (var guild in Guilds) + { + sticker = await guild.GetStickerAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); + + if (sticker != null) + return sticker; + } + + if (mode == CacheMode.CacheOnly) + return null; + + var model = await ApiClient.GetStickerAsync(id, options).ConfigureAwait(false); + + if (model == null) + return null; + + + if (model.GuildId.IsSpecified) + { + var guild = GetGuild(model.GuildId.Value); + sticker = guild.AddOrUpdateSticker(model); + return sticker; + } + else + { + return SocketSticker.Create(_shards[0], model); + } + } + private async Task DownloadDefaultStickersAsync() + { + var models = await ApiClient.ListNitroStickerPacksAsync().ConfigureAwait(false); + + var builder = ImmutableArray.CreateBuilder>(); + + foreach (var model in models.StickerPacks) + { + var stickers = model.Stickers.Select(x => SocketSticker.Create(_shards[0], x)); + + var pack = new StickerPack( + model.Name, + model.Id, + model.SkuId, + model.CoverStickerId.ToNullable(), + model.Description, + model.BannerAssetId, + stickers + ); + + builder.Add(pack); + } + + _defaultStickers = builder.ToImmutable(); + } /// public override SocketUser GetUser(ulong id) @@ -210,12 +345,22 @@ namespace Discord.WebSocket } /// - public override RestVoiceRegion GetVoiceRegion(string id) - => _shards[0].GetVoiceRegion(id); + public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) + { + return await _shards[0].GetVoiceRegionsAsync().ConfigureAwait(false); + } + + /// + public override async ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null) + { + return await _shards[0].GetVoiceRegionAsync(id, options).ConfigureAwait(false); + } - /// Downloads the users list for the provided guilds, if they don't have a complete list. + /// + /// is public override async Task DownloadUsersAsync(IEnumerable guilds) { + if (guilds == null) throw new ArgumentNullException(nameof(guilds)); for (int i = 0; i < _shards.Length; i++) { int id = _shardIds[i]; @@ -233,11 +378,13 @@ namespace Discord.WebSocket return (int)Math.Round(total / (double)_shards.Length); } + /// public override async Task SetStatusAsync(UserStatus status) { for (int i = 0; i < _shards.Length; i++) await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } + /// public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { IActivity activity = null; @@ -247,6 +394,7 @@ namespace Discord.WebSocket activity = new Game(name, type); await SetActivityAsync(activity).ConfigureAwait(false); } + /// public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) @@ -266,14 +414,6 @@ namespace Discord.WebSocket } return Task.Delay(0); }; - if (isPrimary) - { - client.Ready += () => - { - CurrentUser = client.CurrentUser; - return Task.Delay(0); - }; - } client.Connected += () => _shardConnectedEvent.InvokeAsync(client); client.Disconnected += (exception) => _shardDisconnectedEvent.InvokeAsync(exception, client); @@ -286,10 +426,12 @@ namespace Discord.WebSocket client.MessageReceived += (msg) => _messageReceivedEvent.InvokeAsync(msg); client.MessageDeleted += (cache, channel) => _messageDeletedEvent.InvokeAsync(cache, channel); + client.MessagesBulkDeleted += (cache, channel) => _messagesBulkDeletedEvent.InvokeAsync(cache, channel); client.MessageUpdated += (oldMsg, newMsg, channel) => _messageUpdatedEvent.InvokeAsync(oldMsg, newMsg, channel); client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction); client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction); client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel); + client.ReactionsRemovedForEmote += (cache, channel, emote) => _reactionsRemovedForEmoteEvent.InvokeAsync(cache, channel, emote); client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role); client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role); @@ -314,38 +456,118 @@ namespace Discord.WebSocket client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user); client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user); + + client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite); + client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite); + + client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction); + client.ButtonExecuted += (arg) => _buttonExecuted.InvokeAsync(arg); + client.SelectMenuExecuted += (arg) => _selectMenuExecuted.InvokeAsync(arg); + client.SlashCommandExecuted += (arg) => _slashCommandExecuted.InvokeAsync(arg); + client.UserCommandExecuted += (arg) => _userCommandExecuted.InvokeAsync(arg); + client.MessageCommandExecuted += (arg) => _messageCommandExecuted.InvokeAsync(arg); + client.AutocompleteExecuted += (arg) => _autocompleteExecuted.InvokeAsync(arg); + + client.ThreadUpdated += (thread1, thread2) => _threadUpdated.InvokeAsync(thread1, thread2); + client.ThreadCreated += (thread) => _threadCreated.InvokeAsync(thread); + client.ThreadDeleted += (thread) => _threadDeleted.InvokeAsync(thread); + + client.ThreadMemberJoined += (user) => _threadMemberJoined.InvokeAsync(user); + client.ThreadMemberLeft += (user) => _threadMemberLeft.InvokeAsync(user); + client.StageEnded += (stage) => _stageEnded.InvokeAsync(stage); + client.StageStarted += (stage) => _stageStarted.InvokeAsync(stage); + client.StageUpdated += (stage1, stage2) => _stageUpdated.InvokeAsync(stage1, stage2); + + client.RequestToSpeak += (stage, user) => _requestToSpeak.InvokeAsync(stage, user); + client.SpeakerAdded += (stage, user) => _speakerAdded.InvokeAsync(stage, user); + client.SpeakerRemoved += (stage, user) => _speakerRemoved.InvokeAsync(stage, user); + + client.GuildStickerCreated += (sticker) => _guildStickerCreated.InvokeAsync(sticker); + client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker); + client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after); + client.GuildJoinRequestDeleted += (userId, guildId) => _guildJoinRequestDeletedEvent.InvokeAsync(userId, guildId); + + client.GuildScheduledEventCancelled += (arg) => _guildScheduledEventCancelled.InvokeAsync(arg); + client.GuildScheduledEventCompleted += (arg) => _guildScheduledEventCompleted.InvokeAsync(arg); + client.GuildScheduledEventCreated += (arg) => _guildScheduledEventCreated.InvokeAsync(arg); + client.GuildScheduledEventUpdated += (arg1, arg2) => _guildScheduledEventUpdated.InvokeAsync(arg1, arg2); + client.GuildScheduledEventStarted += (arg) => _guildScheduledEventStarted.InvokeAsync(arg); + client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2); + client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2); } + #endregion - //IDiscordClient + #region IDiscordClient + /// + ISelfUser IDiscordClient.CurrentUser => CurrentUser; + + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); + /// + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + { + return await GetVoiceRegionsAsync().ConfigureAwait(false); + } + /// + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + { + return await GetVoiceRegionAsync(id).ConfigureAwait(false); + } + #endregion + + #region Dispose + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + if (_shards != null) + { + foreach (var client in _shards) + client?.Dispose(); + } + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index d42bd7132..3c0f3d4a8 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -1,6 +1,4 @@ -#pragma warning disable CS1591 using Discord.API.Gateway; -using Discord.API.Rest; using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; @@ -13,6 +11,7 @@ using System.IO.Compression; using System.Text; using System.Threading; using System.Threading.Tasks; +using GameModel = Discord.API.Game; namespace Discord.API { @@ -39,8 +38,9 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, - string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) - : base(restClientProvider, userAgent, defaultRetryMode, serializer) + string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, + bool useSystemClock = true) + : base(restClientProvider, userAgent, defaultRetryMode, serializer, useSystemClock) { _gatewayUrl = url; if (url != null) @@ -74,8 +74,15 @@ namespace Discord.API using (var jsonReader = new JsonTextReader(reader)) { var msg = _serializer.Deserialize(jsonReader); + if (msg != null) + { +#if DEBUG_PACKETS + Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}"); +#endif + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } } } }; @@ -86,11 +93,21 @@ namespace Discord.API { var msg = _serializer.Deserialize(jsonReader); if (msg != null) + { +#if DEBUG_PACKETS + Console.WriteLine($"<- {(GatewayOpCode)msg.Operation} [{msg.Type ?? "none"}] : {(msg.Payload as Newtonsoft.Json.Linq.JToken)?.ToString().Length}"); +#endif + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } } }; WebSocketClient.Closed += async ex => { +#if DEBUG_PACKETS + Console.WriteLine(ex); +#endif + await DisconnectAsync().ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); }; @@ -109,6 +126,8 @@ namespace Discord.API } _isDisposed = true; } + + base.Dispose(disposing); } public async Task ConnectAsync() @@ -120,12 +139,16 @@ namespace Discord.API } finally { _stateLock.Release(); } } + /// The client must be logged in before connecting. + /// This client is not configured with WebSocket support. internal override async Task ConnectInternalAsync() { if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting."); + throw new InvalidOperationException("The client must be logged in before connecting."); if (WebSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); + + RequestQueue.ClearGatewayBuckets(); //Re-create streams to reset the zlib state _compressed?.Dispose(); @@ -136,6 +159,7 @@ namespace Discord.API ConnectionState = ConnectionState.Connecting; try { + _connectCancelToken?.Dispose(); _connectCancelToken = new CancellationTokenSource(); if (WebSocketClient != null) WebSocketClient.SetCancelToken(_connectCancelToken.Token); @@ -145,6 +169,11 @@ namespace Discord.API var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}&compress=zlib-stream"; } + +#if DEBUG_PACKETS + Console.WriteLine("Connecting to gateway: " + _gatewayUrl); +#endif + await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; @@ -158,28 +187,20 @@ namespace Discord.API } } - public async Task DisconnectAsync() - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - public async Task DisconnectAsync(Exception ex) + public async Task DisconnectAsync(Exception ex = null) { await _stateLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _stateLock.Release(); } } - internal override async Task DisconnectInternalAsync() + /// This client is not configured with WebSocket support. + internal override async Task DisconnectInternalAsync(Exception ex = null) { if (WebSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; @@ -187,12 +208,15 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } - await WebSocketClient.DisconnectAsync().ConfigureAwait(false); + if (ex is GatewayReconnectException) + await WebSocketClient.DisconnectAsync(4000).ConfigureAwait(false); + else + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } - //Core + #region Core public Task SendGatewayAsync(GatewayOpCode opCode, object payload, RequestOptions options = null) => SendGatewayInternalAsync(opCode, payload, options); private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, RequestOptions options) @@ -204,16 +228,26 @@ namespace Discord.API payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); + + options.IsGatewayBucket = true; + if (options.BucketId == null) + options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id; + await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, opCode == GatewayOpCode.Heartbeat, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); + +#if DEBUG_PACKETS + Console.WriteLine($"-> {opCode}:\n{SerializeJson(payload)}"); +#endif } - - public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) + + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, GatewayIntents gatewayIntents = GatewayIntents.AllUnprivileged, (UserStatus, bool, long?, GameModel)? presence = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var props = new Dictionary { - ["$device"] = "Discord.Net" + ["$device"] = "Discord.Net", + ["$os"] = Environment.OSVersion.Platform.ToString(), + [$"browser"] = "Discord.Net" }; var msg = new IdentifyParams() { @@ -224,6 +258,21 @@ namespace Discord.API if (totalShards > 1) msg.ShardingParams = new int[] { shardID, totalShards }; + options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id; + + msg.Intents = (int)gatewayIntents; + + if (presence.HasValue) + { + msg.Presence = new PresenceUpdateParams + { + Status = presence.Value.Item1, + IsAFK = presence.Value.Item2, + IdleSince = presence.Value.Item3, + Activities = new object[] { presence.Value.Item4 } + }; + } + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) @@ -242,17 +291,18 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } - public async Task SendStatusUpdateAsync(UserStatus status, bool isAFK, long? since, Game game, RequestOptions options = null) + public async Task SendPresenceUpdateAsync(UserStatus status, bool isAFK, long? since, GameModel game, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); - var args = new StatusUpdateParams + var args = new PresenceUpdateParams { Status = status, IdleSince = since, IsAFK = isAFK, - Game = game + Activities = new object[] { game } }; - await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); + options.BucketId = GatewayBucket.Get(GatewayBucketType.PresenceUpdate).Id; + await SendGatewayAsync(GatewayOpCode.PresenceUpdate, args, options: options).ConfigureAwait(false); } public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) { @@ -276,5 +326,6 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); } + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 1222b270e..ab13d93db 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -1,11 +1,12 @@ -using System; +using System; using System.Threading.Tasks; +using Discord.API; namespace Discord.WebSocket -{ +{ public partial class DiscordSocketClient { - //General + #region General /// Fired when connected to the Discord gateway. public event Func Connected { @@ -20,7 +21,13 @@ namespace Discord.WebSocket remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - /// Fired when guild data has finished downloading. + /// + /// Fired when guild data has finished downloading. + /// + /// + /// It is possible that some guilds might be unsynced if + /// was not long enough to receive all GUILD_AVAILABLEs before READY. + /// public event Func Ready { add { _readyEvent.Add(value); } @@ -34,5 +41,10 @@ namespace Discord.WebSocket remove { _latencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + + internal DiscordSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) : base(config, client) + { + } + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 45d60b764..9ef827778 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -19,11 +19,15 @@ using GameModel = Discord.API.Game; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based Discord client. + /// public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient { + #region DiscordSocketClient private readonly ConcurrentQueue _largeGuilds; - private readonly JsonSerializer _serializer; - private readonly SemaphoreSlim _connectionGroupLock; + internal readonly JsonSerializer _serializer; + private readonly DiscordShardedClient _shardedClient; private readonly DiscordSocketClient _parentClient; private readonly ConcurrentQueue _heartbeatTimes; private readonly ConnectionManager _connection; @@ -39,17 +43,29 @@ namespace Discord.WebSocket private int _nextAudioId; private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; - - /// Gets the shard of of this client. + private bool _isDisposed; + private GatewayIntents _gatewayIntents; + private ImmutableArray> _defaultStickers; + + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public override DiscordSocketRestClient Rest { get; } + /// Gets the shard of this client. public int ShardId { get; } /// Gets the current connection state of this client. public ConnectionState ConnectionState => _connection.State; /// public override int Latency { get; protected set; } - public override UserStatus Status { get; protected set; } = UserStatus.Online; - public override IActivity Activity { get; protected set; } + /// + public override UserStatus Status { get => _status ?? UserStatus.Online; protected set => _status = value; } + private UserStatus? _status; + /// + public override IActivity Activity { get => _activity.GetValueOrDefault(); protected set => _activity = Optional.Create(value); } + private Optional _activity; + #endregion - //From DiscordSocketConfig + // From DiscordSocketConfig internal int TotalShards { get; private set; } internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } @@ -58,22 +74,69 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } - - internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + internal bool AlwaysDownloadDefaultStickers { get; private set; } + internal bool AlwaysResolveStickers { get; private set; } + internal bool LogGatewayIntentWarnings { get; private set; } + internal new DiscordSocketApiClient ApiClient => base.ApiClient; + /// public override IReadOnlyCollection Guilds => State.Guilds; + /// + public override IReadOnlyCollection> DefaultStickerPacks + { + get + { + if (_shardedClient != null) + return _shardedClient.DefaultStickerPacks; + else + return _defaultStickers.ToReadOnlyCollection(); + } + } + /// public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; + /// + /// Gets a collection of direct message channels opened in this session. + /// + /// + /// This method returns a collection of currently opened direct message channels. + /// + /// This method will not return previously opened DM channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// + /// A collection of DM channels that have been opened in this session. + /// public IReadOnlyCollection DMChannels => State.PrivateChannels.OfType().ToImmutableArray(); + /// + /// Gets a collection of group channels opened in this session. + /// + /// + /// This method returns a collection of currently opened group channels. + /// + /// This method will not return previously opened group channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// + /// A collection of group channels that have been opened in this session. + /// public IReadOnlyCollection GroupChannels => State.PrivateChannels.OfType().ToImmutableArray(); - public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); - /// Creates a new REST/WebSocket discord client. + /// + /// Initializes a new REST/WebSocket-based Discord client. + /// public DiscordSocketClient() : this(new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// + /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. + /// + /// The configuration to be used with the client. +#pragma warning disable IDISP004 public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } - internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } - private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) + internal DiscordSocketClient(DiscordSocketConfig config, DiscordShardedClient shardedClient, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), shardedClient, parentClient) { } +#pragma warning restore IDISP004 + private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, DiscordShardedClient shardedClient, DiscordSocketClient parentClient) : base(config, client) { ShardId = config.ShardId ?? 0; @@ -83,9 +146,15 @@ namespace Discord.WebSocket UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; + AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; + AlwaysResolveStickers = config.AlwaysResolveStickers; + LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); + Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); + _gatewayIntents = config.GatewayIntents; + _defaultStickers = ImmutableArray.Create>(); _stateLock = new SemaphoreSlim(1, 1); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); @@ -95,7 +164,7 @@ namespace Discord.WebSocket _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); _nextAudioId = 1; - _connectionGroupLock = groupLock; + _shardedClient = shardedClient; _parentClient = parentClient; _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; @@ -116,53 +185,90 @@ namespace Discord.WebSocket GuildAvailable += g => { - if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) + if (_guildDownloadTask?.IsCompleted == true && ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) { var _ = g.DownloadUsersAsync(); } return Task.Delay(0); }; - _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); + /// internal override void Dispose(bool disposing) { - if (disposing) + if (!_isDisposed) { - StopAsync().GetAwaiter().GetResult(); - ApiClient.Dispose(); + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient?.Dispose(); + _stateLock?.Dispose(); + } + _isDisposed = true; } + + base.Dispose(disposing); } internal override async Task OnLoginAsync(TokenType tokenType, string token) { - if (_parentClient == null) + if (_shardedClient == null && _defaultStickers.Length == 0 && AlwaysDownloadDefaultStickers) { - var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); + var models = await ApiClient.ListNitroStickerPacksAsync().ConfigureAwait(false); + + var builder = ImmutableArray.CreateBuilder>(); + + foreach (var model in models.StickerPacks) + { + var stickers = model.Stickers.Select(x => SocketSticker.Create(this, x)); + + var pack = new StickerPack( + model.Name, + model.Id, + model.SkuId, + model.CoverStickerId.ToNullable(), + model.Description, + model.BannerAssetId, + stickers + ); + + builder.Add(pack); + } + + _defaultStickers = builder.ToImmutable(); } - else - _voiceRegions = _parentClient._voiceRegions; + + if(LogGatewayIntentWarnings) + await LogGatewayIntentsWarning().ConfigureAwait(false); } + + /// internal override async Task OnLogoutAsync() { await StopAsync().ConfigureAwait(false); _applicationInfo = null; - _voiceRegions = ImmutableDictionary.Create(); + _voiceRegions = null; + await Rest.OnLogoutAsync(); } + /// public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); + /// public override async Task StopAsync() => await _connection.StopAsync().ConfigureAwait(false); private async Task OnConnectingAsync() { - if (_connectionGroupLock != null) - await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false); + bool locked = false; + if (_shardedClient != null && _sessionId == null) + { + await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false); + locked = true; + } try { await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); @@ -176,29 +282,23 @@ namespace Discord.WebSocket else { await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); } - - //Wait for READY - await _connection.WaitAsync().ConfigureAwait(false); - - await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); - await SendStatusAsync().ConfigureAwait(false); } finally { - if (_connectionGroupLock != null) - { - await Task.Delay(5000).ConfigureAwait(false); - _connectionGroupLock.Release(); - } + if (locked) + _shardedClient.ReleaseIdentifyLock(); } + + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); } private async Task OnDisconnectingAsync(Exception ex) { await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); - await ApiClient.DisconnectAsync().ConfigureAwait(false); + await ApiClient.DisconnectAsync(ex).ConfigureAwait(false); //Wait for tasks to complete await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); @@ -231,7 +331,7 @@ namespace Discord.WebSocket /// public override async Task GetApplicationInfoAsync(RequestOptions options = null) - => _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false)); + => _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false); /// public override SocketGuild GetGuild(ulong id) @@ -240,6 +340,52 @@ namespace Discord.WebSocket /// public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); + /// + /// Gets a generic channel from the cache or does a rest request if unavailable. + /// + /// + /// + /// var channel = await _client.GetChannelAsync(381889909113225237); + /// if (channel != null && channel is IMessageChannel msgChannel) + /// { + /// await msgChannel.SendMessageAsync($"{msgChannel} is created at {msgChannel.CreatedAt}"); + /// } + /// + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the channel associated + /// with the snowflake identifier; null when the channel cannot be found. + /// + public async ValueTask GetChannelAsync(ulong id, RequestOptions options = null) + => GetChannel(id) ?? (IChannel)await ClientHelper.GetChannelAsync(this, id, options).ConfigureAwait(false); + /// + /// Gets a user from the cache or does a rest request if unavailable. + /// + /// + /// + /// var user = await _client.GetUserAsync(168693960628371456); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The snowflake identifier of the user (e.g. `168693960628371456`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the snowflake identifier; null if the user is not found. + /// + public async ValueTask GetUserAsync(ulong id, RequestOptions options = null) + => await ClientHelper.GetUserAsync(this, id, options).ConfigureAwait(false); + /// + /// Clears all cached channels from the client. + /// + public void PurgeChannelCache() => State.PurgeAllChannels(); + /// + /// Clears cached DM channels from the client. + /// + public void PurgeDMChannelCache() => RemoveDMChannels(); /// public override SocketUser GetUser(ulong id) @@ -247,14 +393,94 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); - internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) + + /// + /// Gets a global application command. + /// + /// The id of the command. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains the application command if found, otherwise + /// . + /// + public async ValueTask GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) { - return state.GetOrAddUser(model.Id, x => + var command = State.GetCommand(id); + + if (command != null) + return command; + + var model = await ApiClient.GetGlobalApplicationCommandAsync(id, options); + + if (model == null) + return null; + + command = SocketApplicationCommand.Create(this, model); + + State.AddCommand(command); + + return command; + } + /// + /// Gets a collection of all global commands. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global + /// application commands. + /// + public async Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null) + { + var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); + + foreach(var command in commands) { - var user = SocketGlobalUser.Create(this, state, model); - user.GlobalUser.AddRef(); - return user; - }); + State.AddCommand(command); + } + + return commands.ToImmutableArray(); + } + + public async Task CreateGlobalApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGlobalCommandAsync(this, properties, options).ConfigureAwait(false); + + var entity = State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(this, model)); + + //Update it in case it was cached + entity.Update(model); + + return entity; + } + public async Task> BulkOverwriteGlobalApplicationCommandsAsync( + ApplicationCommandProperties[] properties, RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGlobalCommandsAsync(this, properties, options); + + var entities = models.Select(x => SocketApplicationCommand.Create(this, x)); + + //Purge our previous commands + State.PurgeCommands(x => x.IsGlobalCommand); + + foreach(var entity in entities) + { + State.AddCommand(entity); + } + + return entities.ToImmutableArray(); + } + + /// + /// Clears cached users from the client. + /// + public void PurgeUserCache() => State.PurgeUsers(); + internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) + { + return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); + } + internal SocketUser GetOrCreateTemporaryUser(ClientState state, Discord.API.User model) + { + return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); } internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) { @@ -262,26 +488,101 @@ namespace Discord.WebSocket { var user = SocketGlobalUser.Create(this, state, model); user.GlobalUser.AddRef(); - user.Presence = new SocketPresence(UserStatus.Online, null); + user.Presence = new SocketPresence(UserStatus.Online, null, null); return user; }); } internal void RemoveUser(ulong id) => State.RemoveUser(id); + /// + public override async Task GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var sticker = _defaultStickers.FirstOrDefault(x => x.Stickers.Any(y => y.Id == id))?.Stickers.FirstOrDefault(x => x.Id == id); + + if (sticker != null) + return sticker; + + foreach(var guild in Guilds) + { + sticker = await guild.GetStickerAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); + + if (sticker != null) + return sticker; + } + + if (mode == CacheMode.CacheOnly) + return null; + + var model = await ApiClient.GetStickerAsync(id, options).ConfigureAwait(false); + + if(model == null) + return null; + + + if (model.GuildId.IsSpecified) + { + var guild = State.GetGuild(model.GuildId.Value); + + //Since the sticker can be from another guild, check if we are in the guild or its in the cache + if (guild != null) + sticker = guild.AddOrUpdateSticker(model); + else + sticker = SocketSticker.Create(this, model); + return sticker; + } + else + { + return SocketSticker.Create(this, model); + } + } + + /// + /// Gets a sticker. + /// + /// The unique identifier of the sticker. + /// A sticker if found, otherwise . + public SocketSticker GetSticker(ulong id) + => GetStickerAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + + /// + public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) + { + if (_parentClient == null) + { + if (_voiceRegions == null) + { + options = RequestOptions.CreateOrClone(options); + options.IgnoreState = true; + var voiceRegions = await ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); + } + return _voiceRegions.ToReadOnlyCollection(); + } + return await _parentClient.GetVoiceRegionsAsync().ConfigureAwait(false); + } + /// - public override RestVoiceRegion GetVoiceRegion(string id) + public override async ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null) { - if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) - return region; - return null; + if (_parentClient == null) + { + if (_voiceRegions == null) + await GetVoiceRegionsAsync().ConfigureAwait(false); + if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) + return region; + return null; + } + return await _parentClient.GetVoiceRegionAsync(id, options).ConfigureAwait(false); } - /// Downloads the users list for the provided guilds, if they don't have a complete list. + /// public override async Task DownloadUsersAsync(IEnumerable guilds) { if (ConnectionState == ConnectionState.Connected) { + EnsureGatewayIntent(GatewayIntents.GuildMembers); + //Race condition leads to guilds being requested twice, probably okay await ProcessUserDownloadsAsync(guilds.Select(x => GetGuild(x.Id)).Where(x => x != null)).ConfigureAwait(false); } @@ -290,7 +591,7 @@ namespace Discord.WebSocket { var cachedGuilds = guilds.ToImmutableArray(); - const short batchSize = 50; + const short batchSize = 1; ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; Task[] batchTasks = new Task[batchIds.Length]; int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; @@ -298,7 +599,7 @@ namespace Discord.WebSocket for (int i = 0, k = 0; i < batchCount; i++) { bool isLast = i == batchCount - 1; - int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; + int count = isLast ? (cachedGuilds.Length - (batchCount - 1) * batchSize) : batchSize; for (int j = 0; j < count; j++, k++) { @@ -316,6 +617,13 @@ namespace Discord.WebSocket } } + /// + /// + /// The following example sets the status of the current user to Do Not Disturb. + /// + /// await client.SetStatusAsync(UserStatus.DoNotDisturb); + /// + /// public override async Task SetStatusAsync(UserStatus status) { Status = status; @@ -325,6 +633,21 @@ namespace Discord.WebSocket _statusSince = null; await SendStatusAsync().ConfigureAwait(false); } + /// + /// + /// + /// The following example sets the activity of the current user to the specified game name. + /// + /// await client.SetGameAsync("A Strange Game"); + /// + /// + /// + /// The following example sets the activity of the current user to a streaming status. + /// + /// await client.SetGameAsync("Great Stream 10/10", "https://twitch.tv/MyAmazingStream1337", ActivityType.Streaming); + /// + /// + /// public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { if (!string.IsNullOrEmpty(streamUrl)) @@ -335,6 +658,7 @@ namespace Discord.WebSocket Activity = null; await SendStatusAsync().ConfigureAwait(false); } + /// public override async Task SetActivityAsync(IActivity activity) { Activity = activity; @@ -345,30 +669,97 @@ namespace Discord.WebSocket { if (CurrentUser == null) return; - var status = Status; + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; + CurrentUser.Presence = new SocketPresence(Status, null, activities); + + var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); + + await ApiClient.SendPresenceUpdateAsync( + status: presence.Item1, + isAFK: presence.Item2, + since: presence.Item3, + game: presence.Item4).ConfigureAwait(false); + } + + private (UserStatus, bool, long?, GameModel)? BuildCurrentStatus() + { + var status = _status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, Activity); + var activity = _activity; - var gameModel = new GameModel(); - // Discord only accepts rich presence over RPC, don't even bother building a payload - if (Activity is RichGame) - throw new NotSupportedException("Outgoing Rich Presences are not supported"); + if (status == null && !activity.IsSpecified) + return null; - if (Activity != null) + GameModel game = null; + //Discord only accepts rich presence over RPC, don't even bother building a payload + + if (activity.GetValueOrDefault() != null) { + var gameModel = new GameModel(); + if (activity.Value is RichGame) + throw new NotSupportedException("Outgoing Rich Presences are not supported via WebSocket."); gameModel.Name = Activity.Name; gameModel.Type = Activity.Type; if (Activity is StreamingGame streamGame) gameModel.StreamUrl = streamGame.Url; + game = gameModel; + } + else if (activity.IsSpecified) + game = null; + + return (status ?? UserStatus.Online, + status == UserStatus.AFK, + statusSince != null ? _statusSince.Value.ToUnixTimeMilliseconds() : (long?)null, + game); + } + + private async Task LogGatewayIntentsWarning() + { + if(_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && !_presenceUpdated.HasSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildPresences intent without listening to the PresenceUpdate event, consider removing the intent from your config.").ConfigureAwait(false); + } + + if(!_gatewayIntents.HasFlag(GatewayIntents.GuildPresences) && _presenceUpdated.HasSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the PresenceUpdate event without specifying the GuildPresences intent, consider adding the intent to your config.").ConfigureAwait(false); + } + + bool hasGuildScheduledEventsSubscribers = + _guildScheduledEventCancelled.HasSubscribers || + _guildScheduledEventUserRemove.HasSubscribers || + _guildScheduledEventCompleted.HasSubscribers || + _guildScheduledEventCreated.HasSubscribers || + _guildScheduledEventStarted.HasSubscribers || + _guildScheduledEventUpdated.HasSubscribers || + _guildScheduledEventUserAdd.HasSubscribers; + + if(_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && !hasGuildScheduledEventsSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildScheduledEvents gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); } - await ApiClient.SendStatusUpdateAsync( - status, - status == UserStatus.AFK, - statusSince != null ? _statusSince.Value.ToUnixTimeMilliseconds() : (long?)null, - gameModel).ConfigureAwait(false); + if(!_gatewayIntents.HasFlag(GatewayIntents.GuildScheduledEvents) && hasGuildScheduledEventsSubscribers) + { + await _gatewayLogger.WarningAsync("You're using events related to the GuildScheduledEvents gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); + } + + bool hasInviteEventSubscribers = + _inviteCreatedEvent.HasSubscribers || + _inviteDeletedEvent.HasSubscribers; + + if (_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && !hasInviteEventSubscribers) + { + await _gatewayLogger.WarningAsync("You're using the GuildInvites gateway intent without listening to any events related to that intent, consider removing the intent from your config.").ConfigureAwait(false); + } + + if (!_gatewayIntents.HasFlag(GatewayIntents.GuildInvites) && hasInviteEventSubscribers) + { + await _gatewayLogger.WarningAsync("You're using events related to the GuildInvites gateway intent without specifying the intent, consider adding the intent to your config.").ConfigureAwait(false); + } } + #region ProcessMessageAsync private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { if (seq != null) @@ -416,19 +807,32 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + if (_shardedClient != null) + { + await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false); + try + { + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + } + finally + { + _shardedClient.ReleaseIdentifyLock(); + } + } + else + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - _connection.Error(new Exception("Server requested a reconnect")); + _connection.Error(new GatewayReconnectException("Server requested a reconnect")); } break; case GatewayOpCode.Dispatch: switch (type) { - //Connection + #region Connection case "READY": { try @@ -439,7 +843,11 @@ namespace Discord.WebSocket var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); var currentUser = SocketSelfUser.Create(this, state, data.User); + Rest.CreateRestSelfUser(data.User); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; + currentUser.Presence = new SocketPresence(Status, null, activities); ApiClient.CurrentUserId = currentUser.Id; + Rest.CurrentUser = RestSelfUser.Create(this, data.User); int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { @@ -476,6 +884,9 @@ namespace Discord.WebSocket else if (_connection.CancelToken.IsCancellationRequested) return; + if (BaseConfig.AlwaysDownloadUsers) + _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); }); @@ -498,8 +909,9 @@ namespace Discord.WebSocket await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); } break; + #endregion - //Guilds + #region Guilds case "GUILD_CREATE": { var data = (payload as JToken).ToObject(_serializer); @@ -539,6 +951,7 @@ namespace Discord.WebSocket if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { @@ -588,7 +1001,8 @@ namespace Discord.WebSocket break; case "GUILD_SYNC": { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_SYNC)").ConfigureAwait(false); + /*await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); //TODO remove? userbot related var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.Id); if (guild != null) @@ -605,7 +1019,7 @@ namespace Discord.WebSocket { await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; - } + }*/ } break; case "GUILD_DELETE": @@ -637,6 +1051,7 @@ namespace Discord.WebSocket { await GuildUnavailableAsync(guild).ConfigureAwait(false); await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + (guild as IDisposable).Dispose(); } else { @@ -646,8 +1061,62 @@ namespace Discord.WebSocket } } break; + case "GUILD_STICKERS_UPDATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var newStickers = data.Stickers.Where(x => !guild.Stickers.Any(y => y.Id == x.Id)); + var deletedStickers = guild.Stickers.Where(x => !data.Stickers.Any(y => y.Id == x.Id)); + var updatedStickers = data.Stickers.Select(x => + { + var s = guild.Stickers.FirstOrDefault(y => y.Id == x.Id); + if (s == null) + return null; + + var e = s.Equals(x); + if (!e) + { + return (s, x) as (SocketCustomSticker Entity, API.Sticker Model)?; + } + else + { + return null; + } + }).Where(x => x.HasValue).Select(x => x.Value).ToArray(); + + foreach (var model in newStickers) + { + var entity = guild.AddSticker(model); + await TimedInvokeAsync(_guildStickerCreated, nameof(GuildStickerCreated), entity); + } + foreach (var sticker in deletedStickers) + { + var entity = guild.RemoveSticker(sticker.Id); + await TimedInvokeAsync(_guildStickerDeleted, nameof(GuildStickerDeleted), entity); + } + foreach (var entityModelPair in updatedStickers) + { + var before = entityModelPair.Entity.Clone(); + + entityModelPair.Entity.Update(entityModelPair.Model); + + await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity); + } + } + break; + #endregion - //Channels + #region Channels case "CHANNEL_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); @@ -749,8 +1218,9 @@ namespace Discord.WebSocket } } break; + #endregion - //Members + #region Members case "GUILD_MEMBER_ADD": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); @@ -795,17 +1265,24 @@ namespace Discord.WebSocket if (user != null) { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + var before = user.Clone(); user.Update(State, data); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult((SocketGuildUser)null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { - if (!guild.HasAllMembers) - await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); - else - await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); - return; + user = guild.AddOrUpdateUser(data); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult((SocketGuildUser)null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } else @@ -874,6 +1351,32 @@ namespace Discord.WebSocket } } break; + case "GUILD_JOIN_REQUEST_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var user = guild.RemoveUser(data.UserId); + guild.MemberCount--; + + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Task.FromResult((SocketGuildUser)null)); + + await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false); + } + break; + #endregion + + #region DM Channels + case "CHANNEL_RECIPIENT_ADD": { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); @@ -915,7 +1418,9 @@ namespace Discord.WebSocket } break; - //Roles + #endregion + + #region Roles case "GUILD_ROLE_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); @@ -1007,8 +1512,9 @@ namespace Discord.WebSocket } } break; + #endregion - //Bans + #region Bans case "GUILD_BAN_ADD": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); @@ -1061,55 +1567,71 @@ namespace Discord.WebSocket } } break; + #endregion - //Messages + #region Messages case "MESSAGE_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } + } - SocketUser author; + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + + if (author == null) + { if (guild != null) { - if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } else - author = guild.GetUser(data.Author.Value.Id); + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - - if (author == null) { - if (guild != null) - author = guild.AddOrUpdateUser(data.Member.Value); //per g250k, we can create an entire member now - else if (channel is SocketGroupChannel) - author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); - else - { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); - return; - } + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + return; } - - var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } break; case "MESSAGE_UPDATE": @@ -1117,46 +1639,85 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - SocketMessage before = null, after = null; - SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); - bool isCached = cachedMsg != null; - if (isCached) + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel?.GetCachedMessage(data.Id); + bool isCached = cachedMsg != null; + if (isCached) + { + before = cachedMsg.Clone(); + cachedMsg.Update(State, data); + after = cachedMsg; + } + else + { + //Edited message isn't in cache, create a detached one + SocketUser author; + if (data.Author.IsSpecified) { - before = cachedMsg.Clone(); - cachedMsg.Update(State, data); - after = cachedMsg; - } - else if (data.Author.IsSpecified) - { - //Edited message isnt in cache, create a detached one - SocketUser author; if (guild != null) - author = guild.GetUser(data.Author.Value.Id); + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + author = (channel as SocketChannel)?.GetUser(data.Author.Value.Id); + if (author == null) - author = SocketUnknownUser.Create(this, State, data.Author.Value); + { + if (guild != null) + { + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } + else + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); + } + } + else + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); - after = SocketMessage.Create(this, State, author, channel, data); + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM + { + if (data.Author.IsSpecified) + { + var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + channel = dmChannel; + author = dmChannel.Recipient; + } + else + channel = CreateDMChannel(data.ChannelId, author, State); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } } - var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; + after = SocketMessage.Create(this, State, author, channel, data); } + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } break; case "MESSAGE_DELETE": @@ -1164,68 +1725,448 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); + var cacheableMsg = new Cacheable(msg, data.Id, msg != null, () => Task.FromResult((IMessage)null)); + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + if (data.Member.IsSpecified) { var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + + if (guild != null) + user = guild.AddOrUpdateUser(data.Member.Value); + } + else + user = GetUser(data.UserId); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + else if (!data.GuildId.IsSpecified) + user = GetUser(data.UserId); + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE_ALL": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + + cachedMsg?.ClearReactions(); + + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); + } + break; + case "MESSAGE_REACTION_REMOVE_EMOJI": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => + { + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var emote = data.Emoji.ToIEmote(); + + cachedMsg?.RemoveReactionsForEmote(emote); + + await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE_BULK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableList = new List>(data.Ids.Length); + foreach (ulong id in data.Ids) + { + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, id); + bool isMsgCached = msg != null; + var cacheableMsg = new Cacheable(msg, id, isMsgCached, () => Task.FromResult((IMessage)null)); + cacheableList.Add(cacheableMsg); + } + + await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion + + #region Statuses + case "PRESENCE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + SocketUser user = null; + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + if (!guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); - bool isCached = msg != null; - var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); + user = guild.GetUser(data.User.Id); + if (user == null) + { + if (data.Status == UserStatus.Offline) + { + return; + } + user = guild.AddOrUpdateUser(data); + } + else + { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + } + } + else + { + user = State.GetUser(data.User.Id); + if (user == null) + { + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; + } + } - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); + var before = user.Presence.Clone(); + user.Update(State, data.User); + user.Update(data); + await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); + } + break; + case "TYPING_START": + { + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + var user = (channel as SocketChannel)?.GetUser(data.UserId); + if (user == null) + { + if (guild != null) + user = guild.AddOrUpdateUser(data.Member); + } + var cacheableUser = new Cacheable(user, data.UserId, user != null, async () => await GetUserAsync(data.UserId).ConfigureAwait(false)); + + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); + } + break; + #endregion + + #region Users + case "USER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = CurrentUser.Clone(); + CurrentUser.Update(State, data); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); return; } } break; - case "MESSAGE_REACTION_ADD": + #endregion + + #region Voice + case "VOICE_STATE_UPDATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var data = (payload as JToken).ToObject(_serializer); + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) { - var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isCached = cachedMsg != null; - var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - cachedMsg?.AddReaction(reaction); + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) + { + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ + } + else + { + before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } - await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheable, channel, reaction).ConfigureAwait(false); + //Per g250k, this should always be sent, but apparently not always + user = guild.GetUser(data.UserId) + ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null); + if (user == null) + { + await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + return; + } } else { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; + var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) + { + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); + } + else + { + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + user = groupChannel.GetUser(data.UserId); + if (user == null) + { + await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); + return; + } + } + + if (user is SocketGuildUser guildUser && data.ChannelId.HasValue) + { + SocketStageChannel stage = guildUser.Guild.GetStageChannel(data.ChannelId.Value); + + if (stage != null && before.VoiceChannel != null && after.VoiceChannel != null) + { + if (!before.RequestToSpeakTimestamp.HasValue && after.RequestToSpeakTimestamp.HasValue) + { + await TimedInvokeAsync(_requestToSpeak, nameof(RequestToSpeak), stage, guildUser); + return; + } + if(before.IsSuppressed && !after.IsSuppressed) + { + await TimedInvokeAsync(_speakerAdded, nameof(SpeakerAdded), stage, guildUser); + return; + } + if(!before.IsSuppressed && after.IsSuppressed) + { + await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); + } + } } + + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); } break; - case "MESSAGE_REACTION_REMOVE": + case "VOICE_SERVER_UPDATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + var isCached = guild != null; + var cachedGuild = new Cacheable(guild, data.GuildId, isCached, + () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); + + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + + if (isCached) + { + var endpoint = data.Endpoint; + + //Only strip out the port if the endpoint contains it + var portBegin = endpoint.LastIndexOf(':'); + if (portBegin > 0) + endpoint = endpoint.Substring(0, portBegin); + + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + } + + } + break; + #endregion + + #region Invites + case "INVITE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) { - var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isCached = cachedMsg != null; - var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketGuildUser inviter = data.Inviter.IsSpecified + ? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value)) + : null; - cachedMsg?.RemoveReaction(reaction); + SocketUser target = data.TargetUser.IsSpecified + ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value)) + : null; - await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheable, channel, reaction).ConfigureAwait(false); + var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); + + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); } else { @@ -1234,20 +2175,21 @@ namespace Discord.WebSocket } } break; - case "MESSAGE_REACTION_REMOVE_ALL": + case "INVITE_DELETE": { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) { - var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isCached = cachedMsg != null; - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); - - cachedMsg?.ClearReactions(); + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); } else { @@ -1256,250 +2198,529 @@ namespace Discord.WebSocket } } break; - case "MESSAGE_DELETE_BULK": + #endregion + + #region Interactions + case "INTERACTION_CREATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var data = (payload as JToken).ToObject(_serializer); + + SocketChannel channel = null; + if(data.ChannelId.IsSpecified) + { + channel = State.GetChannel(data.ChannelId.Value); + } + else if (data.User.IsSpecified) + { + channel = State.GetDMChannel(data.User.Value.Id); + } + + if (channel == null) + { + var channelModel = await Rest.ApiClient.GetChannelAsync(data.ChannelId.Value); + + if (data.GuildId.IsSpecified) + channel = SocketTextChannel.Create(State.GetGuild(data.GuildId.Value), State, channelModel); + else + channel = (SocketChannel)SocketChannel.CreatePrivate(this, State, channelModel); + + State.AddChannel(channel); + } + + if (channel is ISocketMessageChannel textChannel) { var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + if (guild != null && !guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } - foreach (ulong id in data.Ids) - { - var msg = SocketChannelHelper.RemoveMessage(channel, this, id); - bool isCached = msg != null; - var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id)); - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); - } + var interaction = SocketInteraction.Create(this, data, channel as ISocketMessageChannel); + + await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false); + + switch (interaction) + { + case SocketSlashCommand slashCommand: + await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); + break; + case SocketMessageComponent messageComponent: + if(messageComponent.Data.Type == ComponentType.SelectMenu) + await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); + if(messageComponent.Data.Type == ComponentType.Button) + await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); + break; + case SocketUserCommand userCommand: + await TimedInvokeAsync(_userCommandExecuted, nameof(UserCommandExecuted), userCommand).ConfigureAwait(false); + break; + case SocketMessageCommand messageCommand: + await TimedInvokeAsync(_messageCommandExecuted, nameof(MessageCommandExecuted), messageCommand).ConfigureAwait(false); + break; + case SocketAutocompleteInteraction autocomplete: + await TimedInvokeAsync(_autocompleteExecuted, nameof(AutocompleteExecuted), autocomplete).ConfigureAwait(false); + break; + } + } + else + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + } + break; + case "APPLICATION_COMMAND_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.AddCommand(applicationCommand); + + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.AddCommand(applicationCommand); + + await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); + } + break; + case "APPLICATION_COMMAND_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + + var applicationCommand = SocketApplicationCommand.Create(this, data); + + State.RemoveCommand(applicationCommand.Id); + + await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); + } + break; + #endregion + + #region Threads + case "THREAD_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + SocketThreadChannel threadChannel = null; + + if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) + { + threadChannel.Update(State, data); + + if(data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + else + { + threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + } + + break; + case "THREAD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + var threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id); + var before = threadChannel != null + ? new Cacheable(threadChannel.Clone(), data.Id, true, () => Task.FromResult((SocketThreadChannel)null)) + : new Cacheable(null, data.Id, false, () => Task.FromResult((SocketThreadChannel)null)); + + if (threadChannel != null) + { + threadChannel.Update(State, data); + + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + else + { + //Thread is updated but was not cached, likely meaning the thread was unarchived. + threadChannel = (SocketThreadChannel)guild.AddChannel(State, data); + if (data.ThreadMember.IsSpecified) + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + } + + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false); + } + break; + case "THREAD_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + + var cacheable = new Cacheable(thread, data.Id, thread != null, null); + + await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); + } + break; + case "THREAD_LIST_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + foreach(var thread in data.Threads) + { + var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); + + if(entity == null) + { + entity = (SocketThreadChannel)guild.AddChannel(State, thread); + } + else + { + entity.Update(State, thread); + } + + foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) + { + var guildMember = guild.GetUser(member.Id.Value); + + entity.AddOrUpdateThreadMember(member, guildMember); + } + } + } + break; + case "THREAD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var thread = (SocketThreadChannel)State.GetChannel(data.Id.Value); + + if (thread == null) + { + await UnknownChannelAsync(type, data.Id.Value); + return; + } + + thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser); + } + + break; + case "THREAD_MEMBERS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - else + + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + + if(thread == null) { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id); return; } - } - break; - //Statuses - case "PRESENCE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + IReadOnlyCollection leftUsers = null; + IReadOnlyCollection joinUsers = null; - var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + if (data.RemovedMemberIds.IsSpecified) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); - return; - } - if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); + } - var user = guild.GetUser(data.User.Id); - if (user == null) + if (data.AddedMembers.IsSpecified) + { + List newThreadMembers = new List(); + foreach(var threadMember in data.AddedMembers.Value) { - if (data.Status == UserStatus.Offline) + SocketGuildUser guildMember; + + if (threadMember.Member.IsSpecified) { - return; + guildMember = guild.AddOrUpdateUser(threadMember.Member.Value); } - user = guild.AddOrUpdateUser(data); - } - else - { - var globalBefore = user.GlobalUser.Clone(); - if (user.GlobalUser.Update(State, data.User)) + else { - //Global data was updated, trigger UserUpdated - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + guildMember = guild.GetUser(threadMember.UserId.Value); } - } - var before = user.Clone(); - user.Update(State, data, true); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); - } - else - { - var globalUser = State.GetUser(data.User.Id); - if (globalUser == null) - { - await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); - return; + newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); } - var before = globalUser.Clone(); - globalUser.Update(State, data.User); - globalUser.Update(State, data); - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); + if (newThreadMembers.Any()) + joinUsers = newThreadMembers.ToImmutableArray(); } - } - break; - case "TYPING_START": - { - await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + if (leftUsers != null) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) + foreach(var threadUser in leftUsers) { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); } + } - var user = (channel as SocketChannel).GetUser(data.UserId); - if (user == null) + if(joinUsers != null) + { + foreach(var threadUser in joinUsers) { - if (guild != null) - user = guild.AddOrUpdateUser(data.Member); + await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); } - if (user != null) - await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), user, channel).ConfigureAwait(false); } } + break; + #endregion - //Users - case "USER_UPDATE": + #region Stage Channels + case "STAGE_INSTANCE_CREATE" or "STAGE_INSTANCE_UPDATE" or "STAGE_INSTANCE_DELETE": { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if(guild == null) { - var before = CurrentUser.Clone(); - CurrentUser.Update(State, data); - await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - else + + var stageChannel = guild.GetStageChannel(data.ChannelId); + + if(stageChannel == null) { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } - } - break; - //Voice - case "VOICE_STATE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + SocketStageChannel before = type == "STAGE_INSTANCE_UPDATE" ? stageChannel.Clone() : null; - var data = (payload as JToken).ToObject(_serializer); - SocketUser user; - SocketVoiceState before, after; - if (data.GuildId != null) + stageChannel.Update(data, type == "STAGE_INSTANCE_CREATE"); + + switch (type) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + case "STAGE_INSTANCE_CREATE": + await TimedInvokeAsync(_stageStarted, nameof(StageStarted), stageChannel).ConfigureAwait(false); return; - } - else if (!guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + case "STAGE_INSTANCE_DELETE": + await TimedInvokeAsync(_stageEnded, nameof(StageEnded), stageChannel).ConfigureAwait(false); return; - } - - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); - /*if (data.UserId == CurrentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - }*/ - } - else - { - before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - - // per g250k, this should always be sent, but apparently not always - user = guild.GetUser(data.UserId) - ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null); - if (user == null) - { - await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + case "STAGE_INSTANCE_UPDATE": + await TimedInvokeAsync(_stageUpdated, nameof(StageUpdated), before, stageChannel).ConfigureAwait(false); return; - } } - else + } + break; + #endregion + + #region Guild Scheduled Events + case "GUILD_SCHEDULED_EVENT_CREATE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild == null) { - var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel == null) - { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; - } - if (data.ChannelId != null) - { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); - } - else - { - before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - user = groupChannel.GetUser(data.UserId); - if (user == null) - { - await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); - return; - } + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); + var newEvent = guild.AddOrUpdateEvent(data); + + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false); } break; - case "VOICE_SERVER_UPDATE": + case "GUILD_SCHEDULED_EVENT_UPDATE": { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); - var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.GuildId); - var isCached = guild != null; - var cachedGuild = new Cacheable(guild, data.GuildId, isCached, - () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); - var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); - await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } - if (isCached) + var before = guild.GetEvent(data.Id); + + var beforeCacheable = new Cacheable(before, data.Id, before != null, () => Task.FromResult((SocketGuildEvent)null)); + + var after = guild.AddOrUpdateEvent(data); + + if((before != null ? before.Status != GuildScheduledEventStatus.Completed : true) && data.Status == GuildScheduledEventStatus.Completed) { - var endpoint = data.Endpoint; + await TimedInvokeAsync(_guildScheduledEventCompleted, nameof(GuildScheduledEventCompleted), after).ConfigureAwait(false); + } + else if((before != null ? before.Status != GuildScheduledEventStatus.Active : false) && data.Status == GuildScheduledEventStatus.Active) + { + await TimedInvokeAsync(_guildScheduledEventStarted, nameof(GuildScheduledEventStarted), after).ConfigureAwait(false); + } + else await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_DELETE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - //Only strip out the port if the endpoint contains it - var portBegin = endpoint.LastIndexOf(':'); - if (portBegin > 0) - endpoint = endpoint.Substring(0, portBegin); + var data = (payload as JToken).ToObject(_serializer); - var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; } - else + + var guildEvent = guild.RemoveEvent(data.Id) ?? SocketGuildEvent.Create(this, guild, data); + + await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false); + } + break; + case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE": + { + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if(guild == null) { await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var guildEvent = guild.GetEvent(data.EventId); + + if (guildEvent == null) + { + await UnknownGuildEventAsync(type, data.EventId, data.GuildId).ConfigureAwait(false); + return; } + var user = (SocketUser)guild.GetUser(data.UserId) ?? State.GetUser(data.UserId); + + var cacheableUser = new Cacheable(user, data.UserId, user != null, () => Rest.GetUserAsync(data.UserId)); + + switch (type) + { + case "GUILD_SCHEDULED_EVENT_USER_ADD": + await TimedInvokeAsync(_guildScheduledEventUserAdd, nameof(GuildScheduledEventUserAdd), cacheableUser, guildEvent).ConfigureAwait(false); + break; + case "GUILD_SCHEDULED_EVENT_USER_REMOVE": + await TimedInvokeAsync(_guildScheduledEventUserRemove, nameof(GuildScheduledEventUserRemove), cacheableUser, guildEvent).ConfigureAwait(false); + break; + } } break; - //Ignored (User only) + #endregion + + #region Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); break; @@ -1521,11 +2742,13 @@ namespace Discord.WebSocket case "WEBHOOKS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); break; + #endregion - //Others + #region Others default: await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); break; + #endregion } break; default: @@ -1538,6 +2761,7 @@ namespace Discord.WebSocket await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); } } + #endregion private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { @@ -1553,7 +2777,7 @@ namespace Discord.WebSocket { if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) { - _connection.Error(new Exception("Server missed last heartbeat")); + _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); return; } } @@ -1593,7 +2817,7 @@ namespace Discord.WebSocket try { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); - while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady)) await Task.Delay(500, cancelToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); } @@ -1622,40 +2846,52 @@ namespace Discord.WebSocket return guild; } internal SocketGuild RemoveGuild(ulong id) - { - var guild = State.RemoveGuild(id); - if (guild != null) - { - foreach (var _ in guild.Channels) - State.RemoveChannel(id); - foreach (var user in guild.Users) - user.GlobalUser.RemoveRef(this); - } - return guild; - } + => State.RemoveGuild(id); + /// Unexpected channel type is created. internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); - if (channel is SocketDMChannel dm) - dm.Recipient.GlobalUser.DMChannel = dm; - return channel; } + internal SocketDMChannel CreateDMChannel(ulong channelId, API.User model, ClientState state) + { + return SocketDMChannel.Create(this, state, channelId, model); + } + internal SocketDMChannel CreateDMChannel(ulong channelId, SocketUser user, ClientState state) + { + return new SocketDMChannel(this, channelId, user); + } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) { var channel = State.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { - if (channel is SocketDMChannel dmChannel) - dmChannel.Recipient.GlobalUser.DMChannel = null; - foreach (var recipient in channel.Recipients) recipient.GlobalUser.RemoveRef(this); } return channel; } + internal void RemoveDMChannels() + { + var channels = State.DMChannels; + State.PurgeDMChannels(); + foreach (var channel in channels) + channel.Recipient.GlobalUser.RemoveRef(this); + } + + internal void EnsureGatewayIntent(GatewayIntents intents) + { + if (!_gatewayIntents.HasFlag(intents)) + { + var vals = Enum.GetValues(typeof(GatewayIntents)).Cast(); + + var missingValues = vals.Where(x => intents.HasFlag(x) && !_gatewayIntents.HasFlag(x)); + + throw new InvalidOperationException($"Missing required gateway intent{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); + } + } private async Task GuildAvailableAsync(SocketGuild guild) { @@ -1743,8 +2979,8 @@ namespace Discord.WebSocket if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) { await _gatewayLogger.WarningAsync($"A {name} handler is blocking the gateway task.").ConfigureAwait(false); - await handlersTask.ConfigureAwait(false); //Ensure the handler completes } + await handlersTask.ConfigureAwait(false); //Ensure the handler completes } catch (Exception ex) { @@ -1797,6 +3033,12 @@ namespace Discord.WebSocket string details = $"{evnt} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); } + + private async Task UnknownGuildEventAsync(string evnt, ulong eventId, ulong guildId) + { + string details = $"{evnt} Event={eventId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Guild Event ({details}).").ConfigureAwait(false); + } private async Task UnsyncedGuildAsync(string evnt, ulong guildId) { string details = $"{evnt} Guild={guildId}"; @@ -1805,45 +3047,69 @@ namespace Discord.WebSocket internal int GetAudioId() => _nextAudioId++; - //IDiscordClient + #region IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetChannel(id)); + /// + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(DMChannels); + /// Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(GroupChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + /// + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => mode == CacheMode.AllowDownload ? await GetUserAsync(id, options).ConfigureAwait(false) : GetUser(id); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); + /// + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => await GetGlobalApplicationCommandAsync(id, options); + /// + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => await GetGlobalApplicationCommandsAsync(options); + /// async Task IDiscordClient.StartAsync() => await StartAsync().ConfigureAwait(false); + /// async Task IDiscordClient.StopAsync() => await StopAsync().ConfigureAwait(false); + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 3f9c18863..f0e6dc857 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -1,41 +1,196 @@ -using Discord.Net.Udp; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; namespace Discord.WebSocket { + /// + /// Represents a configuration class for . + /// + /// + /// This configuration, based on , helps determine several key configurations the + /// socket client depend on. For instance, shards and connection timeout. + /// + /// + /// The following config enables the message cache and configures the client to always download user upon guild + /// availability. + /// + /// var config = new DiscordSocketConfig + /// { + /// AlwaysDownloadUsers = true, + /// MessageCacheSize = 100 + /// }; + /// var client = new DiscordSocketClient(config); + /// + /// public class DiscordSocketConfig : DiscordRestConfig { + /// + /// Returns the encoding gateway should use. + /// public const string GatewayEncoding = "json"; - /// Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. + /// + /// Gets or sets the WebSocket host to connect to. If null, the client will use the + /// /gateway endpoint. + /// public string GatewayHost { get; set; } = null; - /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + /// + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + /// public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the id for this shard. Must be less than TotalShards. + /// + /// Gets or sets the ID for this shard. Must be less than . + /// public int? ShardId { get; set; } = null; - /// Gets or sets the total number of shards for this application. + + /// + /// Gets or sets the total number of shards for this application. + /// + /// + /// If this is left in a sharded client the bot will get the recommended shard + /// count from discord and use that. + /// public int? TotalShards { get; set; } = null; - /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. + /// + /// Gets or sets whether or not the client should download the default stickers on startup. + /// + /// + /// When this is set to default stickers arn't present and cannot be resolved by the client. + /// This will make all default stickers have the type of . + /// + public bool AlwaysDownloadDefaultStickers { get; set; } = false; + + /// + /// Gets or sets whether or not the client should automatically resolve the stickers sent on a message. + /// + /// + /// Note if a sticker isn't cached the client will preform a rest request to resolve it. This + /// may be very rest heavy depending on your bots size, it isn't recommended to use this with large scale bots as you + /// can get ratelimited easily. + /// + public bool AlwaysResolveStickers { get; set; } = false; + + /// + /// 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; } = 0; - /// - /// 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. + + /// + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY + /// packet. The maximum value allowed is 250. /// public int LargeThreshold { get; set; } = 250; - /// Gets or sets the provider used to generate new websocket connections. + /// + /// Gets or sets the provider used to generate new WebSocket connections. + /// public WebSocketProvider WebSocketProvider { get; set; } - /// Gets or sets the provider used to generate new udp sockets. + + /// + /// Gets or sets the provider used to generate new UDP sockets. + /// public UdpSocketProvider UdpSocketProvider { get; set; } - /// Gets or sets whether or not all users should be downloaded as guilds come available. + /// + /// Gets or sets whether or not all users should be downloaded as guilds come available. + /// + /// + /// + /// By default, the Discord gateway will only send offline members if a guild has less than a certain number + /// of members (determined by in this library). This behavior is why + /// sometimes a user may be missing from the WebSocket cache for collections such as + /// . + /// + /// + /// This property ensures that whenever a guild becomes available (determined by + /// ), incomplete user chunks will be + /// downloaded to the WebSocket cache. + /// + /// + /// For more information, please see + /// Request Guild Members + /// on the official Discord API documentation. + /// + /// + /// Please note that it can be difficult to fill the cache completely on large guilds depending on the + /// traffic. If you are using the command system, the default user TypeReader may fail to find the user + /// due to this issue. This may be resolved at v3 of the library. Until then, you may want to consider + /// overriding the TypeReader and use + /// + /// or + /// as a backup. + /// + /// public bool AlwaysDownloadUsers { get; set; } = false; - /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null disables this check. + + /// + /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. + /// Setting this property to nulldisables this check. + /// public int? HandlerTimeout { get; set; } = 3000; + /// + /// Gets or sets the maximum identify concurrency. + /// + /// + /// This information is provided by Discord. + /// It is only used when using a and auto-sharding is disabled. + /// + public int IdentifyMaxConcurrency { get; set; } = 1; + + /// + /// Gets or sets the maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY. + /// If zero, READY will fire as soon as it is received and all guilds will be unavailable. + /// + /// + /// This property is measured in milliseconds; negative values will throw an exception. + /// If a guild is not received before READY, it will be unavailable. + /// + /// + /// A representing the maximum wait time in milliseconds between GUILD_AVAILABLE events + /// before firing READY. + /// + /// Value must be at least 0. + public int MaxWaitBetweenGuildAvailablesBeforeReady + { + get + { + return maxWaitForGuildAvailable; + } + + set + { + Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady)); + maxWaitForGuildAvailable = value; + } + } + + private int maxWaitForGuildAvailable = 10000; + + /// + /// Gets or sets gateway intents to limit what events are sent from Discord. + /// The default is . + /// + /// + /// For more information, please see + /// GatewayIntents + /// on the official Discord API documentation. + /// + public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; + + /// + /// Gets or sets whether or not to log warnings related to guild intents and events. + /// + public bool LogGatewayIntentWarnings { get; set; } = true; + + /// + /// Initializes a new instance of the class with the default configuration. + /// public DiscordSocketConfig() { WebSocketProvider = DefaultWebSocketProvider.Instance; diff --git a/src/Discord.Net.WebSocket/DiscordSocketRestClient.cs b/src/Discord.Net.WebSocket/DiscordSocketRestClient.cs new file mode 100644 index 000000000..5107629a8 --- /dev/null +++ b/src/Discord.Net.WebSocket/DiscordSocketRestClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Discord.Rest; + +namespace Discord.WebSocket +{ + public class DiscordSocketRestClient : DiscordRestClient + { + internal DiscordSocketRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + + public new Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + public new Task LogoutAsync() + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LogoutInternalAsync() + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + } +} diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index 80dec0fd4..62d95402a 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS1591 using Discord.API; using Discord.API.Voice; using Discord.Net.Converters; @@ -16,8 +15,9 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class DiscordVoiceAPIClient + internal class DiscordVoiceAPIClient : IDisposable { + #region DiscordVoiceAPIClient public const int MaxBitrate = 128 * 1024; public const string Mode = "xsalsa20_poly1305"; @@ -36,7 +36,7 @@ namespace Discord.Audio private readonly AsyncEvent> _receivedPacketEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - + private readonly JsonSerializer _serializer; private readonly SemaphoreSlim _connectionLock; private readonly IUdpSocket _udp; @@ -103,8 +103,9 @@ namespace Discord.Audio if (disposing) { _connectCancelToken?.Dispose(); - (_udp as IDisposable)?.Dispose(); - (WebSocketClient as IDisposable)?.Dispose(); + _udp?.Dispose(); + WebSocketClient?.Dispose(); + _connectionLock?.Dispose(); } _isDisposed = true; } @@ -122,11 +123,12 @@ namespace Discord.Audio } public async Task SendAsync(byte[] data, int offset, int bytes) { - await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); + await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); } + #endregion - //WebSocket + #region WebSocket public async Task SendHeartbeatAsync(RequestOptions options = null) { await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); @@ -177,6 +179,7 @@ namespace Discord.Audio ConnectionState = ConnectionState.Connecting; try { + _connectCancelToken?.Dispose(); _connectCancelToken = new CancellationTokenSource(); var cancelToken = _connectCancelToken.Token; @@ -206,10 +209,12 @@ namespace Discord.Audio } private async Task DisconnectInternalAsync() { - if (ConnectionState == ConnectionState.Disconnected) return; + if (ConnectionState == ConnectionState.Disconnected) + return; ConnectionState = ConnectionState.Disconnecting; - - try { _connectCancelToken?.Cancel(false); } + + try + { _connectCancelToken?.Cancel(false); } catch { } //Wait for tasks to complete @@ -218,8 +223,9 @@ namespace Discord.Audio ConnectionState = ConnectionState.Disconnected; } + #endregion - //Udp + #region Udp public async Task SendDiscoveryAsync(uint ssrc) { var packet = new byte[70]; @@ -250,8 +256,9 @@ namespace Discord.Audio { _udp.SetDestination(ip, port); } + #endregion - //Helpers + #region Helpers private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); private string SerializeJson(object value) { @@ -267,5 +274,6 @@ namespace Discord.Audio using (JsonReader reader = new JsonTextReader(text)) return _serializer.Deserialize(reader); } + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7056a4df5..3cb978abf 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -1,5 +1,8 @@ -namespace Discord.WebSocket +namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based audio channel. + /// public interface ISocketAudioChannel : IAudioChannel { } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 5fef7e4cd..1103f8feb 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -5,27 +5,197 @@ using System.Threading.Tasks; namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based channel that can send and receive messages. + /// public interface ISocketMessageChannel : IMessageChannel { - /// Gets all messages in this channel's cache. + /// + /// Gets all messages in this channel's cache. + /// + /// + /// A read-only collection of WebSocket-based messages. + /// IReadOnlyCollection CachedMessages { get; } - /// Sends a message to this message channel. - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - - /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a message to this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message references to be included. Used to reply to specific messages. + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + /// Gets a cached message from this channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return null. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message from the local WebSocket cache and does not send any additional + /// request to Discord. This message may be a message that has been deleted. + /// + /// + /// The snowflake identifier of the message. + /// + /// A WebSocket-based message object; null if it does not exist in the cache or if caching is not + /// enabled. + /// SocketMessage GetCachedMessage(ulong id); - /// Gets the last N messages from this message channel. + /// + /// Gets the last N cached messages from this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. + + /// + /// Gets the last N cached messages starting from a certain message in this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The message ID to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. + /// + /// Gets the last N cached messages starting from a certain message in this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The message to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a read-only collection of pinned messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a read-only collection of messages found in the pinned messages. + /// new Task> GetPinnedMessagesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs index 4e91673dd..08da2237c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based channel that is private to select recipients. + /// public interface ISocketPrivateChannel : IPrivateChannel { new IReadOnlyCollection Recipients { get; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 74ca02dba..9c7dd4fbd 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -3,22 +3,32 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Discord.Audio; -using Discord.Rest; using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based category channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { + #region SocketCategoryChannel + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), ChannelPermission.ViewChannel)).ToImmutableArray(); + /// + /// Gets the child channels of this category. + /// + /// + /// A read-only collection of whose + /// matches the snowflake identifier of this category + /// channel. + /// public IReadOnlyCollection Channels => Guild.Channels.Where(x => x is INestedChannel nestedChannel && nestedChannel.CategoryId == Id).ToImmutableArray(); @@ -32,8 +42,10 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } + #endregion - //Users + #region Users + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -49,21 +61,24 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Name} ({Id}, Category)"; internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + #endregion - // IGuildChannel + #region IGuildChannel + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => throw new NotSupportedException(); - Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => throw new NotSupportedException(); + #endregion - //IChannel + #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 502e61d15..758ee9271 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -1,4 +1,3 @@ -using Discord.Rest; using System; using System.Collections.Generic; using System.Diagnostics; @@ -8,43 +7,66 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel { + #region SocketChannel + /// + /// Gets when the channel is created. + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets a collection of users from the WebSocket cache. + /// public IReadOnlyCollection Users => GetUsersInternal(); internal SocketChannel(DiscordSocketClient discord, ulong id) : base(discord, id) { } + + /// Unexpected channel type is created. internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) { - switch (model.Type) + return model.Type switch { - case ChannelType.DM: - return SocketDMChannel.Create(discord, state, model); - case ChannelType.Group: - return SocketGroupChannel.Create(discord, state, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } + ChannelType.DM => SocketDMChannel.Create(discord, state, model), + ChannelType.Group => SocketGroupChannel.Create(discord, state, model), + _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), + }; } internal abstract void Update(ClientState state, Model model); + #endregion - //User + #region User + /// + /// Gets a generic user from this channel. + /// + /// The snowflake identifier of the user. + /// + /// A generic WebSocket-based user associated with the snowflake identifier. + /// public SocketUser GetUser(ulong id) => GetUserInternal(id); internal abstract SocketUser GetUserInternal(ulong id); internal abstract IReadOnlyCollection GetUsersInternal(); + private string DebuggerDisplay => $"Unknown ({Id}, Channel)"; internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; + #endregion - //IChannel + #region IChannel + /// string IChannel.Name => null; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index ca53315aa..ccbf9b2b6 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -11,23 +11,11 @@ namespace Discord.WebSocket public static IAsyncEnumerable> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { - if (dir == Direction.Around) - throw new NotImplementedException(); //TODO: Impl - - IReadOnlyCollection cachedMessages = null; - IAsyncEnumerable> result = null; - if (dir == Direction.After && fromMessageId == null) return AsyncEnumerable.Empty>(); - if (dir == Direction.Before || mode == CacheMode.CacheOnly) - { - if (messages != null) //Cache enabled - cachedMessages = messages.GetMany(fromMessageId, dir, limit); - else - cachedMessages = ImmutableArray.Create(); - result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); - } + var cachedMessages = GetCachedMessages(channel, discord, messages, fromMessageId, dir, limit); + var result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); if (dir == Direction.Before) { @@ -38,18 +26,35 @@ namespace Discord.WebSocket //Download remaining messages ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options); - return result.Concat(downloadedMessages); + if (cachedMessages.Count != 0) + return result.Concat(downloadedMessages); + else + return downloadedMessages; } - else + else if (dir == Direction.After) { - if (mode == CacheMode.CacheOnly) + limit -= cachedMessages.Count; + if (mode == CacheMode.CacheOnly || limit <= 0) return result; - //Dont use cache in this case + //Download remaining messages + ulong maxId = cachedMessages.Count > 0 ? cachedMessages.Max(x => x.Id) : fromMessageId.Value; + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, maxId, dir, limit, options); + if (cachedMessages.Count != 0) + return result.Concat(downloadedMessages); + else + return downloadedMessages; + } + else //Direction.Around + { + if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count) + return result; + + //Cache isn't useful here since Discord will send them anyways return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options); } } - public static IReadOnlyCollection GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + public static IReadOnlyCollection GetCachedMessages(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, ulong? fromMessageId, Direction dir, int limit) { if (messages != null) //Cache enabled @@ -57,7 +62,7 @@ namespace Discord.WebSocket else return ImmutableArray.Create(); } - + /// Unexpected type. public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, SocketMessage msg) { @@ -65,20 +70,22 @@ namespace Discord.WebSocket { case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; + case SocketThreadChannel threadChannel: threadChannel.AddMessage(msg); break; case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; - default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); } } + /// Unexpected type. public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, ulong id) { - switch (channel) + return channel switch { - case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id); - case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id); - case SocketTextChannel textChannel: return textChannel.RemoveMessage(id); - default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); - } + SocketDMChannel dmChannel => dmChannel.RemoveMessage(id), + SocketGroupChannel groupChannel => groupChannel.RemoveMessage(id), + SocketTextChannel textChannel => textChannel.RemoveMessage(id), + _ => throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."), + }; } } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 9a8be9703..ea00c9e03 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -10,27 +10,34 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based direct-message channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel { - private readonly MessageCache _messages; + #region SocketDMChannel + /// + /// Gets the recipient of the channel. + /// + public SocketUser Recipient { get; } - public SocketUser Recipient { get; private set; } + /// + public IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + /// + /// Gets a collection that is the current logged-in user and the recipient. + /// public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketGlobalUser recipient) + internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketUser recipient) : base(discord, id) { Recipient = recipient; - recipient.GlobalUser.AddRef(); - if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); } internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateUser(state, model.Recipients.Value[0])); + var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); entity.Update(state, model); return entity; } @@ -38,60 +45,154 @@ namespace Discord.WebSocket { Recipient.Update(state, model.Recipients.Value[0]); } + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, ulong channelId, API.User recipient) + { + var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); + entity.Update(state, recipient); + return entity; + } + internal void Update(ClientState state, API.User recipient) + { + Recipient.Update(state, recipient); + } + /// public Task CloseAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + #endregion - //Messages + #region Messages + /// public SocketMessage GetCachedMessage(ulong id) - => _messages?.Get(id); + => null; + /// + /// Gets the message associated with the given . + /// + /// TThe ID of the message. + /// The options to be used when sending the request. + /// + /// The message gotten from either the cache or the download, or null if none is found. + /// public async Task GetMessageAsync(ulong id, RequestOptions options = null) { - IMessage msg = _messages?.Get(id); - if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); - return msg; + return await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + => ImmutableArray.Create(); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + => ImmutableArray.Create(); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + => ImmutableArray.Create(); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); internal void AddMessage(SocketMessage msg) - => _messages?.Add(msg); + { + } internal SocketMessage RemoveMessage(ulong id) - => _messages?.Remove(id); + { + return null; + } + #endregion - //Users + #region Users + /// + /// Gets a user in this channel from the provided . + /// + /// The snowflake identifier of the user. + /// + /// A object that is a recipient of this channel; otherwise null. + /// public new SocketUser GetUser(ulong id) { if (id == Recipient.Id) @@ -102,24 +203,38 @@ namespace Discord.WebSocket return null; } + /// + /// Returns the recipient user. + /// public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; + #endregion - //SocketChannel + #region SocketChannel + /// internal override IReadOnlyCollection GetUsersInternal() => Users; + /// internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + #endregion - //IDMChannel + #region IDMChannel + /// IUser IDMChannel.Recipient => Recipient; + #endregion - //ISocketPrivateChannel + #region ISocketPrivateChannel + /// IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + #endregion - //IPrivateChannel + #region IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + #endregion - //IMessageChannel + #region IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -127,29 +242,45 @@ namespace Discord.WebSocket else return GetCachedMessage(id); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(limit, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(fromMessageId, dir, limit, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(fromMessage.Id, dir, limit, options); + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); - - //IChannel + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion + + #region IChannel + /// string IChannel.Name => $"@{Recipient}"; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 74a1e3725..1bbfa6e97 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -14,19 +14,33 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based private group channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel { + #region SocketGroupChannel private readonly MessageCache _messages; private readonly ConcurrentDictionary _voiceStates; private string _iconId; private ConcurrentDictionary _users; + /// public string Name { get; private set; } + /// public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + + /// + /// Returns a collection representing all of the users in the group. + /// public new IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + + /// + /// Returns a collection representing all users in the group, not including the client. + /// public IReadOnlyCollection Recipients => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); @@ -34,7 +48,7 @@ namespace Discord.WebSocket : base(discord, id) { if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); + _messages = new MessageCache(Discord); _voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); } @@ -62,17 +76,34 @@ namespace Discord.WebSocket _users = users; } + /// public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + /// Voice is not yet supported for group channels. public Task ConnectAsync() { throw new NotSupportedException("Voice is not yet supported for group channels."); } +#endregion - //Messages + #region Messages + /// public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// public async Task GetMessageAsync(ulong id, RequestOptions options = null) { IMessage msg = _messages?.Get(id); @@ -80,37 +111,102 @@ namespace Discord.WebSocket msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); return msg; } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); @@ -118,8 +214,16 @@ namespace Discord.WebSocket => _messages?.Add(msg); internal SocketMessage RemoveMessage(ulong id) => _messages?.Remove(id); + #endregion - //Users + #region Users + /// + /// Gets a user from this group. + /// + /// The snowflake identifier of the user. + /// + /// A WebSocket-based group user associated with the snowflake identifier. + /// public new SocketGroupUser GetUser(ulong id) { if (_users.TryGetValue(id, out SocketGroupUser user)) @@ -147,8 +251,9 @@ namespace Discord.WebSocket } return null; } + #endregion - //Voice States + #region Voice States internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; @@ -169,21 +274,33 @@ namespace Discord.WebSocket return null; } + /// + /// Returns the name of the group. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}, Group)"; internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel; + #endregion - //SocketChannel + #region SocketChannel + /// internal override IReadOnlyCollection GetUsersInternal() => Users; + /// internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + #endregion - //ISocketPrivateChannel + #region ISocketPrivateChannel + /// IReadOnlyCollection ISocketPrivateChannel.Recipients => Recipients; + #endregion - //IPrivateChannel + #region IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => Recipients; + #endregion - //IMessageChannel + #region IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -191,33 +308,51 @@ namespace Discord.WebSocket else return GetCachedMessage(id); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion - //IAudioChannel + #region IAudioChannel + /// + /// Connecting to a group channel is not supported. Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); } + #endregion - //IChannel + #region IChannel + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index bfcffa35f..d38a8975b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -9,16 +9,35 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based guild channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildChannel : SocketChannel, IGuildChannel { + #region SocketGuildChannel private ImmutableArray _overwrites; + /// + /// Gets the guild associated with this channel. + /// + /// + /// A guild object that this channel belongs to. + /// public SocketGuild Guild { get; } + /// public string Name { get; private set; } - public int Position { get; private set; } - - public IReadOnlyCollection PermissionOverwrites => _overwrites; + /// + public int Position { get; private set; } + + /// + public virtual IReadOnlyCollection PermissionOverwrites => _overwrites; + /// + /// Gets a collection of users that are able to view the channel. + /// + /// + /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). + /// public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) @@ -28,37 +47,45 @@ namespace Discord.WebSocket } internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) { - switch (model.Type) + return model.Type switch { - case ChannelType.Text: - return SocketTextChannel.Create(guild, state, model); - case ChannelType.Voice: - return SocketVoiceChannel.Create(guild, state, model); - case ChannelType.Category: - return SocketCategoryChannel.Create(guild, state, model); - default: - // TODO: Proper implementation for channel categories - return new SocketGuildChannel(guild.Discord, model.Id, guild); - } + ChannelType.News => SocketNewsChannel.Create(guild, state, model), + ChannelType.Text => SocketTextChannel.Create(guild, state, model), + ChannelType.Voice => SocketVoiceChannel.Create(guild, state, model), + ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), + ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.Create(guild, state, model), + ChannelType.Stage => SocketStageChannel.Create(guild, state, model), + _ => new SocketGuildChannel(guild.Discord, model.Id, guild), + }; } + /// internal override void Update(ClientState state, Model model) { Name = model.Name.Value; - Position = model.Position.Value; - - var overwrites = model.PermissionOverwrites.Value; + Position = model.Position.GetValueOrDefault(0); + + var overwrites = model.PermissionOverwrites.GetValueOrDefault(new API.Overwrite[0]); var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + /// public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); - public OverwritePermissions? GetPermissionOverwrite(IUser user) + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) { for (int i = 0; i < _overwrites.Length; i++) { @@ -67,7 +94,14 @@ namespace Discord.WebSocket } return null; } - public OverwritePermissions? GetPermissionOverwrite(IRole role) + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) { for (int i = 0; i < _overwrites.Length; i++) { @@ -76,88 +110,119 @@ namespace Discord.WebSocket } return null; } - public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) + + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) { - await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); } - public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) + + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { - await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(perms.AllowValue, perms.DenyValue))); + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); } - public async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Length; i++) - { - if (_overwrites[i].TargetId == user.Id) - { - _overwrites = _overwrites.RemoveAt(i); - return; - } - } } - public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Length; i++) - { - if (_overwrites[i].TargetId == role.Id) - { - _overwrites = _overwrites.RemoveAt(i); - return; - } - } } - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public new virtual SocketGuildUser GetUser(ulong id) => null; + /// + /// Gets the name of the channel. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Guild)"; internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; +#endregion - //SocketChannel + #region SocketChannel + /// internal override IReadOnlyCollection GetUsersInternal() => Users; + /// internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + #endregion - //IGuildChannel + #region IGuildChannel + /// IGuild IGuildChannel.Guild => Guild; + /// ulong IGuildChannel.GuildId => Guild.Id; - async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => await GetInvitesAsync(options).ConfigureAwait(false); - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + #endregion - //IChannel + #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); //Overridden in Text/Voice + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs new file mode 100644 index 000000000..944dd2d7f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based news channel in a guild that has the same properties as a . + /// + /// + /// + /// The property is not supported for news channels. + /// + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketNewsChannel : SocketTextChannel, INewsChannel + { + internal SocketNewsChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + :base(discord, id, guild) + { + } + internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + /// + /// + /// + /// This property is not supported by this type. Attempting to use this property will result in a . + /// + /// + public override int SlowModeInterval + => throw new NotSupportedException("News channels do not support Slow Mode."); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs new file mode 100644 index 000000000..91bca5054 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -0,0 +1,158 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; +using StageInstance = Discord.API.StageInstance; + +namespace Discord.WebSocket +{ + /// + /// Represents a stage channel received over the gateway. + /// + public class SocketStageChannel : SocketVoiceChannel, IStageChannel + { + /// + public string Topic { get; private set; } + + /// + public StagePrivacyLevel? PrivacyLevel { get; private set; } + + /// + public bool? IsDiscoverableDisabled { get; private set; } + + /// + public bool IsLive { get; private set; } + + /// + /// Returns if the current user is a speaker within the stage, otherwise . + /// + public bool IsSpeaker + => !Guild.CurrentUser.IsSuppressed; + + /// + /// Gets a collection of users who are speakers within the stage. + /// + public IReadOnlyCollection Speakers + => Users.Where(x => !x.IsSuppressed).ToImmutableArray(); + + internal new SocketStageChannel Clone() => MemberwiseClone() as SocketStageChannel; + + internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) { } + + internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketStageChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + internal void Update(StageInstance model, bool isLive = false) + { + IsLive = isLive; + if (isLive) + { + Topic = model.Topic; + PrivacyLevel = model.PrivacyLevel; + IsDiscoverableDisabled = model.DiscoverableDisabled; + } + else + { + Topic = null; + PrivacyLevel = null; + IsDiscoverableDisabled = null; + } + } + + /// + public async Task StartStageAsync(string topic, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, RequestOptions options = null) + { + var args = new API.Rest.CreateStageInstanceParams + { + ChannelId = Id, + Topic = topic, + PrivacyLevel = privacyLevel + }; + + var model = await Discord.ApiClient.CreateStageInstanceAsync(args, options).ConfigureAwait(false); + + Update(model, true); + } + + /// + public async Task ModifyInstanceAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options); + + Update(model, true); + } + + /// + public async Task StopStageAsync(RequestOptions options = null) + { + await Discord.ApiClient.DeleteStageInstanceAsync(Id, options); + + Update(null); + } + + /// + public Task RequestToSpeakAsync(RequestOptions options = null) + { + var args = new API.Rest.ModifyVoiceStateParams + { + ChannelId = Id, + RequestToSpeakTimestamp = DateTimeOffset.UtcNow + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task BecomeSpeakerAsync(RequestOptions options = null) + { + var args = new API.Rest.ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = false + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task StopSpeakingAsync(RequestOptions options = null) + { + var args = new API.Rest.ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = true + }; + return Discord.ApiClient.ModifyMyVoiceState(Guild.Id, args, options); + } + + /// + public Task MoveToSpeakerAsync(IGuildUser user, RequestOptions options = null) + { + var args = new API.Rest.ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = false + }; + + return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args); + } + + /// + public Task RemoveFromSpeakerAsync(IGuildUser user, RequestOptions options = null) + { + var args = new API.Rest.ModifyVoiceStateParams + { + ChannelId = Id, + Suppressed = true + }; + + return Discord.ApiClient.ModifyUserVoiceState(Guild.Id, user.Id, args); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 2e9cd90be..aea1bfda5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -10,32 +10,58 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based channel in a guild that can send and receive messages. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel { + #region SocketTextChannel private readonly MessageCache _messages; + /// public string Topic { get; private set; } - public int SlowModeInterval { get; private set; } + /// + public virtual int SlowModeInterval { get; private set; } + /// public ulong? CategoryId { get; private set; } + /// + /// Gets the parent (category) of this channel in the guild's channel list. + /// + /// + /// An representing the parent of this channel; null if none is set. + /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// + public virtual Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); private bool _nsfw; + /// public bool IsNsfw => _nsfw; + /// public string Mention => MentionUtils.MentionChannel(Id); + /// public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), ChannelPermission.ViewChannel)).ToImmutableArray(); + /// + /// Gets a collection of threads within this text channel. + /// + public IReadOnlyCollection Threads + => Guild.ThreadChannels.Where(x => x.ParentChannel.Id == Id).ToImmutableArray(); + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); + _messages = new MessageCache(Discord); } internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) { @@ -47,17 +73,75 @@ namespace Discord.WebSocket { base.Update(state, model); CategoryId = model.CategoryId; - Topic = model.Topic.Value; + Topic = model.Topic.GetValueOrDefault(); SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet? _nsfw = model.Nsfw.GetValueOrDefault(); } - public Task ModifyAsync(Action func, RequestOptions options = null) + /// + public virtual Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); - //Messages + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + public async Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + { + var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); + + var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model); + + await thread.DownloadUsersAsync(); + + return thread; + } +#endregion + + #region Messages + /// public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// public async Task GetMessageAsync(ulong id, RequestOptions options = null) { IMessage msg = _messages?.Get(id); @@ -65,42 +149,112 @@ namespace Discord.WebSocket msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); return msg; } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, isSpoiler, embeds); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, component, stickers, options, embeds); + /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); @@ -108,8 +262,10 @@ namespace Discord.WebSocket => _messages?.Add(msg); internal SocketMessage RemoveMessage(ulong id) => _messages?.Remove(id); + #endregion - //Users + #region Users + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -122,33 +278,91 @@ namespace Discord.WebSocket } return null; } + #endregion - //Webhooks - public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + #region Webhooks + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// + public virtual Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); - public Task GetWebhookAsync(ulong id, RequestOptions options = null) + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + public virtual Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); - public Task> GetWebhooksAsync(RequestOptions options = null) + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// + public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); + #endregion + + #region Invites + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// + public virtual async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + #endregion - //ITextChannel + #region ITextChannel + /// async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) - => await CreateWebhookAsync(name, avatar, options); + => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); + /// + async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, bool? invitable, int? slowmode, RequestOptions options) + => await CreateThreadAsync(name, type, autoArchiveDuration, message, invitable, slowmode, options); + #endregion - //IGuildChannel + #region IGuildChannel + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion - //IMessageChannel + #region IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -156,27 +370,40 @@ namespace Discord.WebSocket else return GetCachedMessage(id); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) - => EnterTypingState(options); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component, ISticker[] stickers, Embed[] embeds) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component, stickers, embeds).ConfigureAwait(false); + #endregion - // INestedChannel + #region INestedChannel + /// Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Category); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs new file mode 100644 index 000000000..7fcafc14a --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -0,0 +1,339 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; +using ThreadMember = Discord.API.ThreadMember; +using System.Collections.Concurrent; + +namespace Discord.WebSocket +{ + /// + /// Represents a thread channel inside of a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketThreadChannel : SocketTextChannel, IThreadChannel + { + /// + public ThreadType Type { get; private set; } + + /// + /// Gets the owner of the current thread. + /// + public SocketThreadUser Owner { get; private set; } + + /// + /// Gets the current users within this thread. + /// + public SocketThreadUser CurrentUser + => Users.FirstOrDefault(x => x.Id == Discord.CurrentUser.Id); + + /// + public bool HasJoined { get; private set; } + + /// + /// if this thread is private, otherwise + /// + public bool IsPrivateThread + => Type == ThreadType.PrivateThread; + + /// + /// Gets the parent channel this thread resides in. + /// + public SocketTextChannel ParentChannel { get; private set; } + + /// + public int MessageCount { get; private set; } + + /// + public int MemberCount { get; private set; } + + /// + public bool IsArchived { get; private set; } + + /// + public DateTimeOffset ArchiveTimestamp { get; private set; } + + /// + public ThreadArchiveDuration AutoArchiveDuration { get; private set; } + + /// + public bool IsLocked { get; private set; } + + /// + /// Gets a collection of cached users within this thread. + /// + public new IReadOnlyCollection Users => + _members.Values.ToImmutableArray(); + + private readonly ConcurrentDictionary _members; + + private string DebuggerDisplay => $"{Name} ({Id}, Thread)"; + + private bool _usersDownloaded; + + private readonly object _downloadLock = new object(); + + internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) + : base(discord, id, guild) + { + ParentChannel = parent; + _members = new ConcurrentDictionary(); + } + + internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) + { + var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + Type = (ThreadType)model.Type; + MessageCount = model.MessageCount.GetValueOrDefault(-1); + MemberCount = model.MemberCount.GetValueOrDefault(-1); + + if (model.ThreadMetadata.IsSpecified) + { + IsArchived = model.ThreadMetadata.Value.Archived; + ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; + AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; + IsLocked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); + } + + if (model.OwnerId.IsSpecified) + { + Owner = GetUser(model.OwnerId.Value); + } + + HasJoined = model.ThreadMember.IsSpecified; + } + + internal IReadOnlyCollection RemoveUsers(ulong[] users) + { + List threadUsers = new(); + + foreach (var userId in users) + { + if (_members.TryRemove(userId, out var user)) + threadUsers.Add(user); + } + + return threadUsers.ToImmutableArray(); + } + + internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuildUser guildMember) + { + if (_members.TryGetValue(model.UserId.Value, out SocketThreadUser member)) + member.Update(model); + else + { + member = SocketThreadUser.Create(Guild, this, model, guildMember); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + } + return member; + } + + /// + public new SocketThreadUser GetUser(ulong id) + { + var user = Users.FirstOrDefault(x => x.Id == id); + return user; + } + + /// + /// Gets all users inside this thread. + /// + /// + /// If all users are not downloaded then this method will call and return the result. + /// + /// The options to be used when sending the request. + /// A task representing the download operation. + public async Task> GetUsersAsync(RequestOptions options = null) + { + // download all users if we havent + if (!_usersDownloaded) + { + await DownloadUsersAsync(options); + _usersDownloaded = true; + } + + return Users; + } + + /// + /// Downloads all users that have access to this thread. + /// + /// The options to be used when sending the request. + /// A task representing the asynchronous download operation. + public async Task DownloadUsersAsync(RequestOptions options = null) + { + var users = await Discord.ApiClient.ListThreadMembersAsync(Id, options); + + lock (_downloadLock) + { + foreach (var threadMember in users) + { + var guildUser = Guild.GetUser(threadMember.UserId.Value); + + AddOrUpdateThreadMember(threadMember, guildUser); + } + } + } + + internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel; + + /// + public Task JoinAsync(RequestOptions options = null) + => Discord.ApiClient.JoinThreadAsync(Id, options); + + /// + public Task LeaveAsync(RequestOptions options = null) + => Discord.ApiClient.LeaveThreadAsync(Id, options); + + /// + /// Adds a user to this thread. + /// + /// The to add. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of adding a member to a thread. + /// + public Task AddUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.AddThreadMemberAsync(Id, user.Id, options); + + /// + /// Removes a user from this thread. + /// + /// The to remove from this thread. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of removing a user from this thread. + /// + public Task RemoveUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.RemoveThreadMemberAsync(Id, user.Id, options); + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetInvitesAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IRole role) + => ParentChannel.GetPermissionOverwrite(role); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IUser user) + => ParentChannel.GetPermissionOverwrite(user); + + /// + /// + /// This method is not supported in threads. + /// + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetWebhooksAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task ModifyAsync(Action func, RequestOptions options = null) + => ThreadHelper.ModifyAsync(this, Discord, func, options); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override IReadOnlyCollection PermissionOverwrites + => throw new NotSupportedException("This method is not supported in threads."); + + /// + /// + /// This method is not supported in threads. + /// + public override Task SyncPermissionsAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + + string IChannel.Name => Name; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 8e6e09a9f..08b976bfe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -10,15 +10,42 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based voice channel in a guild. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel { + #region SocketVoiceChannel + /// public int Bitrate { get; private set; } + /// public int? UserLimit { get; private set; } + + /// public ulong? CategoryId { get; private set; } + /// + /// Gets the parent (category) channel of this channel. + /// + /// + /// A category channel representing the parent of this channel; null if none is set. + /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// + public string Mention => MentionUtils.MentionChannel(Id); + + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + /// + /// Gets a collection of users that are currently connected to this voice channel. + /// + /// + /// A read-only collection of users that are currently connected to this voice channel. + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); @@ -32,6 +59,7 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } + /// internal override void Update(ClientState state, Model model) { base.Update(state, model); @@ -40,17 +68,21 @@ namespace Discord.WebSocket UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + /// public async Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) { return await Guild.ConnectAudioAsync(Id, selfDeaf, selfMute, external).ConfigureAwait(false); } + /// public async Task DisconnectAsync() => await Guild.DisconnectAudioAsync(); + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -58,18 +90,42 @@ namespace Discord.WebSocket return user; return null; } +#endregion + + #region Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + /// + public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + #endregion - //IGuildChannel + #region IGuildChannel + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + #endregion - // INestedChannel + #region INestedChannel + /// Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Category); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 78ea4004a..03c655a34 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -18,54 +19,164 @@ using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; +using StickerModel = Discord.API.Sticker; +using EventModel = Discord.API.GuildScheduledEvent; +using System.IO; namespace Discord.WebSocket { - public class SocketGuild : SocketEntity, IGuild + /// + /// Represents a WebSocket-based guild object. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGuild : SocketEntity, IGuild, IDisposable { + #region SocketGuild +#pragma warning disable IDISP002, IDISP006 private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; - private ConcurrentHashSet _channels; + private ConcurrentDictionary _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; + private ConcurrentDictionary _stickers; + private ConcurrentDictionary _events; private ImmutableArray _emotes; - private ImmutableArray _features; + private AudioClient _audioClient; +#pragma warning restore IDISP002, IDISP006 + /// public string Name { get; private set; } + /// public int AFKTimeout { get; private set; } - public bool IsEmbeddable { get; private set; } + /// + public bool IsWidgetEnabled { get; private set; } + /// public VerificationLevel VerificationLevel { get; private set; } + /// public MfaLevel MfaLevel { get; private set; } + /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + /// + public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } + /// + /// Gets the number of members. + /// + /// + /// This property retrieves the number of members returned by Discord. + /// + /// + /// Due to how this property is returned by Discord instead of relying on the WebSocket cache, the + /// number here is the most accurate in terms of counting the number of users within this guild. + /// + /// + /// Use this instead of enumerating the count of the + /// collection, as you may see discrepancy + /// between that and this property. + /// + /// + /// public int MemberCount { get; internal set; } + /// Gets the number of members downloaded to the local guild cache. public int DownloadedMemberCount { get; private set; } internal bool IsAvailable { get; private set; } + /// Indicates whether the client is connected to this guild. public bool IsConnected { get; internal set; } + /// + public ulong? ApplicationId { get; internal set; } internal ulong? AFKChannelId { get; private set; } - internal ulong? EmbedChannelId { get; private set; } + internal ulong? WidgetChannelId { get; private set; } internal ulong? SystemChannelId { get; private set; } + internal ulong? RulesChannelId { get; private set; } + internal ulong? PublicUpdatesChannelId { get; private set; } + /// public ulong OwnerId { get; private set; } + /// Gets the user that owns this guild. public SocketGuildUser Owner => GetUser(OwnerId); + /// public string VoiceRegionId { get; private set; } + /// public string IconId { get; private set; } + /// public string SplashId { get; private set; } + /// + public string DiscoverySplashId { get; private set; } + /// + public PremiumTier PremiumTier { get; private set; } + /// + public string BannerId { get; private set; } + /// + public string VanityURLCode { get; private set; } + /// + public SystemChannelMessageDeny SystemChannelFlags { get; private set; } + /// + public string Description { get; private set; } + /// + public int PremiumSubscriptionCount { get; private set; } + /// + public string PreferredLocale { get; private set; } + /// + public int? MaxPresences { get; private set; } + /// + public int? MaxMembers { get; private set; } + /// + public int? MaxVideoChannelUsers { get; private set; } + /// + public NsfwLevel NsfwLevel { get; private set; } + /// + public CultureInfo PreferredCulture { get; private set; } + /// + public bool IsBoostProgressBarEnabled { get; private set; } + /// + public GuildFeatures Features { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); - public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; + /// + public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId); + /// + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); + /// Indicates whether the client has all the members downloaded to the local guild cache. + public bool HasAllMembers => MemberCount <= DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; + /// Indicates whether the guild cache is synced to this guild. public bool IsSynced => _syncPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; + /// + /// Gets the associated with this guild. + /// public IAudioClient AudioClient => _audioClient; + /// + /// Gets the default channel in this guild. + /// + /// + /// This property retrieves the first viewable text channel for this guild. + /// + /// This channel does not guarantee the user can send message to it, as it only looks for the first viewable + /// text channel. + /// + /// + /// + /// A representing the first viewable channel that the user has access to. + /// public SocketTextChannel DefaultChannel => TextChannels .Where(c => CurrentUser.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); + /// + /// Gets the AFK voice channel in this guild. + /// + /// + /// A that the AFK users will be moved to after they have idled for too + /// long; if none is set. + /// public SocketVoiceChannel AFKChannel { get @@ -74,14 +185,40 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketGuildChannel EmbedChannel + /// + public int MaxBitrate + { + get + { + return PremiumTier switch + { + PremiumTier.Tier1 => 128000, + PremiumTier.Tier2 => 256000, + PremiumTier.Tier3 => 384000, + _ => 96000, + }; + } + } + /// + /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// + /// A channel set within the server's widget settings; if none is set. + /// + public SocketGuildChannel WidgetChannel { get { - var id = EmbedChannelId; + var id = WidgetChannelId; return id.HasValue ? GetChannel(id.Value) : null; } } + /// + /// Gets the system channel where randomized welcome messages are sent in this guild. + /// + /// + /// A text channel where randomized welcome messages will be sent to; if none is set. + /// public SocketTextChannel SystemChannel { get @@ -90,34 +227,155 @@ namespace Discord.WebSocket return id.HasValue ? GetTextChannel(id.Value) : null; } } + /// + /// Gets the channel with the guild rules. + /// + /// + /// A text channel with the guild rules; if none is set. + /// + public SocketTextChannel RulesChannel + { + get + { + var id = RulesChannelId; + return id.HasValue ? GetTextChannel(id.Value) : null; + } + } + /// + /// Gets the channel where admins and moderators of Community guilds receive + /// notices from Discord. + /// + /// + /// A text channel where admins and moderators of Community guilds receive + /// notices from Discord; if none is set. + /// + public SocketTextChannel PublicUpdatesChannel + { + get + { + var id = PublicUpdatesChannelId; + return id.HasValue ? GetTextChannel(id.Value) : null; + } + } + /// + /// Gets a collection of all text channels in this guild. + /// + /// + /// A read-only collection of message channels found within this guild. + /// public IReadOnlyCollection TextChannels => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all voice channels in this guild. + /// + /// + /// A read-only collection of voice channels found within this guild. + /// public IReadOnlyCollection VoiceChannels => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all stage channels in this guild. + /// + /// + /// A read-only collection of stage channels found within this guild. + /// + public IReadOnlyCollection StageChannels + => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all category channels in this guild. + /// + /// + /// A read-only collection of category channels found within this guild. + /// public IReadOnlyCollection CategoryChannels => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all thread channels in this guild. + /// + /// + /// A read-only collection of thread channels found within this guild. + /// + public IReadOnlyCollection ThreadChannels + => Channels.OfType().ToImmutableArray(); + /// + /// Gets the current logged-in user. + /// public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; + /// + /// Gets the built-in role containing all users in this guild. + /// + /// + /// A role object that represents an @everyone role in this guild. + /// public SocketRole EveryoneRole => GetRole(Id); + /// + /// Gets a collection of all channels in this guild. + /// + /// + /// A read-only collection of generic channels found within this guild. + /// public IReadOnlyCollection Channels { get { var channels = _channels; var state = Discord.State; - return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); + return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); } } + /// public IReadOnlyCollection Emotes => _emotes; - public IReadOnlyCollection Features => _features; + /// + /// Gets a collection of all custom stickers for this guild. + /// + public IReadOnlyCollection Stickers + => _stickers.Select(x => x.Value).ToImmutableArray(); + /// + /// Gets a collection of users in this guild. + /// + /// + /// This property retrieves all users found within this guild. + /// + /// + /// This property may not always return all the members for large guilds (i.e. guilds containing + /// 100+ users). If you are simply looking to get the number of users present in this guild, + /// consider using instead. + /// + /// + /// Otherwise, you may need to enable to fetch + /// the full user list upon startup, or use to manually download + /// the users. + /// + /// + /// + /// + /// A collection of guild users found within this guild. + /// public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + /// + /// Gets a collection of all roles in this guild. + /// + /// + /// A read-only collection of roles found within this guild. + /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + /// + /// Gets a collection of all events within this guild. + /// + /// + /// This field is based off of caching alone, since there is no events returned on the guild model. + /// + /// + /// A read-only collection of guild events found within this guild. + /// + public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); + internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) { _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); - _features = ImmutableArray.Create(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { @@ -130,8 +388,10 @@ namespace Discord.WebSocket IsAvailable = !(model.Unavailable ?? false); if (!IsAvailable) { + if(_events == null) + _events = new ConcurrentDictionary(); if (_channels == null) - _channels = new ConcurrentHashSet(); + _channels = new ConcurrentDictionary(); if (_members == null) _members = new ConcurrentDictionary(); if (_roles == null) @@ -147,15 +407,23 @@ namespace Discord.WebSocket Update(state, model as Model); - var channels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05)); + var channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05)); { for (int i = 0; i < model.Channels.Length; i++) { var channel = SocketGuildChannel.Create(this, state, model.Channels[i]); state.AddChannel(channel); - channels.TryAdd(channel.Id); + channels.TryAdd(channel.Id, channel); + } + + for(int i = 0; i < model.Threads.Length; i++) + { + var threadChannel = SocketThreadChannel.Create(this, state, model.Threads[i]); + state.AddChannel(threadChannel); + channels.TryAdd(threadChannel.Id, threadChannel); } } + _channels = channels; var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); @@ -163,7 +431,8 @@ namespace Discord.WebSocket for (int i = 0; i < model.Members.Length; i++) { var member = SocketGuildUser.Create(this, state, model.Members[i]); - members.TryAdd(member.Id, member); + if (members.TryAdd(member.Id, member)) + member.GlobalUser.AddRef(); } DownloadedMemberCount = members.Count; @@ -189,6 +458,17 @@ namespace Discord.WebSocket } _voiceStates = voiceStates; + var events = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.GuildScheduledEvents.Length * 1.05)); + { + for (int i = 0; i < model.GuildScheduledEvents.Length; i++) + { + var guildEvent = SocketGuildEvent.Create(Discord, this, model.GuildScheduledEvents[i]); + events.TryAdd(guildEvent.Id, guildEvent); + } + } + _events = events; + + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); var _ = _syncPromise.TrySetResultAsync(true); @@ -198,19 +478,42 @@ namespace Discord.WebSocket internal void Update(ClientState state, Model model) { AFKChannelId = model.AFKChannelId; - EmbedChannelId = model.EmbedChannelId; + if (model.WidgetChannelId.IsSpecified) + WidgetChannelId = model.WidgetChannelId.Value; SystemChannelId = model.SystemChannelId; + RulesChannelId = model.RulesChannelId; + PublicUpdatesChannelId = model.PublicUpdatesChannelId; AFKTimeout = model.AFKTimeout; - IsEmbeddable = model.EmbedEnabled; + if (model.WidgetEnabled.IsSpecified) + IsWidgetEnabled = model.WidgetEnabled.Value; IconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; VoiceRegionId = model.Region; SplashId = model.Splash; + DiscoverySplashId = model.DiscoverySplash; VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; DefaultMessageNotifications = model.DefaultMessageNotifications; - + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; + PremiumTier = model.PremiumTier; + VanityURLCode = model.VanityURLCode; + BannerId = model.Banner; + SystemChannelFlags = model.SystemChannelFlags; + Description = model.Description; + PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); + NsfwLevel = model.NsfwLevel; + if (model.MaxPresences.IsSpecified) + MaxPresences = model.MaxPresences.Value ?? 25000; + if (model.MaxMembers.IsSpecified) + MaxMembers = model.MaxMembers.Value; + if (model.MaxVideoChannelUsers.IsSpecified) + MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value; + PreferredLocale = model.PreferredLocale; + PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale); + if (model.IsBoostProgressBarEnabled.IsSpecified) + IsBoostProgressBarEnabled = model.IsBoostProgressBarEnabled.Value; if (model.Emojis != null) { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); @@ -221,10 +524,7 @@ namespace Discord.WebSocket else _emotes = ImmutableArray.Create(); - if (model.Features != null) - _features = model.Features.ToImmutableArray(); - else - _features = ImmutableArray.Create(); + Features = model.Features; var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); if (model.Roles != null) @@ -236,8 +536,27 @@ namespace Discord.WebSocket } } _roles = roles; + + if (model.Stickers != null) + { + var stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Stickers.Length * 1.05)); + for (int i = 0; i < model.Stickers.Length; i++) + { + var sticker = model.Stickers[i]; + if (sticker.User.IsSpecified) + AddOrUpdateUser(sticker.User.Value); + + var entity = SocketCustomSticker.Create(Discord, sticker, this, sticker.User.IsSpecified ? sticker.User.Value.Id : null); + + stickers.TryAdd(sticker.Id, entity); + } + + _stickers = stickers; + } + else + _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } - internal void Update(ClientState state, GuildSyncModel model) + /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related { var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); { @@ -257,9 +576,9 @@ namespace Discord.WebSocket _members = members; var _ = _syncPromise.TrySetResultAsync(true); - /*if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true);*/ - } + //if (!model.Large) + // _ = _downloaderPromise.TrySetResultAsync(true); + }*/ internal void Update(ClientState state, EmojiUpdateModel model) { @@ -268,42 +587,92 @@ namespace Discord.WebSocket emotes.Add(model.Emojis[i].ToEntity()); _emotes = emotes.ToImmutable(); } + #endregion - //General + #region General + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); + /// + /// is . public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); - public Task ModifyEmbedAsync(Action func, RequestOptions options = null) - => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); + + /// + /// is . + public Task ModifyWidgetAsync(Action func, RequestOptions options = null) + => GuildHelper.ModifyWidgetAsync(this, Discord, func, options); + /// public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); + /// public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderRolesAsync(this, Discord, args, options); + /// public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); + #endregion - //Bans + #region Bans + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. + /// public Task GetBanAsync(IUser user, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; if the ban entry cannot be found. + /// public Task GetBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, userId, options); + /// public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + /// public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); + /// public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); + /// public Task RemoveBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, userId, options); + #endregion - //Channels + #region Channels + /// + /// Gets a channel in this guild. + /// + /// The snowflake identifier for the channel. + /// + /// A generic channel associated with the specified ; if none is found. + /// public SocketGuildChannel GetChannel(ulong id) { var channel = Discord.State.GetChannel(id) as SocketGuildChannel; @@ -311,38 +680,291 @@ namespace Discord.WebSocket return channel; return null; } + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// + /// A text channel associated with the specified ; if none is found. + /// public SocketTextChannel GetTextChannel(ulong id) => GetChannel(id) as SocketTextChannel; + /// + /// Gets a thread in this guild. + /// + /// The snowflake identifier for the thread. + /// + /// A thread channel associated with the specified ; if none is found. + /// + public SocketThreadChannel GetThreadChannel(ulong id) + => GetChannel(id) as SocketThreadChannel; + + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// + /// A voice channel associated with the specified ; if none is found. + /// public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; + /// + /// Gets a stage channel in this guild. + /// + /// The snowflake identifier for the stage channel. + /// + /// A stage channel associated with the specified ; if none is found. + /// + public SocketStageChannel GetStageChannel(ulong id) + => GetChannel(id) as SocketStageChannel; + /// + /// Gets a category channel in this guild. + /// + /// The snowflake identifier for the category channel. + /// + /// A category channel associated with the specified ; if none is found. + /// + public SocketCategoryChannel GetCategoryChannel(ulong id) + => GetChannel(id) as SocketCategoryChannel; + + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// var categories = await guild.GetCategoriesAsync(); + /// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); + /// if (targetCategory == null) return; + /// await Context.Guild.CreateTextChannelAsync(name, x => + /// { + /// x.CategoryId = targetCategory.Id; + /// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}."; + /// }); + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new voice channel in this guild. + /// + /// The new name for the voice channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// voice channel. + /// public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); - public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + + /// + /// Creates a new stage channel in this guild. + /// + /// The new name for the stage channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// stage channel. + /// + public Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new channel category in this guild. + /// + /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// category channel. + /// + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); - _channels.TryAdd(model.Id); + _channels.TryAdd(model.Id, channel); state.AddChannel(channel); return channel; } + + internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) + { + if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) + channel.Update(Discord.State, model); + else + { + channel = SocketGuildChannel.Create(this, Discord.State, model); + _channels[channel.Id] = channel; + state.AddChannel(channel); + } + return channel; + } + internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) { - if (_channels.TryRemove(id)) + if (_channels.TryRemove(id, out var _)) return state.RemoveChannel(id) as SocketGuildChannel; return null; } + internal void PurgeChannelCache(ClientState state) + { + foreach (var channelId in _channels) + state.RemoveChannel(channelId.Key); + + _channels.Clear(); + } + #endregion + + #region Voice Regions + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => GuildHelper.GetVoiceRegionsAsync(this, Discord, options); + #endregion - //Integrations + #region Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + #endregion + + #region Interactions + /// + /// Deletes all application commands in the current guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous delete operation. + /// + public Task DeleteApplicationCommandsAsync(RequestOptions options = null) + => InteractionHelper.DeleteAllGuildCommandsAsync(Discord, Id, options); + + /// + /// Gets a collection of slash commands created by the current user in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// slash commands created by the current user. + /// + public async Task> GetApplicationCommandsAsync(RequestOptions options = null) + { + var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id)); + + foreach (var command in commands) + { + Discord.State.AddCommand(command); + } + + return commands.ToImmutableArray(); + } + + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + public async ValueTask GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var command = Discord.State.GetCommand(id); + + if (command != null) + return command; + + if (mode == CacheMode.CacheOnly) + return null; - //Invites + var model = await Discord.ApiClient.GetGlobalApplicationCommandAsync(id, options); + + if (model == null) + return null; + + command = SocketApplicationCommand.Create(Discord, model, Id); + + Discord.State.AddCommand(command); + + return command; + } + + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + public async Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); + + var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); + + entity.Update(model); + + return entity; + } + + /// + /// Overwrites the application commands within this guild. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + /// + public async Task> BulkOverwriteApplicationCommandAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommandsAsync(Discord, Id, properties, options); + + var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); + + Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); + + foreach(var entity in entities) + { + Discord.State.AddCommand(entity); + } + + return entities.ToImmutableArray(); + } + #endregion + + #region Invites + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// public Task> GetInvitesAsync(RequestOptions options = null) => GuildHelper.GetInvitesAsync(this, Discord, options); /// @@ -350,21 +972,49 @@ namespace Discord.WebSocket /// /// The options to be used when sending the request. /// - /// A partial metadata of the vanity invite found within this guild. + /// A task that represents the asynchronous get operation. The task result contains the partial metadata of + /// the vanity invite found within this guild; if none is found. /// public Task GetVanityInviteAsync(RequestOptions options = null) => GuildHelper.GetVanityInviteAsync(this, Discord, options); + #endregion - //Roles + #region Roles + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; if none is found. + /// public SocketRole GetRole(ulong id) { if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } + + /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// is . + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); internal SocketRole AddRole(RoleModel model) { var role = SocketRole.Create(this, Discord.State, model); @@ -378,15 +1028,72 @@ namespace Discord.WebSocket return null; } - //Users + internal SocketRole AddOrUpdateRole(RoleModel model) + { + if (_roles.TryGetValue(model.Id, out SocketRole role)) + _roles[model.Id].Update(Discord.State, model); + else + role = AddRole(model); + + return role; + } + + internal SocketCustomSticker AddSticker(StickerModel model) + { + if (model.User.IsSpecified) + AddOrUpdateUser(model.User.Value); + + var sticker = SocketCustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); + _stickers[model.Id] = sticker; + return sticker; + } + + internal SocketCustomSticker AddOrUpdateSticker(StickerModel model) + { + if (_stickers.TryGetValue(model.Id, out SocketCustomSticker sticker)) + _stickers[model.Id].Update(model); + else + sticker = AddSticker(model); + + return sticker; + } + + internal SocketCustomSticker RemoveSticker(ulong id) + { + if (_stickers.TryRemove(id, out SocketCustomSticker sticker)) + return sticker; + return null; + } + #endregion + + #region Users + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// This may return in the WebSocket implementation due to incomplete user collection in + /// large guilds. + /// + /// + /// The snowflake identifier of the user. + /// + /// A guild user associated with the specified ; if none is found. + /// public SocketGuildUser GetUser(ulong id) { if (_members.TryGetValue(id, out SocketGuildUser member)) return member; return null; } - public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) - => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + /// + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); internal SocketGuildUser AddOrUpdateUser(UserModel model) { @@ -437,7 +1144,44 @@ namespace Discord.WebSocket } return null; } + internal void PurgeGuildUserCache() + { + var members = Users; + var self = CurrentUser; + _members.Clear(); + if (self != null) + _members.TryAdd(self.Id, self); + + _downloaderPromise = new TaskCompletionSource(); + DownloadedMemberCount = _members.Count; + + foreach (var member in members) + { + if (member.Id != self?.Id) + member.GlobalUser.RemoveRef(Discord); + } + } + + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild throught REST. + /// Users returned by this method are not cached. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + { + if (HasAllMembers) + return ImmutableArray.Create(Users).ToAsyncEnumerable>(); + return GuildHelper.GetUsersAsync(this, Discord, null, null, options); + } + /// public async Task DownloadUsersAsync() { await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); @@ -447,27 +1191,340 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } - //Audit logs - public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null) - => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users that the name or nickname starts with the provided at . + /// + public Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null) + => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); + #endregion + + #region Guild Events + + /// + /// Gets an event in this guild. + /// + /// The snowflake identifier for the event. + /// + /// An event that is associated with the specified ; if none is found. + /// + public SocketGuildEvent GetEvent(ulong id) + { + if (_events.TryGetValue(id, out SocketGuildEvent value)) + return value; + return null; + } + + internal SocketGuildEvent RemoveEvent(ulong id) + { + if (_events.TryRemove(id, out SocketGuildEvent value)) + return value; + return null; + } + + internal SocketGuildEvent AddOrUpdateEvent(EventModel model) + { + if (_events.TryGetValue(model.Id, out SocketGuildEvent value)) + value.Update(model); + else + { + value = SocketGuildEvent.Create(Discord, this, model); + _events[model.Id] = value; + } + return value; + } + + /// + /// Gets an event within this guild. + /// + /// The snowflake identifier for the event. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task GetEventAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetGuildEventAsync(Discord, id, this, options); + + /// + /// Gets all active events within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. + /// + public Task> GetEventsAsync(RequestOptions options = null) + => GuildHelper.GetGuildEventsAsync(Discord, this, options); + + /// + /// Creates an event within this guild. + /// + /// The name of the event. + /// The privacy level of the event. + /// The start time of the event. + /// The type of the event. + /// The description of the event. + /// The end time of the event. + /// + /// The channel id of the event. + /// + /// The event must have a type of or + /// in order to use this property. + /// + /// + /// A collection of speakers for the event. + /// The location of the event; links are supported + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. + /// + public Task CreateEventAsync( + string name, + DateTimeOffset startTime, + GuildScheduledEventType type, + GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, + string description = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string location = null, + RequestOptions options = null) + { + // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements + switch (type) + { + case GuildScheduledEventType.Stage: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ManageChannels | GuildPermission.MuteMembers | GuildPermission.MoveMembers); + break; + case GuildScheduledEventType.Voice: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ViewChannel | GuildPermission.Connect); + break; + case GuildScheduledEventType.External: + CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents); + break; + } + + return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + } + + + #endregion + + #region Audit logs + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The options to be used when sending the request. + /// The audit log entry ID to filter entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType); + #endregion - //Webhooks + #region Webhooks + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; if none is found. + /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); + #endregion - //Emotes + #region Emotes + /// + public Task> GetEmotesAsync(RequestOptions options = null) + => GuildHelper.GetEmotesAsync(this, Discord, options); + /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); + /// public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + /// + /// is . public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + /// public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); - //Voice States + /// + /// Moves the user to the voice channel. + /// + /// The user to move. + /// the channel where the user gets moved to. + /// A task that represents the asynchronous operation for moving a user. + public Task MoveAsync(IGuildUser user, IVoiceChannel targetChannel) + => user.ModifyAsync(x => x.Channel = new Optional(targetChannel)); + + /// + /// Disconnects the user from its current voice channel + /// + /// The user to disconnect. + /// A task that represents the asynchronous operation for disconnecting a user. + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + #endregion + + #region Stickers + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the sticker found with the + /// specified ; if none is found. + /// + public async ValueTask GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var sticker = _stickers.FirstOrDefault(x => x.Key == id); + + if (sticker.Value != null) + return sticker.Value; + + if (mode == CacheMode.CacheOnly) + return null; + + var model = await Discord.ApiClient.GetGuildStickerAsync(Id, id, options).ConfigureAwait(false); + + if (model == null) + return null; + + return AddOrUpdateSticker(model); + } + /// + /// Gets a specific sticker within this guild. + /// + /// The id of the sticker to get. + /// A sticker, if none is found then . + public SocketCustomSticker GetSticker(ulong id) + => GetStickerAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + /// + /// Gets a collection of all stickers within this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of stickers found within the guild. + /// + public async ValueTask> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, + RequestOptions options = null) + { + if (Stickers.Count > 0) + return Stickers; + + if (mode == CacheMode.CacheOnly) + return ImmutableArray.Create(); + + var models = await Discord.ApiClient.ListGuildStickersAsync(Id, options).ConfigureAwait(false); + + List stickers = new(); + + foreach (var model in models) + { + stickers.Add(AddOrUpdateSticker(model)); + } + + return stickers; + } + /// + /// Creates a new sticker in this guild. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image of the new emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, + RequestOptions options = null) + { + var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false); + + return AddOrUpdateSticker(model); + } + /// + /// Creates a new sticker in this guild + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The path of the file to upload. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public Task CreateStickerAsync(string name, string description, IEnumerable tags, string path, + RequestOptions options = null) + { + var fs = File.OpenRead(path); + return CreateStickerAsync(name, description, tags, fs, Path.GetFileName(fs.Name), options); + } + /// + /// Creates a new sticker in this guild + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The stream containing the file data. + /// The name of the file with the extension, ex: image.png. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created sticker. + /// + public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, + string filename, RequestOptions options = null) + { + var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, stream, filename, options).ConfigureAwait(false); + + return AddOrUpdateSticker(model); + } + /// + /// Deletes a sticker within this guild. + /// + /// The sticker to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions options = null) + => sticker.DeleteAsync(options); + #endregion + + #region Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; @@ -511,8 +1568,9 @@ namespace Discord.WebSocket } return null; } + #endregion - //Audio + #region Audio internal AudioInStream GetAudioStream(ulong userId) { return _audioClient?.GetInputStream(userId); @@ -530,9 +1588,11 @@ namespace Discord.WebSocket if (external) { +#pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(null); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); return null; +#pragma warning restore IDISP001 } if (_audioClient == null) @@ -555,10 +1615,14 @@ namespace Discord.WebSocket }; audioClient.Connected += () => { +#pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(_audioClient); +#pragma warning restore IDISP001 return Task.Delay(0); }; +#pragma warning disable IDISP003 _audioClient = audioClient; +#pragma warning restore IDISP003 } await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); @@ -576,7 +1640,7 @@ namespace Discord.WebSocket try { var timeoutTask = Task.Delay(15000); - if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) + if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask) throw new TimeoutException(); return await promise.Task.ConfigureAwait(false); } @@ -606,11 +1670,12 @@ namespace Discord.WebSocket if (_audioClient != null) await _audioClient.StopAsync().ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); + _audioClient?.Dispose(); _audioClient = null; } internal async Task FinishConnectAudio(string url, string token) { - //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up + //TODO: Mem Leak: Disconnected/Connected handlers aren't cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; await _audioLock.WaitAsync().ConfigureAwait(false); @@ -650,20 +1715,52 @@ namespace Discord.WebSocket } } + /// + /// Gets the name of the guild. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; + #endregion - //IGuild + #region IGuild + /// ulong? IGuild.AFKChannelId => AFKChannelId; + /// IAudioClient IGuild.AudioClient => null; + /// bool IGuild.Available => true; - ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; - ulong? IGuild.EmbedChannelId => EmbedChannelId; + /// + ulong? IGuild.WidgetChannelId => WidgetChannelId; + /// ulong? IGuild.SystemChannelId => SystemChannelId; + /// + ulong? IGuild.RulesChannelId => RulesChannelId; + /// + ulong? IGuild.PublicUpdatesChannelId => PublicUpdatesChannelId; + /// IRole IGuild.EveryoneRole => EveryoneRole; + /// IReadOnlyCollection IGuild.Roles => Roles; - + /// + int? IGuild.ApproximateMemberCount => null; + /// + int? IGuild.ApproximatePresenceCount => null; + /// + IReadOnlyCollection IGuild.Stickers => Stickers; + /// + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + /// + async Task IGuild.GetEventAsync(ulong id, RequestOptions options) + => await GetEventAsync(id, options).ConfigureAwait(false); + /// + async Task> IGuild.GetEventsAsync(RequestOptions options) + => await GetEventsAsync(options).ConfigureAwait(false); + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); /// @@ -673,71 +1770,182 @@ namespace Discord.WebSocket async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) => await GetBanAsync(userId, options).ConfigureAwait(false); + /// Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Channels); + /// Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(TextChannels); + /// Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetTextChannel(id)); + /// + Task IGuild.GetThreadChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetThreadChannel(id)); + /// + Task> IGuild.GetThreadChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(ThreadChannels); + /// Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); - Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) + /// + Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(CategoryChannels); + /// Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); + /// + Task IGuild.GetStageChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetStageChannel(id)); + /// + Task> IGuild.GetStageChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(StageChannels); + /// Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(AFKChannel); + /// Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); - Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(EmbedChannel); + /// + Task IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(WidgetChannel); + /// Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(SystemChannel); + /// + Task IGuild.GetRulesChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(RulesChannel); + /// + Task IGuild.GetPublicUpdatesChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(PublicUpdatesChannel); + /// async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + /// async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); - async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) - => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); + /// + async Task IGuild.CreateStageChannelAsync(string name, Action func, RequestOptions options) + => await CreateStageChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + /// async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); + /// async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + /// async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); /// async Task IGuild.GetVanityInviteAsync(RequestOptions options) => await GetVanityInviteAsync(options).ConfigureAwait(false); + /// IRole IGuild.GetRole(ulong id) => GetRole(id); + /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload && !HasAllMembers) + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return Users; + } - Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(Users); + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + /// Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); + /// Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); + /// + async Task> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await SearchUsersAsync(query, limit, options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } - async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options) + /// + async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, + ulong? beforeId, ulong? userId, ActionType? actionType) { if (cacheMode == CacheMode.AllowDownload) - return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + return (await GetAuditLogsAsync(limit, options, beforeId: beforeId, userId: userId, actionType: actionType).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } + /// async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); + /// + async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) + => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) + => await CreateStickerAsync(name, description, tags, image, options); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, string filename, RequestOptions options) + => await CreateStickerAsync(name, description, tags, stream, filename, options); + /// + async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, string path, RequestOptions options) + => await CreateStickerAsync(name, description, tags, path, options); + /// + async Task IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options) + => await GetStickerAsync(id, mode, options); + /// + async Task> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options) + => await GetStickersAsync(mode, options); + /// + Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) + => DeleteStickerAsync(_stickers[sticker.Id], options); + /// + async Task IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options) + => await GetApplicationCommandAsync(id, mode, options); + /// + async Task IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options) + => await CreateApplicationCommandAsync(properties, options); + /// + async Task> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options) + => await BulkOverwriteApplicationCommandAsync(properties, options); + + void IDisposable.Dispose() + { + DisconnectAudioAsync().GetAwaiter().GetResult(); + _audioLock?.Dispose(); + _audioClient?.Dispose(); + } + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs new file mode 100644 index 000000000..6974c0498 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -0,0 +1,216 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.GuildScheduledEvent; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based guild event. + /// + public class SocketGuildEvent : SocketEntity, IGuildScheduledEvent + { + /// + /// Gets the guild of the event. + /// + public SocketGuild Guild { get; private set; } + + /// + /// Gets the channel of the event. + /// + public SocketGuildChannel Channel { get; private set; } + + /// + /// Gets the user who created the event. + /// + public SocketGuildUser Creator { get; private set; } + + /// + public string Name { get; private set; } + + /// + public string Description { get; private set; } + + /// + public DateTimeOffset StartTime { get; private set; } + + /// + public DateTimeOffset? EndTime { get; private set; } + + /// + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; private set; } + + /// + public GuildScheduledEventStatus Status { get; private set; } + + /// + public GuildScheduledEventType Type { get; private set; } + + /// + public ulong? EntityId { get; private set; } + + /// + public string Location { get; private set; } + + /// + public int? UserCount { get; private set; } + + internal SocketGuildEvent(DiscordSocketClient client, SocketGuild guild, ulong id) + : base(client, id) + { + Guild = guild; + } + + internal static SocketGuildEvent Create(DiscordSocketClient client, SocketGuild guild, Model model) + { + var entity = new SocketGuildEvent(client, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.ChannelId.IsSpecified && model.ChannelId.Value != null) + { + Channel = Guild.GetChannel(model.ChannelId.Value.Value); + } + + if (model.CreatorId.IsSpecified) + { + var guildUser = Guild.GetUser(model.CreatorId.Value); + + if(guildUser != null) + { + if(model.Creator.IsSpecified) + guildUser.Update(Discord.State, model.Creator.Value); + + Creator = guildUser; + } + else if (guildUser == null && model.Creator.IsSpecified) + { + guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value); + Creator = guildUser; + } + } + + Name = model.Name; + Description = model.Description.GetValueOrDefault(); + + EntityId = model.EntityId; + Location = model.EntityMetadata?.Location.GetValueOrDefault(); + Type = model.EntityType; + + PrivacyLevel = model.PrivacyLevel; + EndTime = model.ScheduledEndTime; + StartTime = model.ScheduledStartTime; + Status = model.Status; + UserCount = model.UserCount.ToNullable(); + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteEventAsync(Discord, this, options); + + /// + public Task StartAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); + + /// + public Task EndAsync(RequestOptions options = null) + => ModifyAsync(x => x.Status = Status == GuildScheduledEventStatus.Scheduled + ? GuildScheduledEventStatus.Cancelled + : GuildScheduledEventStatus.Completed); + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyGuildEventAsync(Discord, func, this, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets a collection of users that are interested in this event. + /// + /// The amount of users to fetch. + /// The options to be used when sending the request. + /// + /// A read-only collection of users. + /// + public Task> GetUsersAsync(int limit = 100, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, limit, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that are interested in the event. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 300 users, and the constant + /// is 100, the request will be split into 3 individual requests; thus returning 3 individual asynchronous + /// responses, hence the need of flattening. + /// + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, null, null, options); + + /// + /// Gets a collection of N users interested in the event. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of users specified under around + /// the user depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 users, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// The ID of the starting user to get the users from. + /// The direction of the users to be gotten from. + /// The numbers of users to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + public IAsyncEnumerable> GetUsersAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxGuildEventUsersPerBatch, RequestOptions options = null) + => GuildHelper.GetEventUsersAsync(Discord, this, fromUserId, dir, limit, options); + + #region IGuildScheduledEvent + + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(RequestOptions options) + => GetUsersAsync(options); + /// + IAsyncEnumerable> IGuildScheduledEvent.GetUsersAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetUsersAsync(fromUserId, dir, limit, options); + /// + IGuild IGuildScheduledEvent.Guild => Guild; + /// + IUser IGuildScheduledEvent.Creator => Creator; + /// + ulong? IGuildScheduledEvent.ChannelId => Channel?.Id; + + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs new file mode 100644 index 000000000..0aa061439 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs @@ -0,0 +1,45 @@ +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketMessageCommand : SocketCommandBase, IMessageCommandInteraction, IDiscordInteraction + { + /// + /// The data associated with this interaction. + /// + public new SocketMessageCommandData Data { get; } + + internal SocketMessageCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketMessageCommandData.Create(client, dataModel, model.Id, guildId); + } + + internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketMessageCommand(client, model, channel); + entity.Update(model); + return entity; + } + + //IMessageCommandInteraction + /// + IMessageCommandInteractionData IMessageCommandInteraction.Data => Data; + + //IDiscordInteraction + /// + IDiscordInteractionData IDiscordInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommandData.cs new file mode 100644 index 000000000..71a30b44a --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommandData.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketMessageCommandData : SocketCommandBaseData, IMessageCommandInteractionData, IDiscordInteractionData + { + /// + /// Gets the message associated with this message command. + /// + public SocketMessage Message + => ResolvableData?.Messages.FirstOrDefault().Value; + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal SocketMessageCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + + internal new static SocketMessageCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketMessageCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + + //IMessageCommandInteractionData + /// + IMessage IMessageCommandInteractionData.Message => Message; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs new file mode 100644 index 000000000..40ee5b537 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs @@ -0,0 +1,45 @@ +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketUserCommand : SocketCommandBase, IUserCommandInteraction, IDiscordInteraction + { + /// + /// The data associated with this interaction. + /// + public new SocketUserCommandData Data { get; } + + internal SocketUserCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketUserCommandData.Create(client, dataModel, model.Id, guildId); + } + + internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketUserCommand(client, model, channel); + entity.Update(model); + return entity; + } + + //IUserCommandInteraction + /// + IUserCommandInteractionData IUserCommandInteraction.Data => Data; + + //IDiscordInteraction + /// + IDiscordInteractionData IDiscordInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommandData.cs new file mode 100644 index 000000000..eaebbcb06 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommandData.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketUserCommandData : SocketCommandBaseData, IUserCommandInteractionData, IDiscordInteractionData + { + /// + /// Gets the user who this command targets. + /// + public SocketUser Member + => (SocketUser)ResolvableData.GuildMembers.Values.FirstOrDefault() ?? ResolvableData.Users.Values.FirstOrDefault(); + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal SocketUserCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + + internal new static SocketUserCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketUserCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + + //IUserCommandInteractionData + /// + IUser IUserCommandInteractionData.User => Member; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs new file mode 100644 index 000000000..928a4302a --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -0,0 +1,436 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.MessageComponentInteractionData; +using Discord.Rest; +using System.Collections.Generic; +using Discord.Net.Rest; +using System.IO; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based interaction type for Message Components. + /// + public class SocketMessageComponent : SocketInteraction, IComponentInteraction, IDiscordInteraction + { + /// + /// Gets the data received with this interaction, contains the button that was clicked. + /// + public new SocketMessageComponentData Data { get; } + + /// + /// Gets the message that contained the trigger for this interaction. + /// + public SocketUserMessage Message { get; private set; } + + private object _lock = new object(); + public override bool HasResponded { get; internal set; } = false; + + internal SocketMessageComponent(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model.Id, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data = new SocketMessageComponentData(dataModel); + } + + internal new static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketMessageComponent(client, model, channel); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + if (model.Message.IsSpecified) + { + if (Message == null) + { + SocketUser author = null; + if (Channel is SocketGuildChannel channel) + { + if (model.Message.Value.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(channel.Guild, Discord.State, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); + else if (model.Message.Value.Author.IsSpecified) + author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); + } + else if (model.Message.Value.Author.IsSpecified) + author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); + + Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); + } + else + { + Message.Update(Discord.State, model.Message.Value); + } + } + } + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text ?? Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel(), + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + } + }; + + if (ephemeral) + response.Data.Value.Flags = MessageFlags.Ephemeral; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + /// + /// Updates the message which this component resides in with the type + /// + /// A delegate containing the properties to modify the message with. + /// The request options for this request. + /// A task that represents the asynchronous operation of updating the message. + public async Task UpdateAsync(Action func, RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed."); + } + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(Message.Content); + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || Message.Embeds.Any(); + + if (!hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue) + { + var allowedMentions = args.AllowedMentions.Value; + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) + && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) + && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false); + } + + /// + public override async Task FollowupWithFileAsync( + Stream fileStream, + string fileName, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); + Preconditions.NotNullOrWhitespace(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false); + } + + /// + public override async Task FollowupWithFileAsync( + string filePath, + string text = null, + string fileName = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNullOrWhitespace(filePath, nameof(filePath), "Path must exist"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options).ConfigureAwait(false); + } + + /// + /// Defers an interaction and responds with type 5 () + /// + /// to send this message ephemerally, otherwise . + /// The request options for this request. + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.Unspecified + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + /// + public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds of no response/acknowledgement"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredUpdateMessage, + Data = ephemeral ? new API.InteractionCallbackData { Flags = MessageFlags.Ephemeral } : Optional.Unspecified + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + //IComponentInteraction + /// + IComponentInteractionData IComponentInteraction.Data => Data; + + /// + IUserMessage IComponentInteraction.Message => Message; + + //IDiscordInteraction + /// + IDiscordInteractionData IDiscordInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs new file mode 100644 index 000000000..71e1d0395 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Model = Discord.API.MessageComponentInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data sent with a . + /// + public class SocketMessageComponentData : IComponentInteractionData + { + /// + /// Gets the components Custom Id that was clicked. + /// + public string CustomId { get; } + + /// + /// Gets the type of the component clicked. + /// + public ComponentType Type { get; } + + /// + /// Gets the value(s) of a interaction response. + /// + public IReadOnlyCollection Values { get; } + + internal SocketMessageComponentData(Model model) + { + CustomId = model.CustomId; + Type = model.ComponentType; + Values = model.Values.GetValueOrDefault(); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs new file mode 100644 index 000000000..5637cb6f0 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs @@ -0,0 +1,126 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.AutocompleteInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents a received over the gateway. + /// + public class SocketAutocompleteInteraction : SocketInteraction, IAutocompleteInteraction, IDiscordInteraction + { + /// + /// The autocomplete data of this interaction. + /// + public new SocketAutocompleteInteractionData Data { get; } + + public override bool HasResponded { get; internal set; } + private object _lock = new object(); + + internal SocketAutocompleteInteraction(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model.Id, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + if (dataModel != null) + Data = new SocketAutocompleteInteractionData(dataModel); + } + + internal new static SocketAutocompleteInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketAutocompleteInteraction(client, model, channel); + entity.Update(model); + return entity; + } + + /// + /// Responds to this interaction with a set of choices. + /// + /// + /// The set of choices for the user to pick from. + /// + /// A max of 20 choices are allowed. Passing for this argument will show the executing user that + /// there is no choices for their autocompleted input. + /// + /// + /// The request options for this response. + /// + /// A task that represents the asynchronous operation of responding to this interaction. + /// + public async Task RespondAsync(IEnumerable result, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + await InteractionHelper.SendAutocompleteResultAsync(Discord, result, Id, Token, options).ConfigureAwait(false); + lock (_lock) + { + HasResponded = true; + } + } + + /// + /// Responds to this interaction with a set of choices. + /// + /// The request options for this response. + /// + /// The set of choices for the user to pick from. + /// + /// A max of 20 choices are allowed. Passing for this argument will show the executing user that + /// there is no choices for their autocompleted input. + /// + /// + /// + /// A task that represents the asynchronous operation of responding to this interaction. + /// + public Task RespondAsync(RequestOptions options = null, params AutocompleteResult[] result) + => RespondAsync(result, options); + + /// + [Obsolete("Autocomplete interactions cannot be deferred!", true)] + public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have followups!", true)] + public override Task FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + /// + [Obsolete("Autocomplete interactions cannot have normal responses!", true)] + public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null) + => throw new NotSupportedException("Autocomplete interactions cannot be deferred!"); + + //IAutocompleteInteraction + /// + IAutocompleteInteractionData IAutocompleteInteraction.Data => Data; + + //IDiscordInteraction + /// + IDiscordInteractionData IDiscordInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteractionData.cs new file mode 100644 index 000000000..1d9803c02 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteractionData.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using DataModel = Discord.API.AutocompleteInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents data for a slash commands autocomplete interaction. + /// + public class SocketAutocompleteInteractionData : IAutocompleteInteractionData, IDiscordInteractionData + { + /// + /// Gets the name of the invoked command. + /// + public string CommandName { get; } + + /// + /// Gets the id of the invoked command. + /// + public ulong CommandId { get; } + + /// + /// Gets the type of the invoked command. + /// + public ApplicationCommandType Type { get; } + + /// + /// Gets the version of the invoked command. + /// + public ulong Version { get; } + + /// + /// Gets the current autocomplete option that is actively being filled out. + /// + public AutocompleteOption Current { get; } + + /// + /// Gets a collection of all the other options the executing users has filled out. + /// + public IReadOnlyCollection Options { get; } + + internal SocketAutocompleteInteractionData(DataModel model) + { + var options = model.Options.SelectMany(GetOptions); + + Current = options.FirstOrDefault(x => x.Focused); + Options = options.ToImmutableArray(); + + if (Options.Count == 1 && Current == null) + Current = Options.FirstOrDefault(); + + CommandName = model.Name; + CommandId = model.Id; + Type = model.Type; + Version = model.Version; + } + + private List GetOptions(API.AutocompleteInteractionDataOption model) + { + var options = new List(); + + options.Add(new AutocompleteOption(model.Type, model.Name, model.Value.GetValueOrDefault(null), model.Focused.GetValueOrDefault(false))); + + if (model.Options.IsSpecified) + { + options.AddRange(model.Options.Value.SelectMany(GetOptions)); + } + + return options; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs new file mode 100644 index 000000000..5343bb225 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs @@ -0,0 +1,45 @@ +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketSlashCommand : SocketCommandBase, ISlashCommandInteraction, IDiscordInteraction + { + /// + /// The data associated with this interaction. + /// + public new SocketSlashCommandData Data { get; } + + internal SocketSlashCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketSlashCommandData.Create(client, dataModel, guildId); + } + + internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketSlashCommand(client, model, channel); + entity.Update(model); + return entity; + } + + //ISlashCommandInteraction + /// + IApplicationCommandInteractionData ISlashCommandInteraction.Data => Data; + + //IDiscordInteraction + /// + IDiscordInteractionData IDiscordInteraction.Data => Data; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandData.cs new file mode 100644 index 000000000..c385ce825 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandData.cs @@ -0,0 +1,30 @@ +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketSlashCommandData : SocketCommandBaseData, IDiscordInteractionData + { + internal SocketSlashCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + + internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong? guildId) + { + var entity = new SocketSlashCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new SocketSlashCommandDataOption(this, x)).ToImmutableArray() + : ImmutableArray.Create(); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandDataOption.cs new file mode 100644 index 000000000..265eda75b --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommandDataOption.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionDataOption; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based received by the gateway. + /// + public class SocketSlashCommandDataOption : IApplicationCommandInteractionDataOption + { + #region SocketSlashCommandDataOption + /// + public string Name { get; private set; } + + /// + public object Value { get; private set; } + + /// + public ApplicationCommandOptionType Type { get; private set; } + + /// + /// The sub command options received for this sub command group. + /// + public IReadOnlyCollection Options { get; private set; } + + internal SocketSlashCommandDataOption() { } + internal SocketSlashCommandDataOption(SocketSlashCommandData data, Model model) + { + Name = model.Name; + Type = model.Type; + + if (model.Value.IsSpecified) + { + switch (Type) + { + case ApplicationCommandOptionType.User: + case ApplicationCommandOptionType.Role: + case ApplicationCommandOptionType.Channel: + case ApplicationCommandOptionType.Mentionable: + if (ulong.TryParse($"{model.Value.Value}", out var valueId)) + { + switch (Type) + { + case ApplicationCommandOptionType.User: + { + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; + + if (guildUser != null) + Value = guildUser; + else + Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; + } + break; + case ApplicationCommandOptionType.Channel: + Value = data.ResolvableData.Channels.FirstOrDefault(x => x.Key == valueId).Value; + break; + case ApplicationCommandOptionType.Role: + Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; + break; + case ApplicationCommandOptionType.Mentionable: + { + if (data.ResolvableData.GuildMembers.Any(x => x.Key == valueId) || data.ResolvableData.Users.Any(x => x.Key == valueId)) + { + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; + + if (guildUser != null) + Value = guildUser; + else + Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; + } + else if (data.ResolvableData.Roles.Any(x => x.Key == valueId)) + { + Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; + } + } + break; + default: + Value = model.Value.Value; + break; + } + } + break; + case ApplicationCommandOptionType.String: + Value = model.Value.ToString(); + break; + case ApplicationCommandOptionType.Integer: + { + if (model.Value.Value is long val) + Value = val; + else if (long.TryParse(model.Value.Value.ToString(), out long res)) + Value = res; + } + break; + case ApplicationCommandOptionType.Boolean: + { + if (model.Value.Value is bool val) + Value = val; + else if (bool.TryParse(model.Value.Value.ToString(), out bool res)) + Value = res; + } + break; + case ApplicationCommandOptionType.Number: + { + if (model.Value.Value is int val) + Value = val; + else if (double.TryParse(model.Value.Value.ToString(), out double res)) + Value = res; + } + break; + } + } + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => new SocketSlashCommandDataOption(data, x)).ToImmutableArray() + : ImmutableArray.Create(); + } + #endregion + + #region Converters + public static explicit operator bool(SocketSlashCommandDataOption option) + => (bool)option.Value; + public static explicit operator int(SocketSlashCommandDataOption option) + => (int)option.Value; + public static explicit operator string(SocketSlashCommandDataOption option) + => option.Value.ToString(); + #endregion + + #region IApplicationCommandInteractionDataOption + IReadOnlyCollection IApplicationCommandInteractionDataOption.Options + => Options; + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs new file mode 100644 index 000000000..d986a93f3 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -0,0 +1,116 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using GatewayModel = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based . + /// + public class SocketApplicationCommand : SocketEntity, IApplicationCommand + { + #region SocketApplicationCommand + /// + /// if this command is a global command, otherwise . + /// + public bool IsGlobalCommand + => Guild == null; + + /// + public ulong ApplicationId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public ApplicationCommandType Type { get; private set; } + + /// + public string Description { get; private set; } + + /// + public bool IsDefaultPermission { get; private set; } + + /// + /// A collection of 's for this command. + /// + /// + /// If the is not a slash command, this field will be an empty collection. + /// + public IReadOnlyCollection Options { get; private set; } + + /// + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(Id); + + /// + /// Returns the guild this command resides in, if this command is a global command then it will return + /// + public SocketGuild Guild + => GuildId.HasValue ? Discord.GetGuild(GuildId.Value) : null; + + private ulong? GuildId { get; set; } + + internal SocketApplicationCommand(DiscordSocketClient client, ulong id, ulong? guildId) + : base(client, id) + { + GuildId = guildId; + } + internal static SocketApplicationCommand Create(DiscordSocketClient client, GatewayModel model) + { + var entity = new SocketApplicationCommand(client, model.Id, model.GuildId.ToNullable()); + entity.Update(model); + return entity; + } + + internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model, ulong? guildId = null) + { + var entity = new SocketApplicationCommand(client, model.Id, guildId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + ApplicationId = model.ApplicationId; + Description = model.Description; + Name = model.Name; + IsDefaultPermission = model.DefaultPermissions.GetValueOrDefault(true); + Type = model.Type; + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() + : ImmutableArray.Create(); + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => InteractionHelper.DeleteUnknownApplicationCommandAsync(Discord, GuildId, this, options); + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + { + return ModifyAsync(func, options); + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + var command = IsGlobalCommand + ? await InteractionHelper.ModifyGlobalCommandAsync(Discord, this, func, options).ConfigureAwait(false) + : await InteractionHelper.ModifyGuildCommandAsync(Discord, this, GuildId.Value, func, options); + + Update(command); + } + #endregion + + #region IApplicationCommand + IReadOnlyCollection IApplicationCommand.Options => Options; + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs new file mode 100644 index 000000000..e70efa27b --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.ApplicationCommandOptionChoice; + +namespace Discord.WebSocket +{ + /// + /// Represents a choice for a . + /// + public class SocketApplicationCommandChoice : IApplicationCommandOptionChoice + { + /// + public string Name { get; private set; } + + /// + public object Value { get; private set; } + + internal SocketApplicationCommandChoice() { } + internal static SocketApplicationCommandChoice Create(Model model) + { + var entity = new SocketApplicationCommandChoice(); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + Value = model.Value; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs new file mode 100644 index 000000000..a19068d48 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandOption; + +namespace Discord.WebSocket +{ + /// + /// Represents an option for a . + /// + public class SocketApplicationCommandOption : IApplicationCommandOption + { + /// + public string Name { get; private set; } + + /// + public ApplicationCommandOptionType Type { get; private set; } + + /// + public string Description { get; private set; } + + /// + public bool? IsDefault { get; private set; } + + /// + public bool? IsRequired { get; private set; } + + /// + public double? MinValue { get; private set; } + + /// + public double? MaxValue { get; private set; } + + /// + /// Choices for string and int types for the user to pick from. + /// + public IReadOnlyCollection Choices { get; private set; } + + /// + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. + /// + public IReadOnlyCollection Options { get; private set; } + + /// + /// The allowed channel types for this option. + /// + public IReadOnlyCollection ChannelTypes { get; private set; } + + internal SocketApplicationCommandOption() { } + internal static SocketApplicationCommandOption Create(Model model) + { + var entity = new SocketApplicationCommandOption(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + Description = model.Description; + + IsDefault = model.Default.ToNullable(); + + IsRequired = model.Required.ToNullable(); + + MinValue = model.MinValue.ToNullable(); + + MaxValue = model.MaxValue.ToNullable(); + + Choices = model.Choices.IsSpecified + ? model.Choices.Value.Select(SocketApplicationCommandChoice.Create).ToImmutableArray() + : ImmutableArray.Create(); + + Options = model.Options.IsSpecified + ? model.Options.Value.Select(Create).ToImmutableArray() + : ImmutableArray.Create(); + + ChannelTypes = model.ChannelTypes.IsSpecified + ? model.ChannelTypes.Value.ToImmutableArray() + : ImmutableArray.Create(); + } + + IReadOnlyCollection IApplicationCommandOption.Choices => Choices; + IReadOnlyCollection IApplicationCommandOption.Options => Options; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs new file mode 100644 index 000000000..92303d488 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -0,0 +1,300 @@ +using Discord.Net.Rest; +using Discord.Rest; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Base class for User, Message, and Slash command interactions. + /// + public class SocketCommandBase : SocketInteraction + { + /// + /// Gets the name of the invoked command. + /// + public string CommandName + => Data.Name; + + /// + /// Gets the id of the invoked command. + /// + public ulong CommandId + => Data.Id; + + /// + /// The data associated with this interaction. + /// + internal new SocketCommandBaseData Data { get; } + + public override bool HasResponded { get; internal set; } + + private object _lock = new object(); + + internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model.Id, channel) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketCommandBaseData.Create(client, dataModel, model.Id, guildId); + } + + internal new static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketCommandBase(client, model, channel); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + var data = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + Data.Update(data); + + base.Update(model); + } + + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + TTS = isTTS, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + public override async Task FollowupWithFileAsync( + Stream fileStream, + string fileName, + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNull(fileStream, nameof(fileStream), "File Stream must have data"); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + public override async Task FollowupWithFileAsync( + string filePath, + string text = null, + string fileName = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); + + fileName ??= Path.GetFileName(filePath); + Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds.Select(x => x.ToModel()).ToArray(), + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + File = !string.IsNullOrEmpty(filePath) ? new MultipartFile(new MemoryStream(File.ReadAllBytes(filePath), false), fileName) : Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public override async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) + { + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot defer an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond or defer twice to the same interaction"); + } + } + + await Discord.Rest.ApiClient.CreateInteractionResponseAsync(response, Id, Token, options).ConfigureAwait(false); + + lock (_lock) + { + HasResponded = true; + } + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs new file mode 100644 index 000000000..cb2f01f5f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the base data tied with the interaction. + /// + public class SocketCommandBaseData : SocketEntity, IApplicationCommandInteractionData where TOption : IApplicationCommandInteractionDataOption + { + /// + public string Name { get; private set; } + + /// + /// The received with this interaction. + /// + public virtual IReadOnlyCollection Options { get; internal set; } + + internal readonly SocketResolvableData ResolvableData; + + private ApplicationCommandType Type { get; set; } + + internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model.Id) + { + Type = model.Type; + + if (model.Resolved.IsSpecified) + { + ResolvableData = new SocketResolvableData(client, guildId, model); + } + } + + internal static SocketCommandBaseData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketCommandBaseData(client, model, guildId); + entity.Update(model); + return entity; + } + + internal virtual void Update(Model model) + { + Name = model.Name; + } + + IReadOnlyCollection IApplicationCommandInteractionData.Options + => (IReadOnlyCollection)Options; + } + + /// + /// Represents the base data tied with the interaction. + /// + public class SocketCommandBaseData : SocketCommandBaseData + { + internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs new file mode 100644 index 000000000..c065637ca --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; + +namespace Discord.WebSocket +{ + internal class SocketResolvableData where T : API.IResolvable + { + internal readonly Dictionary GuildMembers + = new Dictionary(); + internal readonly Dictionary Users + = new Dictionary(); + internal readonly Dictionary Channels + = new Dictionary(); + internal readonly Dictionary Roles + = new Dictionary(); + + internal readonly Dictionary Messages + = new Dictionary(); + + internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T model) + { + var guild = guildId.HasValue ? discord.GetGuild(guildId.Value) : null; + + var resolved = model.Resolved.Value; + + if (resolved.Users.IsSpecified) + { + foreach (var user in resolved.Users.Value) + { + var socketUser = discord.GetOrCreateUser(discord.State, user.Value); + + Users.Add(ulong.Parse(user.Key), socketUser); + } + } + + if (resolved.Channels.IsSpecified) + { + foreach (var channel in resolved.Channels.Value) + { + SocketChannel socketChannel = guild != null + ? guild.GetChannel(channel.Value.Id) + : discord.GetChannel(channel.Value.Id); + + if (socketChannel == null) + { + var channelModel = guild != null + ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() + : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + } + + discord.State.AddChannel(socketChannel); + Channels.Add(ulong.Parse(channel.Key), socketChannel); + } + } + + if (resolved.Members.IsSpecified) + { + foreach (var member in resolved.Members.Value) + { + member.Value.User = resolved.Users.Value[member.Key]; + var user = guild.AddOrUpdateUser(member.Value); + GuildMembers.Add(ulong.Parse(member.Key), user); + } + } + + if (resolved.Roles.IsSpecified) + { + foreach (var role in resolved.Roles.Value) + { + var socketRole = guild.AddOrUpdateRole(role.Value); + Roles.Add(ulong.Parse(role.Key), socketRole); + } + } + + if (resolved.Messages.IsSpecified) + { + foreach (var msg in resolved.Messages.Value) + { + var channel = discord.GetChannel(msg.Value.ChannelId) as ISocketMessageChannel; + + SocketUser author; + if (guild != null) + { + if (msg.Value.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); + else + author = guild.GetUser(msg.Value.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id); + + if (channel == null) + { + if (!msg.Value.GuildId.IsSpecified) // assume it is a DM + { + channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); + } + } + + var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); + Messages.Add(message.Id, message); + } + } + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs new file mode 100644 index 000000000..f0465d336 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -0,0 +1,243 @@ +using Discord.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.Interaction; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using System.IO; + +namespace Discord.WebSocket +{ + /// + /// Represents an Interaction received over the gateway. + /// + public abstract class SocketInteraction : SocketEntity, IDiscordInteraction + { + #region SocketInteraction + /// + /// The this interaction was used in. + /// + public ISocketMessageChannel Channel { get; private set; } + + /// + /// The who triggered this interaction. + /// + public SocketUser User { get; private set; } + + /// + /// The type of this interaction. + /// + public InteractionType Type { get; private set; } + + /// + /// The token used to respond to this interaction. + /// + public string Token { get; private set; } + + /// + /// The data sent with this interaction. + /// + public IDiscordInteractionData Data { get; private set; } + + /// + /// The version of this interaction. + /// + public int Version { get; private set; } + + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + /// Gets whether or not this interaction has been responded to. + /// + /// + /// This property is locally set -- if you're running multiple bots + /// off the same token then this property won't be in sync with them. + /// + public abstract bool HasResponded { get; internal set; } + + /// + /// if the token is valid for replying to, otherwise . + /// + public bool IsValidToken + => InteractionHelper.CanRespondOrFollowup(this); + + internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel) + : base(client, id) + { + Channel = channel; + + CreatedAt = client.UseInteractionSnowflakeDate + ? SnowflakeUtils.FromSnowflake(Id) + : DateTime.UtcNow; + } + + internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + if (model.Type == InteractionType.ApplicationCommand) + { + var dataModel = model.Data.IsSpecified + ? (DataModel)model.Data.Value + : null; + + if (dataModel == null) + return null; + + return dataModel.Type switch + { + ApplicationCommandType.Slash => SocketSlashCommand.Create(client, model, channel), + ApplicationCommandType.Message => SocketMessageCommand.Create(client, model, channel), + ApplicationCommandType.User => SocketUserCommand.Create(client, model, channel), + _ => null + }; + } + + if (model.Type == InteractionType.MessageComponent) + return SocketMessageComponent.Create(client, model, channel); + + if (model.Type == InteractionType.ApplicationCommandAutocomplete) + return SocketAutocompleteInteraction.Create(client, model, channel); + + return null; + } + + internal virtual void Update(Model model) + { + Data = model.Data.IsSpecified + ? model.Data.Value + : null; + Token = model.Token; + Version = model.Version; + Type = model.Type; + + if (User == null) + { + if (model.Member.IsSpecified && model.GuildId.IsSpecified) + { + User = SocketGuildUser.Create(Discord.State.GetGuild(model.GuildId.Value), Discord.State, model.Member.Value); + } + else + { + User = SocketGlobalUser.Create(Discord, Discord.State, model.User.Value); + } + } + } + + /// + /// Responds to an Interaction with type . + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// Message content is too long, length must be less or equal to . + /// The parameters provided were invalid or the token was invalid. + public abstract Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, + bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public abstract Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public abstract Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Sends a followup message for this interaction. + /// + /// The text of the message to be sent. + /// The file to upload. + /// The file name of the attachment. + /// A array of embeds to send with this response. Max 10. + /// if the message should be read out by a text-to-speech reader, otherwise . + /// if the response should be hidden to everyone besides the invoker of the command, otherwise . + /// The allowed mentions for this response. + /// The request options for this response. + /// A to be sent with this response. + /// A single embed to send with this response. If this is passed alongside an array of embeds, the single embed will be ignored. + /// + /// The sent message. + /// + public abstract Task FollowupWithFileAsync(string filePath, string text = null, string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent component = null, Embed embed = null); + + /// + /// Gets the original response for this interaction. + /// + /// The request options for this request. + /// A that represents the initial response. + public Task GetOriginalResponseAsync(RequestOptions options = null) + => InteractionHelper.GetOriginalResponseAsync(Discord, Channel, this, options); + + /// + /// Edits original response for this interaction. + /// + /// A delegate containing the properties to modify the message with. + /// The request options for this request. + /// A that represents the initial response. + public async Task ModifyOriginalResponseAsync(Action func, RequestOptions options = null) + { + var model = await InteractionHelper.ModifyInteractionResponseAsync(Discord, Token, func, options); + return RestInteractionMessage.Create(Discord, model, Token, Channel); + } + + /// + /// Acknowledges this interaction. + /// + /// to send this message ephemerally, otherwise . + /// The request options for this request. + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public abstract Task DeferAsync(bool ephemeral = false, RequestOptions options = null); + + #endregion + + #region IDiscordInteraction + /// + async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, + RequestOptions options, MessageComponent component, Embed embed) + => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component, embed).ConfigureAwait(false); + + /// + async Task IDiscordInteraction.GetOriginalResponseAsync(RequestOptions options) + => await GetOriginalResponseAsync(options).ConfigureAwait(false); + + /// + async Task IDiscordInteraction.ModifyOriginalResponseAsync(Action func, RequestOptions options) + => await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false); + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs new file mode 100644 index 000000000..2b64e170e --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs @@ -0,0 +1,146 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Gateway.InviteCreateEvent; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based invite to a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketInvite : SocketEntity, IInviteMetadata + { + private long _createdAtTicks; + + /// + public ulong ChannelId { get; private set; } + /// + /// Gets the channel where this invite was created. + /// + public SocketGuildChannel Channel { get; private set; } + /// + public ulong? GuildId { get; private set; } + /// + /// Gets the guild where this invite was created. + /// + public SocketGuild Guild { get; private set; } + /// + ChannelType IInvite.ChannelType + { + get + { + return Channel switch + { + IVoiceChannel voiceChannel => ChannelType.Voice, + ICategoryChannel categoryChannel => ChannelType.Category, + IDMChannel dmChannel => ChannelType.DM, + IGroupChannel groupChannel => ChannelType.Group, + INewsChannel newsChannel => ChannelType.News, + ITextChannel textChannel => ChannelType.Text, + _ => throw new InvalidOperationException("Invalid channel type."), + }; + } + } + /// + string IInvite.ChannelName => Channel.Name; + /// + string IInvite.GuildName => Guild.Name; + /// + int? IInvite.PresenceCount => throw new NotImplementedException(); + /// + int? IInvite.MemberCount => throw new NotImplementedException(); + /// + public bool IsTemporary { get; private set; } + /// + int? IInviteMetadata.MaxAge { get => MaxAge; } + /// + int? IInviteMetadata.MaxUses { get => MaxUses; } + /// + int? IInviteMetadata.Uses { get => Uses; } + /// + /// Gets the time (in seconds) until the invite expires. + /// + public int MaxAge { get; private set; } + /// + /// Gets the max number of uses this invite may have. + /// + public int MaxUses { get; private set; } + /// + /// Gets the number of times this invite has been used. + /// + public int Uses { get; private set; } + /// + /// Gets the user that created this invite if available. + /// + public SocketGuildUser Inviter { get; private set; } + /// + DateTimeOffset? IInviteMetadata.CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + /// + /// Gets when this invite was created. + /// + public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + /// + /// Gets the user targeted by this invite if available. + /// + public SocketUser TargetUser { get; private set; } + /// + /// Gets the type of the user targeted by this invite. + /// + public TargetUserType TargetUserType { get; private set; } + + /// + public string Code => Id; + /// + public string Url => $"{DiscordConfig.InviteUrl}{Code}"; + + internal SocketInvite(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, string id) + : base(discord, id) + { + Guild = guild; + Channel = channel; + Inviter = inviter; + TargetUser = target; + } + internal static SocketInvite Create(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, Model model) + { + var entity = new SocketInvite(discord, guild, channel, inviter, target, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + ChannelId = model.ChannelId; + GuildId = model.GuildId.IsSpecified ? model.GuildId.Value : Guild.Id; + IsTemporary = model.Temporary; + MaxAge = model.MaxAge; + MaxUses = model.MaxUses; + Uses = model.Uses; + _createdAtTicks = model.CreatedAt.UtcTicks; + TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined; + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => InviteHelper.DeleteAsync(this, Discord, options); + + /// + /// Gets the URL of the invite. + /// + /// + /// A string that resolves to the Url of the invite. + /// + public override string ToString() => Url; + private string DebuggerDisplay => $"{Url} ({Guild?.Name} / {Channel.Name})"; + + /// + IGuild IInvite.Guild => Guild; + /// + IChannel IInvite.Channel => Channel; + /// + IUser IInvite.Inviter => Inviter; + /// + IUser IInvite.TargetUser => TargetUser; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 6ebd0fa1c..6baf56879 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -14,7 +14,7 @@ namespace Discord.WebSocket public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); - public MessageCache(DiscordSocketClient discord, IChannel channel) + public MessageCache(DiscordSocketClient discord) { _size = discord.MessageCacheSize; _messages = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(_size * 1.05)); @@ -44,6 +44,8 @@ namespace Discord.WebSocket return result; return null; } + + /// is less than 0. public IReadOnlyCollection GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); @@ -54,11 +56,23 @@ namespace Discord.WebSocket cachedMessageIds = _orderedMessages; else if (dir == Direction.Before) cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); - else + else if (dir == Direction.After) cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); + else //Direction.Around + { + if (!_messages.TryGetValue(fromMessageId.Value, out SocketMessage msg)) + return ImmutableArray.Empty; + int around = limit / 2; + var before = GetMany(fromMessageId, Direction.Before, around); + var after = GetMany(fromMessageId, Direction.After, around).Reverse(); + + return after.Concat(new SocketMessage[] { msg }).Concat(before).ToImmutableArray(); + } if (dir == Direction.Before) cachedMessageIds = cachedMessageIds.Reverse(); + if (dir == Direction.Around) //Only happens if fromMessageId is null, should only get "around" and itself (+1) + limit = limit / 2 + 1; return cachedMessageIds .Select(x => diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 5442c888a..4be9f4c5a 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Rest; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -8,27 +9,117 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message. + /// public abstract class SocketMessage : SocketEntity, IMessage { + #region SocketMessage private long _timestampTicks; - + private readonly List _reactions = new List(); + private ImmutableArray _userMentions = ImmutableArray.Create(); + + /// + /// Gets the author of this message. + /// + /// + /// A WebSocket-based user object. + /// public SocketUser Author { get; } + /// + /// Gets the source channel of the message. + /// + /// + /// A WebSocket-based message channel. + /// public ISocketMessageChannel Channel { get; } + /// public MessageSource Source { get; } + /// public string Content { get; private set; } + /// + public string CleanContent => MessageHelper.SanitizeMessage(this); + + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public virtual bool IsTTS => false; + /// public virtual bool IsPinned => false; + /// + public virtual bool IsSuppressed => false; + /// public virtual DateTimeOffset? EditedTimestamp => null; + /// + public virtual bool MentionedEveryone => false; + + /// + public MessageActivity Activity { get; private set; } + + /// + public MessageApplication Application { get; private set; } + + /// + public MessageReference Reference { get; private set; } + + /// + public IReadOnlyCollection Components { get; private set; } + + /// + /// Gets the interaction this message is a response to. + /// + public MessageInteraction Interaction { get; private set; } + + /// + public MessageFlags? Flags { get; private set; } + + /// + public MessageType Type { get; private set; } + + /// + /// Returns all attachments included in this message. + /// + /// + /// Collection of attachments. + /// public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + /// + /// Returns all embeds included in this message. + /// + /// + /// Collection of embed objects. + /// public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// + /// Returns the channels mentioned in this message. + /// + /// + /// Collection of WebSocket-based guild channels. + /// public virtual IReadOnlyCollection MentionedChannels => ImmutableArray.Create(); + /// + /// Returns the roles mentioned in this message. + /// + /// + /// Collection of WebSocket-based roles. + /// public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); - public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - + /// + public virtual IReadOnlyCollection Stickers => ImmutableArray.Create(); + /// + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); + /// + /// Returns the users mentioned in this message. + /// + /// + /// Collection of WebSocket-based users. + /// + public IReadOnlyCollection MentionedUsers => _userMentions; + /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) @@ -40,34 +131,220 @@ namespace Discord.WebSocket } internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { - if (model.Type == MessageType.Default) + if (model.Type == MessageType.Default || + model.Type == MessageType.Reply || + model.Type == MessageType.ApplicationCommand || + model.Type == MessageType.ThreadStarterMessage) return SocketUserMessage.Create(discord, state, author, channel, model); else return SocketSystemMessage.Create(discord, state, author, channel, model); } internal virtual void Update(ClientState state, Model model) { + Type = model.Type; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; if (model.Content.IsSpecified) + { Content = model.Content.Value; + } + + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.GetValueOrDefault() + }; + } + + if (model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + InternalChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + + if (model.Components.IsSpecified) + { + Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select(y => + { + switch (y.Type) + { + case ComponentType.Button: + { + var parsed = (API.ButtonComponent)y; + return new Discord.ButtonComponent( + parsed.Style, + parsed.Label.GetValueOrDefault(), + parsed.Emote.IsSpecified + ? parsed.Emote.Value.Id.HasValue + ? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault()) + : new Emoji(parsed.Emote.Value.Name) + : null, + parsed.CustomId.GetValueOrDefault(), + parsed.Url.GetValueOrDefault(), + parsed.Disabled.GetValueOrDefault()); + } + case ComponentType.SelectMenu: + { + var parsed = (API.SelectMenuComponent)y; + return new SelectMenuComponent( + parsed.CustomId, + parsed.Options.Select(z => new SelectMenuOption( + z.Label, + z.Value, + z.Description.GetValueOrDefault(), + z.Emoji.IsSpecified + ? z.Emoji.Value.Id.HasValue + ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) + : new Emoji(z.Emoji.Value.Name) + : null, + z.Default.ToNullable())).ToList(), + parsed.Placeholder.GetValueOrDefault(), + parsed.MinValues, + parsed.MaxValues, + parsed.Disabled + ); + } + default: + return null; + } + }).ToList())).ToImmutableArray(); + } + else + Components = new List(); + + if (model.UserMentions.IsSpecified) + { + var value = model.UserMentions.Value; + if (value.Length > 0) + { + var newMentions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + { + var val = value[i]; + if (val != null) + { + var user = Channel.GetUserAsync(val.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; + if (user != null) + newMentions.Add(user); + else + newMentions.Add(SocketUnknownUser.Create(Discord, state, val)); + } + } + _userMentions = newMentions.ToImmutable(); + } + } + + if (model.Interaction.IsSpecified) + { + Interaction = new MessageInteraction(model.Interaction.Value.Id, + model.Interaction.Value.Type, + model.Interaction.Value.Name, + SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User)); + } + + if (model.Flags.IsSpecified) + Flags = model.Flags.Value; } + /// public Task DeleteAsync(RequestOptions options = null) => MessageHelper.DeleteAsync(this, Discord, options); + /// + /// Gets the content of the message. + /// + /// + /// Content of the message. + /// public override string ToString() => Content; internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; +#endregion - //IMessage + #region IMessage + /// IUser IMessage.Author => Author; + /// IMessageChannel IMessage.Channel => Channel; - MessageType IMessage.Type => MessageType.Default; + /// IReadOnlyCollection IMessage.Attachments => Attachments; + /// IReadOnlyCollection IMessage.Embeds => Embeds; + /// IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + + /// + IReadOnlyCollection IMessage.Components => Components; + + /// + IMessageInteraction IMessage.Interaction => Interaction; + + /// + IReadOnlyCollection IMessage.Stickers => Stickers; + + + internal void AddReaction(SocketReaction reaction) + { + _reactions.Add(reaction); + } + internal void RemoveReaction(SocketReaction reaction) + { + if (_reactions.Contains(reaction)) + _reactions.Remove(reaction); + } + internal void ClearReactions() + { + _reactions.Clear(); + } + internal void RemoveReactionsForEmote(IEmote emote) + { + _reactions.RemoveAll(x => x.Emote.Equals(emote)); + } + + /// + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, userId, emote, Discord, options); + /// + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// + public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); + /// + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index e8fa17a35..32cac7d8b 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -1,14 +1,67 @@ -using Model = Discord.API.Gateway.Reaction; +using Model = Discord.API.Gateway.Reaction; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based reaction object. + /// public class SocketReaction : IReaction { + /// + /// Gets the ID of the user who added the reaction. + /// + /// + /// This property retrieves the snowflake identifier of the user responsible for this reaction. This + /// property will always contain the user identifier in event that + /// cannot be retrieved. + /// + /// + /// A user snowflake identifier associated with the user. + /// public ulong UserId { get; } + /// + /// Gets the user who added the reaction if possible. + /// + /// + /// + /// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from + /// the client. In other words, when the user is not in the WebSocket cache, this property may not + /// contain a value, leaving the only identifiable information to be + /// . + /// + /// + /// If you wish to obtain an identifiable user object, consider utilizing + /// which will attempt to retrieve the user from REST. + /// + /// + /// + /// A user object where possible; a value is not always returned. + /// + /// public Optional User { get; } + /// + /// Gets the ID of the message that has been reacted to. + /// + /// + /// A message snowflake identifier associated with the message. + /// public ulong MessageId { get; } + /// + /// Gets the message that has been reacted to if possible. + /// + /// + /// A WebSocket-based message where possible; a value is not always returned. + /// + /// public Optional Message { get; } + /// + /// Gets the channel where the reaction takes place in. + /// + /// + /// A WebSocket-based message channel. + /// public ISocketMessageChannel Channel { get; } + /// public IEmote Emote { get; } internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) @@ -30,6 +83,7 @@ namespace Discord.WebSocket return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } + /// public override bool Equals(object other) { if (other == null) return false; @@ -41,6 +95,7 @@ namespace Discord.WebSocket return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); } + /// public override int GetHashCode() { unchecked diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index e6c67159f..ec22a7703 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -1,13 +1,14 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message sent by the system. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { - public MessageType Type { get; private set; } - internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) : base(discord, id, channel, author, MessageSource.System) { @@ -21,8 +22,6 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - - Type = model.Type; } private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index bf817e008..e5776a089 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -9,32 +9,51 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message sent by a user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketUserMessage : SocketMessage, IUserMessage { - private readonly List _reactions = new List(); private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ImmutableArray _attachments; - private ImmutableArray _embeds; - private ImmutableArray _tags; - + private IUserMessage _referencedMessage; + private ImmutableArray _attachments = ImmutableArray.Create(); + private ImmutableArray _embeds = ImmutableArray.Create(); + private ImmutableArray _tags = ImmutableArray.Create(); + private ImmutableArray _roleMentions = ImmutableArray.Create(); + private ImmutableArray _stickers = ImmutableArray.Create(); + + /// public override bool IsTTS => _isTTS; + /// public override bool IsPinned => _isPinned; + /// + public override bool IsSuppressed => Flags.HasValue && Flags.Value.HasFlag(MessageFlags.SuppressEmbeds); + /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + /// + public override bool MentionedEveryone => _isMentioningEveryone; + /// public override IReadOnlyCollection Attachments => _attachments; + /// public override IReadOnlyCollection Embeds => _embeds; + /// public override IReadOnlyCollection Tags => _tags; + /// public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); - public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); - public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); - public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); + /// + public override IReadOnlyCollection MentionedRoles => _roleMentions; + /// + public override IReadOnlyCollection Stickers => _stickers; + /// + public IUserMessage ReferencedMessage => _referencedMessage; internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); @@ -45,6 +64,8 @@ namespace Discord.WebSocket { base.Update(state, model); + SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + if (model.IsTextToSpeech.IsSpecified) _isTTS = model.IsTextToSpeech.Value; if (model.Pinned.IsSpecified) @@ -53,6 +74,8 @@ namespace Discord.WebSocket _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; + if (model.RoleMentions.IsSpecified) + _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); if (model.Attachments.IsSpecified) { @@ -82,69 +105,108 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection - if (model.UserMentions.IsSpecified) + if (model.Content.IsSpecified) { - var value = model.UserMentions.Value; - if (value.Length > 0) + var text = model.Content.Value; + _tags = MessageHelper.ParseTags(text, Channel, guild, MentionedUsers); + model.Content = text; + } + + if (model.ReferencedMessage.IsSpecified && model.ReferencedMessage.Value != null) + { + var refMsg = model.ReferencedMessage.Value; + ulong? webhookId = refMsg.WebhookId.ToNullable(); + SocketUser refMsgAuthor = null; + if (refMsg.Author.IsSpecified) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) + if (guild != null) { - var val = value[i]; - if (val.Object != null) - newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); + if (webhookId != null) + refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value); + else + refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } - mentions = newMentions.ToImmutable(); + else + refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); + if (refMsgAuthor == null) + refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); } + else + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + refMsgAuthor = new SocketUnknownUser(Discord, id: 0); + _referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg); } - if (model.Content.IsSpecified) + if (model.StickerItems.IsSpecified) { - var text = model.Content.Value; - var guild = (Channel as SocketGuildChannel)?.Guild; - _tags = MessageHelper.ParseTags(text, Channel, guild, mentions); - model.Content = text; + var value = model.StickerItems.Value; + if (value.Length > 0) + { + var stickers = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + { + var stickerItem = value[i]; + SocketSticker sticker = null; + + if (guild != null) + sticker = guild.GetSticker(stickerItem.Id); + + if (sticker == null) + sticker = Discord.GetSticker(stickerItem.Id); + + // if they want to auto resolve + if (Discord.AlwaysResolveStickers) + { + sticker = Task.Run(async () => await Discord.GetStickerAsync(stickerItem.Id).ConfigureAwait(false)).GetAwaiter().GetResult(); + } + + // if its still null, create an unknown + if (sticker == null) + sticker = SocketUnknownSticker.Create(Discord, stickerItem); + + stickers.Add(sticker); + } + + _stickers = stickers.ToImmutable(); + } + else + _stickers = ImmutableArray.Create(); } } - internal void AddReaction(SocketReaction reaction) - { - _reactions.Add(reaction); - } - internal void RemoveReaction(SocketReaction reaction) - { - if (_reactions.Contains(reaction)) - _reactions.Remove(reaction); - } - internal void ClearReactions() - { - _reactions.Clear(); - } + /// + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . public Task ModifyAsync(Action func, RequestOptions options = null) => MessageHelper.ModifyAsync(this, Discord, func, options); - public Task AddReactionAsync(IEmote emote, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emote, Discord, options); - public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); - public Task RemoveAllReactionsAsync(RequestOptions options = null) - => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); - public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); - + /// public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); + /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is INewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index c366258cc..1e90b8f5c 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,6 +1,6 @@ -using Discord.Rest; +using Discord.Rest; using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -8,23 +8,58 @@ using Model = Discord.API.Role; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based role to be given to a guild user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketRole : SocketEntity, IRole { + #region SocketRole + /// + /// Gets the guild that owns this role. + /// + /// + /// A representing the parent guild of this role. + /// public SocketGuild Guild { get; } + /// public Color Color { get; private set; } + /// public bool IsHoisted { get; private set; } + /// public bool IsManaged { get; private set; } + /// public bool IsMentionable { get; private set; } + /// public string Name { get; private set; } + /// + public Emoji Emoji { get; private set; } + /// + public string Icon { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public int Position { get; private set; } + /// + public RoleTags Tags { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Returns a value that determines if the role is an @everyone role. + /// + /// + /// true if the role is @everyone; otherwise false. + /// public bool IsEveryone => Id == Guild.Id; + /// public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); - public IEnumerable Members + + /// + /// Returns an IEnumerable containing all that have this role. + /// + public IEnumerable Members => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) @@ -47,20 +82,48 @@ namespace Discord.WebSocket Position = model.Position; Color = new Color(model.Color); Permissions = new GuildPermissions(model.Permissions); + if (model.Tags.IsSpecified) + Tags = model.Tags.Value.ToEntity(); + + if (model.Icon.IsSpecified) + { + Icon = model.Icon.Value; + } + + if (model.Emoji.IsSpecified) + { + Emoji = new Emoji(model.Emoji.Value); + } } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => RoleHelper.ModifyAsync(this, Discord, func, options); + /// public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); + /// + public string GetIconUrl() + => CDN.GetGuildRoleIconUrl(Id, Icon); + + /// + /// Gets the name of the role. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketRole Clone() => MemberwiseClone() as SocketRole; + /// public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + #endregion - //IRole + #region IRole + /// IGuild IRole.Guild => Guild; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs index c8e14fb6c..f76694e6f 100644 --- a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs +++ b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord.WebSocket { @@ -6,6 +6,7 @@ namespace Discord.WebSocket where T : IEquatable { internal DiscordSocketClient Discord { get; } + /// public T Id { get; } internal SocketEntity(DiscordSocketClient discord, T id) diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketCustomSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketCustomSticker.cs new file mode 100644 index 000000000..6a5104012 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Stickers/SocketCustomSticker.cs @@ -0,0 +1,81 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Sticker; + +namespace Discord.WebSocket +{ + /// + /// Represents a custom sticker within a guild received over the gateway. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketCustomSticker : SocketSticker, ICustomSticker + { + #region SocketCustomSticker + /// + /// Gets the user that uploaded the guild sticker. + /// + /// + /// + /// This may return in the WebSocket implementation due to incomplete user collection in + /// large guilds, or the bot doesn't have the MANAGE_EMOJIS_AND_STICKERS permission. + /// + /// + public SocketGuildUser Author + => AuthorId.HasValue ? Guild.GetUser(AuthorId.Value) : null; + + /// + /// Gets the guild the sticker was created in. + /// + public SocketGuild Guild { get; } + + /// + public ulong? AuthorId { get; set; } + + internal SocketCustomSticker(DiscordSocketClient client, ulong id, SocketGuild guild, ulong? authorId = null) + : base(client, id) + { + Guild = guild; + AuthorId = authorId; + } + + internal static SocketCustomSticker Create(DiscordSocketClient client, Model model, SocketGuild guild, ulong? authorId = null) + { + var entity = new SocketCustomSticker(client, model.Id, guild, authorId); + entity.Update(model); + return entity; + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + if (!Guild.CurrentUser.GuildPermissions.Has(GuildPermission.ManageEmojisAndStickers)) + throw new InvalidOperationException($"Missing permission {nameof(GuildPermission.ManageEmojisAndStickers)}"); + + var model = await GuildHelper.ModifyStickerAsync(Discord, Guild.Id, this, func, options); + + Update(model); + } + + /// + public async Task DeleteAsync(RequestOptions options = null) + { + await GuildHelper.DeleteStickerAsync(Discord, Guild.Id, this, options); + Guild.RemoveSticker(Id); + } + + internal SocketCustomSticker Clone() => MemberwiseClone() as SocketCustomSticker; + + private new string DebuggerDisplay => Guild == null ? base.DebuggerDisplay : $"{Name} in {Guild.Name} ({Id})"; + #endregion + + #region ICustomSticker + ulong? ICustomSticker.AuthorId + => AuthorId; + + IGuild ICustomSticker.Guild + => Guild; + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketSticker.cs new file mode 100644 index 000000000..ee45720b5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Stickers/SocketSticker.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Model = Discord.API.Sticker; + +namespace Discord.WebSocket +{ + /// + /// Represents a general sticker received over the gateway. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketSticker : SocketEntity, ISticker + { + /// + public virtual ulong PackId { get; private set; } + + /// + public string Name { get; protected set; } + + /// + public virtual string Description { get; private set; } + + /// + public virtual IReadOnlyCollection Tags { get; private set; } + + /// + public virtual StickerType Type { get; private set; } + + /// + public StickerFormatType Format { get; protected set; } + + /// + public virtual bool? IsAvailable { get; protected set; } + + /// + public virtual int? SortOrder { get; private set; } + + /// + public string GetStickerUrl() + => CDN.GetStickerUrl(Id, Format); + + internal SocketSticker(DiscordSocketClient client, ulong id) + : base(client, id) { } + + internal static SocketSticker Create(DiscordSocketClient client, Model model) + { + var entity = model.GuildId.IsSpecified + ? new SocketCustomSticker(client, model.Id, client.GetGuild(model.GuildId.Value), model.User.IsSpecified ? model.User.Value.Id : null) + : new SocketSticker(client, model.Id); + + entity.Update(model); + return entity; + } + + internal virtual void Update(Model model) + { + Name = model.Name; + Description = model.Description; + PackId = model.PackId; + IsAvailable = model.Available; + Format = model.FormatType; + Type = model.Type; + SortOrder = model.SortValue; + + Tags = model.Tags.IsSpecified + ? model.Tags.Value.Split(',').Select(x => x.Trim()).ToImmutableArray() + : ImmutableArray.Create(); + } + + internal string DebuggerDisplay => $"{Name} ({Id})"; + + /// + public override bool Equals(object obj) + { + if (obj is Model stickerModel) + { + return stickerModel.Name == Name && + stickerModel.Description == Description && + stickerModel.FormatType == Format && + stickerModel.Id == Id && + stickerModel.PackId == PackId && + stickerModel.Type == Type && + stickerModel.SortValue == SortOrder && + stickerModel.Available == IsAvailable && + (!stickerModel.Tags.IsSpecified || stickerModel.Tags.Value == string.Join(", ", Tags)); + } + + return base.Equals(obj); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs new file mode 100644 index 000000000..ca7d2d0f1 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.StickerItem; + +namespace Discord.WebSocket +{ + /// + /// Represents an unknown sticker received over the gateway. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketUnknownSticker : SocketSticker + { + /// + public override IReadOnlyCollection Tags + => null; + + /// + public override string Description + => null; + + /// + public override ulong PackId + => 0; + /// + public override bool? IsAvailable + => null; + + /// + public override int? SortOrder + => null; + + /// + public new StickerType? Type + => null; + + internal SocketUnknownSticker(DiscordSocketClient client, ulong id) + : base(client, id) { } + + internal static SocketUnknownSticker Create(DiscordSocketClient client, Model model) + { + var entity = new SocketUnknownSticker(client, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Format = model.FormatType; + } + + /// + /// Attempts to try to find the sticker. + /// + /// + /// The sticker representing this unknown stickers Id, if none is found then . + /// + public Task ResolveAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + => Discord.GetStickerAsync(Id, mode, options); + + private new string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 3117eb14c..525ae0b34 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,7 +1,6 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Linq; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -12,7 +11,6 @@ namespace Discord.WebSocket public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - public SocketDMChannel DMChannel { get; internal set; } internal override SocketPresence Presence { get; set; } public override bool IsWebhook => false; @@ -48,13 +46,8 @@ namespace Discord.WebSocket discord.RemoveUser(Id); } } - - internal void Update(ClientState state, PresenceModel model) - { - Presence = SocketPresence.Create(model); - DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); - } - + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 8d1b360e3..fe19a41ec 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -1,20 +1,38 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using Model = Discord.API.User; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based group user. + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class SocketGroupUser : SocketUser, IGroupUser { + #region SocketGroupUser + /// + /// Gets the group channel of the user. + /// + /// + /// A representing the channel of which the user belongs to. + /// public SocketGroupChannel Channel { get; } + /// internal override SocketGlobalUser GlobalUser { get; } + /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + /// public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + /// public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + /// public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + /// internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + /// public override bool IsWebhook => false; internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) @@ -30,15 +48,29 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; + #endregion - //IVoiceState + #region IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + /// + DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 66af20bb6..ae3319227 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; @@ -12,40 +12,91 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based guild user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildUser : SocketUser, IGuildUser { + #region SocketGuildUser + private long? _premiumSinceTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; internal override SocketGlobalUser GlobalUser { get; } + /// + /// Gets the guild the user is in. + /// public SocketGuild Guild { get; } + /// public string Nickname { get; private set; } - + /// + public string GuildAvatarId { get; private set; } + /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + /// public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + /// public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + /// public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + + /// public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); internal override SocketPresence Presence { get; set; } + /// public override bool IsWebhook => false; + /// public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + /// public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + /// public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + /// public bool IsDeafened => VoiceState?.IsDeafened ?? false; + /// public bool IsMuted => VoiceState?.IsMuted ?? false; + /// + public bool IsStreaming => VoiceState?.IsStreaming ?? false; + /// + public DateTimeOffset? RequestToSpeakTimestamp => VoiceState?.RequestToSpeakTimestamp ?? null; + /// + public bool? IsPending { get; private set; } + + + /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - public IReadOnlyCollection Roles + /// + /// Returns a collection of roles that the user possesses. + /// + public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); + /// + /// Returns the voice channel the user is in, or null if none. + /// public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; + /// public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; + /// + /// Gets the voice connection status of the user if any. + /// + /// + /// A representing the user's voice status; null if the user is not + /// connected to a voice channel. + /// public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); public AudioInStream AudioStream => Guild.GetAudioStream(Id); + /// + public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); - /// The position of the user within the role hierarchy. - /// The returned value equal to the position of the highest role the user has, - /// or int.MaxValue if user is the server owner. + /// + /// Returns the position of the user within the role hierarchy. + /// + /// + /// The returned value equal to the position of the highest role the user has, or + /// if user is the server owner. + /// public int Hierarchy { get @@ -81,12 +132,16 @@ namespace Discord.WebSocket { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model); + if (!model.Roles.IsSpecified) + entity.UpdateRoles(new ulong[0]); return entity; } internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model, false); + if (!model.Roles.IsSpecified) + entity.UpdateRoles(new ulong[0]); return entity; } internal void Update(ClientState state, MemberModel model) @@ -96,21 +151,35 @@ namespace Discord.WebSocket _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Avatar.IsSpecified) + GuildAvatarId = model.Avatar.Value; if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.Pending.IsSpecified) + IsPending = model.Pending.Value; } internal void Update(ClientState state, PresenceModel model, bool updatePresence) { if (updatePresence) { - Presence = SocketPresence.Create(model); - GlobalUser.Update(state, model); + Update(model); } if (model.Nick.IsSpecified) Nickname = model.Nick.Value; if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + } + + internal override void Update(PresenceModel model) + { + Presence.Update(model); + GlobalUser.Update(model); } + private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); @@ -119,35 +188,59 @@ namespace Discord.WebSocket roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); } - + + /// public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); + /// public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) + => AddRolesAsync(new[] { roleId }, options); + /// public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRolesAsync(new[] { role }, options); + => AddRoleAsync(role.Id, options); + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roleIds, options); /// public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Discord, roles, options); + => AddRolesAsync(roles.Select(x => x.Id), options); + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) + => RemoveRolesAsync(new[] { roleId }, options); /// public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRolesAsync(new[] { role }, options); + => RemoveRoleAsync(role.Id, options); + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roleIds, options); /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + => RemoveRolesAsync(roles.Select(x => x.Id)); + /// public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + #endregion - //IGuildUser + #region IGuildUser + /// IGuild IGuildUser.Guild => Guild; + /// ulong IGuildUser.GuildId => Guild.Id; + /// IReadOnlyCollection IGuildUser.RoleIds => _roleIds; - + //IVoiceState + /// IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 7d7ba16ce..5250e15ad 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -1,28 +1,100 @@ -using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; using Model = Discord.API.Presence; namespace Discord.WebSocket { - //TODO: C#7 Candidate for record type + /// + /// Represents the WebSocket user's presence status. This may include their online status and their activity. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct SocketPresence : IPresence + public class SocketPresence : IPresence { - public UserStatus Status { get; } - public IActivity Activity { get; } + /// + public UserStatus Status { get; private set; } + /// + public IReadOnlyCollection ActiveClients { get; private set; } + /// + public IReadOnlyCollection Activities { get; private set; } - internal SocketPresence(UserStatus status, IActivity activity) + internal SocketPresence() { } + internal SocketPresence(UserStatus status, IImmutableSet activeClients, IImmutableList activities) { Status = status; - Activity= activity; + ActiveClients = activeClients ?? ImmutableHashSet.Empty; + Activities = activities ?? ImmutableList.Empty; } + internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game?.ToEntity()); + var entity = new SocketPresence(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Status = model.Status; + ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray.Empty; + Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray.Empty; + } + + /// + /// Creates a new containing all of the client types + /// where a user is active from the data supplied in the Presence update frame. + /// + /// + /// A dictionary keyed by the + /// and where the value is the . + /// + /// + /// A collection of all s that this user is active. + /// + private static IReadOnlyCollection ConvertClientTypesDict(IDictionary clientTypesDict) + { + if (clientTypesDict == null || clientTypesDict.Count == 0) + return ImmutableHashSet.Empty; + var set = new HashSet(); + foreach (var key in clientTypesDict.Keys) + { + if (Enum.TryParse(key, true, out ClientType type)) + set.Add(type); + // quietly discard ClientTypes that do not match + } + return set.ToImmutableHashSet(); + } + /// + /// Creates a new containing all the activities + /// that a user has from the data supplied in the Presence update frame. + /// + /// + /// A list of . + /// + /// + /// A list of all that this user currently has available. + /// + private static IImmutableList ConvertActivitiesList(IList activities) + { + if (activities == null || activities.Count == 0) + return ImmutableList.Empty; + var list = new List(); + foreach (var activity in activities) + list.Add(activity.ToEntity()); + return list.ToImmutableList(); } + /// + /// Gets the status of the user. + /// + /// + /// A string that resolves to . + /// public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; + private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; - internal SocketPresence Clone() => this; + internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index b7c02c2db..7b11257a3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -6,20 +6,38 @@ using Model = Discord.API.User; namespace Discord.WebSocket { + /// + /// Represents the logged-in WebSocket-based user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSelfUser : SocketUser, ISelfUser { + /// public string Email { get; private set; } + /// public bool IsVerified { get; private set; } + /// public bool IsMfaEnabled { get; private set; } internal override SocketGlobalUser GlobalUser { get; } + /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + /// public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + /// public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + /// public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + /// internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + /// + public UserProperties Flags { get; internal set; } + /// + public PremiumType PremiumType { get; internal set; } + /// + public string Locale { get; internal set; } + /// public override bool IsWebhook => false; internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) @@ -51,12 +69,29 @@ namespace Discord.WebSocket IsMfaEnabled = model.MfaEnabled.Value; hasGlobalChanges = true; } + if (model.Flags.IsSpecified && model.Flags.Value != Flags) + { + Flags = (UserProperties)model.Flags.Value; + hasGlobalChanges = true; + } + if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) + { + PremiumType = model.PremiumType.Value; + hasGlobalChanges = true; + } + if (model.Locale.IsSpecified && model.Locale.Value != Locale) + { + Locale = model.Locale.Value; + hasGlobalChanges = true; + } return hasGlobalChanges; } - + + /// public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs new file mode 100644 index 000000000..b2311dd7d --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.ThreadMember; +using System.Collections.Immutable; + +namespace Discord.WebSocket +{ + /// + /// Represents a thread user received over the gateway. + /// + public class SocketThreadUser : SocketUser, IGuildUser + { + /// + /// Gets the this user is in. + /// + public SocketThreadChannel Thread { get; private set; } + + /// + /// Gets the timestamp for when this user joined this thread. + /// + public DateTimeOffset ThreadJoinedAt { get; private set; } + + /// + /// Gets the guild this user is in. + /// + public SocketGuild Guild { get; private set; } + + /// + public DateTimeOffset? JoinedAt + => GuildUser.JoinedAt; + + /// + public string Nickname + => GuildUser.Nickname; + + /// + public DateTimeOffset? PremiumSince + => GuildUser.PremiumSince; + + /// + public bool? IsPending + => GuildUser.IsPending; + /// + public int Hierarchy + => GuildUser.Hierarchy; + + /// + public override string AvatarId + { + get => GuildUser.AvatarId; + internal set => GuildUser.AvatarId = value; + } + /// + public string GuildAvatarId + => GuildUser.GuildAvatarId; + + /// + public override ushort DiscriminatorValue + { + get => GuildUser.DiscriminatorValue; + internal set => GuildUser.DiscriminatorValue = value; + } + + /// + public override bool IsBot + { + get => GuildUser.IsBot; + internal set => GuildUser.IsBot = value; + } + + /// + public override bool IsWebhook + => GuildUser.IsWebhook; + + /// + public override string Username + { + get => GuildUser.Username; + internal set => GuildUser.Username = value; + } + + /// + public bool IsDeafened + => GuildUser.IsDeafened; + + /// + public bool IsMuted + => GuildUser.IsMuted; + + /// + public bool IsSelfDeafened + => GuildUser.IsSelfDeafened; + + /// + public bool IsSelfMuted + => GuildUser.IsSelfMuted; + + /// + public bool IsSuppressed + => GuildUser.IsSuppressed; + + /// + public IVoiceChannel VoiceChannel + => GuildUser.VoiceChannel; + + /// + public string VoiceSessionId + => GuildUser.VoiceSessionId; + + /// + public bool IsStreaming + => GuildUser.IsStreaming; + + /// + public DateTimeOffset? RequestToSpeakTimestamp + => GuildUser.RequestToSpeakTimestamp; + + private SocketGuildUser GuildUser { get; set; } + + internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member) + : base(guild.Discord, member.Id) + { + Thread = thread; + Guild = guild; + GuildUser = member; + } + + internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) + { + var entity = new SocketThreadUser(guild, thread, member); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + ThreadJoinedAt = model.JoinTimestamp; + + if (model.Presence.IsSpecified) + { + GuildUser.Update(Discord.State, model.Presence.Value, true); + } + + if (model.Member.IsSpecified) + { + GuildUser.Update(Discord.State, model.Member.Value); + } + } + + /// + public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); + + /// + public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); + + /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); + + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); + + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); + + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); + + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); + + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); + + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); + + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); + + /// + GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; + + /// + IGuild IGuildUser.Guild => Guild; + + /// + ulong IGuildUser.GuildId => Guild.Id; + + /// + IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); + + internal override SocketGlobalUser GlobalUser => GuildUser.GlobalUser; + + internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } + + /// + /// Gets the guild user of this thread user. + /// + /// + public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index c7f6cb846..a15f7e747 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -1,21 +1,36 @@ -using System; +using System; using System.Diagnostics; using Model = Discord.API.User; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based user that is yet to be recognized by the client. + /// + /// + /// A user may not be recognized due to the user missing from the cache or failed to be recognized properly. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketUnknownUser : SocketUser { + /// public override string Username { get; internal set; } + /// public override ushort DiscriminatorValue { get; internal set; } + /// public override string AvatarId { get; internal set; } + + /// public override bool IsBot { get; internal set; } + /// public override bool IsWebhook => false; - - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + /// + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + /// + /// This field is not supported for an unknown user. + internal override SocketGlobalUser GlobalUser => + throw new NotSupportedException(); internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) @@ -28,6 +43,7 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 00899d47e..b38bd8a4a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,25 +1,57 @@ -using Discord.Rest; using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; +using Discord.Rest; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketUser : SocketEntity, IUser { + /// public abstract bool IsBot { get; internal set; } + /// public abstract string Username { get; internal set; } + /// public abstract ushort DiscriminatorValue { get; internal set; } + /// public abstract string AvatarId { get; internal set; } + /// public abstract bool IsWebhook { get; } + /// + public UserProperties? PublicFlags { get; private set; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string Discriminator => DiscriminatorValue.ToString("D4"); + /// public string Mention => MentionUtils.MentionUser(Id); - public IActivity Activity => Presence.Activity; + /// public UserStatus Status => Presence.Status; + /// + public IReadOnlyCollection ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; + /// + public IReadOnlyCollection Activities => Presence.Activities ?? ImmutableList.Empty; + /// + /// Gets mutual guilds shared with this user. + /// + /// + /// This property will only include guilds in the same . + /// + public IReadOnlyCollection MutualGuilds + => Discord.Guilds.Where(g => g.GetUser(Id) != null).ToImmutableArray(); internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) @@ -35,10 +67,10 @@ namespace Discord.WebSocket } if (model.Discriminator.IsSpecified) { - var newVal = ushort.Parse(model.Discriminator.Value); + var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); if (newVal != DiscriminatorValue) { - DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); hasChanges = true; } } @@ -52,20 +84,39 @@ namespace Discord.WebSocket Username = model.Username.Value; hasChanges = true; } + if (model.PublicFlags.IsSpecified && model.PublicFlags.Value != PublicFlags) + { + PublicFlags = model.PublicFlags.Value; + hasChanges = true; + } return hasChanges; } - public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) - => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; + internal virtual void Update(PresenceModel model) + { + Presence.Update(model); + } + + /// + public async Task CreateDMChannelAsync(RequestOptions options = null) + => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + /// public string GetDefaultAvatarUrl() => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + /// + /// Gets the full name of the user (e.g. Example#0001). + /// + /// + /// The full name of the user. + /// + public override string ToString() => Format.UsernameAndDiscriminator(this); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 480103326..816a839fc 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -1,14 +1,19 @@ -using System; +using System; using System.Diagnostics; using Model = Discord.API.VoiceState; namespace Discord.WebSocket { - //TODO: C#7 Candidate for record type + /// + /// Represents a WebSocket user's voice connection status. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct SocketVoiceState : IVoiceState { - public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, false, false, false, false, false); + /// + /// Initializes a default with everything set to null or false. + /// + public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, null, false, false, false, false, false, false); [Flags] private enum Flags : byte @@ -19,23 +24,39 @@ namespace Discord.WebSocket Deafened = 0x04, SelfMuted = 0x08, SelfDeafened = 0x10, + SelfStream = 0x20, } private readonly Flags _voiceStates; - + + /// + /// Gets the voice channel that the user is currently in; or null if none. + /// public SocketVoiceChannel VoiceChannel { get; } + /// public string VoiceSessionId { get; } + /// + public DateTimeOffset? RequestToSpeakTimestamp { get; private set; } + /// public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + /// public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + /// public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + /// public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + /// public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + /// + public bool IsStreaming => (_voiceStates & Flags.SelfStream) != 0; + - internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed) + internal SocketVoiceState(SocketVoiceChannel voiceChannel, DateTimeOffset? requestToSpeak, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream) { VoiceChannel = voiceChannel; VoiceSessionId = sessionId; + RequestToSpeakTimestamp = requestToSpeak; Flags voiceStates = Flags.Normal; if (isSelfMuted) @@ -48,17 +69,26 @@ namespace Discord.WebSocket voiceStates |= Flags.Deafened; if (isSuppressed) voiceStates |= Flags.Suppressed; + if (isStream) + voiceStates |= Flags.SelfStream; _voiceStates = voiceStates; } internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) { - return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress); + return new SocketVoiceState(voiceChannel, model.RequestToSpeakTimestamp.IsSpecified ? model.RequestToSpeakTimestamp.Value : null, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream); } + /// + /// Gets the name of this voice channel. + /// + /// + /// A string that resolves to name of this voice channel; otherwise "Unknown". + /// public override string ToString() => VoiceChannel?.Name ?? "Unknown"; private string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; internal SocketVoiceState Clone() => this; + /// IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index dd80648d2..bccfe1a29 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -7,21 +7,35 @@ using Model = Discord.API.User; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based webhook user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketWebhookUser : SocketUser, IWebhookUser { + #region SocketWebhookUser + /// Gets the guild of this webhook. public SocketGuild Guild { get; } + /// public ulong WebhookId { get; } + /// public override string Username { get; internal set; } + /// public override ushort DiscriminatorValue { get; internal set; } + /// public override string AvatarId { get; internal set; } + + + /// public override bool IsBot { get; internal set; } + /// public override bool IsWebhook => true; - - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + /// + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override SocketGlobalUser GlobalUser => + throw new NotSupportedException(); internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id) @@ -36,51 +50,106 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; +#endregion - - //IGuildUser + #region IGuildUser + /// IGuild IGuildUser.Guild => Guild; + /// ulong IGuildUser.GuildId => Guild.Id; + /// IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// DateTimeOffset? IGuildUser.JoinedAt => null; + /// string IGuildUser.Nickname => null; + /// + string IGuildUser.GuildAvatarId => null; + /// + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; + /// + DateTimeOffset? IGuildUser.PremiumSince => null; + /// + bool? IGuildUser.IsPending => null; + /// + int IGuildUser.Hierarchy => 0; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(string reason, RequestOptions options) - { + /// + /// Webhook users cannot be kicked. + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); - } - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) - { + + /// + /// Webhook users cannot be modified. + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); - } - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) - { + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRoleAsync(ulong roleId, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) - { + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRolesAsync(IEnumerable roleIds, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - //IVoiceState + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRoleAsync(ulong roleId, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + #endregion + + #region IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + /// + DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs index 57abf1d03..c5f13b1a9 100644 --- a/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs +++ b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs @@ -2,12 +2,33 @@ using System.Diagnostics; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based voice server. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketVoiceServer { - public Cacheable Guild { get; private set; } - public string Endpoint { get; private set; } - public string Token { get; private set; } + /// + /// Gets the guild associated with the voice server. + /// + /// + /// A cached entity of the guild. + /// + public Cacheable Guild { get; } + /// + /// Gets the endpoint URL of the voice server host. + /// + /// + /// An URL representing the voice server host. + /// + public string Endpoint { get; } + /// + /// Gets the voice connection token. + /// + /// + /// A voice connection token. + /// + public string Token { get; } internal SocketVoiceServer(Cacheable guild, string endpoint, string token) { diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index e8dc4b5f0..46f5c1a26 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -1,3 +1,5 @@ +using Discord.Rest; +using System; using System.Collections.Immutable; using System.Linq; @@ -7,12 +9,26 @@ namespace Discord.WebSocket { public static IActivity ToEntity(this API.Game model) { - // Spotify Game + #region Custom Status Game + if (model.Id.IsSpecified && model.Id.Value == "custom") + { + return new CustomStatusGame() + { + Type = ActivityType.CustomStatus, + Name = model.Name, + State = model.State.IsSpecified ? model.State.Value : null, + Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), + }; + } + #endregion + + #region Spotify Game if (model.SyncId.IsSpecified) { var assets = model.Assets.GetValueOrDefault()?.ToEntity(); string albumText = assets?[1]?.Text; - string albumArtId = assets?[1]?.ImageId?.Replace("spotify:",""); + string albumArtId = assets?[1]?.ImageId?.Replace("spotify:", ""); var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; return new SpotifyGame { @@ -22,14 +38,18 @@ namespace Discord.WebSocket TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), AlbumTitle = albumText, TrackTitle = model.Details.GetValueOrDefault(), - Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(), + Artists = model.State.GetValueOrDefault()?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), + StartedAt = timestamps?.Start, + EndsAt = timestamps?.End, Duration = timestamps?.End - timestamps?.Start, AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, - Type = ActivityType.Listening + Type = ActivityType.Listening, + Flags = model.Flags.GetValueOrDefault(), }; } + #endregion - // Rich Game + #region Rich Game if (model.ApplicationId.IsSpecified) { ulong appId = model.ApplicationId.Value; @@ -44,18 +64,30 @@ namespace Discord.WebSocket LargeAsset = assets?[1], Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, - Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null + Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null, + Flags = model.Flags.GetValueOrDefault() }; } - // Stream Game + #endregion + + #region Stream Game if (model.StreamUrl.IsSpecified) { return new StreamingGame( - model.Name, - model.StreamUrl.Value); + model.Name, + model.StreamUrl.Value) + { + Flags = model.Flags.GetValueOrDefault(), + Details = model.Details.GetValueOrDefault() + }; } - // Normal Game - return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing); + #endregion + + #region Normal Game + return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing, + model.Flags.IsSpecified ? model.Flags.Value : ActivityProperties.None, + model.Details.GetValueOrDefault()); + #endregion } // (Small, Large) diff --git a/src/Discord.Net.WebSocket/GatewayReconnectException.cs b/src/Discord.Net.WebSocket/GatewayReconnectException.cs new file mode 100644 index 000000000..c5b15e007 --- /dev/null +++ b/src/Discord.Net.WebSocket/GatewayReconnectException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.WebSocket +{ + /// + /// The exception thrown when the gateway client has been requested to reconnect. + /// + public class GatewayReconnectException : Exception + { + /// + /// Initializes a new instance of the class with the reconnection + /// message. + /// + /// The reason why the gateway has been requested to reconnect. + public GatewayReconnectException(string message) + : base(message) + { } + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 251a761d4..82079e9bd 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -13,24 +13,29 @@ namespace Discord.Net.Udp private readonly SemaphoreSlim _lock; private UdpClient _udp; private IPEndPoint _destination; - private CancellationTokenSource _cancelTokenSource; + private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private Task _task; private bool _isDisposed; - + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public DefaultUdpSocket() { _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _stopCancelTokenSource = new CancellationTokenSource(); } private void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) + { StopInternalAsync(true).GetAwaiter().GetResult(); + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } _isDisposed = true; } } @@ -56,9 +61,14 @@ namespace Discord.Net.Udp { await StopInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + + _stopCancelTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + _udp?.Dispose(); _udp = new UdpClient(0); _task = RunAsync(_cancelToken); @@ -77,7 +87,7 @@ namespace Discord.Net.Udp } public async Task StopInternalAsync(bool isDisposing = false) { - try { _cancelTokenSource.Cancel(false); } catch { } + try { _stopCancelTokenSource.Cancel(false); } catch { } if (!isDisposing) await (_task ?? Task.Delay(0)).ConfigureAwait(false); @@ -96,8 +106,11 @@ namespace Discord.Net.Udp } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); + _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count) @@ -117,6 +130,14 @@ namespace Discord.Net.Udp while (!cancelToken.IsCancellationRequested) { var receiveTask = _udp.ReceiveAsync(); + + _ = receiveTask.ContinueWith((receiveResult) => + { + //observe the exception as to not receive as unhandled exception + _ = receiveResult.Exception; + + }, TaskContinuationOptions.OnlyOnFaulted); + var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); if (task == closeTask) break; diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index dc5201ac1..82e2f0573 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -25,14 +25,14 @@ namespace Discord.Net.WebSockets private readonly IWebProxy _proxy; private ClientWebSocket _client; private Task _task; - private CancellationTokenSource _cancelTokenSource; + private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private bool _isDisposed, _isDisconnecting; public DefaultWebSocketClient(IWebProxy proxy = null) { _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _disconnectTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _headers = new Dictionary(); @@ -43,7 +43,12 @@ namespace Discord.Net.WebSockets if (!_isDisposed) { if (disposing) - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + { + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } _isDisposed = true; } } @@ -68,9 +73,14 @@ namespace Discord.Net.WebSockets { await DisconnectInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _disconnectTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + + _client?.Dispose(); _client = new ClientWebSocket(); _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = TimeSpan.Zero; @@ -84,42 +94,45 @@ namespace Discord.Net.WebSockets _task = RunAsync(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private async Task DisconnectInternalAsync(bool isDisposing = false) + private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { - try { _cancelTokenSource.Cancel(false); } catch { } - _isDisconnecting = true; - try - { - await (_task ?? Task.Delay(0)).ConfigureAwait(false); - _task = null; - } - finally { _isDisconnecting = false; } + + try { _disconnectTokenSource.Cancel(false); } + catch { } if (_client != null) { if (!isDisposing) { - try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + var status = (WebSocketCloseStatus)closeCode; + try { await _client.CloseOutputAsync(status, "", new CancellationToken()); } catch { } } try { _client.Dispose(); } catch { } - + _client = null; } + + try + { + await (_task ?? Task.Delay(0)).ConfigureAwait(false); + _task = null; + } + finally { _isDisconnecting = false; } } private async Task OnClosed(Exception ex) { @@ -129,7 +142,7 @@ namespace Discord.Net.WebSockets await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(false); + await DisconnectInternalAsync(isDisposing: false); } finally { @@ -144,13 +157,23 @@ namespace Discord.Net.WebSockets } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); + _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count, bool isText) { - await _lock.WaitAsync().ConfigureAwait(false); + try + { + await _lock.WaitAsync(_cancelToken).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + return; + } try { if (_client == null) return; @@ -166,7 +189,7 @@ namespace Discord.Net.WebSockets frameSize = count - (i * SendChunkSize); else frameSize = SendChunkSize; - + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); } @@ -176,7 +199,7 @@ namespace Discord.Net.WebSockets _lock.Release(); } } - + private async Task RunAsync(CancellationToken cancelToken) { var buffer = new ArraySegment(new byte[ReceiveChunkSize]); @@ -188,7 +211,7 @@ namespace Discord.Net.WebSockets WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); byte[] result; int resultCount; - + if (socketResult.MessageType == WebSocketMessageType.Close) throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); @@ -219,7 +242,7 @@ namespace Discord.Net.WebSockets resultCount = socketResult.Count; result = buffer.Array; } - + if (socketResult.MessageType == WebSocketMessageType.Text) { string text = Encoding.UTF8.GetString(result, 0, resultCount); diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 2d66d5900..bc580c410 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -7,6 +7,7 @@ namespace Discord.Net.WebSockets { public static readonly WebSocketProvider Instance = Create(); + /// The default WebSocketProvider is not supported on this platform. public static WebSocketProvider Create(IWebProxy proxy = null) { return () => diff --git a/src/Discord.Net.Webhook/AssemblyInfo.cs b/src/Discord.Net.Webhook/AssemblyInfo.cs index c6b5997b4..bbbaca3be 100644 --- a/src/Discord.Net.Webhook/AssemblyInfo.cs +++ b/src/Discord.Net.Webhook/AssemblyInfo.cs @@ -1,3 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index ba7bbcff8..24ae442d7 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -1,13 +1,14 @@ + Discord.Net.Webhook Discord.Webhook A core Discord.Net library containing the Webhook client and models. - netstandard1.3 + netstandard2.0;netstandard2.1 - \ No newline at end of file + diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 67a5462be..f7ad7301c 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Discord.Logging; using Discord.Rest; namespace Discord.Webhook { + /// A client responsible for connecting as a Webhook. public class DiscordWebhookClient : IDisposable { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } @@ -19,14 +22,20 @@ namespace Discord.Webhook internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(IWebhook webhook) : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken) : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(string webhookUrl) + : this(webhookUrl, new DiscordRestConfig()) { } - /// Creates a new Webhook discord client. + // regex pattern to match webhook urls + private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) : this(config) { @@ -34,7 +43,7 @@ namespace Discord.Webhook ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) : this(config) { @@ -42,6 +51,20 @@ namespace Discord.Webhook _webhookId = Webhook.Id; } + /// + /// Creates a new Webhook Discord client. + /// + /// The url of the webhook. + /// The configuration options to use for this client. + /// Thrown if the is an invalid format. + /// Thrown if the is null or whitespace. + public DiscordWebhookClient(string webhookUrl, DiscordRestConfig config) : this(config) + { + ParseWebhookUrl(webhookUrl, out _webhookId, out string token); + ApiClient.LoginAsync(TokenType.Webhook, token).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, _webhookId).GetAwaiter().GetResult(); + } + private DiscordWebhookClient(DiscordRestConfig config) { ApiClient = CreateApiClient(config); @@ -50,32 +73,85 @@ namespace Discord.Webhook _restLogger = LogManager.CreateLogger("Rest"); - ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - - /// Sends a message using to the channel for this webhook. Returns the ID of the created message. + /// Sends a message to the channel for this webhook. + /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, - string username = null, string avatarUrl = null, RequestOptions options = null) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent component = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, component); - /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. - public Task SendFileAsync(string filePath, string text, bool isTTS = false, - IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) - => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options); + /// + /// Modifies a message posted using this webhook. + /// + /// + /// This method can only modify messages that were sent using the same webhook. + /// + /// ID of the modified message. + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options); + + /// + /// Deletes a message posted using this webhook. + /// + /// + /// This method can only delete messages that were sent using the same webhook. + /// + /// ID of the deleted message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous deletion operation. + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, options); - /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFileAsync(string filePath, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, + allowedMentions, options, isSpoiler, components); + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, - IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) - => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options); + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, + avatarUrl, allowedMentions, options, isSpoiler, components); + + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, + avatarUrl, allowedMentions, components, options); + + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, + allowedMentions, components, options); + /// Modifies the properties of this webhook. public Task ModifyWebhookAsync(Action func, RequestOptions options = null) @@ -92,5 +168,31 @@ namespace Discord.Webhook { ApiClient?.Dispose(); } + + internal static void ParseWebhookUrl(string webhookUrl, out ulong webhookId, out string webhookToken) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) + throw new ArgumentNullException(paramName: nameof(webhookUrl), message: + "The given webhook Url cannot be null or whitespace."); + + // thrown when groups are not populated/valid, or when there is no match + ArgumentException ex(string reason = null) + => new ArgumentException(paramName: nameof(webhookUrl), message: + $"The given webhook Url was not in a valid format. {reason}"); + var match = WebhookUrlRegex.Match(webhookUrl); + if (match != null) + { + // ensure that the first group is a ulong, set the _webhookId + // 0th group is always the entire match, and 1 is the domain; so start at index 2 + if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) + throw ex("The webhook Id could not be parsed."); + + if (!match.Groups[3].Success) + throw ex("The webhook token could not be parsed."); + webhookToken = match.Groups[3].Value; + } + else + throw ex(); + } } } diff --git a/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs new file mode 100644 index 000000000..ca2ff10a0 --- /dev/null +++ b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Discord.Webhook +{ + /// + /// Properties that are used to modify an Webhook message with the specified changes. + /// + public class WebhookMessageProperties + { + /// + /// Gets or sets the content of the message. + /// + /// + /// This must be less than the constant defined by . + /// + public Optional Content { get; set; } + /// + /// Gets or sets the embed array that the message should display. + /// + public Optional> Embeds { get; set; } + /// + /// Gets or sets the allowed mentions of the message. + /// + public Optional AllowedMentions { get; set; } + /// + /// Gets or sets the components that the message should display. + /// + public Optional Components { get; set; } + } +} diff --git a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs index cd35d731c..2a5c4786e 100644 --- a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Webhook; @@ -11,12 +11,13 @@ namespace Discord.Webhook private DiscordWebhookClient _client; public ulong Id { get; } - public ulong ChannelId { get; } public string Token { get; } + public ulong ChannelId { get; private set; } public string Name { get; private set; } public string AvatarId { get; private set; } public ulong? GuildId { get; private set; } + public ulong? ApplicationId { get; private set; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -36,12 +37,16 @@ namespace Discord.Webhook internal void Update(Model model) { + if (ChannelId != model.ChannelId) + ChannelId = model.ChannelId; if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; if (model.GuildId.IsSpecified) GuildId = model.GuildId.Value; if (model.Name.IsSpecified) Name = model.Name.Value; + + ApplicationId = model.ApplicationId; } public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -49,7 +54,7 @@ namespace Discord.Webhook public async Task ModifyAsync(Action func, RequestOptions options = null) { - var model = await WebhookClientHelper.ModifyAsync(_client, func, options); + var model = await WebhookClientHelper.ModifyAsync(_client, func, options).ConfigureAwait(false); Update(model); } diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index d3cac9703..8b4bb5d2a 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -12,44 +12,136 @@ namespace Discord.Webhook { internal static class WebhookClientHelper { + /// Could not find a webhook with the supplied credentials. public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) { - var model = await client.ApiClient.GetWebhookAsync(webhookId); + var model = await client.ApiClient.GetWebhookAsync(webhookId).ConfigureAwait(false); if (model == null) - throw new InvalidOperationException("Could not find a webhook for the supplied credentials."); + throw new InvalidOperationException("Could not find a webhook with the supplied credentials."); return RestInternalWebhook.Create(client, model); } - public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + public static async Task SendMessageAsync(DiscordWebhookClient client, + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, MessageComponent component) { - var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + var args = new CreateWebhookMessageParams + { + Content = text, + IsTTS = isTTS + }; + if (embeds != null) args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); if (username != null) args.Username = username; if (avatarUrl != null) args.AvatarUrl = avatarUrl; + if (allowedMentions != null) + args.AllowedMentions = allowedMentions.ToModel(); + if (component != null) + args.Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray(); var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); return model.Id; } - public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + public static async Task ModifyMessageAsync(DiscordWebhookClient client, ulong messageId, + Action func, RequestOptions options) + { + var args = new WebhookMessageProperties(); + func(args); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), + "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), + "A max of 100 user Ids are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions?.AllowedTypes != null) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", + nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", + nameof(allowedMentions)); + } + } + } + + var apiArgs = new ModifyWebhookMessageParams + { + Content = args.Content.IsSpecified ? args.Content.Value : Optional.Create(), + Embeds = + args.Embeds.IsSpecified + ? args.Embeds.Value.Select(embed => embed.ToModel()).ToArray() + : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified + ? args.AllowedMentions.Value.ToModel() + : Optional.Create(), + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + }; + + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options) + .ConfigureAwait(false); + } + public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options) + { + await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); + } + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false); } - public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, + MessageComponent components) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + + public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + + public static async Task SendFilesAsync(DiscordWebhookClient client, + IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) { - var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = avatarUrl; - if (embeds != null) - args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + embeds ??= Array.Empty(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Count(), 10, nameof(embeds), "A max of 10 embeds are allowed."); + + foreach (var attachment in attachments) + { + Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); + } + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 2bf531cb1..cb773a379 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,37 +2,38 @@ Discord.Net - 2.0.0-beta2$suffix$ + 3.0.0-dev$suffix$ Discord.Net Discord.Net Contributors - RogueException + foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT false + https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + - + \ No newline at end of file diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 000000000..bb9056963 --- /dev/null +++ b/stylecop.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "indentation": { + "indentationSize": 4, + "tabSize": 4, + "useTabs": false + }, + "documentationRules": { + "documentExposedElements": true, + "documentInternalElements": false, + "documentPrivateElements": false, + "documentInterfaces": true, + "documentPrivateFields": true, + "xmlHeader": false, + "documentationCulture": "en-US" + } + } +} diff --git a/test/Discord.Net.Analyzers.Tests/Discord.Net.Analyzers.Tests.csproj b/test/Discord.Net.Analyzers.Tests/Discord.Net.Analyzers.Tests.csproj new file mode 100644 index 000000000..1257041e4 --- /dev/null +++ b/test/Discord.Net.Analyzers.Tests/Discord.Net.Analyzers.Tests.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs b/test/Discord.Net.Analyzers.Tests/GuildAccessTests.cs similarity index 96% rename from test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs rename to test/Discord.Net.Analyzers.Tests/GuildAccessTests.cs index 073cc1de7..4cb5cefcb 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs +++ b/test/Discord.Net.Analyzers.Tests/GuildAccessTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Threading.Tasks; +using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Discord.Analyzers; diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs b/test/Discord.Net.Analyzers.Tests/Helpers/CodeFixVerifier.Helper.cs similarity index 95% rename from test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs rename to test/Discord.Net.Analyzers.Tests/Helpers/CodeFixVerifier.Helper.cs index 0f73d0643..42f7b08c1 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs +++ b/test/Discord.Net.Analyzers.Tests/Helpers/CodeFixVerifier.Helper.cs @@ -18,7 +18,7 @@ namespace TestHelper /// Apply the inputted CodeAction to the inputted document. /// Meant to be used to apply codefixes. /// - /// The Document to apply the fix on + /// The Document to apply the fix on. /// A CodeAction that will be applied to the Document. /// A Document with the changes from the CodeAction private static Document ApplyFix(Document document, CodeAction codeAction) @@ -33,8 +33,8 @@ namespace TestHelper /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, /// this method may not necessarily return the new one. /// - /// The Diagnostics that existed in the code before the CodeFix was applied - /// The Diagnostics that exist in the code after the CodeFix was applied + /// The Diagnostics that existed in the code before the CodeFix was applied. + /// The Diagnostics that exist in the code after the CodeFix was applied. /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) { @@ -61,7 +61,7 @@ namespace TestHelper /// /// Get the existing compiler diagnostics on the inputted document. /// - /// The Document to run the compiler diagnostic analyzers on + /// The Document to run the compiler diagnostic analyzers on. /// The compiler diagnostics that were found in the code private static IEnumerable GetCompilerDiagnostics(Document document) { @@ -71,7 +71,7 @@ namespace TestHelper /// /// Given a document, turn it into a string based on the syntax root /// - /// The Document to be converted to a string + /// The Document to be converted to a string. /// A string containing the syntax of the Document after formatting private static string GetStringFromDocument(Document document) { diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs b/test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticResult.cs similarity index 74% rename from test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs rename to test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticResult.cs index 5ae6f528e..87d915494 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs +++ b/test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticResult.cs @@ -20,9 +20,9 @@ namespace TestHelper throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); } - this.Path = path; - this.Line = line; - this.Column = column; + Path = path; + Line = line; + Column = column; } public string Path { get; } @@ -41,16 +41,16 @@ namespace TestHelper { get { - if (this.locations == null) + if (locations == null) { - this.locations = new DiagnosticResultLocation[] { }; + locations = new DiagnosticResultLocation[] { }; } - return this.locations; + return locations; } set { - this.locations = value; + locations = value; } } @@ -64,7 +64,7 @@ namespace TestHelper { get { - return this.Locations.Length > 0 ? this.Locations[0].Path : ""; + return Locations.Length > 0 ? Locations[0].Path : ""; } } @@ -72,7 +72,7 @@ namespace TestHelper { get { - return this.Locations.Length > 0 ? this.Locations[0].Line : -1; + return Locations.Length > 0 ? Locations[0].Line : -1; } } @@ -80,7 +80,7 @@ namespace TestHelper { get { - return this.Locations.Length > 0 ? this.Locations[0].Column : -1; + return Locations.Length > 0 ? Locations[0].Column : -1; } } } diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs b/test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticVerifier.Helper.cs similarity index 97% rename from test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs rename to test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticVerifier.Helper.cs index 7a8eb2e9c..23bb319a6 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs +++ b/test/Discord.Net.Analyzers.Tests/Helpers/DiagnosticVerifier.Helper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; -using Discord; using Discord.Commands; namespace TestHelper @@ -36,9 +35,9 @@ namespace TestHelper /// /// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document. /// - /// Classes in the form of strings - /// The language the source classes are in - /// The analyzer to be run on the sources + /// Classes in the form of strings. + /// The language the source classes are in. + /// The analyzer to be run on the sources. /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) { @@ -49,8 +48,8 @@ namespace TestHelper /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. /// The returned diagnostics are then ordered by location in the source document. /// - /// The analyzer to run on the documents - /// The Documents that the analyzer will be run on + /// The analyzer to run on the documents. + /// The Documents that the analyzer will be run on. /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) { @@ -94,7 +93,7 @@ namespace TestHelper /// /// Sort diagnostics by location in source document /// - /// The list of Diagnostics to be sorted + /// The list of Diagnostics to be sorted. /// An IEnumerable containing the Diagnostics in order of Location private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) { @@ -107,8 +106,8 @@ namespace TestHelper /// /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. /// - /// Classes in the form of strings - /// The language the source code is in + /// Classes in the form of strings. + /// The language the source code is in. /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant private static Document[] GetDocuments(string[] sources, string language) { @@ -131,8 +130,8 @@ namespace TestHelper /// /// Create a Document from a string through creating a project that contains it. /// - /// Classes in the form of a string - /// The language the source code is in + /// Classes in the form of a string. + /// The language the source code is in. /// A Document created from the source string protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) { @@ -142,8 +141,8 @@ namespace TestHelper /// /// Create a project using the inputted strings as sources. /// - /// Classes in the form of strings - /// The language the source code is in + /// Classes in the form of strings. + /// The language the source code is in. /// A Project created out of the Documents created from the source strings private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) { @@ -188,7 +187,7 @@ namespace TestHelper private static HashSet RecursiveReferencedAssemblies(Assembly a, HashSet assemblies = null) { - assemblies = assemblies ?? new HashSet(); + assemblies ??= new HashSet(); if (assemblies.Add(a)) { foreach (var referencedAssemblyName in a.GetReferencedAssemblies()) diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs b/test/Discord.Net.Analyzers.Tests/Verifiers/CodeFixVerifier.cs similarity index 90% rename from test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs rename to test/Discord.Net.Analyzers.Tests/Verifiers/CodeFixVerifier.cs index 5d057b610..d1cb6cd1b 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs +++ b/test/Discord.Net.Analyzers.Tests/Verifiers/CodeFixVerifier.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; @@ -38,10 +38,10 @@ namespace TestHelper /// /// Called to test a C# codefix when applied on the inputted string as a source /// - /// A class in the form of a string before the CodeFix was applied to it - /// A class in the form of a string after the CodeFix was applied to it - /// Index determining which codefix to apply if there are multiple - /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + /// A class in the form of a string before the CodeFix was applied to it. + /// A class in the form of a string after the CodeFix was applied to it. + /// Index determining which codefix to apply if there are multiple. + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied. protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) { VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); @@ -50,10 +50,10 @@ namespace TestHelper /// /// Called to test a VB codefix when applied on the inputted string as a source /// - /// A class in the form of a string before the CodeFix was applied to it - /// A class in the form of a string after the CodeFix was applied to it - /// Index determining which codefix to apply if there are multiple - /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + /// A class in the form of a string before the CodeFix was applied to it. + /// A class in the form of a string after the CodeFix was applied to it. + /// Index determining which codefix to apply if there are multiple. + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied. protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) { VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); @@ -65,13 +65,13 @@ namespace TestHelper /// Then gets the string after the codefix is applied and compares it with the expected result. /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. /// - /// The language the source code is in - /// The analyzer to be applied to the source code - /// The codefix to be applied to the code wherever the relevant Diagnostic is found - /// A class in the form of a string before the CodeFix was applied to it - /// A class in the form of a string after the CodeFix was applied to it - /// Index determining which codefix to apply if there are multiple - /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + /// The language the source code is in. + /// The analyzer to be applied to the source code. + /// The codefix to be applied to the code wherever the relevant Diagnostic is found. + /// A class in the form of a string before the CodeFix was applied to it. + /// A class in the form of a string after the CodeFix was applied to it. + /// Index determining which codefix to apply if there are multiple. + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied. private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) { var document = CreateDocument(oldSource, language); @@ -126,4 +126,4 @@ namespace TestHelper Assert.Equal(newSource, actual); } } -} \ No newline at end of file +} diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs b/test/Discord.Net.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs similarity index 92% rename from test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs rename to test/Discord.Net.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs index 498e5ef27..9b0219a63 100644 --- a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs +++ b/test/Discord.Net.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; //using Microsoft.VisualStudio.TestTools.UnitTesting; using Xunit; @@ -38,8 +37,8 @@ namespace TestHelper /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source /// Note: input a DiagnosticResult for each Diagnostic expected /// - /// A class in the form of a string to run the analyzer on - /// DiagnosticResults that should appear after the analyzer is run on the source + /// A class in the form of a string to run the analyzer on. + /// DiagnosticResults that should appear after the analyzer is run on the source. protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) { VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); @@ -49,8 +48,8 @@ namespace TestHelper /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source /// Note: input a DiagnosticResult for each Diagnostic expected /// - /// A class in the form of a string to run the analyzer on - /// DiagnosticResults that should appear after the analyzer is run on the source + /// A class in the form of a string to run the analyzer on. + /// DiagnosticResults that should appear after the analyzer is run on the source. protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) { VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); @@ -60,8 +59,8 @@ namespace TestHelper /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source /// Note: input a DiagnosticResult for each Diagnostic expected /// - /// An array of strings to create source documents from to run the analyzers on - /// DiagnosticResults that should appear after the analyzer is run on the sources + /// An array of strings to create source documents from to run the analyzers on. + /// DiagnosticResults that should appear after the analyzer is run on the sources. protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) { VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); @@ -71,8 +70,8 @@ namespace TestHelper /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source /// Note: input a DiagnosticResult for each Diagnostic expected /// - /// An array of strings to create source documents from to run the analyzers on - /// DiagnosticResults that should appear after the analyzer is run on the sources + /// An array of strings to create source documents from to run the analyzers on. + /// DiagnosticResults that should appear after the analyzer is run on the sources. protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected) { VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); @@ -82,10 +81,10 @@ namespace TestHelper /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, /// then verifies each of them. /// - /// An array of strings to create source documents from to run the analyzers on - /// The language of the classes represented by the source strings - /// The analyzer to be run on the source code - /// DiagnosticResults that should appear after the analyzer is run on the sources + /// An array of strings to create source documents from to run the analyzers on. + /// The language of the classes represented by the source strings. + /// The analyzer to be run on the source code. + /// DiagnosticResults that should appear after the analyzer is run on the sources. private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) { var diagnostics = GetSortedDiagnostics(sources, language, analyzer); @@ -99,12 +98,12 @@ namespace TestHelper /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. /// - /// The Diagnostics found by the compiler after running the analyzer on the source code - /// The analyzer that was being run on the sources - /// Diagnostic Results that should have appeared in the code + /// The Diagnostics found by the compiler after running the analyzer on the source code. + /// The analyzer that was being run on the sources. + /// Diagnostic Results that should have appeared in the code. private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) { - int expectedCount = expectedResults.Count(); + int expectedCount = expectedResults.Length; int actualCount = actualResults.Count(); if (expectedCount != actualCount) @@ -174,10 +173,10 @@ namespace TestHelper /// /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. /// - /// The analyzer that was being run on the sources - /// The diagnostic that was found in the code - /// The Location of the Diagnostic found in the code - /// The DiagnosticResultLocation that should have been found + /// The analyzer that was being run on the sources. + /// The diagnostic that was found in the code. + /// The Location of the Diagnostic found in the code. + /// The DiagnosticResultLocation that should have been found. private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) { var actualSpan = actual.GetLineSpan(); @@ -216,8 +215,8 @@ namespace TestHelper /// /// Helper method to format a Diagnostic into an easily readable string /// - /// The analyzer that this verifier tests - /// The Diagnostics to be formatted + /// The analyzer that this verifier tests. + /// The Diagnostics to be formatted. /// The Diagnostics formatted as a string private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) { diff --git a/test/Discord.Net.Tests.Integration/ChannelsTests.cs b/test/Discord.Net.Tests.Integration/ChannelsTests.cs new file mode 100644 index 000000000..9bb30c4ef --- /dev/null +++ b/test/Discord.Net.Tests.Integration/ChannelsTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Discord +{ + /// + /// Tests that channels can be created and modified. + /// + [CollectionDefinition("ChannelsTests", DisableParallelization = true)] + public class ChannelsTests : IClassFixture + { + private IGuild guild; + private readonly ITestOutputHelper output; + + public ChannelsTests(RestGuildFixture guildFixture, ITestOutputHelper output) + { + guild = guildFixture.Guild; + output = output; + output.WriteLine($"RestGuildFixture using guild: {guild.Id}"); + // capture all console output + guildFixture.Client.Log += LogAsync; + } + private Task LogAsync(LogMessage message) + { + output.WriteLine(message.ToString()); + return Task.CompletedTask; + } + + /// + /// Checks that a text channel can be created and modified. + /// + [Fact] + public async Task ModifyTextChannel() + { + // create a text channel to modify + var channel = await guild.CreateTextChannelAsync("text"); + try + { + Assert.NotNull(channel); + // check that it can be modified + await channel.ModifyAsync(x => + { + x.IsNsfw = true; + x.Name = "updated"; + x.SlowModeInterval = 50; + x.Topic = "topic"; + x.CategoryId = null; + }); + // check the results of modifying this channel + Assert.True(channel.IsNsfw); + Assert.Equal("updated", channel.Name); + Assert.Equal(50, channel.SlowModeInterval); + Assert.Equal("topic", channel.Topic); + Assert.Null(channel.CategoryId); + } + finally + { + // delete the channel when finished + await channel?.DeleteAsync(); + } + } + + /// + /// Checks that a voice channel can be created, modified, and deleted. + /// + [Fact] + public async Task ModifyVoiceChannel() + { + var channel = await guild.CreateVoiceChannelAsync("voice"); + try + { + Assert.NotNull(channel); + // try to modify it + await channel.ModifyAsync(x => + { + x.Bitrate = 9001; + x.Name = "updated"; + x.UserLimit = 1; + }); + // check that these were updated + Assert.Equal(9001, channel.Bitrate); + Assert.Equal("updated", channel.Name); + Assert.Equal(1, channel.UserLimit); + } + finally + { + // delete the channel when done + await channel.DeleteAsync(); + } + } + + /// + /// Creates a category channel, a voice channel, and a text channel, then tries to assign them under that category. + /// + [Fact] + public async Task ModifyChannelCategories() + { + // util method for checking if a category is set + async Task CheckAsync(INestedChannel channel, ICategoryChannel cat) + { + // check that the category is not set + if (cat == null) + { + Assert.Null(channel.CategoryId); + Assert.Null(await channel.GetCategoryAsync()); + } + else + { + Assert.NotNull(channel.CategoryId); + Assert.Equal(cat.Id, channel.CategoryId); + var getCat = await channel.GetCategoryAsync(); + Assert.NotNull(getCat); + Assert.Equal(cat.Id, getCat.Id); + } + } + // initially create these not under the category + var category = await guild.CreateCategoryAsync("category"); + var text = await guild.CreateTextChannelAsync("text"); + var voice = await guild.CreateVoiceChannelAsync("voice"); + + try + { + Assert.NotNull(category); + Assert.NotNull(text); + Assert.NotNull(voice); + // check that the category is not set for either + await CheckAsync(text, null); + await CheckAsync(voice, null); + + // set the category + await text.ModifyAsync(x => x.CategoryId = category.Id); + await voice.ModifyAsync(x => x.CategoryId = category.Id); + + // check that this is set, and that it's the category that was created earlier + await CheckAsync(text, category); + await CheckAsync(voice, category); + + // create one more channel immediately under this category + var newText = await guild.CreateTextChannelAsync("new-text", x => x.CategoryId = category.Id); + try + { + Assert.NotNull(newText); + await CheckAsync(newText, category); + } + finally + { + await newText?.DeleteAsync(); + } + } + finally + { + // clean up + await category?.DeleteAsync(); + await text?.DeleteAsync(); + await voice?.DeleteAsync(); + } + } + } +} diff --git a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj new file mode 100644 index 000000000..8b16b2971 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/Discord.Net.Tests.Integration/DiscordRestClientFixture.cs b/test/Discord.Net.Tests.Integration/DiscordRestClientFixture.cs new file mode 100644 index 000000000..810e45876 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/DiscordRestClientFixture.cs @@ -0,0 +1,34 @@ +using Discord.Rest; +using System; +using Xunit; + +namespace Discord +{ + /// + /// Test fixture type for integration tests which sets up the client from + /// the token provided in environment variables. + /// + public class DiscordRestClientFixture : IDisposable + { + public DiscordRestClient Client { get; private set; } + + public DiscordRestClientFixture() + { + var token = Environment.GetEnvironmentVariable("DNET_TEST_TOKEN", EnvironmentVariableTarget.Process); + if (string.IsNullOrWhiteSpace(token)) + throw new Exception("The DNET_TEST_TOKEN environment variable was not provided."); + Client = new DiscordRestClient(new DiscordRestConfig() + { + LogLevel = LogSeverity.Debug, + DefaultRetryMode = RetryMode.AlwaysRetry + }); + Client.LoginAsync(TokenType.Bot, token).Wait(); + } + + public void Dispose() + { + Client.LogoutAsync().Wait(); + Client.Dispose(); + } + } +} diff --git a/test/Discord.Net.Tests.Integration/GuildTests.cs b/test/Discord.Net.Tests.Integration/GuildTests.cs new file mode 100644 index 000000000..c309b0ed1 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/GuildTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Discord +{ + [CollectionDefinition("GuildTests", DisableParallelization = true)] + public class GuildTests : IClassFixture + { + private IDiscordClient client; + private IGuild guild; + private readonly ITestOutputHelper output; + + public GuildTests(RestGuildFixture guildFixture, ITestOutputHelper output) + { + client = guildFixture.Client; + guild = guildFixture.Guild; + output = output; + output.WriteLine($"RestGuildFixture using guild: {guild.Id}"); + guildFixture.Client.Log += LogAsync; + } + private Task LogAsync(LogMessage message) + { + output.WriteLine(message.ToString()); + return Task.CompletedTask; + } + /// + /// Ensures that the CurrentUser is the owner of the guild. + /// + [Fact] + public void CheckOwner() + { + Assert.Equal(client.CurrentUser.Id, guild.OwnerId); + } + /// + /// Checks that a Guild can be modified to non-default values. + /// + [Fact] + public async Task ModifyGuild() + { + // set some initial properties of the guild that are not the defaults + await guild.ModifyAsync(x => + { + x.ExplicitContentFilter = ExplicitContentFilterLevel.AllMembers; + x.Name = "updated"; + x.DefaultMessageNotifications = DefaultMessageNotifications.MentionsOnly; + x.AfkTimeout = 900; // 15 minutes + x.VerificationLevel = VerificationLevel.None; + }); + // check that they were set + Assert.Equal("updated", guild.Name); + Assert.Equal(ExplicitContentFilterLevel.AllMembers, guild.ExplicitContentFilter); + Assert.Equal(DefaultMessageNotifications.MentionsOnly, guild.DefaultMessageNotifications); + Assert.Equal(VerificationLevel.None, guild.VerificationLevel); + Assert.Equal(900, guild.AFKTimeout); + } + /// + /// Checks that the SystemChannel property of a guild can be modified. + /// + [Fact] + public async Task ModifySystemChannel() + { + var systemChannel = await guild.CreateTextChannelAsync("system"); + // set using the Id + await guild.ModifyAsync(x => x.SystemChannelId = systemChannel.Id); + Assert.Equal(systemChannel.Id, guild.SystemChannelId); + // unset it + await guild.ModifyAsync(x => x.SystemChannelId = null); + Assert.Null(guild.SystemChannelId); + Assert.Null(await guild.GetSystemChannelAsync()); + + // set using the ITextChannel + await guild.ModifyAsync(x => { x.SystemChannel = new Optional(systemChannel); }); + Assert.Equal(systemChannel.Id, guild.SystemChannelId); + + await Assert.ThrowsAsync( async () => + { + await guild.ModifyAsync(x => x.SystemChannel = null); + }); + + await systemChannel.DeleteAsync(); + } + /// + /// Checks that the AFK channel of a guild can be set. + /// + [Fact] + public async Task ModifyAfkChannel() + { + var afkChannel = await guild.CreateVoiceChannelAsync("afk"); + // set using the Id + await guild.ModifyAsync(x => x.AfkChannelId = afkChannel.Id); + Assert.Equal(afkChannel.Id, guild.AFKChannelId); + + // unset using Id + await guild.ModifyAsync(x => x.AfkChannelId = null); + Assert.Null(guild.AFKChannelId); + Assert.Null(await guild.GetAFKChannelAsync()); + + // the same, but with the AfkChannel property + await guild.ModifyAsync(x => x.AfkChannel = new Optional(afkChannel)); + Assert.Equal(afkChannel.Id, guild.AFKChannelId); + + await Assert.ThrowsAsync( async () => + { + await guild.ModifyAsync(x => x.AfkChannel = null); + }); + + await afkChannel.DeleteAsync(); + } + } +} diff --git a/test/Discord.Net.Tests.Integration/RestGuildFixture.cs b/test/Discord.Net.Tests.Integration/RestGuildFixture.cs new file mode 100644 index 000000000..40b9ca9b2 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/RestGuildFixture.cs @@ -0,0 +1,44 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace Discord +{ + /// + /// Gets or creates a guild to use for testing. + /// + public class RestGuildFixture : DiscordRestClientFixture + { + public RestGuild Guild { get; private set; } + + public RestGuildFixture() : base() + { + var guilds = Client.GetGuildsAsync().Result.Where(x => x.OwnerId == Client.CurrentUser.Id).ToList(); + if (guilds.Count == 0) + { + // create a new guild if none exists already + var region = Client.GetOptimalVoiceRegionAsync().Result; + Guild = Client.CreateGuildAsync("DNET INTEGRATION TEST", region).Result; + RemoveAllChannels(); + } + else + { + // get the first one if there is a guild already created + Guild = guilds.First(); + } + } + + /// + /// Removes all channels in the guild. + /// + private void RemoveAllChannels() + { + foreach (var channel in Guild.GetChannelsAsync().Result) + { + channel.DeleteAsync().Wait(); + } + } + } +} diff --git a/test/Discord.Net.Tests.Unit/ChannelPermissionsTests.cs b/test/Discord.Net.Tests.Unit/ChannelPermissionsTests.cs new file mode 100644 index 000000000..2cab8fa21 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/ChannelPermissionsTests.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Discord +{ + /// + /// Tests the behavior of the type and related functions. + /// + public class ChannelPermissionsTests + { + /// + /// Tests the default value of the constructor. + /// + [Fact] + public void DefaultConstructor() + { + var permission = new ChannelPermissions(); + Assert.Equal((ulong)0, permission.RawValue); + Assert.Equal(ChannelPermissions.None.RawValue, permission.RawValue); + } + + /// + /// Tests the behavior of the raw value constructor. + /// + [Fact] + public void RawValueConstructor() + { + // returns all of the values that will be tested + // a Theory cannot be used here, because these values are not all constants + IEnumerable GetTestValues() + { + yield return 0; + yield return ChannelPermissions.Category.RawValue; + yield return ChannelPermissions.DM.RawValue; + yield return ChannelPermissions.Group.RawValue; + yield return ChannelPermissions.None.RawValue; + yield return ChannelPermissions.Text.RawValue; + yield return ChannelPermissions.Voice.RawValue; + }; + + foreach (var rawValue in GetTestValues()) + { + var p = new ChannelPermissions(rawValue); + Assert.Equal(rawValue, p.RawValue); + } + } + + /// + /// Tests the behavior of the constructor for each + /// of it's flags. + /// + [Fact] + public void FlagsConstructor() + { + // util method for asserting that the constructor sets the given flag + void AssertFlag(Func cstr, ChannelPermission flag) + { + var p = cstr(); + // ensure that this flag is set to true + Assert.True(p.Has(flag)); + // ensure that only this flag is set + Assert.Equal((ulong)flag, p.RawValue); + } + + AssertFlag(() => new ChannelPermissions(createInstantInvite: true), ChannelPermission.CreateInstantInvite); + AssertFlag(() => new ChannelPermissions(manageChannel: true), ChannelPermission.ManageChannels); + AssertFlag(() => new ChannelPermissions(addReactions: true), ChannelPermission.AddReactions); + AssertFlag(() => new ChannelPermissions(viewChannel: true), ChannelPermission.ViewChannel); + AssertFlag(() => new ChannelPermissions(sendMessages: true), ChannelPermission.SendMessages); + AssertFlag(() => new ChannelPermissions(sendTTSMessages: true), ChannelPermission.SendTTSMessages); + AssertFlag(() => new ChannelPermissions(manageMessages: true), ChannelPermission.ManageMessages); + AssertFlag(() => new ChannelPermissions(embedLinks: true), ChannelPermission.EmbedLinks); + AssertFlag(() => new ChannelPermissions(attachFiles: true), ChannelPermission.AttachFiles); + AssertFlag(() => new ChannelPermissions(readMessageHistory: true), ChannelPermission.ReadMessageHistory); + AssertFlag(() => new ChannelPermissions(mentionEveryone: true), ChannelPermission.MentionEveryone); + AssertFlag(() => new ChannelPermissions(useExternalEmojis: true), ChannelPermission.UseExternalEmojis); + AssertFlag(() => new ChannelPermissions(connect: true), ChannelPermission.Connect); + AssertFlag(() => new ChannelPermissions(speak: true), ChannelPermission.Speak); + AssertFlag(() => new ChannelPermissions(muteMembers: true), ChannelPermission.MuteMembers); + AssertFlag(() => new ChannelPermissions(deafenMembers: true), ChannelPermission.DeafenMembers); + AssertFlag(() => new ChannelPermissions(moveMembers: true), ChannelPermission.MoveMembers); + AssertFlag(() => new ChannelPermissions(useVoiceActivation: true), ChannelPermission.UseVAD); + AssertFlag(() => new ChannelPermissions(prioritySpeaker: true), ChannelPermission.PrioritySpeaker); + AssertFlag(() => new ChannelPermissions(stream: true), ChannelPermission.Stream); + AssertFlag(() => new ChannelPermissions(manageRoles: true), ChannelPermission.ManageRoles); + AssertFlag(() => new ChannelPermissions(manageWebhooks: true), ChannelPermission.ManageWebhooks); + AssertFlag(() => new ChannelPermissions(useApplicationCommands: true), ChannelPermission.UseApplicationCommands); + AssertFlag(() => new ChannelPermissions(createPrivateThreads: true), ChannelPermission.CreatePrivateThreads); + AssertFlag(() => new ChannelPermissions(createPublicThreads: true), ChannelPermission.CreatePublicThreads); + AssertFlag(() => new ChannelPermissions(sendMessagesInThreads: true), ChannelPermission.SendMessagesInThreads); + } + + /// + /// Tests the behavior of + /// with each of the parameters. + /// + [Fact] + public void Modify() + { + // asserts that a channel permission flag value can be checked + // and that modify can set and unset each flag + // and that ToList performs as expected + void AssertUtil(ChannelPermission permission, + Func has, + Func modify) + { + var perm = new ChannelPermissions(); + // ensure permission initially false + // use both the function and Has to ensure that the GetPermission + // function is working + Assert.False(has(perm)); + Assert.False(perm.Has(permission)); + + // enable it, and ensure that it gets set + perm = modify(perm, true); + Assert.True(has(perm)); + Assert.True(perm.Has(permission)); + + // check ToList behavior + var list = perm.ToList(); + Assert.Contains(permission, list); + Assert.Single(list); + + // set it false again + perm = modify(perm, false); + Assert.False(has(perm)); + Assert.False(perm.Has(permission)); + + // ensure that no perms are set now + Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + } + + AssertUtil(ChannelPermission.CreateInstantInvite, x => x.CreateInstantInvite, (p, enable) => p.Modify(createInstantInvite: enable)); + AssertUtil(ChannelPermission.ManageChannels, x => x.ManageChannel, (p, enable) => p.Modify(manageChannel: enable)); + AssertUtil(ChannelPermission.AddReactions, x => x.AddReactions, (p, enable) => p.Modify(addReactions: enable)); + AssertUtil(ChannelPermission.ViewChannel, x => x.ViewChannel, (p, enable) => p.Modify(viewChannel: enable)); + AssertUtil(ChannelPermission.SendMessages, x => x.SendMessages, (p, enable) => p.Modify(sendMessages: enable)); + AssertUtil(ChannelPermission.SendTTSMessages, x => x.SendTTSMessages, (p, enable) => p.Modify(sendTTSMessages: enable)); + AssertUtil(ChannelPermission.ManageMessages, x => x.ManageMessages, (p, enable) => p.Modify(manageMessages: enable)); + AssertUtil(ChannelPermission.EmbedLinks, x => x.EmbedLinks, (p, enable) => p.Modify(embedLinks: enable)); + AssertUtil(ChannelPermission.AttachFiles, x => x.AttachFiles, (p, enable) => p.Modify(attachFiles: enable)); + AssertUtil(ChannelPermission.ReadMessageHistory, x => x.ReadMessageHistory, (p, enable) => p.Modify(readMessageHistory: enable)); + AssertUtil(ChannelPermission.MentionEveryone, x => x.MentionEveryone, (p, enable) => p.Modify(mentionEveryone: enable)); + AssertUtil(ChannelPermission.UseExternalEmojis, x => x.UseExternalEmojis, (p, enable) => p.Modify(useExternalEmojis: enable)); + AssertUtil(ChannelPermission.Connect, x => x.Connect, (p, enable) => p.Modify(connect: enable)); + AssertUtil(ChannelPermission.Speak, x => x.Speak, (p, enable) => p.Modify(speak: enable)); + AssertUtil(ChannelPermission.MuteMembers, x => x.MuteMembers, (p, enable) => p.Modify(muteMembers: enable)); + AssertUtil(ChannelPermission.DeafenMembers, x => x.DeafenMembers, (p, enable) => p.Modify(deafenMembers: enable)); + AssertUtil(ChannelPermission.MoveMembers, x => x.MoveMembers, (p, enable) => p.Modify(moveMembers: enable)); + AssertUtil(ChannelPermission.UseVAD, x => x.UseVAD, (p, enable) => p.Modify(useVoiceActivation: enable)); + AssertUtil(ChannelPermission.ManageRoles, x => x.ManageRoles, (p, enable) => p.Modify(manageRoles: enable)); + AssertUtil(ChannelPermission.ManageWebhooks, x => x.ManageWebhooks, (p, enable) => p.Modify(manageWebhooks: enable)); + AssertUtil(ChannelPermission.PrioritySpeaker, x => x.PrioritySpeaker, (p, enable) => p.Modify(prioritySpeaker: enable)); + AssertUtil(ChannelPermission.Stream, x => x.Stream, (p, enable) => p.Modify(stream: enable)); + } + + /// + /// Tests that for a null channel will throw an . + /// + [Fact] + public void ChannelTypeResolution_Null() + { + Assert.Throws(() => + { + ChannelPermissions.All(null); + }); + } + + /// + /// Tests that for an will return a value + /// equivalent to . + /// + [Fact] + public void ChannelTypeResolution_Text() + { + Assert.Equal(ChannelPermissions.Text.RawValue, ChannelPermissions.All(new MockedTextChannel()).RawValue); + } + + /// + /// Tests that for an will return a value + /// equivalent to . + /// + [Fact] + public void ChannelTypeResolution_Voice() + { + Assert.Equal(ChannelPermissions.Voice.RawValue, ChannelPermissions.All(new MockedVoiceChannel()).RawValue); + } + + /// + /// Tests that for an will return a value + /// equivalent to . + /// + [Fact] + public void ChannelTypeResolution_Category() + { + Assert.Equal(ChannelPermissions.Category.RawValue, ChannelPermissions.All(new MockedCategoryChannel()).RawValue); + } + + /// + /// Tests that for an will return a value + /// equivalent to . + /// + [Fact] + public void ChannelTypeResolution_DM() + { + Assert.Equal(ChannelPermissions.DM.RawValue, ChannelPermissions.All(new MockedDMChannel()).RawValue); + } + + /// + /// Tests that for an will return a value + /// equivalent to . + /// + [Fact] + public void ChannelTypeResolution_Group() + { + Assert.Equal(ChannelPermissions.Group.RawValue, ChannelPermissions.All(new MockedGroupChannel()).RawValue); + } + + /// + /// Tests that for an invalid channel will throw an . + /// + [Fact] + public void ChannelTypeResolution_Invalid() + { + Assert.Throws(() => ChannelPermissions.All(new MockedInvalidChannel())); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.Colors.cs b/test/Discord.Net.Tests.Unit/ColorTests.cs similarity index 94% rename from test/Discord.Net.Tests/Tests.Colors.cs rename to test/Discord.Net.Tests.Unit/ColorTests.cs index 10b0bbdac..46d8feabb 100644 --- a/test/Discord.Net.Tests/Tests.Colors.cs +++ b/test/Discord.Net.Tests.Unit/ColorTests.cs @@ -1,16 +1,20 @@ using System; +using System.Collections.Generic; +using System.Text; using Xunit; namespace Discord { + /// + /// Tests for the type. + /// public class ColorTests { - [Fact] public void Color_New() { Assert.Equal(0u, new Color().RawValue); Assert.Equal(uint.MinValue, new Color(uint.MinValue).RawValue); - Assert.Equal(uint.MaxValue, new Color(uint.MaxValue).RawValue); + Assert.Throws(() => new Color(uint.MaxValue)); } [Fact] public void Color_Default() diff --git a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj new file mode 100644 index 000000000..716c3ebc4 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs new file mode 100644 index 000000000..83c6ede19 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Discord +{ + /// + /// Tests the class. + /// + public class EmbedBuilderTests + { + private const string Name = "chrisj"; + private const string Icon = "https://meowpuffygottem.fun/blob.png"; + private const string Url = "https://meowpuffygottem.fun/"; + + /// + /// Tests the behavior of . + /// + [Fact] + public void WithAuthor_Strings() + { + var builder = new EmbedBuilder(); + // null by default + Assert.Null(builder.Author); + + builder = new EmbedBuilder() + .WithAuthor(Name, Icon, Url); + + Assert.NotNull(builder.Author); + Assert.Equal(Name, builder.Author.Name); + Assert.Equal(Icon, builder.Author.IconUrl); + Assert.Equal(Url, builder.Author.Url); + } + + /// + /// Tests the behavior of + /// + [Fact] + public void WithAuthor_AuthorBuilder() + { + var author = new EmbedAuthorBuilder() + .WithIconUrl(Icon) + .WithName(Name) + .WithUrl(Url); + var builder = new EmbedBuilder() + .WithAuthor(author); + Assert.NotNull(builder.Author); + Assert.Equal(Name, builder.Author.Name); + Assert.Equal(Icon, builder.Author.IconUrl); + Assert.Equal(Url, builder.Author.Url); + } + + /// + /// Tests the behavior of + /// + [Fact] + public void WithAuthor_ActionAuthorBuilder() + { + var builder = new EmbedBuilder() + .WithAuthor((author) => + author.WithIconUrl(Icon) + .WithName(Name) + .WithUrl(Url)); + Assert.NotNull(builder.Author); + Assert.Equal(Name, builder.Author.Name); + Assert.Equal(Icon, builder.Author.IconUrl); + Assert.Equal(Url, builder.Author.Url); + } + + /// + /// Tests the behavior of . + /// + [Fact] + public void EmbedAuthorBuilder() + { + var builder = new EmbedAuthorBuilder() + .WithIconUrl(Icon) + .WithName(Name) + .WithUrl(Url); + Assert.Equal(Icon, builder.IconUrl); + Assert.Equal(Name, builder.Name); + Assert.Equal(Url, builder.Url); + } + + /// + /// Tests that invalid titles throw an . + /// + /// The embed title to set. + [Theory] + // 257 chars + [InlineData("jVyLChmA7aBZozXQuZ3VDEcwW6zOq0nteOVYBZi31ny73rpXfSSBXR4Jw6FiplDKQseKskwRMuBZkUewrewqAbkBZpslHirvC5nEzRySoDIdTRnkVvTXZUXg75l3bQCjuuHxDd6DfrY8ihd6yZX1Y0XFeg239YBcYV4TpL9uQ8H3HFYxrWhLlG2PRVjUmiglP5iXkawszNwMVm1SZ5LZT4jkMZHxFegVi7170d16iaPWOovu50aDDHy087XBtLKVa")] + // 257 chars of whitespace + [InlineData(" ")] + public void Title_Invalid(string title) + { + Assert.Throws(() => + { + var builder = new EmbedBuilder + { + Title = title + }; + }); + Assert.Throws(() => + { + new EmbedBuilder().WithTitle(title); + }); + } + + /// + /// Tests that valid titles do not throw any exceptions. + /// + /// The embed title to set. + [Theory] + // 256 chars + [InlineData("jVyLChmA7aBZozXQuZ3VDEcwW6zOq0nteOVYBZi31ny73rpXfSSBXR4Jw6FiplDKQseKskwRMuBZkUewrewqAbkBZpslHirvC5nEzRySoDIdTRnkVvTXZUXg75l3bQCjuuHxDd6DfrY8ihd6yZX1Y0XFeg239YBcYV4TpL9uQ8H3HFYxrWhLlG2PRVjUmiglP5iXkawszNwMVm1SZ5LZT4jkMZHxFegVi7170d16iaPWOovu50aDDHy087XBtLKV")] + public void Tile_Valid(string title) + { + var builder = new EmbedBuilder + { + Title = title + }; + new EmbedBuilder().WithTitle(title); + } + + /// + /// Tests that invalid descriptions throw an . + /// + [Fact] + public void Description_Invalid() + { + IEnumerable GetInvalid() + { + yield return new string('a', 4097); + } + foreach (var description in GetInvalid()) + { + Assert.Throws(() => new EmbedBuilder().WithDescription(description)); + Assert.Throws(() => + { + var b = new EmbedBuilder + { + Description = description + }; + }); + } + } + + /// + /// Tests that valid descriptions do not throw any exceptions. + /// + [Fact] + public void Description_Valid() + { + IEnumerable GetValid() + { + yield return string.Empty; + yield return null; + yield return new string('a', 4096); + } + foreach (var description in GetValid()) + { + var b = new EmbedBuilder().WithDescription(description); + Assert.Equal(description, b.Description); + + b = new EmbedBuilder + { + Description = description + }; + Assert.Equal(description, b.Description); + } + } + + /// + /// Tests that valid url's do not throw any exceptions. + /// + /// The url to set. + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("https://docs.stillu.cc")] + public void Url_Valid(string url) + { + // does not throw an exception + var result = new EmbedBuilder() + .WithUrl(url) + .WithImageUrl(url) + .WithThumbnailUrl(url); + Assert.Equal(result.Url, url); + Assert.Equal(result.ImageUrl, url); + Assert.Equal(result.ThumbnailUrl, url); + + result = new EmbedBuilder + { + Url = url, + ImageUrl = url, + ThumbnailUrl = url + }; + Assert.Equal(result.Url, url); + Assert.Equal(result.ImageUrl, url); + Assert.Equal(result.ThumbnailUrl, url); + } + + /// + /// Tests the value of the property when there are no fields set. + /// + [Fact] + public void Length_Empty() + { + var empty = new EmbedBuilder(); + Assert.Equal(0, empty.Length); + } + + /// + /// Tests the value of the property when all fields are set. + /// + [Fact] + public void Length() + { + var e = new EmbedBuilder() + .WithAuthor(Name, Icon, Url) + .WithColor(Color.Blue) + .WithDescription("This is the test description.") + .WithFooter("This is the footer", Url) + .WithImageUrl(Url) + .WithThumbnailUrl(Url) + .WithTimestamp(DateTimeOffset.MinValue) + .WithTitle("This is the title") + .WithUrl(Url) + .AddField("Field 1", "Inline", true) + .AddField("Field 2", "Not Inline", false); + Assert.Equal(100, e.Length); + } + + /// + /// Tests the behavior of . + /// + [Fact] + public void WithCurrentTimestamp() + { + var e = new EmbedBuilder() + .WithCurrentTimestamp(); + // ensure within a second of accuracy + Assert.Equal(DateTime.UtcNow, e.Timestamp.Value.UtcDateTime, TimeSpan.FromSeconds(1)); + } + + /// + /// Tests the behavior of . + /// + [Fact] + public void WithColor() + { + // use WithColor + var e = new EmbedBuilder().WithColor(Color.Red); + Assert.Equal(Color.Red.RawValue, e.Color.Value.RawValue); + } + + /// + /// Tests the behavior of + /// + [Fact] + public void WithFooter_ActionFooterBuilder() + { + var e = new EmbedBuilder() + .WithFooter(x => + { + x.IconUrl = Url; + x.Text = Name; + }); + Assert.Equal(Url, e.Footer.IconUrl); + Assert.Equal(Name, e.Footer.Text); + } + + /// + /// Tests the behavior of + /// + [Fact] + public void WithFooter_FooterBuilder() + { + var footer = new EmbedFooterBuilder() + { + IconUrl = Url, + Text = Name + }; + var e = new EmbedBuilder() + .WithFooter(footer); + Assert.Equal(Url, e.Footer.IconUrl); + Assert.Equal(Name, e.Footer.Text); + // use the property + e = new EmbedBuilder + { + Footer = footer + }; + Assert.Equal(Url, e.Footer.IconUrl); + Assert.Equal(Name, e.Footer.Text); + } + + /// + /// Tests the behavior of + /// + [Fact] + public void WithFooter_Strings() + { + var e = new EmbedBuilder() + .WithFooter(Name, Url); + Assert.Equal(Url, e.Footer.IconUrl); + Assert.Equal(Name, e.Footer.Text); + } + + /// + /// Tests the behavior of . + /// + [Fact] + public void EmbedFooterBuilder() + { + var footer = new EmbedFooterBuilder() + .WithIconUrl(Url) + .WithText(Name); + Assert.Equal(Url, footer.IconUrl); + Assert.Equal(Name, footer.Text); + } + /// + /// Tests that invalid text throws an . + /// + [Fact] + public void EmbedFooterBuilder_InvalidText() + { + Assert.Throws(() => + { + new EmbedFooterBuilder().WithText(new string('a', 2049)); + }); + } + [Fact] + public void AddField_Strings() + { + var e = new EmbedBuilder() + .AddField("name", "value", true); + Assert.Equal("name", e.Fields[0].Name); + Assert.Equal("value", e.Fields[0].Value); + Assert.True(e.Fields[0].IsInline); + } + [Fact] + public void AddField_EmbedFieldBuilder() + { + var field = new EmbedFieldBuilder() + .WithIsInline(true) + .WithValue("value") + .WithName("name"); + var e = new EmbedBuilder() + .AddField(field); + Assert.Equal("name", e.Fields[0].Name); + Assert.Equal("value", e.Fields[0].Value); + Assert.True(e.Fields[0].IsInline); + } + [Fact] + public void AddField_ActionEmbedFieldBuilder() + { + var e = new EmbedBuilder() + .AddField(x => x + .WithName("name") + .WithValue("value") + .WithIsInline(true)); + Assert.Equal("name", e.Fields[0].Name); + Assert.Equal("value", e.Fields[0].Value); + Assert.True(e.Fields[0].IsInline); + } + [Fact] + public void AddField_TooManyFields() + { + var e = new EmbedBuilder(); + for (var i = 0; i < 25; i++) + { + e = e.AddField("name", "value", false); + } + Assert.Throws(() => + { + e = e.AddField("name", "value", false); + }); + } + [Fact] + public void EmbedFieldBuilder() + { + var e = new EmbedFieldBuilder() + .WithIsInline(true) + .WithName("name") + .WithValue("value"); + Assert.Equal("name", e.Name); + Assert.Equal("value", e.Value); + Assert.True(e.IsInline); + // use the properties + e = new EmbedFieldBuilder + { + IsInline = true, + Name = "name", + Value = "value" + }; + Assert.Equal("name", e.Name); + Assert.Equal("value", e.Value); + Assert.True(e.IsInline); + } + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + // 257 chars + [InlineData("jVyLChmA7aBZozXQuZ3VDEcwW6zOq0nteOVYBZi31ny73rpXfSSBXR4Jw6FiplDKQseKskwRMuBZkUewrewqAbkBZpslHirvC5nEzRySoDIdTRnkVvTXZUXg75l3bQCjuuHxDd6DfrY8ihd6yZX1Y0XFeg239YBcYV4TpL9uQ8H3HFYxrWhLlG2PRVjUmiglP5iXkawszNwMVm1SZ5LZT4jkMZHxFegVi7170d16iaPWOovu50aDDHy087XBtLKVa")] + // 257 chars of whitespace + [InlineData(" ")] + public void EmbedFieldBuilder_InvalidName(string name) + { + Assert.Throws(() => new EmbedFieldBuilder().WithName(name)); + } + [Fact] + public void EmbedFieldBuilder_InvalidValue() + { + IEnumerable GetInvalidValue() + { + yield return null; + yield return string.Empty; + yield return " "; + yield return new string('a', 1025); + }; + foreach (var v in GetInvalidValue()) + Assert.Throws(() => new EmbedFieldBuilder().WithValue(v)); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.Emotes.cs b/test/Discord.Net.Tests.Unit/EmoteTests.cs similarity index 97% rename from test/Discord.Net.Tests/Tests.Emotes.cs rename to test/Discord.Net.Tests.Unit/EmoteTests.cs index eeadbddf8..a4f44170a 100644 --- a/test/Discord.Net.Tests/Tests.Emotes.cs +++ b/test/Discord.Net.Tests.Unit/EmoteTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Text; using Xunit; namespace Discord diff --git a/test/Discord.Net.Tests.Unit/FormatTests.cs b/test/Discord.Net.Tests.Unit/FormatTests.cs new file mode 100644 index 000000000..c015c7e15 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/FormatTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + public class FormatTests + { + [Theory] + [InlineData("@everyone", "@everyone")] + [InlineData(@"\", @"\\")] + [InlineData(@"*text*", @"\*text\*")] + [InlineData(@"~text~", @"\~text\~")] + [InlineData(@"`text`", @"\`text\`")] + [InlineData(@"_text_", @"\_text\_")] + [InlineData(@"> text", @"\> text")] + public void Sanitize(string input, string expected) + { + Assert.Equal(expected, Format.Sanitize(input)); + } + [Fact] + public void Code() + { + // no language + Assert.Equal("`test`", Format.Code("test")); + Assert.Equal("```\nanother\none\n```", Format.Code("another\none")); + // language specified + Assert.Equal("```cs\ntest\n```", Format.Code("test", "cs")); + Assert.Equal("```cs\nanother\none\n```", Format.Code("another\none", "cs")); + } + [Fact] + public void QuoteNullString() + { + Assert.Null(Format.Quote(null)); + } + [Theory] + [InlineData("", "")] + [InlineData("\n", "\n")] + [InlineData("foo\n\nbar", "> foo\n> \n> bar")] + [InlineData("input", "> input")] // single line + // should work with CR or CRLF + [InlineData("inb4\ngreentext", "> inb4\n> greentext")] + [InlineData("inb4\r\ngreentext", "> inb4\r\n> greentext")] + public void Quote(string input, string expected) + { + Assert.Equal(expected, Format.Quote(input)); + } + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("\n", "\n")] + [InlineData("foo\n\nbar", ">>> foo\n\nbar")] + [InlineData("input", ">>> input")] // single line + // should work with CR or CRLF + [InlineData("inb4\ngreentext", ">>> inb4\ngreentext")] + [InlineData("inb4\r\ngreentext", ">>> inb4\r\ngreentext")] + public void BlockQuote(string input, string expected) + { + Assert.Equal(expected, Format.BlockQuote(input)); + } + + [Theory] + [InlineData("", "")] + [InlineData("\n", "\n")] + [InlineData("**hi**", "hi")] + [InlineData("__uwu__", "uwu")] + [InlineData(">>__uwu__", "uwu")] + [InlineData("```uwu```", "uwu")] + [InlineData("~uwu~", "uwu")] + [InlineData("berries __and__ *Cream**, I'm a little lad who loves berries and cream", "berries and Cream, I'm a little lad who loves berries and cream")] + public void StripMarkdown(string input, string expected) + { + var test = Format.StripMarkDown(input); + Assert.Equal(expected, test); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs new file mode 100644 index 000000000..f0b0b2db7 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + /// + /// Tests the behavior of the type and related functions. + /// + public class GuildPermissionsTests + { + /// + /// Tests the default value of the constructor. + /// + [Fact] + public void DefaultConstructor() + { + var p = new GuildPermissions(); + Assert.Equal((ulong)0, p.RawValue); + Assert.Equal(GuildPermissions.None.RawValue, p.RawValue); + } + + /// + /// Tests the behavior of the raw value constructor. + /// + [Fact] + public void RawValueConstructor() + { + // returns all of the values that will be tested + // a Theory cannot be used here, because these values are not all constants + IEnumerable GetTestValues() + { + yield return 0; + yield return GuildPermissions.None.RawValue; + yield return GuildPermissions.All.RawValue; + yield return GuildPermissions.Webhook.RawValue; + }; + + foreach (var rawValue in GetTestValues()) + { + var p = new GuildPermissions(rawValue); + Assert.Equal(rawValue, p.RawValue); + } + } + + /// + /// Tests the behavior of the constructor for each + /// of it's flags. + /// + [Fact] + public void FlagsConstructor() + { + // util method for asserting that the constructor sets the given flag + void AssertFlag(Func cstr, GuildPermission flag) + { + var p = cstr(); + // ensure flag set to true + Assert.True(p.Has(flag)); + // ensure only this flag is set + Assert.Equal((ulong)flag, p.RawValue); + } + + AssertFlag(() => new GuildPermissions(createInstantInvite: true), GuildPermission.CreateInstantInvite); + AssertFlag(() => new GuildPermissions(kickMembers: true), GuildPermission.KickMembers); + AssertFlag(() => new GuildPermissions(banMembers: true), GuildPermission.BanMembers); + AssertFlag(() => new GuildPermissions(administrator: true), GuildPermission.Administrator); + AssertFlag(() => new GuildPermissions(manageChannels: true), GuildPermission.ManageChannels); + AssertFlag(() => new GuildPermissions(manageGuild: true), GuildPermission.ManageGuild); + AssertFlag(() => new GuildPermissions(addReactions: true), GuildPermission.AddReactions); + AssertFlag(() => new GuildPermissions(viewAuditLog: true), GuildPermission.ViewAuditLog); + AssertFlag(() => new GuildPermissions(viewGuildInsights: true), GuildPermission.ViewGuildInsights); + AssertFlag(() => new GuildPermissions(viewChannel: true), GuildPermission.ViewChannel); + AssertFlag(() => new GuildPermissions(sendMessages: true), GuildPermission.SendMessages); + AssertFlag(() => new GuildPermissions(sendTTSMessages: true), GuildPermission.SendTTSMessages); + AssertFlag(() => new GuildPermissions(manageMessages: true), GuildPermission.ManageMessages); + AssertFlag(() => new GuildPermissions(embedLinks: true), GuildPermission.EmbedLinks); + AssertFlag(() => new GuildPermissions(attachFiles: true), GuildPermission.AttachFiles); + AssertFlag(() => new GuildPermissions(readMessageHistory: true), GuildPermission.ReadMessageHistory); + AssertFlag(() => new GuildPermissions(mentionEveryone: true), GuildPermission.MentionEveryone); + AssertFlag(() => new GuildPermissions(useExternalEmojis: true), GuildPermission.UseExternalEmojis); + AssertFlag(() => new GuildPermissions(connect: true), GuildPermission.Connect); + AssertFlag(() => new GuildPermissions(speak: true), GuildPermission.Speak); + AssertFlag(() => new GuildPermissions(muteMembers: true), GuildPermission.MuteMembers); + AssertFlag(() => new GuildPermissions(deafenMembers: true), GuildPermission.DeafenMembers); + AssertFlag(() => new GuildPermissions(moveMembers: true), GuildPermission.MoveMembers); + AssertFlag(() => new GuildPermissions(useVoiceActivation: true), GuildPermission.UseVAD); + AssertFlag(() => new GuildPermissions(prioritySpeaker: true), GuildPermission.PrioritySpeaker); + AssertFlag(() => new GuildPermissions(stream: true), GuildPermission.Stream); + AssertFlag(() => new GuildPermissions(changeNickname: true), GuildPermission.ChangeNickname); + AssertFlag(() => new GuildPermissions(manageNicknames: true), GuildPermission.ManageNicknames); + AssertFlag(() => new GuildPermissions(manageRoles: true), GuildPermission.ManageRoles); + AssertFlag(() => new GuildPermissions(manageWebhooks: true), GuildPermission.ManageWebhooks); + AssertFlag(() => new GuildPermissions(manageEmojisAndStickers: true), GuildPermission.ManageEmojisAndStickers); + AssertFlag(() => new GuildPermissions(useApplicationCommands: true), GuildPermission.UseApplicationCommands); + AssertFlag(() => new GuildPermissions(requestToSpeak: true), GuildPermission.RequestToSpeak); + AssertFlag(() => new GuildPermissions(manageEvents: true), GuildPermission.ManageEvents); + AssertFlag(() => new GuildPermissions(manageThreads: true), GuildPermission.ManageThreads); + AssertFlag(() => new GuildPermissions(createPublicThreads: true), GuildPermission.CreatePublicThreads); + AssertFlag(() => new GuildPermissions(createPrivateThreads: true), GuildPermission.CreatePrivateThreads); + AssertFlag(() => new GuildPermissions(useExternalStickers: true), GuildPermission.UseExternalStickers); + } + + /// + /// Tests the behavior of + /// with each of the parameters. + /// + [Fact] + public void Modify() + { + // asserts that flag values can be checked + // and that flag values can be toggled on and off + // and that the behavior of ToList works as expected + void AssertUtil(GuildPermission permission, + Func has, + Func modify) + { + var perm = new GuildPermissions(); + // ensure permission initially false + // use both the function and Has to ensure that the GetPermission + // function is working + Assert.False(has(perm)); + Assert.False(perm.Has(permission)); + + // enable it, and ensure that it gets set + perm = modify(perm, true); + Assert.True(has(perm)); + Assert.True(perm.Has(permission)); + + // check ToList behavior + var list = perm.ToList(); + Assert.Contains(permission, list); + Assert.Single(list); + + // set it false again + perm = modify(perm, false); + Assert.False(has(perm)); + Assert.False(perm.Has(permission)); + + // ensure that no perms are set now + Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + } + + AssertUtil(GuildPermission.CreateInstantInvite, x => x.CreateInstantInvite, (p, enable) => p.Modify(createInstantInvite: enable)); + AssertUtil(GuildPermission.KickMembers, x => x.KickMembers, (p, enable) => p.Modify(kickMembers: enable)); + AssertUtil(GuildPermission.BanMembers, x => x.BanMembers, (p, enable) => p.Modify(banMembers: enable)); + AssertUtil(GuildPermission.Administrator, x => x.Administrator, (p, enable) => p.Modify(administrator: enable)); + AssertUtil(GuildPermission.ManageChannels, x => x.ManageChannels, (p, enable) => p.Modify(manageChannels: enable)); + AssertUtil(GuildPermission.ManageGuild, x => x.ManageGuild, (p, enable) => p.Modify(manageGuild: enable)); + AssertUtil(GuildPermission.AddReactions, x => x.AddReactions, (p, enable) => p.Modify(addReactions: enable)); + AssertUtil(GuildPermission.ViewAuditLog, x => x.ViewAuditLog, (p, enable) => p.Modify(viewAuditLog: enable)); + AssertUtil(GuildPermission.ViewGuildInsights, x => x.ViewGuildInsights, (p, enable) => p.Modify(viewGuildInsights: enable)); + AssertUtil(GuildPermission.ViewChannel, x => x.ViewChannel, (p, enable) => p.Modify(viewChannel: enable)); + AssertUtil(GuildPermission.SendMessages, x => x.SendMessages, (p, enable) => p.Modify(sendMessages: enable)); + AssertUtil(GuildPermission.SendTTSMessages, x => x.SendTTSMessages, (p, enable) => p.Modify(sendTTSMessages: enable)); + AssertUtil(GuildPermission.ManageMessages, x => x.ManageMessages, (p, enable) => p.Modify(manageMessages: enable)); + AssertUtil(GuildPermission.EmbedLinks, x => x.EmbedLinks, (p, enable) => p.Modify(embedLinks: enable)); + AssertUtil(GuildPermission.AttachFiles, x => x.AttachFiles, (p, enable) => p.Modify(attachFiles: enable)); + AssertUtil(GuildPermission.ReadMessageHistory, x => x.ReadMessageHistory, (p, enable) => p.Modify(readMessageHistory: enable)); + AssertUtil(GuildPermission.MentionEveryone, x => x.MentionEveryone, (p, enable) => p.Modify(mentionEveryone: enable)); + AssertUtil(GuildPermission.UseExternalEmojis, x => x.UseExternalEmojis, (p, enable) => p.Modify(useExternalEmojis: enable)); + AssertUtil(GuildPermission.Connect, x => x.Connect, (p, enable) => p.Modify(connect: enable)); + AssertUtil(GuildPermission.Speak, x => x.Speak, (p, enable) => p.Modify(speak: enable)); + AssertUtil(GuildPermission.MuteMembers, x => x.MuteMembers, (p, enable) => p.Modify(muteMembers: enable)); + AssertUtil(GuildPermission.MoveMembers, x => x.MoveMembers, (p, enable) => p.Modify(moveMembers: enable)); + AssertUtil(GuildPermission.UseVAD, x => x.UseVAD, (p, enable) => p.Modify(useVoiceActivation: enable)); + AssertUtil(GuildPermission.ChangeNickname, x => x.ChangeNickname, (p, enable) => p.Modify(changeNickname: enable)); + AssertUtil(GuildPermission.ManageNicknames, x => x.ManageNicknames, (p, enable) => p.Modify(manageNicknames: enable)); + AssertUtil(GuildPermission.ManageRoles, x => x.ManageRoles, (p, enable) => p.Modify(manageRoles: enable)); + AssertUtil(GuildPermission.ManageWebhooks, x => x.ManageWebhooks, (p, enable) => p.Modify(manageWebhooks: enable)); + AssertUtil(GuildPermission.ManageEmojisAndStickers, x => x.ManageEmojisAndStickers, (p, enable) => p.Modify(manageEmojisAndStickers: enable)); + AssertUtil(GuildPermission.UseApplicationCommands, x => x.UseApplicationCommands, (p, enable) => p.Modify(useApplicationCommands: enable)); + AssertUtil(GuildPermission.RequestToSpeak, x => x.RequestToSpeak, (p, enable) => p.Modify(requestToSpeak: enable)); + AssertUtil(GuildPermission.ManageEvents, x => x.ManageEvents, (p, enable) => p.Modify(manageEvents: enable)); + AssertUtil(GuildPermission.ManageThreads, x => x.ManageThreads, (p, enable) => p.Modify(manageThreads: enable)); + AssertUtil(GuildPermission.CreatePublicThreads, x => x.CreatePublicThreads, (p, enable) => p.Modify(createPublicThreads: enable)); + AssertUtil(GuildPermission.CreatePrivateThreads, x => x.CreatePrivateThreads, (p, enable) => p.Modify(createPrivateThreads: enable)); + AssertUtil(GuildPermission.UseExternalStickers, x => x.UseExternalStickers, (p, enable) => p.Modify(useExternalStickers: enable)); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/MentionUtilsTests.cs b/test/Discord.Net.Tests.Unit/MentionUtilsTests.cs new file mode 100644 index 000000000..abd1191c8 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MentionUtilsTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + /// + /// Tests the methods provided in . + /// + public class MentionUtilsTests + { + /// + /// Tests + /// + [Fact] + public void MentionUser() + { + Assert.Equal("<@!123>", MentionUtils.MentionUser(123u)); + Assert.Equal("<@!123>", MentionUtils.MentionUser("123")); + Assert.Equal("<@!123>", MentionUtils.MentionUser("123", true)); + Assert.Equal("<@123>", MentionUtils.MentionUser("123", false)); + } + /// + /// Tests + /// + [Fact] + public void MentionChannel() + { + Assert.Equal("<#123>", MentionUtils.MentionChannel(123u)); + Assert.Equal("<#123>", MentionUtils.MentionChannel("123")); + } + /// + /// Tests + /// + [Fact] + public void MentionRole() + { + Assert.Equal("<@&123>", MentionUtils.MentionRole(123u)); + Assert.Equal("<@&123>", MentionUtils.MentionRole("123")); + } + [Theory] + [InlineData("<@!123>", 123)] + [InlineData("<@123>", 123)] + public void ParseUser_Pass(string user, ulong id) + { + var parsed = MentionUtils.ParseUser(user); + Assert.Equal(id, parsed); + + Assert.True(MentionUtils.TryParseUser(user, out ulong result)); + Assert.Equal(id, result); + } + [Theory] + [InlineData(" ")] + [InlineData("invalid")] + [InlineData("<12!3@>")] + [InlineData("<123>")] + public void ParseUser_Fail(string user) + { + Assert.Throws(() => MentionUtils.ParseUser(user)); + Assert.False(MentionUtils.TryParseUser(user, out _)); + } + [Fact] + public void ParseUser_Null() + { + Assert.Throws(() => MentionUtils.ParseUser(null)); + Assert.Throws(() => MentionUtils.TryParseUser(null, out _)); + } + [Theory] + [InlineData("<#123>", 123)] + public void ParseChannel_Pass(string channel, ulong id) + { + var parsed = MentionUtils.ParseChannel(channel); + Assert.Equal(id, parsed); + + Assert.True(MentionUtils.TryParseChannel(channel, out ulong result)); + Assert.Equal(id, result); + } + [Theory] + [InlineData(" ")] + [InlineData("invalid")] + [InlineData("<12#3>")] + [InlineData("<123>")] + public void ParseChannel_Fail(string channel) + { + Assert.Throws(() => MentionUtils.ParseChannel(channel)); + Assert.False(MentionUtils.TryParseChannel(channel, out _)); + } + [Fact] + public void ParseChannel_Null() + { + Assert.Throws(() => MentionUtils.ParseChannel(null)); + Assert.Throws(() => MentionUtils.TryParseChannel(null, out _)); + } + [Theory] + [InlineData("<@&123>", 123)] + public void ParseRole_Pass(string role, ulong id) + { + var parsed = MentionUtils.ParseRole(role); + Assert.Equal(id, parsed); + + Assert.True(MentionUtils.TryParseRole(role, out ulong result)); + Assert.Equal(id, result); + } + [Theory] + [InlineData(" ")] + [InlineData("invalid")] + [InlineData("<12@&3>")] + [InlineData("<123>")] + public void ParseRole_Fail(string role) + { + Assert.Throws(() => MentionUtils.ParseRole(role)); + Assert.False(MentionUtils.TryParseRole(role, out _)); + } + [Fact] + public void ParseRole_Null() + { + Assert.Throws(() => MentionUtils.ParseRole(null)); + Assert.Throws(() => MentionUtils.TryParseRole(null, out _)); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/MessageHelperTests.cs b/test/Discord.Net.Tests.Unit/MessageHelperTests.cs new file mode 100644 index 000000000..0c329f192 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MessageHelperTests.cs @@ -0,0 +1,118 @@ +using Xunit; +using Discord.Rest; + +namespace Discord +{ + /// + /// Tests for parsing. + /// + public class MessageHelperTests + { + /// + /// Tests that no tags are parsed while in code blocks + /// or inline code. + /// + [Theory] + [InlineData("`@everyone`")] + [InlineData("`<@163184946742034432>`")] + [InlineData("```@everyone```")] + [InlineData("```cs \n @everyone```")] + [InlineData("```cs <@163184946742034432> ```")] + [InlineData("``` test ``` ```cs <@163184946742034432> ```")] + [InlineData("`<:test:537920404019216384>`")] + [InlineData("``` @everyone `")] // discord client handles these weirdly + [InlineData("``` @everyone ``")] + [InlineData("` @here `")] + [InlineData("` @everyone @here <@163184946742034432> <@&163184946742034432> <#163184946742034432> <:test:537920404019216384> `")] + public void ParseTagsInCode(string testData) + { + // don't care that I'm passing in null channels/guilds/users + // as they shouldn't be required + var result = MessageHelper.ParseTags(testData, null, null, null); + Assert.Empty(result); + } + + /// Tests parsing tags that surround inline code or a code block. + [Theory] + [InlineData("`` <@&163184946742034432>")] + [InlineData("``` code block 1 ``` ``` code block 2 ``` <@&163184946742034432>")] + [InlineData("` code block 1 ``` ` code block 2 ``` <@&163184946742034432>")] + [InlineData("<@&163184946742034432> ``` code block 1 ```")] + [InlineData("``` code ``` ``` code ``` @here ``` code ``` ``` more ```")] + [InlineData("``` code ``` @here ``` more ```")] + public void ParseTagsAroundCode(string testData) + { + // don't care that I'm passing in null channels/guilds/users + // as they shouldn't be required + var result = MessageHelper.ParseTags(testData, null, null, null); + Assert.NotEmpty(result); + } + + [Theory] + [InlineData(@"\` @everyone \`")] + [InlineData(@"\`\`\` @everyone \`\`\`")] + [InlineData(@"hey\`\`\`@everyone\`\`\`!!")] + public void IgnoreEscapedCodeBlocks(string testData) + { + var result = MessageHelper.ParseTags(testData, null, null, null); + Assert.NotEmpty(result); + } + + // cannot test parsing a user, as it uses the ReadOnlyCollection arg. + // this could be done if mocked entities are merged in PR #1290 + + /// Tests parsing a mention of a role. + [Theory] + [InlineData("<@&163184946742034432>")] + [InlineData("**<@&163184946742034432>**")] + [InlineData("__<@&163184946742034432>__")] + [InlineData("<><@&163184946742034432>")] + public void ParseRole(string roleTag) + { + var result = MessageHelper.ParseTags(roleTag, null, null, null); + Assert.Contains(result, x => x.Type == TagType.RoleMention); + } + + /// Tests parsing a channel. + [Theory] + [InlineData("<#429115823748284417>")] + [InlineData("**<#429115823748284417>**")] + [InlineData("<><#429115823748284417>")] + public void ParseChannel(string channelTag) + { + var result = MessageHelper.ParseTags(channelTag, null, null, null); + Assert.Contains(result, x => x.Type == TagType.ChannelMention); + } + + /// Tests parsing an emoji. + [Theory] + [InlineData("<:test:537920404019216384>")] + [InlineData("**<:test:537920404019216384>**")] + [InlineData("<><:test:537920404019216384>")] + public void ParseEmoji(string emoji) + { + var result = MessageHelper.ParseTags(emoji, null, null, null); + Assert.Contains(result, x => x.Type == TagType.Emoji); + } + + /// Tests parsing a mention of @everyone. + [Theory] + [InlineData("@everyone")] + [InlineData("**@everyone**")] + public void ParseEveryone(string everyone) + { + var result = MessageHelper.ParseTags(everyone, null, null, null); + Assert.Contains(result, x => x.Type == TagType.EveryoneMention); + } + + /// Tests parsing a mention of @here. + [Theory] + [InlineData("@here")] + [InlineData("**@here**")] + public void ParseHere(string here) + { + var result = MessageHelper.ParseTags(here, null, null, null); + Assert.Contains(result, x => x.Type == TagType.HereMention); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs new file mode 100644 index 000000000..712570467 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal sealed class MockedCategoryChannel : ICategoryChannel + { + public int Position => throw new NotImplementedException(); + + public IGuild Guild => throw new NotImplementedException(); + + public ulong GuildId => throw new NotImplementedException(); + + public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id => throw new NotImplementedException(); + + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs new file mode 100644 index 000000000..519bab4d9 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal sealed class MockedDMChannel : IDMChannel + { + public IUser Recipient => throw new NotImplementedException(); + + public IReadOnlyCollection Recipients => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id => throw new NotImplementedException(); + + public Task CloseAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IDisposable EnterTypingState(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task TriggerTypingAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs new file mode 100644 index 000000000..6b134d92f --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Discord.Audio; + +namespace Discord +{ + internal sealed class MockedGroupChannel : IGroupChannel + { + public IReadOnlyCollection Recipients => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id => throw new NotImplementedException(); + + public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DisconnectAsync() + { + throw new NotImplementedException(); + } + + public IDisposable EnterTypingState(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task LeaveAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task TriggerTypingAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs new file mode 100644 index 000000000..362eeb979 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedInvalidChannel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a channel that is of an unrecognized type. + /// + internal sealed class MockedInvalidChannel : IChannel + { + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id => throw new NotImplementedException(); + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs new file mode 100644 index 000000000..ad0af04b2 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + internal sealed class MockedTextChannel : ITextChannel + { + public bool IsNsfw => throw new NotImplementedException(); + + public string Topic => throw new NotImplementedException(); + + public int SlowModeInterval => throw new NotImplementedException(); + + public string Mention => throw new NotImplementedException(); + + public ulong? CategoryId => throw new NotImplementedException(); + + public int Position => throw new NotImplementedException(); + + public IGuild Guild => throw new NotImplementedException(); + + public ulong GuildId => throw new NotImplementedException(); + + public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + + public ulong Id => throw new NotImplementedException(); + + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + { + throw new NotImplementedException(); + } + public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IDisposable EnterTypingState(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetInvitesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + throw new NotImplementedException(); + } + + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetWebhooksAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + { + throw new NotImplementedException(); + } + + public Task SyncPermissionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task TriggerTypingAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs new file mode 100644 index 000000000..4514dfc97 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Discord.Audio; + +namespace Discord +{ + internal sealed class MockedVoiceChannel : IVoiceChannel + { + public int Bitrate => throw new NotImplementedException(); + + public int? UserLimit => throw new NotImplementedException(); + + public string Mention => throw new NotImplementedException(); + + public ulong? CategoryId => throw new NotImplementedException(); + + public int Position => throw new NotImplementedException(); + + public IGuild Guild => throw new NotImplementedException(); + + public ulong GuildId => throw new NotImplementedException(); + + public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public DateTimeOffset CreatedAt => throw new NotImplementedException(); + public ulong Id => throw new NotImplementedException(); + + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) + { + throw new NotImplementedException(); + } + + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + { + throw new NotImplementedException(); + } + public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + + public Task DeleteAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DisconnectAsync() + { + throw new NotImplementedException(); + } + + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task> GetInvitesAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + throw new NotImplementedException(); + } + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + throw new NotImplementedException(); + } + + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SyncPermissionsAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/SnowflakeUtilsTests.cs b/test/Discord.Net.Tests.Unit/SnowflakeUtilsTests.cs new file mode 100644 index 000000000..f7cbf9298 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/SnowflakeUtilsTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + public class SnowflakeUtilsTests + { + [Fact] + public void FromSnowflake() + { + // snowflake from a userid + var id = 163184946742034432u; + Assert.Equal(new DateTime(2016, 3, 26, 7, 18, 43), SnowflakeUtils.FromSnowflake(id).UtcDateTime, TimeSpan.FromSeconds(1)); + } + [Fact] + public void ToSnowflake() + { + // most significant digits should match, but least significant digits cannot be determined from here + Assert.Equal(163184946184192000u, SnowflakeUtils.ToSnowflake(new DateTimeOffset(2016, 3, 26, 7, 18, 43, TimeSpan.Zero))); + } + } +} diff --git a/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs b/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs new file mode 100644 index 000000000..4cd9cae09 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs @@ -0,0 +1,70 @@ +using Discord.Commands; +using System; +using Xunit; + +namespace Discord +{ + public class TimeSpanTypeReaderTests + { + [Theory] + [InlineData("4d3h2m1s", false)] // tests format "%d'd'%h'h'%m'm'%s's'" + [InlineData("4d3h2m", false)] // tests format "%d'd'%h'h'%m'm'" + [InlineData("4d3h1s", false)] // tests format "%d'd'%h'h'%s's'" + [InlineData("4d3h", false)] // tests format "%d'd'%h'h'" + [InlineData("4d2m1s", false)] // tests format "%d'd'%m'm'%s's'" + [InlineData("4d2m", false)] // tests format "%d'd'%m'm'" + [InlineData("4d1s", false)] // tests format "%d'd'%s's'" + [InlineData("4d", false)] // tests format "%d'd'" + [InlineData("3h2m1s", false)] // tests format "%h'h'%m'm'%s's'" + [InlineData("3h2m", false)] // tests format "%h'h'%m'm'" + [InlineData("3h1s", false)] // tests format "%h'h'%s's'" + [InlineData("3h", false)] // tests format "%h'h'" + [InlineData("2m1s", false)] // tests format "%m'm'%s's'" + [InlineData("2m", false)] // tests format "%m'm'" + [InlineData("1s", false)] // tests format "%s's'" + // Negatives + [InlineData("-4d3h2m1s", true)] // tests format "-%d'd'%h'h'%m'm'%s's'" + [InlineData("-4d3h2m", true)] // tests format "-%d'd'%h'h'%m'm'" + [InlineData("-4d3h1s", true)] // tests format "-%d'd'%h'h'%s's'" + [InlineData("-4d3h", true)] // tests format "-%d'd'%h'h'" + [InlineData("-4d2m1s", true)] // tests format "-%d'd'%m'm'%s's'" + [InlineData("-4d2m", true)] // tests format "-%d'd'%m'm'" + [InlineData("-4d1s", true)] // tests format "-%d'd'%s's'" + [InlineData("-4d", true)] // tests format "-%d'd'" + [InlineData("-3h2m1s", true)] // tests format "-%h'h'%m'm'%s's'" + [InlineData("-3h2m", true)] // tests format "-%h'h'%m'm'" + [InlineData("-3h1s", true)] // tests format "-%h'h'%s's'" + [InlineData("-3h", true)] // tests format "-%h'h'" + [InlineData("-2m1s", true)] // tests format "-%m'm'%s's'" + [InlineData("-2m", true)] // tests format "-%m'm'" + [InlineData("-1s", true)] // tests format "-%s's'" + public void TestTimeSpanParse(string input, bool isNegative) + { + var reader = new TimeSpanTypeReader(); + var result = reader.ReadAsync(null, input, null).Result; + Assert.True(result.IsSuccess); + + var actual = (TimeSpan)result.BestMatch; + Assert.True(actual != TimeSpan.Zero); + + if (isNegative) + { + Assert.True(actual < TimeSpan.Zero); + + Assert.True(actual.Seconds == 0 || actual.Seconds == -1); + Assert.True(actual.Minutes == 0 || actual.Minutes == -2); + Assert.True(actual.Hours == 0 || actual.Hours == -3); + Assert.True(actual.Days == 0 || actual.Days == -4); + } + else + { + Assert.True(actual > TimeSpan.Zero); + + Assert.True(actual.Seconds == 0 || actual.Seconds == 1); + Assert.True(actual.Minutes == 0 || actual.Minutes == 2); + Assert.True(actual.Hours == 0 || actual.Hours == 3); + Assert.True(actual.Days == 0 || actual.Days == 4); + } + } + } +} diff --git a/test/Discord.Net.Tests/Tests.TokenUtils.cs b/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs similarity index 56% rename from test/Discord.Net.Tests/Tests.TokenUtils.cs rename to test/Discord.Net.Tests.Unit/TokenUtilsTests.cs index dc5a93e34..4306fa9e2 100644 --- a/test/Discord.Net.Tests/Tests.TokenUtils.cs +++ b/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs @@ -5,6 +5,9 @@ using Xunit; namespace Discord { + /// + /// Tests for the methods. + /// public class TokenUtilsTests { /// @@ -18,14 +21,14 @@ namespace Discord [InlineData(" ")] [InlineData(" ")] [InlineData("\t")] - public void TestNullOrWhitespaceToken(string token) + public void NullOrWhitespaceToken(string token) { // an ArgumentNullException should be thrown, regardless of the TokenType Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bearer, token)); Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Webhook, token)); } - + /// /// Tests the behavior of /// to see that valid Webhook tokens do not throw Exceptions. @@ -39,7 +42,7 @@ namespace Discord [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] // client secret [InlineData("937it3ow87i4ery69876wqire")] - public void TestWebhookTokenDoesNotThrowExceptions(string token) + public void WebhookTokenDoesNotThrowExceptions(string token) { TokenUtils.ValidateToken(TokenType.Webhook, token); } @@ -59,7 +62,7 @@ namespace Discord [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] // client secret [InlineData("937it3ow87i4ery69876wqire")] - public void TestBearerTokenDoesNotThrowExceptions(string token) + public void BearerTokenDoesNotThrowExceptions(string token) { TokenUtils.ValidateToken(TokenType.Bearer, token); } @@ -69,17 +72,18 @@ namespace Discord /// /// Tests the behavior of /// to see that valid Bot tokens do not throw Exceptions. - /// Valid Bot tokens can be strings of length 59 or above. + /// Valid Bot tokens can be strings of length 58 or above. /// [Theory] + // missing a single character from the end, 58 char. still should be valid + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] + // 59 char token [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] - [InlineData("This appears to be completely invalid, however the current validation rules are not very strict.")] [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] - [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] - public void TestBotTokenDoesNotThrowExceptions(string token) + public void BotTokenDoesNotThrowExceptions(string token) { // This example token is pulled from the Discord Docs - // https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header + // https://discord.com/developers/docs/reference#authentication-example-bot-token-authorization-header // should not throw any exception TokenUtils.ValidateToken(TokenType.Bot, token); } @@ -90,13 +94,26 @@ namespace Discord /// [Theory] [InlineData("This is invalid")] - // missing a single character from the end - [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] // bearer token [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] // client secret [InlineData("937it3ow87i4ery69876wqire")] - public void TestBotTokenInvalidThrowsArgumentException(string token) + // 57 char bot token + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")] + // ends with invalid characters + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k ")] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k\n")] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k\t")] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k\r\n")] + // starts with invalid characters + [InlineData(" MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k")] + [InlineData("\nMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k")] + [InlineData("\tMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k")] + [InlineData("\r\nMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7k")] + [InlineData("This is an invalid token, but it passes the check for string length.")] + // valid token, but passed in twice + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] + public void BotTokenInvalidThrowsArgumentException(string token) { Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); } @@ -110,15 +127,53 @@ namespace Discord /// The type is treated as an invalid . /// [Theory] - // TokenType.User - [InlineData(0)] // out of range TokenType + [InlineData(-1)] [InlineData(4)] [InlineData(7)] - public void TestUnrecognizedTokenType(int type) + public void UnrecognizedTokenType(int type) { Assert.Throws(() => TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); } + + /// + /// Checks the method for expected output. + /// + /// The Bot Token to test. + /// The expected result. + [Theory] + // this method only checks the first part of the JWT + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4..", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4. this part is invalid. this part is also invalid", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.", false)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4", false)] + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw.xxxx.xxxxx", true)] + // should not throw an unexpected exception + [InlineData("", false)] + [InlineData(null, false)] + public void CheckBotTokenValidity(string token, bool expected) + { + Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token)); + } + + [Theory] + // cannot pass a ulong? as a param in InlineData, so have to have a separate param + // indicating if a value is null + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw", false, 428477944009195520)] + // should return null w/o throwing other exceptions + [InlineData("", true, 0)] + [InlineData(" ", true, 0)] + [InlineData(null, true, 0)] + [InlineData("these chars aren't allowed @U#)*@#!)*", true, 0)] + public void DecodeBase64UserId(string encodedUserId, bool isNull, ulong expectedUserId) + { + var result = TokenUtils.DecodeBase64UserId(encodedUserId); + if (isNull) + Assert.Null(result); + else + Assert.Equal(expectedUserId, result); + } } } diff --git a/test/Discord.Net.Tests.Unit/TypeReaderTests.cs b/test/Discord.Net.Tests.Unit/TypeReaderTests.cs new file mode 100644 index 000000000..59eb3136c --- /dev/null +++ b/test/Discord.Net.Tests.Unit/TypeReaderTests.cs @@ -0,0 +1,142 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public sealed class TypeReaderTests + { + [Fact] + public async Task TestNamedArgumentReader() + { + using (var commands = new CommandService()) + { + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "bar: hello foo: 42"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + } + + [Fact] + public async Task TestQuotedArgumentValue() + { + using (var commands = new CommandService()) + { + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foo: 42 bar: 《hello》"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + } + + [Fact] + public async Task TestNonPatternInput() + { + using (var commands = new CommandService()) + { + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foobar"); + Assert.False(result.IsSuccess); + Assert.Equal(expected: CommandError.Exception, actual: result.Error); + } + } + + [Fact] + public async Task TestMultiple() + { + using (var commands = new CommandService()) + { + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: new int[] { 1, 2, 3, 4, 5, 6, 7 }, actual: m.ManyInts); + } + } + } + + [NamedArgumentType] + public sealed class ArgumentType + { + public int Foo { get; set; } + + [OverrideTypeReader(typeof(CustomTypeReader))] + public string Bar { get; set; } + + public IEnumerable ManyInts { get; set; } + } + + public sealed class CustomTypeReader : TypeReader + { + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + => Task.FromResult(TypeReaderResult.FromSuccess(input)); + } + + public sealed class TestModule : ModuleBase + { + [Command("test")] + public Task TestCommand(ArgumentType arg) => Task.Delay(0); + } +} diff --git a/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs b/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs deleted file mode 100644 index 729bc385c..000000000 --- a/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using System.Reflection; -using Microsoft.DotNet.PlatformAbstractions; -using Microsoft.Extensions.DependencyModel; - -namespace System -{ - /// Polyfill of the AppDomain class from full framework. - internal class AppDomain - { - public static AppDomain CurrentDomain { get; private set; } - - private AppDomain() - { - } - - static AppDomain() - { - CurrentDomain = new AppDomain(); - } - - public Assembly[] GetAssemblies() - { - var rid = RuntimeEnvironment.GetRuntimeIdentifier(); - var ass = DependencyContext.Default.GetRuntimeAssemblyNames(rid); - - return ass.Select(xan => Assembly.Load(xan)).ToArray(); - } - } -} \ 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 deleted file mode 100644 index 60491a96f..000000000 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - Exe - Discord - netcoreapp1.1 - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - diff --git a/test/Discord.Net.Tests/Net/CacheInfo.cs b/test/Discord.Net.Tests/Net/CacheInfo.cs deleted file mode 100644 index ed2820b8e..000000000 --- a/test/Discord.Net.Tests/Net/CacheInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.Net -{ - internal class CacheInfo - { - [JsonProperty("guild_id")] - public ulong? GuildId { get; set; } - [JsonProperty("version")] - public uint Version { get; set; } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs deleted file mode 100644 index 4bc8a386a..000000000 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Akavache; -using Akavache.Sqlite3; -using Discord.Net.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Threading; -using System.Threading.Tasks; -using Splat; - -namespace Discord.Net -{ - internal class CachedRestClient : IRestClient - { - private readonly Dictionary _headers; - private IBlobCache _blobCache; - private string _baseUrl; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken, _parentToken; - private bool _isDisposed; - - public CacheInfo Info { get; private set; } - - public CachedRestClient() - { - _headers = new Dictionary(); - - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationToken.None; - _parentToken = CancellationToken.None; - - Locator.CurrentMutable.Register(() => Scheduler.Default, typeof(IScheduler), "Taskpool"); - Locator.CurrentMutable.Register(() => new FilesystemProvider(), typeof(IFilesystemProvider), null); - Locator.CurrentMutable.Register(() => new HttpMixin(), typeof(IAkavacheHttpMixin), null); - //new Akavache.Sqlite3.Registrations().Register(Locator.CurrentMutable); - _blobCache = new SQLitePersistentBlobCache("cache.db"); - } - private void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - _blobCache.Dispose(); - _isDisposed = true; - } - } - public void Dispose() - { - Dispose(true); - } - - public void SetUrl(string url) - { - _baseUrl = url; - } - public void SetHeader(string key, string value) - { - _headers[key] = value; - } - public void SetCancelToken(CancellationToken cancelToken) - { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - } - - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) - { - if (method != "GET") - throw new InvalidOperationException("This RestClient only supports GET requests."); - - string uri = Path.Combine(_baseUrl, endpoint); - var bytes = await _blobCache.DownloadUrl(uri, _headers); - return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); - } - public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) - { - throw new InvalidOperationException("This RestClient does not support payloads."); - } - public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) - { - throw new InvalidOperationException("This RestClient does not support multipart requests."); - } - - public async Task ClearAsync() - { - await _blobCache.InvalidateAll(); - } - - public async Task LoadInfoAsync(ulong guildId) - { - if (Info != null) - return; - - bool needsReset = false; - try - { - Info = await _blobCache.GetObject("info"); - if (Info.GuildId != guildId) - needsReset = true; - } - catch (KeyNotFoundException) - { - needsReset = true; - } - if (needsReset) - { - Info = new CacheInfo() { GuildId = guildId, Version = 0 }; - await SaveInfoAsync().ConfigureAwait(false); - } - } - public async Task SaveInfoAsync() - { - await ClearAsync().ConfigureAwait(false); //Version changed, invalidate cache - await _blobCache.InsertObject("info", Info); - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/FilesystemProvider.cs b/test/Discord.Net.Tests/Net/FilesystemProvider.cs deleted file mode 100644 index ae1b9a301..000000000 --- a/test/Discord.Net.Tests/Net/FilesystemProvider.cs +++ /dev/null @@ -1,128 +0,0 @@ -//From https://github.com/akavache/Akavache -//Copyright (c) 2012 GitHub -//TODO: Remove once netstandard support is added - -using Akavache; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reactive; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Reflection; - -namespace Discord -{ - public class FilesystemProvider : IFilesystemProvider - { - public IObservable OpenFileForReadAsync(string path, IScheduler scheduler) - { - return SafeOpenFileAsync(path, FileMode.Open, FileAccess.Read, FileShare.Read, scheduler); - } - - public IObservable OpenFileForWriteAsync(string path, IScheduler scheduler) - { - return SafeOpenFileAsync(path, FileMode.Create, FileAccess.Write, FileShare.None, scheduler); - } - - public IObservable CreateRecursive(string path) - { - CreateRecursive(new DirectoryInfo(path)); - return Observable.Return(Unit.Default); - } - - public IObservable Delete(string path) - { - return Observable.Start(() => File.Delete(path), Scheduler.Default); - } - - public string GetDefaultRoamingCacheDirectory() - { - throw new NotSupportedException(); - } - - public string GetDefaultSecretCacheDirectory() - { - throw new NotSupportedException(); - } - - public string GetDefaultLocalMachineCacheDirectory() - { - throw new NotSupportedException(); - } - - protected static string GetAssemblyDirectoryName() - { - var assemblyDirectoryName = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - Debug.Assert(assemblyDirectoryName != null, "The directory name of the assembly location is null"); - return assemblyDirectoryName; - } - - private static IObservable SafeOpenFileAsync(string path, FileMode mode, FileAccess access, FileShare share, IScheduler scheduler = null) - { - scheduler = scheduler ?? Scheduler.Default; - var ret = new AsyncSubject(); - - Observable.Start(() => - { - try - { - var createModes = new[] - { - FileMode.Create, - FileMode.CreateNew, - FileMode.OpenOrCreate, - }; - - - // NB: We do this (even though it's incorrect!) because - // throwing lots of 1st chance exceptions makes debugging - // obnoxious, as well as a bug in VS where it detects - // exceptions caught by Observable.Start as Unhandled. - if (!createModes.Contains(mode) && !File.Exists(path)) - { - ret.OnError(new FileNotFoundException()); - return; - } - - Observable.Start(() => new FileStream(path, mode, access, share, 4096, false), scheduler).Cast().Subscribe(ret); - } - catch (Exception ex) - { - ret.OnError(ex); - } - }, scheduler); - - return ret; - } - private static void CreateRecursive(DirectoryInfo info) - { - SplitFullPath(info).Aggregate((parent, dir) => - { - var path = Path.Combine(parent, dir); - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - return path; - }); - } - - private static IEnumerable SplitFullPath(DirectoryInfo info) - { - var root = Path.GetPathRoot(info.FullName); - var components = new List(); - for (var path = info.FullName; path != root && path != null; path = Path.GetDirectoryName(path)) - { - var filename = Path.GetFileName(path); - if (String.IsNullOrEmpty(filename)) - continue; - components.Add(filename); - } - components.Add(root); - components.Reverse(); - return components; - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/HttpMixin.cs b/test/Discord.Net.Tests/Net/HttpMixin.cs deleted file mode 100644 index c4a78ce0b..000000000 --- a/test/Discord.Net.Tests/Net/HttpMixin.cs +++ /dev/null @@ -1,145 +0,0 @@ -//From https://github.com/akavache/Akavache -//Copyright (c) 2012 GitHub -//TODO: Remove once netstandard support is added - -#pragma warning disable CS0618 - -using Akavache; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Text; -using System.Reactive; -using System.Reactive.Threading.Tasks; - -namespace Discord.Net -{ - public class HttpMixin : IAkavacheHttpMixin - { - /// - /// Download data from an HTTP URL and insert the result into the - /// cache. If the data is already in the cache, this returns - /// a cached value. The URL itself is used as the key. - /// - /// The URL to download. - /// An optional Dictionary containing the HTTP - /// request headers. - /// Force a web request to always be issued, skipping the cache. - /// An optional expiration date. - /// The data downloaded from the URL. - public IObservable DownloadUrl(IBlobCache This, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) - { - return This.DownloadUrl(url, url, headers, fetchAlways, absoluteExpiration); - } - - /// - /// Download data from an HTTP URL and insert the result into the - /// cache. If the data is already in the cache, this returns - /// a cached value. An explicit key is provided rather than the URL itself. - /// - /// The key to store with. - /// The URL to download. - /// An optional Dictionary containing the HTTP - /// request headers. - /// Force a web request to always be issued, skipping the cache. - /// An optional expiration date. - /// The data downloaded from the URL. - public IObservable DownloadUrl(IBlobCache This, string key, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) - { - var doFetch = MakeWebRequest(new Uri(url), headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); - var fetchAndCache = doFetch.SelectMany(x => This.Insert(key, x, absoluteExpiration).Select(_ => x)); - - var ret = default(IObservable); - if (!fetchAlways) - { - ret = This.Get(key).Catch(fetchAndCache); - } - else - { - ret = fetchAndCache; - } - - var conn = ret.PublishLast(); - conn.Connect(); - return conn; - } - - IObservable ProcessWebResponse(WebResponse wr, string url, DateTimeOffset? absoluteExpiration) - { - var hwr = (HttpWebResponse)wr; - Debug.Assert(hwr != null, "The Web Response is somehow null but shouldn't be."); - if ((int)hwr.StatusCode >= 400) - { - return Observable.Throw(new WebException(hwr.StatusDescription)); - } - - var ms = new MemoryStream(); - using (var responseStream = hwr.GetResponseStream()) - { - Debug.Assert(responseStream != null, "The response stream is somehow null"); - responseStream.CopyTo(ms); - } - - var ret = ms.ToArray(); - return Observable.Return(ret); - } - - static IObservable MakeWebRequest( - Uri uri, - IDictionary headers = null, - string content = null, - int retries = 3, - TimeSpan? timeout = null) - { - IObservable request; - - request = Observable.Defer(() => - { - var hwr = CreateWebRequest(uri, headers); - - if (content == null) - return Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)(); - - var buf = Encoding.UTF8.GetBytes(content); - - // NB: You'd think that BeginGetResponse would never block, - // seeing as how it's asynchronous. You'd be wrong :-/ - var ret = new AsyncSubject(); - Observable.Start(() => - { - Observable.FromAsyncPattern(hwr.BeginGetRequestStream, hwr.EndGetRequestStream)() - .SelectMany(x => WriteAsyncRx(x, buf, 0, buf.Length)) - .SelectMany(_ => Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)()) - .Multicast(ret).Connect(); - }, BlobCache.TaskpoolScheduler); - - return ret; - }); - - return request.Timeout(timeout ?? TimeSpan.FromSeconds(15), BlobCache.TaskpoolScheduler).Retry(retries); - } - - private static WebRequest CreateWebRequest(Uri uri, IDictionary headers) - { - var hwr = WebRequest.Create(uri); - if (headers != null) - { - foreach (var x in headers) - { - hwr.Headers[x.Key] = x.Value; - } - } - return hwr; - } - - private static IObservable WriteAsyncRx(Stream stream, byte[] data, int start, int length) - { - return stream.WriteAsync(data, start, length).ToObservable(); - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/TestConfig.cs b/test/Discord.Net.Tests/TestConfig.cs deleted file mode 100644 index bdab13ea7..000000000 --- a/test/Discord.Net.Tests/TestConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; -using System.IO; -using System; - -namespace Discord -{ - internal class TestConfig - { - [JsonProperty("token")] - public string Token { get; private set; } - [JsonProperty("guild_id")] - public ulong GuildId { get; private set; } - - public static TestConfig LoadFile(string path) - { - if (File.Exists(path)) - { - using (var stream = new FileStream(path, FileMode.Open)) - using (var reader = new StreamReader(stream)) - using (var jsonReader = new JsonTextReader(reader)) - return new JsonSerializer().Deserialize(jsonReader); - } - else - { - return new TestConfig() - { - Token = Environment.GetEnvironmentVariable("DNET_TEST_TOKEN"), - GuildId = ulong.Parse(Environment.GetEnvironmentVariable("DNET_TEST_GUILDID")) - }; - } - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs deleted file mode 100644 index dd87c2e24..000000000 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ /dev/null @@ -1,349 +0,0 @@ -using System; -using System.Threading.Tasks; -using Xunit; - -namespace Discord -{ - public class ChannelPermissionsTests - { - [Fact] - public Task TestChannelPermission() - { - var perm = new ChannelPermissions(); - - // check initial values - Assert.Equal((ulong)0, perm.RawValue); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // permissions list empty by default - Assert.Empty(perm.ToList()); - - // test modify with no parameters - var copy = perm.Modify(); - Assert.Equal((ulong)0, copy.RawValue); - - // test modify with no parameters after using all - copy = ChannelPermissions.Text; - var modified = copy.Modify(); // no params should not change the result - Assert.Equal(ChannelPermissions.Text.RawValue, modified.RawValue); - - copy = ChannelPermissions.Voice; - modified = copy.Modify(); // no params should not change the result - Assert.Equal(ChannelPermissions.Voice.RawValue, modified.RawValue); - - copy = ChannelPermissions.Group; - modified = copy.Modify(); // no params should not change the result - Assert.Equal(ChannelPermissions.Group.RawValue, modified.RawValue); - - copy = ChannelPermissions.DM; - modified = copy.Modify(); // no params should not change the result - Assert.Equal(ChannelPermissions.DM.RawValue, modified.RawValue); - - copy = new ChannelPermissions(useExternalEmojis: true); - modified = copy.Modify(); - Assert.Equal(copy.RawValue, modified.RawValue); - - // test the values that are returned by ChannelPermission.All - Assert.Equal((ulong)0, ChannelPermissions.None.RawValue); - - // for text channels - ulong textChannel = (ulong)( ChannelPermission.CreateInstantInvite - | ChannelPermission.ManageChannels - | ChannelPermission.AddReactions - | ChannelPermission.ViewChannel - | ChannelPermission.SendMessages - | ChannelPermission.SendTTSMessages - | ChannelPermission.ManageMessages - | ChannelPermission.EmbedLinks - | ChannelPermission.AttachFiles - | ChannelPermission.ReadMessageHistory - | ChannelPermission.MentionEveryone - | ChannelPermission.UseExternalEmojis - | ChannelPermission.ManageRoles - | ChannelPermission.ManageWebhooks); - - Assert.Equal(textChannel, ChannelPermissions.Text.RawValue); - - // voice channels - ulong voiceChannel = (ulong)( - ChannelPermission.CreateInstantInvite - | ChannelPermission.ManageChannels - | ChannelPermission.ViewChannel - | ChannelPermission.Connect - | ChannelPermission.Speak - | ChannelPermission.MuteMembers - | ChannelPermission.DeafenMembers - | ChannelPermission.MoveMembers - | ChannelPermission.UseVAD - | ChannelPermission.ManageRoles); - - Assert.Equal(voiceChannel, ChannelPermissions.Voice.RawValue); - - // DM Channels - ulong dmChannel = (ulong)( - ChannelPermission.ViewChannel - | ChannelPermission.SendMessages - | ChannelPermission.EmbedLinks - | ChannelPermission.AttachFiles - | ChannelPermission.ReadMessageHistory - | ChannelPermission.UseExternalEmojis - | ChannelPermission.Connect - | ChannelPermission.Speak - | ChannelPermission.UseVAD - ); - Assert.Equal(dmChannel, ChannelPermissions.DM.RawValue); - - // group channel - ulong groupChannel = (ulong)( - ChannelPermission.SendMessages - | ChannelPermission.EmbedLinks - | ChannelPermission.AttachFiles - | ChannelPermission.SendTTSMessages - | ChannelPermission.Connect - | ChannelPermission.Speak - | ChannelPermission.UseVAD - ); - Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); - return Task.CompletedTask; - } - [Fact] - public Task TestChannelPermissionModify() - { - // test channel permission modify - - var perm = new ChannelPermissions(); - - // ensure that the permission is initially false - Assert.False(perm.CreateInstantInvite); - - // ensure that when modified it works - perm = perm.Modify(createInstantInvite: true); - Assert.True(perm.CreateInstantInvite); - Assert.Equal((ulong)ChannelPermission.CreateInstantInvite, perm.RawValue); - - // set false again, move on to next permission - perm = perm.Modify(createInstantInvite: false); - Assert.False(perm.CreateInstantInvite); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ManageChannel); - - perm = perm.Modify(manageChannel: true); - Assert.True(perm.ManageChannel); - Assert.Equal((ulong)ChannelPermission.ManageChannels, perm.RawValue); - - perm = perm.Modify(manageChannel: false); - Assert.False(perm.ManageChannel); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.AddReactions); - - perm = perm.Modify(addReactions: true); - Assert.True(perm.AddReactions); - Assert.Equal((ulong)ChannelPermission.AddReactions, perm.RawValue); - - perm = perm.Modify(addReactions: false); - Assert.False(perm.AddReactions); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ViewChannel); - - perm = perm.Modify(viewChannel: true); - Assert.True(perm.ViewChannel); - Assert.Equal((ulong)ChannelPermission.ViewChannel, perm.RawValue); - - perm = perm.Modify(viewChannel: false); - Assert.False(perm.ViewChannel); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.SendMessages); - - perm = perm.Modify(sendMessages: true); - Assert.True(perm.SendMessages); - Assert.Equal((ulong)ChannelPermission.SendMessages, perm.RawValue); - - perm = perm.Modify(sendMessages: false); - Assert.False(perm.SendMessages); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.SendTTSMessages); - - perm = perm.Modify(sendTTSMessages: true); - Assert.True(perm.SendTTSMessages); - Assert.Equal((ulong)ChannelPermission.SendTTSMessages, perm.RawValue); - - perm = perm.Modify(sendTTSMessages: false); - Assert.False(perm.SendTTSMessages); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ManageMessages); - - perm = perm.Modify(manageMessages: true); - Assert.True(perm.ManageMessages); - Assert.Equal((ulong)ChannelPermission.ManageMessages, perm.RawValue); - - perm = perm.Modify(manageMessages: false); - Assert.False(perm.ManageMessages); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.EmbedLinks); - - perm = perm.Modify(embedLinks: true); - Assert.True(perm.EmbedLinks); - Assert.Equal((ulong)ChannelPermission.EmbedLinks, perm.RawValue); - - perm = perm.Modify(embedLinks: false); - Assert.False(perm.EmbedLinks); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.AttachFiles); - - perm = perm.Modify(attachFiles: true); - Assert.True(perm.AttachFiles); - Assert.Equal((ulong)ChannelPermission.AttachFiles, perm.RawValue); - - perm = perm.Modify(attachFiles: false); - Assert.False(perm.AttachFiles); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ReadMessageHistory); - - perm = perm.Modify(readMessageHistory: true); - Assert.True(perm.ReadMessageHistory); - Assert.Equal((ulong)ChannelPermission.ReadMessageHistory, perm.RawValue); - - perm = perm.Modify(readMessageHistory: false); - Assert.False(perm.ReadMessageHistory); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.MentionEveryone); - - perm = perm.Modify(mentionEveryone: true); - Assert.True(perm.MentionEveryone); - Assert.Equal((ulong)ChannelPermission.MentionEveryone, perm.RawValue); - - perm = perm.Modify(mentionEveryone: false); - Assert.False(perm.MentionEveryone); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.UseExternalEmojis); - - perm = perm.Modify(useExternalEmojis: true); - Assert.True(perm.UseExternalEmojis); - Assert.Equal((ulong)ChannelPermission.UseExternalEmojis, perm.RawValue); - - perm = perm.Modify(useExternalEmojis: false); - Assert.False(perm.UseExternalEmojis); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.Connect); - - perm = perm.Modify(connect: true); - Assert.True(perm.Connect); - Assert.Equal((ulong)ChannelPermission.Connect, perm.RawValue); - - perm = perm.Modify(connect: false); - Assert.False(perm.Connect); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.Speak); - - perm = perm.Modify(speak: true); - Assert.True(perm.Speak); - Assert.Equal((ulong)ChannelPermission.Speak, perm.RawValue); - - perm = perm.Modify(speak: false); - Assert.False(perm.Speak); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.MuteMembers); - - perm = perm.Modify(muteMembers: true); - Assert.True(perm.MuteMembers); - Assert.Equal((ulong)ChannelPermission.MuteMembers, perm.RawValue); - - perm = perm.Modify(muteMembers: false); - Assert.False(perm.MuteMembers); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.DeafenMembers); - - perm = perm.Modify(deafenMembers: true); - Assert.True(perm.DeafenMembers); - Assert.Equal((ulong)ChannelPermission.DeafenMembers, perm.RawValue); - - perm = perm.Modify(deafenMembers: false); - Assert.False(perm.DeafenMembers); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.MoveMembers); - - perm = perm.Modify(moveMembers: true); - Assert.True(perm.MoveMembers); - Assert.Equal((ulong)ChannelPermission.MoveMembers, perm.RawValue); - - perm = perm.Modify(moveMembers: false); - Assert.False(perm.MoveMembers); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.UseVAD); - - perm = perm.Modify(useVoiceActivation: true); - Assert.True(perm.UseVAD); - Assert.Equal((ulong)ChannelPermission.UseVAD, perm.RawValue); - - perm = perm.Modify(useVoiceActivation: false); - Assert.False(perm.UseVAD); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ManageRoles); - - perm = perm.Modify(manageRoles: true); - Assert.True(perm.ManageRoles); - Assert.Equal((ulong)ChannelPermission.ManageRoles, perm.RawValue); - - perm = perm.Modify(manageRoles: false); - Assert.False(perm.ManageRoles); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - - // individual permission test - Assert.False(perm.ManageWebhooks); - - perm = perm.Modify(manageWebhooks: true); - Assert.True(perm.ManageWebhooks); - Assert.Equal((ulong)ChannelPermission.ManageWebhooks, perm.RawValue); - - perm = perm.Modify(manageWebhooks: false); - Assert.False(perm.ManageWebhooks); - Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); - return Task.CompletedTask; - } - - [Fact] - public Task TestChannelTypeResolution() - { - ITextChannel someChannel = null; - // null channels will throw exception - Assert.Throws(() => ChannelPermissions.All(someChannel)); - return Task.CompletedTask; - } - } -} diff --git a/test/Discord.Net.Tests/Tests.Channels.cs b/test/Discord.Net.Tests/Tests.Channels.cs deleted file mode 100644 index 46e28b9da..000000000 --- a/test/Discord.Net.Tests/Tests.Channels.cs +++ /dev/null @@ -1,218 +0,0 @@ -using Discord.Rest; -using System; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace Discord -{ - public partial class Tests - { - internal static async Task Migration_CreateTextChannels(DiscordRestClient client, RestGuild guild) - { - var text1 = await guild.GetDefaultChannelAsync(); - var text2 = await guild.CreateTextChannelAsync("text2"); - var text3 = await guild.CreateTextChannelAsync("text3"); - var text4 = await guild.CreateTextChannelAsync("text4"); - var text5 = await guild.CreateTextChannelAsync("text5"); - - // create a channel category - var cat1 = await guild.CreateCategoryChannelAsync("cat1"); - - if (text1 == null) - { - // the guild did not have a default channel, so make a new one - text1 = await guild.CreateTextChannelAsync("default"); - } - - //Modify #general - await text1.ModifyAsync(x => - { - x.Name = "text1"; - x.Position = 1; - x.Topic = "Topic1"; - x.CategoryId = cat1.Id; - }); - - await text2.ModifyAsync(x => - { - x.Position = 2; - x.CategoryId = cat1.Id; - }); - await text3.ModifyAsync(x => - { - x.Topic = "Topic2"; - }); - await text4.ModifyAsync(x => - { - x.Position = 3; - x.Topic = "Topic2"; - }); - await text5.ModifyAsync(x => - { - }); - - CheckTextChannels(guild, text1, text2, text3, text4, text5); - } - [Fact] - public async Task TestTextChannels() - { - CheckTextChannels(_guild, (await _guild.GetTextChannelsAsync()).ToArray()); - } - private static void CheckTextChannels(RestGuild guild, params RestTextChannel[] textChannels) - { - Assert.Equal(5, textChannels.Length); - Assert.All(textChannels, x => - { - Assert.NotNull(x); - Assert.NotEqual(0UL, x.Id); - Assert.True(x.Position >= 0); - }); - - var text1 = textChannels.FirstOrDefault(x => x.Name == "text1"); - var text2 = textChannels.FirstOrDefault(x => x.Name == "text2"); - var text3 = textChannels.FirstOrDefault(x => x.Name == "text3"); - var text4 = textChannels.FirstOrDefault(x => x.Name == "text4"); - var text5 = textChannels.FirstOrDefault(x => x.Name == "text5"); - - Assert.NotNull(text1); - //Assert.True(text1.Id == guild.DefaultChannelId); - Assert.Equal(1, text1.Position); - Assert.Equal("Topic1", text1.Topic); - - Assert.NotNull(text2); - Assert.Equal(2, text2.Position); - Assert.Null(text2.Topic); - - Assert.NotNull(text3); - Assert.Equal("Topic2", text3.Topic); - - Assert.NotNull(text4); - Assert.Equal(3, text4.Position); - Assert.Equal("Topic2", text4.Topic); - - Assert.NotNull(text5); - Assert.Null(text5.Topic); - } - - internal static async Task Migration_CreateVoiceChannels(DiscordRestClient client, RestGuild guild) - { - var voice1 = await guild.CreateVoiceChannelAsync("voice1"); - var voice2 = await guild.CreateVoiceChannelAsync("voice2"); - var voice3 = await guild.CreateVoiceChannelAsync("voice3"); - - var cat2 = await guild.CreateCategoryChannelAsync("cat2"); - - await voice1.ModifyAsync(x => - { - x.Bitrate = 96000; - x.Position = 1; - x.CategoryId = cat2.Id; - }); - await voice2.ModifyAsync(x => - { - x.UserLimit = null; - }); - await voice3.ModifyAsync(x => - { - x.Bitrate = 8000; - x.Position = 1; - x.UserLimit = 16; - x.CategoryId = cat2.Id; - }); - - CheckVoiceChannels(voice1, voice2, voice3); - } - [Fact] - public async Task TestVoiceChannels() - { - CheckVoiceChannels((await _guild.GetVoiceChannelsAsync()).ToArray()); - } - private static void CheckVoiceChannels(params RestVoiceChannel[] voiceChannels) - { - Assert.Equal(3, voiceChannels.Length); - Assert.All(voiceChannels, x => - { - Assert.NotNull(x); - Assert.NotEqual(0UL, x.Id); - Assert.NotEqual(0, x.UserLimit); - Assert.True(x.Bitrate > 0); - Assert.True(x.Position >= 0); - }); - - var voice1 = voiceChannels.FirstOrDefault(x => x.Name == "voice1"); - var voice2 = voiceChannels.FirstOrDefault(x => x.Name == "voice2"); - var voice3 = voiceChannels.FirstOrDefault(x => x.Name == "voice3"); - - Assert.NotNull(voice1); - Assert.Equal(96000, voice1.Bitrate); - Assert.Equal(1, voice1.Position); - - Assert.NotNull(voice2); - Assert.Null(voice2.UserLimit); - - Assert.NotNull(voice3); - Assert.Equal(8000, voice3.Bitrate); - Assert.Equal(1, voice3.Position); - Assert.Equal(16, voice3.UserLimit); - } - - [Fact] - public async Task TestChannelCategories() - { - // (await _guild.GetVoiceChannelsAsync()).ToArray() - var channels = await _guild.GetCategoryChannelsAsync(); - - await CheckChannelCategories(channels.ToArray(), (await _guild.GetChannelsAsync()).ToArray()); - } - - private async Task CheckChannelCategories(RestCategoryChannel[] categories, RestGuildChannel[] allChannels) - { - // 2 categories - Assert.Equal(2, categories.Length); - - var cat1 = categories.Where(x => x.Name == "cat1").FirstOrDefault(); - var cat2 = categories.Where(x => x.Name == "cat2").FirstOrDefault(); - - Assert.NotNull(cat1); - Assert.NotNull(cat2); - - // get text1, text2, ensure they have category id == cat1 - var text1 = allChannels.Where(x => x.Name == "text1").FirstOrDefault() as RestTextChannel; - var text2 = allChannels.Where(x => x.Name == "text2").FirstOrDefault() as RestTextChannel; - - Assert.NotNull(text1); - Assert.NotNull(text2); - - // check that CategoryID and .GetCategoryAsync work correctly - // for both of the text channels - Assert.Equal(text1.CategoryId, cat1.Id); - var text1Cat = await text1.GetCategoryAsync(); - Assert.Equal(text1Cat.Id, cat1.Id); - Assert.Equal(text1Cat.Name, cat1.Name); - - Assert.Equal(text2.CategoryId, cat1.Id); - var text2Cat = await text2.GetCategoryAsync(); - Assert.Equal(text2Cat.Id, cat1.Id); - Assert.Equal(text2Cat.Name, cat1.Name); - - // do the same for the voice channels - var voice1 = allChannels.Where(x => x.Name == "voice1").FirstOrDefault() as RestVoiceChannel; - var voice3 = allChannels.Where(x => x.Name == "voice3").FirstOrDefault() as RestVoiceChannel; - - Assert.NotNull(voice1); - Assert.NotNull(voice3); - - Assert.Equal(voice1.CategoryId, cat2.Id); - var voice1Cat = await voice1.GetCategoryAsync(); - Assert.Equal(voice1Cat.Id, cat2.Id); - Assert.Equal(voice1Cat.Name, cat2.Name); - - Assert.Equal(voice3.CategoryId, cat2.Id); - var voice3Cat = await voice3.GetCategoryAsync(); - Assert.Equal(voice3Cat.Id, cat2.Id); - Assert.Equal(voice3Cat.Name, cat2.Name); - - } - } -} diff --git a/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs b/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs new file mode 100644 index 000000000..52c39005b --- /dev/null +++ b/test/Discord.Net.Tests/Tests.DiscordWebhookClient.cs @@ -0,0 +1,60 @@ +using Discord.Webhook; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + /// + /// Tests the function. + /// + public class DiscordWebhookClientTests + { + [Theory] + [InlineData("https://discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // ptb, canary, etc will have slightly different urls + [InlineData("https://ptb.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + [InlineData("https://canary.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // don't care about https + [InlineData("http://canary.discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // this is the minimum that the regex cares about + [InlineData("discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK", + 123412347732897802, "_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + public void TestWebhook_Valid(string webhookurl, ulong expectedId, string expectedToken) + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + + Assert.Equal(expectedId, id); + Assert.Equal(expectedToken, token); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void TestWebhook_Null(string webhookurl) + { + Assert.Throws(() => + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + }); + } + + [Theory] + [InlineData("123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK")] + // trailing slash + [InlineData("https://discord.com/api/webhooks/123412347732897802/_abcde123456789-ABCDEFGHIJKLMNOP12345678-abcdefghijklmnopABCDEFGHIJK/")] + public void TestWebhook_Invalid(string webhookurl) + { + Assert.Throws(() => + { + DiscordWebhookClient.ParseWebhookUrl(webhookurl, out ulong id, out string token); + }); + } + } +} diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs deleted file mode 100644 index bbd6621b5..000000000 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ /dev/null @@ -1,324 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace Discord -{ - public class GuidPermissionsTests - { - [Fact] - public Task TestGuildPermission() - { - // Test Guild Permission Constructors - var perm = new GuildPermissions(); - - // the default raw value is 0 - Assert.Equal((ulong)0, perm.RawValue); - // also check that it is the same as none - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // permissions list is empty by default - Assert.Empty(perm.ToList()); - Assert.NotNull(perm.ToList()); - - // Test modify with no parameters - var copy = perm.Modify(); - // ensure that the raw values match - Assert.Equal((ulong)0, copy.RawValue); - - // test modify with no parameters - copy = GuildPermissions.None.Modify(); - Assert.Equal(GuildPermissions.None.RawValue, copy.RawValue); - - // test modify with no paramters on all permissions - copy = GuildPermissions.All.Modify(); - Assert.Equal(GuildPermissions.All.RawValue, copy.RawValue); - - // test modify with no paramters on webhook permissions - copy = GuildPermissions.Webhook.Modify(); - Assert.Equal(GuildPermissions.Webhook.RawValue, copy.RawValue); - - // Get all distinct values (ReadMessages = ViewChannel) - var enumValues = (Enum.GetValues(typeof(GuildPermission)) as GuildPermission[]) - .Distinct() - .ToArray(); - // test GuildPermissions.All - ulong sumOfAllGuildPermissions = 0; - foreach(var v in enumValues) - { - sumOfAllGuildPermissions |= (ulong)v; - } - - // assert that the raw values match - Assert.Equal(sumOfAllGuildPermissions, GuildPermissions.All.RawValue); - Assert.Equal((ulong)0, GuildPermissions.None.RawValue); - - // assert that GuildPermissions.All contains the same number of permissions as the - // GuildPermissions enum - Assert.Equal(enumValues.Length, GuildPermissions.All.ToList().Count); - - // assert that webhook has the same raw value - ulong webHookPermissions = (ulong)( - GuildPermission.SendMessages | GuildPermission.SendTTSMessages | GuildPermission.EmbedLinks | - GuildPermission.AttachFiles); - Assert.Equal(webHookPermissions, GuildPermissions.Webhook.RawValue); - - return Task.CompletedTask; - } - - [Fact] - public Task TestGuildPermissionModify() - { - var perm = new GuildPermissions(); - - // tests each of the parameters of Modify one by one - - // test modify with each of the parameters - // test initially false state - Assert.False(perm.CreateInstantInvite); - - // ensure that when we modify it the parameter works - perm = perm.Modify(createInstantInvite: true); - Assert.True(perm.CreateInstantInvite); - Assert.Equal((ulong)GuildPermission.CreateInstantInvite, perm.RawValue); - - // set it false again, then move on to the next permission - perm = perm.Modify(createInstantInvite: false); - Assert.False(perm.CreateInstantInvite); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(kickMembers: true); - Assert.True(perm.KickMembers); - Assert.Equal((ulong)GuildPermission.KickMembers, perm.RawValue); - - perm = perm.Modify(kickMembers: false); - Assert.False(perm.KickMembers); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(banMembers: true); - Assert.True(perm.BanMembers); - Assert.Equal((ulong)GuildPermission.BanMembers, perm.RawValue); - - perm = perm.Modify(banMembers: false); - Assert.False(perm.BanMembers); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(administrator: true); - Assert.True(perm.Administrator); - Assert.Equal((ulong)GuildPermission.Administrator, perm.RawValue); - - perm = perm.Modify(administrator: false); - Assert.False(perm.Administrator); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageChannels: true); - Assert.True(perm.ManageChannels); - Assert.Equal((ulong)GuildPermission.ManageChannels, perm.RawValue); - - perm = perm.Modify(manageChannels: false); - Assert.False(perm.ManageChannels); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageGuild: true); - Assert.True(perm.ManageGuild); - Assert.Equal((ulong)GuildPermission.ManageGuild, perm.RawValue); - - perm = perm.Modify(manageGuild: false); - Assert.False(perm.ManageGuild); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - - // individual permission test - perm = perm.Modify(addReactions: true); - Assert.True(perm.AddReactions); - Assert.Equal((ulong)GuildPermission.AddReactions, perm.RawValue); - - perm = perm.Modify(addReactions: false); - Assert.False(perm.AddReactions); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - - // individual permission test - perm = perm.Modify(viewAuditLog: true); - Assert.True(perm.ViewAuditLog); - Assert.Equal((ulong)GuildPermission.ViewAuditLog, perm.RawValue); - - perm = perm.Modify(viewAuditLog: false); - Assert.False(perm.ViewAuditLog); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - - // individual permission test - perm = perm.Modify(viewChannel: true); - Assert.True(perm.ViewChannel); - Assert.Equal((ulong)GuildPermission.ViewChannel, perm.RawValue); - - perm = perm.Modify(viewChannel: false); - Assert.False(perm.ViewChannel); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - - // individual permission test - perm = perm.Modify(sendMessages: true); - Assert.True(perm.SendMessages); - Assert.Equal((ulong)GuildPermission.SendMessages, perm.RawValue); - - perm = perm.Modify(sendMessages: false); - Assert.False(perm.SendMessages); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(embedLinks: true); - Assert.True(perm.EmbedLinks); - Assert.Equal((ulong)GuildPermission.EmbedLinks, perm.RawValue); - - perm = perm.Modify(embedLinks: false); - Assert.False(perm.EmbedLinks); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(attachFiles: true); - Assert.True(perm.AttachFiles); - Assert.Equal((ulong)GuildPermission.AttachFiles, perm.RawValue); - - perm = perm.Modify(attachFiles: false); - Assert.False(perm.AttachFiles); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(readMessageHistory: true); - Assert.True(perm.ReadMessageHistory); - Assert.Equal((ulong)GuildPermission.ReadMessageHistory, perm.RawValue); - - perm = perm.Modify(readMessageHistory: false); - Assert.False(perm.ReadMessageHistory); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(mentionEveryone: true); - Assert.True(perm.MentionEveryone); - Assert.Equal((ulong)GuildPermission.MentionEveryone, perm.RawValue); - - perm = perm.Modify(mentionEveryone: false); - Assert.False(perm.MentionEveryone); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(useExternalEmojis: true); - Assert.True(perm.UseExternalEmojis); - Assert.Equal((ulong)GuildPermission.UseExternalEmojis, perm.RawValue); - - perm = perm.Modify(useExternalEmojis: false); - Assert.False(perm.UseExternalEmojis); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(connect: true); - Assert.True(perm.Connect); - Assert.Equal((ulong)GuildPermission.Connect, perm.RawValue); - - perm = perm.Modify(connect: false); - Assert.False(perm.Connect); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(speak: true); - Assert.True(perm.Speak); - Assert.Equal((ulong)GuildPermission.Speak, perm.RawValue); - - perm = perm.Modify(speak: false); - Assert.False(perm.Speak); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(muteMembers: true); - Assert.True(perm.MuteMembers); - Assert.Equal((ulong)GuildPermission.MuteMembers, perm.RawValue); - - perm = perm.Modify(muteMembers: false); - Assert.False(perm.MuteMembers); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(deafenMembers: true); - Assert.True(perm.DeafenMembers); - Assert.Equal((ulong)GuildPermission.DeafenMembers, perm.RawValue); - - perm = perm.Modify(deafenMembers: false); - Assert.False(perm.DeafenMembers); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(moveMembers: true); - Assert.True(perm.MoveMembers); - Assert.Equal((ulong)GuildPermission.MoveMembers, perm.RawValue); - - perm = perm.Modify(moveMembers: false); - Assert.False(perm.MoveMembers); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(useVoiceActivation: true); - Assert.True(perm.UseVAD); - Assert.Equal((ulong)GuildPermission.UseVAD, perm.RawValue); - - perm = perm.Modify(useVoiceActivation: false); - Assert.False(perm.UseVAD); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(changeNickname: true); - Assert.True(perm.ChangeNickname); - Assert.Equal((ulong)GuildPermission.ChangeNickname, perm.RawValue); - - perm = perm.Modify(changeNickname: false); - Assert.False(perm.ChangeNickname); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageNicknames: true); - Assert.True(perm.ManageNicknames); - Assert.Equal((ulong)GuildPermission.ManageNicknames, perm.RawValue); - - perm = perm.Modify(manageNicknames: false); - Assert.False(perm.ManageNicknames); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageRoles: true); - Assert.True(perm.ManageRoles); - Assert.Equal((ulong)GuildPermission.ManageRoles, perm.RawValue); - - perm = perm.Modify(manageRoles: false); - Assert.False(perm.ManageRoles); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageWebhooks: true); - Assert.True(perm.ManageWebhooks); - Assert.Equal((ulong)GuildPermission.ManageWebhooks, perm.RawValue); - - perm = perm.Modify(manageWebhooks: false); - Assert.False(perm.ManageWebhooks); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - // individual permission test - perm = perm.Modify(manageEmojis: true); - Assert.True(perm.ManageEmojis); - Assert.Equal((ulong)GuildPermission.ManageEmojis, perm.RawValue); - - perm = perm.Modify(manageEmojis: false); - Assert.False(perm.ManageEmojis); - Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); - - return Task.CompletedTask; - } - - } -} diff --git a/test/Discord.Net.Tests/Tests.Migrations.cs b/test/Discord.Net.Tests/Tests.Migrations.cs deleted file mode 100644 index 23e55a737..000000000 --- a/test/Discord.Net.Tests/Tests.Migrations.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Threading.Tasks; -using Discord.Rest; - -namespace Discord -{ - public partial class TestsFixture - { - public const uint MigrationCount = 3; - - public async Task MigrateAsync() - { - DiscordRestClient client = null; - RestGuild guild = null; - - await _cache.LoadInfoAsync(_config.GuildId).ConfigureAwait(false); - while (_cache.Info.Version != MigrationCount) - { - if (client == null) - { - client = new DiscordRestClient(); - await client.LoginAsync(TokenType.Bot, _config.Token, false).ConfigureAwait(false); - guild = await client.GetGuildAsync(_config.GuildId); - } - - uint nextVer = _cache.Info.Version + 1; - try - { - await DoMigrateAsync(client, guild, nextVer).ConfigureAwait(false); - _cache.Info.Version = nextVer; - await _cache.SaveInfoAsync().ConfigureAwait(false); - } - catch - { - await _cache.ClearAsync().ConfigureAwait(false); - throw; - } - } - } - - private static Task DoMigrateAsync(DiscordRestClient client, RestGuild guild, uint toVersion) - { - switch (toVersion) - { - case 1: return Migration_WipeGuild(client, guild); - case 2: return Tests.Migration_CreateTextChannels(client, guild); - case 3: return Tests.Migration_CreateVoiceChannels(client, guild); - default: throw new InvalidOperationException("Unknown migration: " + toVersion); - } - } - - private static async Task Migration_WipeGuild(DiscordRestClient client, RestGuild guild) - { - var textChannels = await guild.GetTextChannelsAsync(); - var voiceChannels = await guild.GetVoiceChannelsAsync(); - var roles = guild.Roles; - - foreach (var channel in textChannels) - { - //if (channel.Id != guild.DefaultChannelId) - await channel.DeleteAsync(); - } - foreach (var channel in voiceChannels) - await channel.DeleteAsync(); - foreach (var role in roles) - { - if (role.Id != guild.EveryoneRole.Id) - await role.DeleteAsync(); - } - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.Permissions.cs b/test/Discord.Net.Tests/Tests.Permissions.cs deleted file mode 100644 index 2f72f272d..000000000 --- a/test/Discord.Net.Tests/Tests.Permissions.cs +++ /dev/null @@ -1,773 +0,0 @@ -using System; -using System.Threading.Tasks; -using Xunit; - -namespace Discord -{ - public class PermissionsTests - { - private void TestHelper(ChannelPermissions value, ChannelPermission permission, bool expected = false) - => TestHelper(value.RawValue, (ulong)permission, expected); - - private void TestHelper(GuildPermissions value, GuildPermission permission, bool expected = false) - => TestHelper(value.RawValue, (ulong)permission, expected); - - /// - /// Tests the flag of the given permissions value to the expected output - /// and then tries to toggle the flag on and off - /// - /// - /// - /// - private void TestHelper(ulong rawValue, ulong flagValue, bool expected) - { - Assert.Equal(expected, Permissions.GetValue(rawValue, flagValue)); - - // check that toggling the bit works - Permissions.UnsetFlag(ref rawValue, flagValue); - Assert.False(Permissions.GetValue(rawValue, flagValue)); - Permissions.SetFlag(ref rawValue, flagValue); - Assert.True(Permissions.GetValue(rawValue, flagValue)); - - // do the same, but with the SetValue method - Permissions.SetValue(ref rawValue, true, flagValue); - Assert.True(Permissions.GetValue(rawValue, flagValue)); - Permissions.SetValue(ref rawValue, false, flagValue); - Assert.False(Permissions.GetValue(rawValue, flagValue)); - } - - /// - /// Tests that flag of the given permissions value to be the expected output - /// and then tries cycling through the states of the allow and deny values - /// for that flag - /// - /// - /// - /// - private void TestHelper(OverwritePermissions value, ChannelPermission flag, PermValue expected) - { - // check that the value matches - Assert.Equal(expected, Permissions.GetValue(value.AllowValue, value.DenyValue, flag)); - - // check toggling bits for both allow and deny - // have to make copies to get around read only property - ulong allow = value.AllowValue; - ulong deny = value.DenyValue; - - // both unset should be inherit - Permissions.UnsetFlag(ref allow, (ulong)flag); - Permissions.UnsetFlag(ref deny, (ulong)flag); - Assert.Equal(PermValue.Inherit, Permissions.GetValue(allow, deny, flag)); - - // allow set should be allow - Permissions.SetFlag(ref allow, (ulong)flag); - Permissions.UnsetFlag(ref deny, (ulong)flag); - Assert.Equal(PermValue.Allow, Permissions.GetValue(allow, deny, flag)); - - // deny should be deny - Permissions.UnsetFlag(ref allow, (ulong)flag); - Permissions.SetFlag(ref deny, (ulong)flag); - Assert.Equal(PermValue.Deny, Permissions.GetValue(allow, deny, flag)); - - // allow takes precedence - Permissions.SetFlag(ref allow, (ulong)flag); - Permissions.SetFlag(ref deny, (ulong)flag); - Assert.Equal(PermValue.Allow, Permissions.GetValue(allow, deny, flag)); - } - - /// - /// Tests for the class. - /// - /// Tests that text channel permissions get the right value - /// from the Has method. - /// - /// - [Fact] - public Task TestPermissionsHasChannelPermissionText() - { - var value = ChannelPermissions.Text; - // check that the result of GetValue matches for all properties of text channel - TestHelper(value, ChannelPermission.CreateInstantInvite, true); - TestHelper(value, ChannelPermission.ManageChannels, true); - TestHelper(value, ChannelPermission.AddReactions, true); - TestHelper(value, ChannelPermission.ViewChannel, true); - TestHelper(value, ChannelPermission.SendMessages, true); - TestHelper(value, ChannelPermission.SendTTSMessages, true); - TestHelper(value, ChannelPermission.ManageMessages, true); - TestHelper(value, ChannelPermission.EmbedLinks, true); - TestHelper(value, ChannelPermission.AttachFiles, true); - TestHelper(value, ChannelPermission.ReadMessageHistory, true); - TestHelper(value, ChannelPermission.MentionEveryone, true); - TestHelper(value, ChannelPermission.UseExternalEmojis, true); - TestHelper(value, ChannelPermission.ManageRoles, true); - TestHelper(value, ChannelPermission.ManageWebhooks, true); - - TestHelper(value, ChannelPermission.Connect, false); - TestHelper(value, ChannelPermission.Speak, false); - TestHelper(value, ChannelPermission.MuteMembers, false); - TestHelper(value, ChannelPermission.DeafenMembers, false); - TestHelper(value, ChannelPermission.MoveMembers, false); - TestHelper(value, ChannelPermission.UseVAD, false); - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Tests that no channel permissions get the right value - /// from the Has method. - /// - /// - [Fact] - public Task TestPermissionsHasChannelPermissionNone() - { - // check that none will fail all - var value = ChannelPermissions.None; - - TestHelper(value, ChannelPermission.CreateInstantInvite, false); - TestHelper(value, ChannelPermission.ManageChannels, false); - TestHelper(value, ChannelPermission.AddReactions, false); - TestHelper(value, ChannelPermission.ViewChannel, false); - TestHelper(value, ChannelPermission.SendMessages, false); - TestHelper(value, ChannelPermission.SendTTSMessages, false); - TestHelper(value, ChannelPermission.ManageMessages, false); - TestHelper(value, ChannelPermission.EmbedLinks, false); - TestHelper(value, ChannelPermission.AttachFiles, false); - TestHelper(value, ChannelPermission.ReadMessageHistory, false); - TestHelper(value, ChannelPermission.MentionEveryone, false); - TestHelper(value, ChannelPermission.UseExternalEmojis, false); - TestHelper(value, ChannelPermission.ManageRoles, false); - TestHelper(value, ChannelPermission.ManageWebhooks, false); - TestHelper(value, ChannelPermission.Connect, false); - TestHelper(value, ChannelPermission.Speak, false); - TestHelper(value, ChannelPermission.MuteMembers, false); - TestHelper(value, ChannelPermission.DeafenMembers, false); - TestHelper(value, ChannelPermission.MoveMembers, false); - TestHelper(value, ChannelPermission.UseVAD, false); - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Tests that the dm channel permissions get the right value - /// from the Has method. - /// - /// - [Fact] - public Task TestPermissionsHasChannelPermissionDM() - { - // check that none will fail all - var value = ChannelPermissions.DM; - - TestHelper(value, ChannelPermission.CreateInstantInvite, false); - TestHelper(value, ChannelPermission.ManageChannels, false); - TestHelper(value, ChannelPermission.AddReactions, false); - TestHelper(value, ChannelPermission.ViewChannel, true); - TestHelper(value, ChannelPermission.SendMessages, true); - TestHelper(value, ChannelPermission.SendTTSMessages, false); - TestHelper(value, ChannelPermission.ManageMessages, false); - TestHelper(value, ChannelPermission.EmbedLinks, true); - TestHelper(value, ChannelPermission.AttachFiles, true); - TestHelper(value, ChannelPermission.ReadMessageHistory, true); - TestHelper(value, ChannelPermission.MentionEveryone, false); - TestHelper(value, ChannelPermission.UseExternalEmojis, true); - TestHelper(value, ChannelPermission.ManageRoles, false); - TestHelper(value, ChannelPermission.ManageWebhooks, false); - TestHelper(value, ChannelPermission.Connect, true); - TestHelper(value, ChannelPermission.Speak, true); - TestHelper(value, ChannelPermission.MuteMembers, false); - TestHelper(value, ChannelPermission.DeafenMembers, false); - TestHelper(value, ChannelPermission.MoveMembers, false); - TestHelper(value, ChannelPermission.UseVAD, true); - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Tests that the group channel permissions get the right value - /// from the Has method. - /// - /// - [Fact] - public Task TestPermissionsHasChannelPermissionGroup() - { - var value = ChannelPermissions.Group; - - TestHelper(value, ChannelPermission.CreateInstantInvite, false); - TestHelper(value, ChannelPermission.ManageChannels, false); - TestHelper(value, ChannelPermission.AddReactions, false); - TestHelper(value, ChannelPermission.ViewChannel, false); - TestHelper(value, ChannelPermission.SendMessages, true); - TestHelper(value, ChannelPermission.SendTTSMessages, true); - TestHelper(value, ChannelPermission.ManageMessages, false); - TestHelper(value, ChannelPermission.EmbedLinks, true); - TestHelper(value, ChannelPermission.AttachFiles, true); - TestHelper(value, ChannelPermission.ReadMessageHistory, false); - TestHelper(value, ChannelPermission.MentionEveryone, false); - TestHelper(value, ChannelPermission.UseExternalEmojis, false); - TestHelper(value, ChannelPermission.ManageRoles, false); - TestHelper(value, ChannelPermission.ManageWebhooks, false); - TestHelper(value, ChannelPermission.Connect, true); - TestHelper(value, ChannelPermission.Speak, true); - TestHelper(value, ChannelPermission.MuteMembers, false); - TestHelper(value, ChannelPermission.DeafenMembers, false); - TestHelper(value, ChannelPermission.MoveMembers, false); - TestHelper(value, ChannelPermission.UseVAD, true); - - return Task.CompletedTask; - } - - - /// - /// Tests for the class. - /// - /// Tests that the voice channel permissions get the right value - /// from the Has method. - /// - /// - [Fact] - public Task TestPermissionsHasChannelPermissionVoice() - { - // make a flag with all possible values for Voice channel permissions - var value = ChannelPermissions.Voice; - - TestHelper(value, ChannelPermission.CreateInstantInvite, true); - TestHelper(value, ChannelPermission.ManageChannels, true); - TestHelper(value, ChannelPermission.AddReactions, false); - TestHelper(value, ChannelPermission.ViewChannel, true); - TestHelper(value, ChannelPermission.SendMessages, false); - TestHelper(value, ChannelPermission.SendTTSMessages, false); - TestHelper(value, ChannelPermission.ManageMessages, false); - TestHelper(value, ChannelPermission.EmbedLinks, false); - TestHelper(value, ChannelPermission.AttachFiles, false); - TestHelper(value, ChannelPermission.ReadMessageHistory, false); - TestHelper(value, ChannelPermission.MentionEveryone, false); - TestHelper(value, ChannelPermission.UseExternalEmojis, false); - TestHelper(value, ChannelPermission.ManageRoles, true); - TestHelper(value, ChannelPermission.ManageWebhooks, false); - TestHelper(value, ChannelPermission.Connect, true); - TestHelper(value, ChannelPermission.Speak, true); - TestHelper(value, ChannelPermission.MuteMembers, true); - TestHelper(value, ChannelPermission.DeafenMembers, true); - TestHelper(value, ChannelPermission.MoveMembers, true); - TestHelper(value, ChannelPermission.UseVAD, true); - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Test that that the Has method of - /// returns the correct value when no permissions are set. - /// - /// - [Fact] - public Task TestPermissionsHasGuildPermissionNone() - { - var value = GuildPermissions.None; - - TestHelper(value, GuildPermission.CreateInstantInvite, false); - TestHelper(value, GuildPermission.KickMembers, false); - TestHelper(value, GuildPermission.BanMembers, false); - TestHelper(value, GuildPermission.Administrator, false); - TestHelper(value, GuildPermission.ManageChannels, false); - TestHelper(value, GuildPermission.ManageGuild, false); - TestHelper(value, GuildPermission.AddReactions, false); - TestHelper(value, GuildPermission.ViewAuditLog, false); - TestHelper(value, GuildPermission.ViewChannel, false); - TestHelper(value, GuildPermission.SendMessages, false); - TestHelper(value, GuildPermission.SendTTSMessages, false); - TestHelper(value, GuildPermission.ManageMessages, false); - TestHelper(value, GuildPermission.EmbedLinks, false); - TestHelper(value, GuildPermission.AttachFiles, false); - TestHelper(value, GuildPermission.ReadMessageHistory, false); - TestHelper(value, GuildPermission.MentionEveryone, false); - TestHelper(value, GuildPermission.UseExternalEmojis, false); - TestHelper(value, GuildPermission.Connect, false); - TestHelper(value, GuildPermission.Speak, false); - TestHelper(value, GuildPermission.MuteMembers, false); - TestHelper(value, GuildPermission.MoveMembers, false); - TestHelper(value, GuildPermission.UseVAD, false); - TestHelper(value, GuildPermission.ChangeNickname, false); - TestHelper(value, GuildPermission.ManageNicknames, false); - TestHelper(value, GuildPermission.ManageRoles, false); - TestHelper(value, GuildPermission.ManageWebhooks, false); - TestHelper(value, GuildPermission.ManageEmojis, false); - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Test that that the Has method of - /// returns the correct value when all permissions are set. - /// - /// - [Fact] - public Task TestPermissionsHasGuildPermissionAll() - { - var value = GuildPermissions.All; - - TestHelper(value, GuildPermission.CreateInstantInvite, true); - TestHelper(value, GuildPermission.KickMembers, true); - TestHelper(value, GuildPermission.BanMembers, true); - TestHelper(value, GuildPermission.Administrator, true); - TestHelper(value, GuildPermission.ManageChannels, true); - TestHelper(value, GuildPermission.ManageGuild, true); - TestHelper(value, GuildPermission.AddReactions, true); - TestHelper(value, GuildPermission.ViewAuditLog, true); - TestHelper(value, GuildPermission.ViewChannel, true); - TestHelper(value, GuildPermission.SendMessages, true); - TestHelper(value, GuildPermission.SendTTSMessages, true); - TestHelper(value, GuildPermission.ManageMessages, true); - TestHelper(value, GuildPermission.EmbedLinks, true); - TestHelper(value, GuildPermission.AttachFiles, true); - TestHelper(value, GuildPermission.ReadMessageHistory, true); - TestHelper(value, GuildPermission.MentionEveryone, true); - TestHelper(value, GuildPermission.UseExternalEmojis, true); - TestHelper(value, GuildPermission.Connect, true); - TestHelper(value, GuildPermission.Speak, true); - TestHelper(value, GuildPermission.MuteMembers, true); - TestHelper(value, GuildPermission.MoveMembers, true); - TestHelper(value, GuildPermission.UseVAD, true); - TestHelper(value, GuildPermission.ChangeNickname, true); - TestHelper(value, GuildPermission.ManageNicknames, true); - TestHelper(value, GuildPermission.ManageRoles, true); - TestHelper(value, GuildPermission.ManageWebhooks, true); - TestHelper(value, GuildPermission.ManageEmojis, true); - - - return Task.CompletedTask; - } - - /// - /// Tests for the class. - /// - /// Test that that the Has method of - /// returns the correct value when webhook permissions are set. - /// - /// - [Fact] - public Task TestPermissionsHasGuildPermissionWebhook() - { - var value = GuildPermissions.Webhook; - - TestHelper(value, GuildPermission.CreateInstantInvite, false); - TestHelper(value, GuildPermission.KickMembers, false); - TestHelper(value, GuildPermission.BanMembers, false); - TestHelper(value, GuildPermission.Administrator, false); - TestHelper(value, GuildPermission.ManageChannels, false); - TestHelper(value, GuildPermission.ManageGuild, false); - TestHelper(value, GuildPermission.AddReactions, false); - TestHelper(value, GuildPermission.ViewAuditLog, false); - TestHelper(value, GuildPermission.ViewChannel, false); - TestHelper(value, GuildPermission.SendMessages, true); - TestHelper(value, GuildPermission.SendTTSMessages, true); - TestHelper(value, GuildPermission.ManageMessages, false); - TestHelper(value, GuildPermission.EmbedLinks, true); - TestHelper(value, GuildPermission.AttachFiles, true); - TestHelper(value, GuildPermission.ReadMessageHistory, false); - TestHelper(value, GuildPermission.MentionEveryone, false); - TestHelper(value, GuildPermission.UseExternalEmojis, false); - TestHelper(value, GuildPermission.Connect, false); - TestHelper(value, GuildPermission.Speak, false); - TestHelper(value, GuildPermission.MuteMembers, false); - TestHelper(value, GuildPermission.MoveMembers, false); - TestHelper(value, GuildPermission.UseVAD, false); - TestHelper(value, GuildPermission.ChangeNickname, false); - TestHelper(value, GuildPermission.ManageNicknames, false); - TestHelper(value, GuildPermission.ManageRoles, false); - TestHelper(value, GuildPermission.ManageWebhooks, false); - TestHelper(value, GuildPermission.ManageEmojis, false); - - return Task.CompletedTask; - } - - /// - /// Test - /// for when all text permissions are allowed and denied - /// - /// - [Fact] - public Task TestOverwritePermissionsText() - { - // allow all for text channel - var value = new OverwritePermissions(ChannelPermissions.Text.RawValue, ChannelPermissions.None.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Allow); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Allow); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Allow); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Allow); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Allow); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Allow); - TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); - TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); - - value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Text.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Deny); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Deny); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Deny); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Deny); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Deny); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Deny); - TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); - TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); - - return Task.CompletedTask; - } - - /// - /// Test - /// for when none of the permissions are set. - /// - /// - [Fact] - public Task TestOverwritePermissionsNone() - { - // allow all for text channel - var value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.None.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); - TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); - - value = new OverwritePermissions(); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); - TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); - - value = OverwritePermissions.InheritAll; - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); - TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); - - return Task.CompletedTask; - } - - /// - /// Test - /// for when all dm permissions are allowed and denied - /// - /// - [Fact] - public Task TestOverwritePermissionsDM() - { - // allow all for text channel - var value = new OverwritePermissions(ChannelPermissions.DM.RawValue, ChannelPermissions.None.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Allow); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Allow); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Allow); - TestHelper(value, ChannelPermission.Speak, PermValue.Allow); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); - - value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.DM.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Deny); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Deny); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Deny); - TestHelper(value, ChannelPermission.Speak, PermValue.Deny); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); - - return Task.CompletedTask; - } - - /// - /// Test - /// for when all group permissions are allowed and denied - /// - /// - [Fact] - public Task TestOverwritePermissionsGroup() - { - // allow all for group channels - var value = new OverwritePermissions(ChannelPermissions.Group.RawValue, ChannelPermissions.None.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Allow); - TestHelper(value, ChannelPermission.Speak, PermValue.Allow); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); - - value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Group.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Deny); - TestHelper(value, ChannelPermission.Speak, PermValue.Deny); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); - - return Task.CompletedTask; - } - - /// - /// Test - /// for when all group permissions are allowed and denied - /// - /// - [Fact] - public Task TestOverwritePermissionsVoice() - { - // allow all for group channels - var value = new OverwritePermissions(ChannelPermissions.Voice.RawValue, ChannelPermissions.None.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Allow); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Allow); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Allow); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Allow); - TestHelper(value, ChannelPermission.Speak, PermValue.Allow); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Allow); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Allow); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Allow); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); - - value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Voice.RawValue); - - TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageChannels, PermValue.Deny); - TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); - TestHelper(value, ChannelPermission.ViewChannel, PermValue.Deny); - TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); - TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); - TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); - TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); - TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); - TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); - TestHelper(value, ChannelPermission.ManageRoles, PermValue.Deny); - TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); - TestHelper(value, ChannelPermission.Connect, PermValue.Deny); - TestHelper(value, ChannelPermission.Speak, PermValue.Deny); - TestHelper(value, ChannelPermission.MuteMembers, PermValue.Deny); - TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Deny); - TestHelper(value, ChannelPermission.MoveMembers, PermValue.Deny); - TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); - - return Task.CompletedTask; - } - - /// - /// Tests for the - /// method to ensure that the default no-param call does not modify the resulting value - /// of the OverwritePermissions. - /// - /// - [Fact] - public Task TestOverwritePermissionModifyNoParam() - { - // test for all Text allowed, none denied - var original = new OverwritePermissions(ChannelPermissions.Text.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, text denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Text.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // category allowed, none denied - original = new OverwritePermissions(ChannelPermissions.Category.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, category denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Category.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // DM allowed, none denied - original = new OverwritePermissions(ChannelPermissions.DM.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, DM denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.DM.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // voice allowed, none denied - original = new OverwritePermissions(ChannelPermissions.Voice.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, voice denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Voice.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // group allowed, none denied - original = new OverwritePermissions(ChannelPermissions.Group.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, group denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Group.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - // none allowed, none denied - original = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.None.RawValue); - Assert.Equal(original.AllowValue, original.Modify().AllowValue); - Assert.Equal(original.DenyValue, original.Modify().DenyValue); - - return Task.CompletedTask; - } - } -} diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs deleted file mode 100644 index df156d254..000000000 --- a/test/Discord.Net.Tests/Tests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Discord.Net; -using Discord.Rest; -using Xunit; - -namespace Discord -{ - public partial class TestsFixture : IDisposable - { - private readonly TestConfig _config; - private readonly CachedRestClient _cache; - internal readonly DiscordRestClient _client; - internal readonly RestGuild _guild; - - public TestsFixture() - { - _cache = new CachedRestClient(); - - _config = TestConfig.LoadFile("./config.json"); - var config = new DiscordRestConfig - { - RestClientProvider = url => - { - _cache.SetUrl(url); - return _cache; - } - }; - _client = new DiscordRestClient(config); - _client.LoginAsync(TokenType.Bot, _config.Token).Wait(); - - MigrateAsync().Wait(); - _guild = _client.GetGuildAsync(_config.GuildId).Result; - } - - public void Dispose() - { - _client.Dispose(); - _cache.Dispose(); - } - } - - public partial class Tests : IClassFixture - { - private DiscordRestClient _client; - private RestGuild _guild; - - public Tests(TestsFixture fixture) - { - _client = fixture._client; - _guild = fixture._guild; - } - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/config.json.example b/test/Discord.Net.Tests/config.json.example deleted file mode 100644 index 403afa3bd..000000000 --- a/test/Discord.Net.Tests/config.json.example +++ /dev/null @@ -1,4 +0,0 @@ -{ - "token": "AAA.BBB.CCC", - "guild_id": 1234567890 -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/xunit.runner.json b/test/Discord.Net.Tests/xunit.runner.json deleted file mode 100644 index ac3e63046..000000000 --- a/test/Discord.Net.Tests/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "diagnosticMessages": true, - "methodDisplay": "classAndMethod" -} \ No newline at end of file diff --git a/voice-natives/README.md b/voice-natives/README.md new file mode 100644 index 000000000..a89fad45f --- /dev/null +++ b/voice-natives/README.md @@ -0,0 +1,12 @@ +# Voice binaries + +These binaries were taken from the [DSharpPlus](https://dsharpplus.github.io/natives/index.html) website and are temporary until we resolve the old url for them. + +**NOTE**: You need to rename libopus.dll to opus.dll before use, otherwise audio client will complain about missing libraries. + +#### Licenses + +| Library | License | +| :-------: | :-------------------------------------------------------- | +| Opus | https://opus-codec.org/license/ | +| libsodium | https://github.com/jedisct1/libsodium/blob/master/LICENSE | diff --git a/voice-natives/vnext_natives_win32_x64.zip b/voice-natives/vnext_natives_win32_x64.zip new file mode 100644 index 000000000..a447803e5 Binary files /dev/null and b/voice-natives/vnext_natives_win32_x64.zip differ diff --git a/voice-natives/vnext_natives_win32_x86.zip b/voice-natives/vnext_natives_win32_x86.zip new file mode 100644 index 000000000..35522e1ec Binary files /dev/null and b/voice-natives/vnext_natives_win32_x86.zip differ