diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 891ed0b23..e51e00e63 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,4 +25,3 @@ Please delete options that are not relevant. - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have checked my changes haven't lowered code coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a65d5e9..584de7b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,190 +1,327 @@ +## 6.4.3 +__08.10.2024__ + +- bug: Handle missing `mention_roles` field in some message snapshots. ([`#709`](https://github.com/nyxx-discord/nyxx/pull/709)) - ([`d458d9c`](https://github.com/nyxx-discord/nyxx/commit/d458d9c82e621ca24044533cd2ce2d66c0a29186)) + +## 6.4.2 +__05.10.2024__ + +- bug: Prevent caches from growing forever. ([`#704`](https://github.com/nyxx-discord/nyxx/pull/704)) - ([`c62c23c`](https://github.com/nyxx-discord/nyxx/commit/c62c23c1abe3aa01e970f137579ab9ddbe6f87da)) + +## 6.4.1 +__04.10.2024__ + +- bug: Fix exports ([`d2312de`](https://github.com/nyxx-discord/nyxx/commit/d2312de445547c955a565fc13511cf55fa8bbf76)) + +## 6.4.0 +__04.10.2024__ + +- feat: Add new permissions ([`#679`](https://github.com/nyxx-discord/nyxx/pull/679)) - ([`c91f050`](https://github.com/nyxx-discord/nyxx/commit/c91f05002d2735990b09296ca67b01359b2b3bf4)) +- bug: Make webhook execute apply name/avatar when message doesn't contain any attachments ([`#681`](https://github.com/nyxx-discord/nyxx/pull/681)) - ([`cac408e`](https://github.com/nyxx-discord/nyxx/commit/cac408e0ba3870f7716ee64f938ef63d195ab6ce)) +- bug: Add missing `approximateUserInstallCount` to `Application` ([`#683`](https://github.com/nyxx-discord/nyxx/pull/683)) - ([`3e2de40`](https://github.com/nyxx-discord/nyxx/commit/3e2de400d5adb67bc8bc8187198bb90b62a4130a)) +- feat: Add missing audit log event types ([`#684`](https://github.com/nyxx-discord/nyxx/pull/684)) - ([`b1b6414`](https://github.com/nyxx-discord/nyxx/commit/b1b6414145cec7b4e4c20b4b199f7e2360290d22)) +- feat: Add auditLogReason to followChannel ([`#685`](https://github.com/nyxx-discord/nyxx/pull/685)) - ([`8f7f629`](https://github.com/nyxx-discord/nyxx/commit/8f7f6295d075a6b244d9db33d720a9b324c9063e)) +- feat: Add recurrence rules for scheduled events ([`#686`](https://github.com/nyxx-discord/nyxx/pull/686)) - ([`ffc0c08`](https://github.com/nyxx-discord/nyxx/commit/ffc0c08d561bff4aa549f656be8636da1bd8eab7)) +- bug: Add missing fields to Message ([`#689`](https://github.com/nyxx-discord/nyxx/pull/689)) - ([`8a469c4`](https://github.com/nyxx-discord/nyxx/commit/8a469c4cb6874c867396dd307271e9379c997377)) +- feat: Add type field to Invite ([`#688`](https://github.com/nyxx-discord/nyxx/pull/688)) - ([`e38dbf8`](https://github.com/nyxx-discord/nyxx/commit/e38dbf80b580a165a9a9750fd5c90f2759531678)) +- bug: Delete any commands left over in tests ([`#693`](https://github.com/nyxx-discord/nyxx/pull/693)) - ([`7022767`](https://github.com/nyxx-discord/nyxx/commit/7022767bb317ce3bf7cf5eaa44b8e332880f2406)) +- feat: Add fetch voice state endpoints ([`#692`](https://github.com/nyxx-discord/nyxx/pull/692)) - ([`b91d782`](https://github.com/nyxx-discord/nyxx/commit/b91d782ea4c10c296e15a6531d6c9c114f5d2a3e)) +- feat: Add support for subscriptions ([`#690`](https://github.com/nyxx-discord/nyxx/pull/690)) - ([`8e50682`](https://github.com/nyxx-discord/nyxx/commit/8e50682d752ce0a3576dc493e61d19fe18b354a8)) +- feat: Add update onboarding endpoint to GuildManager ([`#687`](https://github.com/nyxx-discord/nyxx/pull/687)) - ([`002cdfc`](https://github.com/nyxx-discord/nyxx/commit/002cdfc53944eb3255d4c1d4173cbc6442fa6b0f)) +- feat: Add application emojis ([`#678`](https://github.com/nyxx-discord/nyxx/pull/678)) - ([`56ec264`](https://github.com/nyxx-discord/nyxx/commit/56ec264f25e01edfda97663961b7c7f3afd6929d)) +- feat: Improve cache implementation ([`#694`](https://github.com/nyxx-discord/nyxx/pull/694)) - ([`59c0b39`](https://github.com/nyxx-discord/nyxx/commit/59c0b399dfba170d1303040606d16e707b408420)) +- bug: Don't modify Cache.keys during iteration ([`#698`](https://github.com/nyxx-discord/nyxx/pull/698)) - ([`8b4b451`](https://github.com/nyxx-discord/nyxx/commit/8b4b4514c6bc1a8612caa17d500b27e2e9ec2876)) + +## 6.3.1 +__11.07.2024__ + +- bug: poll answer in message object throwing an error. ([`#673`](https://github.com/nyxx-discord/nyxx/pull/673)) - ([`0824294`](https://github.com/nyxx-discord/nyxx/commit/0824294b9d8c372cb93d2cc3c41d0db01be7df38)) + +## 6.3.0 +__07.07.2024__ + +- feat: Add one time purchase SKUs support. ([`#651`](https://github.com/nyxx-discord/nyxx/pull/651)) - ([`b874066`](https://github.com/nyxx-discord/nyxx/commit/b87406632d664085e068920333644c087de20372)) +- bug: Include query parameters in CDN requests. ([`#653`](https://github.com/nyxx-discord/nyxx/pull/653)) - ([`f2c33ce`](https://github.com/nyxx-discord/nyxx/commit/f2c33ce13d97b029de4ccc39ad5be0943de7c025)) +- feat: Add warning logs for rate limits. ([`#654`](https://github.com/nyxx-discord/nyxx/pull/654)) - ([`976d290`](https://github.com/nyxx-discord/nyxx/commit/976d29038868e6f7b91b62652805e721c07694af)) +- feat: Add `deleteMessages` parameter to `Member.ban`. ([`#656`](https://github.com/nyxx-discord/nyxx/pull/656)) - ([`55c94c3`](https://github.com/nyxx-discord/nyxx/commit/55c94c33992acaa34f6f9612b4deb7d1c620e074)) +- bug: Ensure `client.close()` cleans up all pending operations. ([`#655`](https://github.com/nyxx-discord/nyxx/pull/655)) - ([`e8d9d5d`](https://github.com/nyxx-discord/nyxx/commit/e8d9d5d8c5cfd07ac005bb790186303a269855cd)) +- feat: Add polls support. ([`#644`](https://github.com/nyxx-discord/nyxx/pull/644)) - ([`7908853`](https://github.com/nyxx-discord/nyxx/commit/7908853845a4051b02da2ebc4bd092f8f6bba136)) +- bug: Allow non-ascii characters in audit log reasons. ([`#659`](https://github.com/nyxx-discord/nyxx/pull/659)) - ([`27b74b5`](https://github.com/nyxx-discord/nyxx/commit/27b74b5d8c3ec197badf53817d2343546f71d0a0)) +- feat: Add `memberUpdate` automod type. ([`#662`](https://github.com/nyxx-discord/nyxx/pull/662)) - ([`a502121`](https://github.com/nyxx-discord/nyxx/commit/a50212110d54e283accaac43e9a3d7a906410b2d)) +- feat: Add `blockMemberInteraction` automod action type. ([`#664`](https://github.com/nyxx-discord/nyxx/pull/664)) - ([`50f3377`](https://github.com/nyxx-discord/nyxx/commit/50f3377d9a224687e69c7377111779c862f76ad5)) +- feat: Add support for premium buttons. ([`#690`](https://github.com/nyxx-discord/nyxx/pull/690)) - ([`8e50682`](https://github.com/nyxx-discord/nyxx/commit/8e50682d752ce0a3576dc493e61d19fe18b354a8)) +- bug: Allow parsing unknown enum values. ([`#655`](https://github.com/nyxx-discord/nyxx/pull/665)) - ([`0a212fd`](https://github.com/nyxx-discord/nyxx/commit/0a212fd94a92423f8e969c298a7e9db5061ad7af)) +- bug: Fix parsing of message interaction metadata user. ([`#688`](https://github.com/nyxx-discord/nyxx/pull/688)) - ([`9eb5569`](https://github.com/nyxx-discord/nyxx/commit/9eb5569d4fb0580e0aee9dfd2798492f9dbdce95)) + + +## 6.2.1 +__03.04.2024__ + +- bug: Fix parsing integration types when they are not present. ([`#647`](https://github.com/nyxx-discord/nyxx/pull/647)) - ([`89068e7`](https://github.com/nyxx-discord/nyxx/commit/89068e7c2666089c8873daa8863aa41b55a6a0d4)) + +## 6.2.0 +__20.03.2024__ + +- feat: Add support for Group DM endpoints when using an OAuth client. ([`#600`](https://github.com/nyxx-discord/nyxx/pull/600)) - ([`6ed9657`](https://github.com/nyxx-discord/nyxx/commit/6ed96579dea4ebe32f9aae1dbe846f2bcd4c0cc4)) +- feat: Add support for `username` and `avatarUrl` parameters for webhooks. ([`#611`](https://github.com/nyxx-discord/nyxx/pull/611)) - ([`982e0d9`](https://github.com/nyxx-discord/nyxx/commit/982e0d9d2c718c54213dee5c37da4c907f6f1ff7)) +- feat: Add `Spanish, LATAM` locale. ([`#610`](https://github.com/nyxx-discord/nyxx/pull/610)) - ([`896004a`](https://github.com/nyxx-discord/nyxx/commit/896004a498d9c2afb6f0f6185ffe1cd5270e0774)) +- bug: Fix events being dropped when plugins had async initialization. ([`#612`](https://github.com/nyxx-discord/nyxx/pull/612)) - ([`bb54de1`](https://github.com/nyxx-discord/nyxx/commit/bb54de16257cdf2382976beb755f621f433e356a)) +- bug: Return an empty list instead of throwing when fetching the permission overrides of a command that has none. ([`#613`](https://github.com/nyxx-discord/nyxx/pull/613)) - ([`1a4ed82`](https://github.com/nyxx-discord/nyxx/commit/1a4ed8294a244e1a5fe1212f2b41da05c41e4f58)) +- feat: Add ratelimits when sending Gateway events. ([`#614`](https://github.com/nyxx-discord/nyxx/pull/614)) - ([`f6ef61b`](https://github.com/nyxx-discord/nyxx/commit/f6ef61bd9f467ba1f7df8d495514464081c0da02)) +- bug: Allow any `Flags` in `RoleUpdateBuilder.permissions`. ([`#617`](https://github.com/nyxx-discord/nyxx/pull/617)) - ([`4c7f7fa`](https://github.com/nyxx-discord/nyxx/commit/4c7f7fa6beebc68b10494b7a5d11c85b73240483)) +- bug: Export types that were previously kept private. ([`#616`])(https://github.com/nyxx-discord/nyxx/pull/616) - ([`a1b2bc6`](https://github.com/nyxx-discord/nyxx/commit/a1b2bc68529b8b646e9f75d0b5ae202bb5604332)) +- feat: Allow plugins to intercept HTTP requests and Gateway events. ([`#615`](https://github.com/nyxx-discord/nyxx/pull/615)) - ([`ff92f86`](https://github.com/nyxx-discord/nyxx/commit/ff92f86258fa02f6c2a0cf6863523c70924f1fe2)) +- bug: Fix `MessageManager.bulkDelete` not serializing the request correctly. ([`#618`](https://github.com/nyxx-discord/nyxx/pull/618)) - ([`797579b`](https://github.com/nyxx-discord/nyxx/commit/797579b7bfd9128386e3cf4e66ac3dc62f948cff)) +- bug: Fix `GuildDeleteEvent`s not being parsed when `unavailable` was not explicitly set. ([`#619`](https://github.com/nyxx-discord/nyxx/pull/619)) - ([`7ea4070`](https://github.com/nyxx-discord/nyxx/commit/7ea4070d0792dfc7d688eb1fa0a8a20583258f98)) +- bug: Correct serialization of guild builders. ([`#621`](https://github.com/nyxx-discord/nyxx/pull/621)) - ([`d9593c5`](https://github.com/nyxx-discord/nyxx/commit/d9593c57488285f7b6bb0ad15b6ea3b4db288ed3)) +- bug: Correct value of `TriggerType.spam`. ([`#623`](https://github.com/nyxx-discord/nyxx/pull/623)) - ([`463cda9`](https://github.com/nyxx-discord/nyxx/commit/463cda91ed98197b06fd125487722f43fa6a7d35)) +- docs: Hide constructors from documentation. ([`#624`](https://github.com/nyxx-discord/nyxx/pull/624)) - ([`3e0ecdf`](https://github.com/nyxx-discord/nyxx/commit/3e0ecdfe4b92ab1ad1639e824973565f50b9e35c)) +- bug: Fix parsing role flags in guild templates. ([`#625`](https://github.com/nyxx-discord/nyxx/pull/625)) - ([`495b88f`](https://github.com/nyxx-discord/nyxx/commit/495b88fc03688b84a2b30eb81b5bc0382b943acd)) +- bug: Fix `isHoisted` attribute in role builders. ([`#627`](https://github.com/nyxx-discord/nyxx/pull/627)) - ([`bb62547`](https://github.com/nyxx-discord/nyxx/commit/bb6254780068aa121c063096f74f0648aa220155)) +- bug: Fix all audit log parameters in `StickerManager`, `EmojiManager` and `WebhookManager.update` ([`#628`](https://github.com/nyxx-discord/nyxx/pull/628)) - ([`4ece7f7`](https://github.com/nyxx-discord/nyxx/commit/4ece7f763b80a95816219a9ca09ec45780c266af)) +- bug: Fix `interactionsEndpointUrl` being ignored in `ApplicationUpdateBuilder` ([`#628`](https://github.com/nyxx-discord/nyxx/pull/628)) - ([`4ece7f7`](https://github.com/nyxx-discord/nyxx/commit/4ece7f763b80a95816219a9ca09ec45780c266af)) +- feat: Add more shortcut methods on models. ([`#628`](https://github.com/nyxx-discord/nyxx/pull/628)) - ([`4ece7f7`](https://github.com/nyxx-discord/nyxx/commit/4ece7f763b80a95816219a9ca09ec45780c266af)) +- feat: Add `enforceNonce` to `MessageBuilder`. ([`#631`](https://github.com/nyxx-discord/nyxx/pull/631)) - ([`7407767`](https://github.com/nyxx-discord/nyxx/commit/7407767cd58601103e69397b31043aa5f52e3ef2)) +- feat: Add missing role tags fields. ([`#632`](https://github.com/nyxx-discord/nyxx/pull/632)) - ([`0deacf8`](https://github.com/nyxx-discord/nyxx/commit/0deacf842177d7e45990828facff4eaa792fe182)) +- bug: Correct the default `User-Agent` header. ([`#633`](https://github.com/nyxx-discord/nyxx/pull/633)) - ([`a1d89ca`](https://github.com/nyxx-discord/nyxx/commit/a1d89cad765e965a641ed18dbe7465a88b34d352)) +- bug: Don't require OAuth2 identify scope when using `NyxxOauth2`. ([`#634`](https://github.com/nyxx-discord/nyxx/pull/634)) - ([`408fe03`](https://github.com/nyxx-discord/nyxx/commit/408fe03e998cbf8d17df68b482502c164cfcbefd)) +- feat: Add field to delete events containing the cached entity before it was deleted. ([`#635`](https://github.com/nyxx-discord/nyxx/pull/635)) - ([`1e8d5e5`](https://github.com/nyxx-discord/nyxx/commit/1e8d5e54f3356d144558f0c4114aad67a1aca41d)) +- feat: Add builders for auto moderation actions. ([`#636`](https://github.com/nyxx-discord/nyxx/pull/636)) - ([`2e09030`](https://github.com/nyxx-discord/nyxx/commit/2e09030cdbb92c3c6b7222bf34b9c373f38dac44)) +- bug: Initialize login sooner to avoid dropping logs. ([`#637`](https://github.com/nyxx-discord/nyxx/pull/637)) - ([`02ee8a6`](https://github.com/nyxx-discord/nyxx/commit/02ee8a670cdb5ae8b09156c4a2149487f4aa8304)) +- feat: Add `banner` to `UserUpdateBuilder`. ([`#638`](https://github.com/nyxx-discord/nyxx/pull/638)) - ([`42f7bef`](https://github.com/nyxx-discord/nyxx/commit/42f7bef041de3bab6ff432ecc70b63fca21a8621)) +- feat: Add `SkuFlags.available`. ([`#638`](https://github.com/nyxx-discord/nyxx/pull/638)) - ([`42f7bef`](https://github.com/nyxx-discord/nyxx/commit/42f7bef041de3bab6ff432ecc70b63fca21a8621)) +- feat: Add bungie, domain and roblox connection types. ([`#639`](https://github.com/nyxx-discord/nyxx/pull/639)) - ([`70c9d04`](https://github.com/nyxx-discord/nyxx/commit/70c9d044cf4f61fe312e8ba511f3578d94293c28)) +- feat: Add support for user applications. ([`#641`](https://github.com/nyxx-discord/nyxx/pull/641)) - ([`10181ac`](https://github.com/nyxx-discord/nyxx/commit/10181ac7c3f3e3b71433cfa725e31aafcdd56cec)) +- feat: Add `bulkBan` to `GuildManager`. ([`#640`](https://github.com/nyxx-discord/nyxx/pull/640)) - ([`6adc5ff`](https://github.com/nyxx-discord/nyxx/commit/6adc5ffbdf97ea0caacd58b32330b1f0347b4279)) + +## 6.1.0 +__09.12.2023__ + +- feat: Add payload to `EntitlementDeleteEvent`. ([`#599`](https://github.com/nyxx-discord/nyxx/pull/599)) - ([`7a0f346`](https://github.com/nyxx-discord/nyxx/commit/7a0f346677b5c7a8edf7064db722087765864722)) +- feat: Add `flags` field to `Sku`. ([`#602`](https://github.com/nyxx-discord/nyxx/pull/602)) - ([`74bf507`](https://github.com/nyxx-discord/nyxx/commit/74bf50762025d35ae8f9dc31379fe81e4e0036d0)) +- feat: Add support for select menu default values. ([`#601`](https://github.com/nyxx-discord/nyxx/pull/601)) - ([`5a833f3`](https://github.com/nyxx-discord/nyxx/commit/5a833f3233763b7507c8526d74954db6e62877ac)) +- feat: Add `GuildUpdateBuilder.safetyAlertsChannelId`. ([`#596`](https://github.com/nyxx-discord/nyxx/pull/596)) - ([`c1eab88`](https://github.com/nyxx-discord/nyxx/commit/c1eab8883ceb34c1ab0822fe40ea8377a8e5d7d7)) +- docs: Enable link to source in package documentation. ([`#607`](https://github.com/nyxx-discord/nyxx/pull/607)) - ([`57ce193`](https://github.com/nyxx-discord/nyxx/commit/57ce193445f9b95304eb9771a9e44a165e423138)) +- feat: Add AutoMod message types. ([`#597`](https://github.com/nyxx-discord/nyxx/pull/597)) - ([`3ba7e7e`](https://github.com/nyxx-discord/nyxx/commit/3ba7e7e71a39497863e8f43e7058939eaeb0a878)) +- bug: Fix `ButtonBuilder` serialization. ([`#595`](https://github.com/nyxx-discord/nyxx/pull/595)) - ([`a81ee15`](https://github.com/nyxx-discord/nyxx/commit/a81ee15c7cd90c0970a1aa1ac0885d2b13624caf)) +- bug: Fix `GuildUpdateBuilder` not being able to unset certain settings. ([`#596`](https://github.com/nyxx-discord/nyxx/pull/596)) - ([`c1eab88`](https://github.com/nyxx-discord/nyxx/commit/c1eab8883ceb34c1ab0822fe40ea8377a8e5d7d7)) +- bug: Fix incorrect `PermissionOverwriteBuilder` serialization when creating/updating channels. ([`#596`](https://github.com/nyxx-discord/nyxx/pull/596)) - ([`c1eab88`](https://github.com/nyxx-discord/nyxx/commit/c1eab8883ceb34c1ab0822fe40ea8377a8e5d7d7)) +- bug: Fix `GuildManager.listBans` ignoring the provided parameters. ([`#598`](https://github.com/nyxx-discord/nyxx/pull/598)) - ([`d04db1a`](https://github.com/nyxx-discord/nyxx/commit/d04db1ae240ff1f47764f585b95edd5f477c2ba5)) +- bug: Correctly export `Credentials` from `package:oauth2` for OAuth2 support. ([`#604`](https://github.com/nyxx-discord/nyxx/pull/604)) - ([`f23cfd2`](https://github.com/nyxx-discord/nyxx/commit/f23cfd27b87560a4fe2da04d21c2be8bb46a3f06)) +- bug: Fix members in message interactions not having their guild set. ([`#603`](https://github.com/nyxx-discord/nyxx/pull/603)) - ([`c12ba89`](https://github.com/nyxx-discord/nyxx/commit/c12ba8915f11845df869c981ee025aa12c5b750b)) + +## 6.0.3 +__26.11.2023__ + +- bug: Fix incorrect serialization of autocompletion interaction responses (again). ([`#591`](https://github.com/nyxx-discord/nyxx/pull/591)) - ([`573c9bc`](https://github.com/nyxx-discord/nyxx/commit/573c9bc6046474b6dfc48c152d938f9bb3e8af1a)) +- bug: Try to fix invalid sessions triggered by Gateway reconnects. ([`#592`](https://github.com/nyxx-discord/nyxx/pull/592)) - ([`46f128c`](https://github.com/nyxx-discord/nyxx/commit/46f128c9d165775ec1cee6d129238ee8ec8ae0d8)) + +## 6.0.2 +__16.11.2023__ + +- bug: Fix incorrect assertions in interaction.respond. ([`#584`](https://github.com/nyxx-discord/nyxx/pull/584)) - ([`f7abb73`](https://github.com/nyxx-discord/nyxx/commit/f7abb730c501bd744a10cf79ff1b92c86be9e9ac)) +- bug: Fix incorrect serialization of autocompletion interaction responses. ([`#585`](https://github.com/nyxx-discord/nyxx/pull/585)) - ([`55a8c76`](https://github.com/nyxx-discord/nyxx/commit/55a8c760b3a2cf0b481a3e7b941c9b9fe62cc359)) + +## 6.0.1 +__01.11.2023__ + +- bug: Fix incorrect serialization of CommandOptionBuilder. ([`#571`](https://github.com/nyxx-discord/nyxx/pull/571)) - ([`9728833`](https://github.com/nyxx-discord/nyxx/commit/9728833a6c28dba05f0032430fae035d56d1bf17)) +- bug: Fix customId missing from ButtonBuilder constructor. ([`#570`](https://github.com/nyxx-discord/nyxx/pull/570)) - ([`31ab26d`](https://github.com/nyxx-discord/nyxx/commit/31ab26d4eb1709bb36284029f29be6f13ec0b83b)) +- bug: Fix voice states not being cached correctly. ([`#575`](https://github.com/nyxx-discord/nyxx/pull/575)) - ([`758648c`](https://github.com/nyxx-discord/nyxx/commit/758648cc0f0bb3c5741354437498208905e73843)) +- bug: Fix incorrect parsing of scheduled events. ([`#576`](https://github.com/nyxx-discord/nyxx/pull/576)) - ([`a78b1f8`](https://github.com/nyxx-discord/nyxx/commit/a78b1f83899b185bb326e53b8c24cfd8872b1f82)) +- bug: Fix `ephemeral` parameter not working when responding to message component interactions. ([`#577`](https://github.com/nyxx-discord/nyxx/pull/577)) - ([`2afc142`](https://github.com/nyxx-discord/nyxx/commit/2afc142b84fbf5fef2e2bc5eb042bd65dcf88b57)) +- bug: Fix parsing button labels when they are not set. ([`#578`](https://github.com/nyxx-discord/nyxx/pull/578)) - ([`1919613`](https://github.com/nyxx-discord/nyxx/commit/19196139611c945510148ec0e8f8fdee93bd45e2)) +- bug: Fix incorrect serialization of TextInputBuilder. ([`#579`](https://github.com/nyxx-discord/nyxx/pull/579)) - ([`08a14be`](https://github.com/nyxx-discord/nyxx/commit/08a14bef7416bf7ce8b39e7134e09b16f7a9a3d4)) +- bug: Fix some entities not being cached. ([`#580`](https://github.com/nyxx-discord/nyxx/pull/580)) - ([`ab35012`](https://github.com/nyxx-discord/nyxx/commit/ab3501240db83f40322d84f447738b707154623e)) +- bug: Fix entities getting "stuck" in cache due to momentary high use. ([`#580`](https://github.com/nyxx-discord/nyxx/pull/580)) - ([`ab35012`](https://github.com/nyxx-discord/nyxx/commit/ab3501240db83f40322d84f447738b707154623e)) +- feat: Add more places entities can be cached from. ([`#580`](https://github.com/nyxx-discord/nyxx/pull/580)) - ([`ab35012`](https://github.com/nyxx-discord/nyxx/commit/ab3501240db83f40322d84f447738b707154623e)) + ## 6.0.0 __16.10.2023__ -- rewrite: The entire library has been rewritten from the ground up. No pre-`6.0.0` code is compatible. +- rewrite: The entire library has been rewritten from the ground up. No pre-`6.0.0` code is compatible. - To explore the rewrite, check out [the API documentation](https://pub.dev/documentation/nyxx) or the [documentation website](https://nyxx.l7ssha.xyz). For help migrating, use the [migration tool](https://github.com/abitofevrything/nyxx-migration-script) or join [our Discord server](https://discord.gg/nyxx) for additional help. #### Changes since 6.0.0-dev.3 -- bug: Fix `ModalBuilder` having the incorrect data type. -- feat: Add the new `state` field to `ActivityBuilder`. -- bug: Fix `activities` not being sent in the presence update payload. -- bug: Fix casts when parsing `Snowflake`s triggering errors when using ETF. -- bug: Fix incorrect payload preventing the client from muting/deafening itself. -- bug: Correctly export `NyxxPluginState`. -- feat: Implement all new features since the start of the rewrite (including premium subscriptions). -- bug: Fix incorrect parsing of `MessageUpdateEvent`s. -- feat: Add `logger` to plugins and make `name` inferred from `runtimeType` by default. +- bug: Fix `ModalBuilder` having the incorrect data type. ([`#535`](https://github.com/nyxx-discord/nyxx/pull/535)) - ([`d130e4c`](https://github.com/nyxx-discord/nyxx/commit/d130e4c502acf5c0f8f726c18310f16060310d25)) +- feat: Add the new `state` field to `ActivityBuilder`. ([`#556`](https://github.com/nyxx-discord/nyxx/pull/556)) - ([`3630696`](https://github.com/nyxx-discord/nyxx/commit/363069676690b7407ed5d98ace94938b9cf08309)) +- bug: Fix `activities` not being sent in the presence update payload. ([`#557`](https://github.com/nyxx-discord/nyxx/pull/557)) - ([`650ee17`](https://github.com/nyxx-discord/nyxx/commit/650ee1756283b14b0cbf056268a14b968ba56660)) +- bug: Fix casts when parsing `Snowflake`s triggering errors when using ETF. ([`#559`](https://github.com/nyxx-discord/nyxx/pull/559)) - ([`3402435`](https://github.com/nyxx-discord/nyxx/commit/34024358f1ff733f3122854304cd51e5bb6905c3)) +- bug: Fix incorrect payload preventing the client from muting/deafening itself. ([`#561`](https://github.com/nyxx-discord/nyxx/pull/561)) - ([`c31514b`](https://github.com/nyxx-discord/nyxx/commit/c31514b5c260abf1560b9f866ec9c8fbfa46d8e5)) +- bug: Correctly export `NyxxPluginState`. ([`#561`](https://github.com/nyxx-discord/nyxx/pull/561)) - ([`c31514b`](https://github.com/nyxx-discord/nyxx/commit/c31514b5c260abf1560b9f866ec9c8fbfa46d8e5)) +- feat: Implement all new features since the start of the rewrite (including premium subscriptions). ([`#562`](https://github.com/nyxx-discord/nyxx/pull/562)) - ([`141e444`](https://github.com/nyxx-discord/nyxx/commit/141e444e46828a40c42092f34018cd23fef35a46)) +- bug: Fix incorrect parsing of `MessageUpdateEvent`s. ([`#564`](https://github.com/nyxx-discord/nyxx/pull/564)) - ([`17dfca5`](https://github.com/nyxx-discord/nyxx/commit/17dfca56346dcf0314e10ca3485ef7abfecaaa46)) +- feat: Add `logger` to plugins and make `name` inferred from `runtimeType` by default. ([`#565`](https://github.com/nyxx-discord/nyxx/pull/565)) - ([`32507f3`](https://github.com/nyxx-discord/nyxx/commit/32507f365461426ab15f175f59337c75cc75d88f)) ## 6.0.0-dev.3 __16.09.2023__ -- rewrite: Interaction responses now throw errors instead of using assertions. -- rewrite: Improved plugin interface with support for plugin state and a simpler API. -- feat: Added constructors to most builders with multiple configurations. -- feat: Added support for authenticating via OAuth2. -- feat: Added `HttpHandler.onRateLimit` for tracking client rate limiting. -- feat: Parse emoji in reaction events. -- feat: Allow specifying `stdout` and `stderr` in `Logging`. -- feat: Add `NyxxRest.user` to get the current user. -- feat: `Attachment` now implements `CdnAsset` for easier fetching. -- bug: Fixed emoji in SelectMenuBuilder not being sent correctly. -- bug: Fixed parsing members in interaction data. -- bug: `DiscordColor` did not allow a value of `0xffffff` (white). -- bug: Fixed parsing role mentions as role objects in messages. +- rewrite: Interaction responses now throw errors instead of using assertions. ([`#529`](https://github.com/nyxx-discord/nyxx/pull/529)) - ([`0108748`](https://github.com/nyxx-discord/nyxx/commit/0108748a044060f5cc7ec8a4252fd12fbbf4dcc4)) +- rewrite: Improved plugin interface with support for plugin state and a simpler API. ([`#545`](https://github.com/nyxx-discord/nyxx/pull/545)) - ([`e2cd7b4`](https://github.com/nyxx-discord/nyxx/commit/e2cd7b413493b6477bb196ec6b273ab1bde0f07b)) +- feat: Added constructors to most builders with multiple configurations. ([`#531`](https://github.com/nyxx-discord/nyxx/pull/531)) - ([`d5fd47d`](https://github.com/nyxx-discord/nyxx/commit/d5fd47d7220e51c71d141b8b43ad57a5d2b776ec)) +- feat: Added support for authenticating via OAuth2. ([`0a41de7`](https://github.com/nyxx-discord/nyxx/commit/0a41de773bdf8073bb9e1b11da2daa04c5d0b73f)) +- feat: Added `HttpHandler.onRateLimit` for tracking client rate limiting. ([`#532`](https://github.com/nyxx-discord/nyxx/pull/532)) - ([`76209f8`](https://github.com/nyxx-discord/nyxx/commit/76209f8c0c4cf267ff96de7f67354625c607bc2a)) +- feat: Parse emoji in reaction events. ([`#533`](https://github.com/nyxx-discord/nyxx/pull/533)) - ([`9d79001`](https://github.com/nyxx-discord/nyxx/commit/9d790014fb7b5c77cb3982253e57d1b96253e898)) +- feat: Allow specifying `stdout` and `stderr` in `Logging`. ([`#549`](https://github.com/nyxx-discord/nyxx/pull/549)) - ([`cf5c17d`](https://github.com/nyxx-discord/nyxx/commit/cf5c17debddd7b021c26ab02a98d5c2c3d550ab7)) +- feat: Add `NyxxRest.user` to get the current user. ([`#551`](https://github.com/nyxx-discord/nyxx/pull/551)) - ([`18bafed`](https://github.com/nyxx-discord/nyxx/commit/18bafed50f24a5028e46845f26418b3b854a902e)) +- feat: `Attachment` now implements `CdnAsset` for easier fetching. ([`#547`](https://github.com/nyxx-discord/nyxx/pull/547)) - ([`e1d7679`](https://github.com/nyxx-discord/nyxx/commit/e1d7679c6e1e650154d778e42e18e23f7fb5b049)) +- bug: Fixed emoji in SelectMenuBuilder not being sent correctly. ([`#528`](https://github.com/nyxx-discord/nyxx/pull/528)) - ([`e4f62ab`](https://github.com/nyxx-discord/nyxx/commit/e4f62ab6187df6c3be9440ab68d8c501b296f31c)) +- bug: Fixed parsing members in interaction data. - +- bug: `DiscordColor` did not allow a value of `0xffffff` (white). ([`#550`](https://github.com/nyxx-discord/nyxx/pull/550)) - ([`e30a8ea`](https://github.com/nyxx-discord/nyxx/commit/e30a8ea9850e8b09b93a3a925e1c5d136e8ccbce)) +- bug: Fixed parsing role mentions as role objects in messages. ([`#552`](https://github.com/nyxx-discord/nyxx/pull/552)) - ([`7410c78`](https://github.com/nyxx-discord/nyxx/commit/7410c78719ebdac8bf89d5cb1706b9d4b31ddd23)) ## 6.0.0-dev.2 __24.08.2023__ -- rewrite: Changed `MessageBuilder.embeds` and `MessageUpdateBuilder.embeds` to use a new `EmbedBuilder` instead of `Embed` objects. -- rewrite: Changed all builders to be mutable. -- rewrite: Implement the interactions & message components API. -- rewrite: `ActivityBuilder` is now exported. -- rewrite: Fixed some typos: `ChannelManager.parseForumChanel` -> `ChannelManager.parseForumChannel` and `chanel` -> `channel` in the docs for `VoiceChannel.videoQualityMode`. -- rewrite: Added wrappers around CDN endpoints and assets. -- feat: Added `Permissions.allPermissions`, the set of permission flags with all permissions. -- feat: Added `HttpHandler.latency`, `HttpHandler.realLatency`, `Gateway.latency` and `Shard.latency` for tracking the client's latency. -- feat: `Flags` now has the `~` and the `^` operators. -- feat: Added `HttpHandler.onRequest` and `HttpHandler.onResponse` streams for tracking HTTP requests and responses. -- bug: Fixed `MessageUpdateEvent`s causing a parsing error. -- bug: Fixed classes creating uncaught async errors when `toString()` was invoked on them. -- bug: Empty caches are no longer stored. -- bug: Fixed stickers causing a parsing error. -- bug: Fixed rate limits not applying correctly when multiple requests were queued. -- bug: Fixed `applyGlobalRatelimit` in `HttpRequest` not doing anything. +- rewrite: Changed `MessageBuilder.embeds` and `MessageUpdateBuilder.embeds` to use a new `EmbedBuilder` instead of `Embed` objects. ([`#525`](https://github.com/nyxx-discord/nyxx/pull/525)) - ([`28e6ab1`](https://github.com/nyxx-discord/nyxx/commit/28e6ab160a1f8cedfe9912c2a2982f2ae3c76b77)) +- rewrite: Changed all builders to be mutable. ([`#524`](https://github.com/nyxx-discord/nyxx/pull/524)) - ([`7b414b8`](https://github.com/nyxx-discord/nyxx/commit/7b414b8e38ef2f0c30186ffbb119cd60a42d42d3)) +- rewrite: Implement the interactions & message components API. ([#694](https://github.com/nyxx-discord/nyxx/pull/694)) - ([59c0b39](https://github.com/nyxx-discord/nyxx/commit/59c0b399dfba170d1303040606d16e707b408420)) +- rewrite: `ActivityBuilder` is now exported. ([`#513`](https://github.com/nyxx-discord/nyxx/pull/513)) - ([`8989abc`](https://github.com/nyxx-discord/nyxx/commit/8989abc69ee6432cd46104cc55da237e0688502b)) +- rewrite: Fixed some typos: `ChannelManager.parseForumChanel` -> `ChannelManager.parseForumChannel` and `chanel` -> `channel` in the docs for `VoiceChannel.videoQualityMode`. ([`#514`](https://github.com/nyxx-discord/nyxx/pull/514)) - ([`1533c76`](https://github.com/nyxx-discord/nyxx/commit/1533c763c62f1fb919fc50ea15483e387378835b)) +- rewrite: Added wrappers around CDN endpoints and assets. ([`#511`](https://github.com/nyxx-discord/nyxx/pull/511)) - ([`05c021c`](https://github.com/nyxx-discord/nyxx/commit/05c021c89037bf33ed7e381984561ac1739d897e)) +- feat: Added `Permissions.allPermissions`, the set of permission flags with all permissions. ([`#522`](https://github.com/nyxx-discord/nyxx/pull/522)) - ([`b4ea059`](https://github.com/nyxx-discord/nyxx/commit/b4ea0595ed6218a3165aa8435a36f696e508c07a)) +- feat: Added `HttpHandler.latency`, `HttpHandler.realLatency`, `Gateway.latency` and `Shard.latency` for tracking the client's latency. ([`#517`](https://github.com/nyxx-discord/nyxx/pull/517)) - ([`fd56dbc`](https://github.com/nyxx-discord/nyxx/commit/fd56dbcd35e77765bebea2ac4de5418198d4b4c0)) +- feat: `Flags` now has the `~` and the `^` operators. ([`#510`](https://github.com/nyxx-discord/nyxx/pull/510)) - ([`ebbf2a9`](https://github.com/nyxx-discord/nyxx/commit/ebbf2a9e5d372e62cf10493eed0e88d8fe4ef70a)) +- feat: Added `HttpHandler.onRequest` and `HttpHandler.onResponse` streams for tracking HTTP requests and responses. ([`#507`](https://github.com/nyxx-discord/nyxx/pull/507)) - ([`f83aa06`](https://github.com/nyxx-discord/nyxx/commit/f83aa06aa49cf8cdb71f6a8844cd2617f446f432)) +- bug: Fixed `MessageUpdateEvent`s causing a parsing error. ([`#521`](https://github.com/nyxx-discord/nyxx/pull/521)) - ([`bce7eaa`](https://github.com/nyxx-discord/nyxx/commit/bce7eaa774fe4b0ad15aa5ca7b07fa5cfbb1386e)) +- bug: Fixed classes creating uncaught async errors when `toString()` was invoked on them. ([`#520`](https://github.com/nyxx-discord/nyxx/pull/520)) - ([`4ad3c76`](https://github.com/nyxx-discord/nyxx/commit/4ad3c76eeb6195ac40daa9093a67303aaa527778)) +- bug: Empty caches are no longer stored. ([`#518`](https://github.com/nyxx-discord/nyxx/pull/518)) - ([`0e96354`](https://github.com/nyxx-discord/nyxx/commit/0e963547b7da649423c5bb734f0985953c2181f2)) +- bug: Fixed stickers causing a parsing error. ([`#509`](https://github.com/nyxx-discord/nyxx/pull/509)) - ([`6ebc790`](https://github.com/nyxx-discord/nyxx/commit/6ebc7907d15334250203cb85dff59c7c72d66697)) +- bug: Fixed rate limits not applying correctly when multiple requests were queued. ([`#507`](https://github.com/nyxx-discord/nyxx/pull/507)) - ([`f83aa06`](https://github.com/nyxx-discord/nyxx/commit/f83aa06aa49cf8cdb71f6a8844cd2617f446f432)) +- bug: Fixed `applyGlobalRatelimit` in `HttpRequest` not doing anything. ([`#507`](https://github.com/nyxx-discord/nyxx/pull/507)) - ([`f83aa06`](https://github.com/nyxx-discord/nyxx/commit/f83aa06aa49cf8cdb71f6a8844cd2617f446f432)) ## 6.0.0-dev.1 __03.07.2023__ -- rewrite: The entire library has been rewritten from the ground up. No pre-`6.0.0-dev.1` code is compatible. +- rewrite: The entire library has been rewritten from the ground up. No pre-`6.0.0-dev.1` code is compatible. - Join our Discord server for updates concerning the migration path and help upgrading. For now, check out the new examples and play around with the rewrite to get a feel for it. ## 5.1.1 __11.08.2023__ -- bug: Error on ThreadMemberUpdateEvent due invalid event deserialization +- bug: Error on ThreadMemberUpdateEvent due invalid event deserialization. ([`c7d1fa5`](https://github.com/nyxx-discord/nyxx/commit/c7d1fa5f482ba9b9b97ab5210789b232b2348256)) ## 5.1.0 __16.06.2023__ -- feature: Support the new unique username system with global display names. -- bug: remove the `!` in the mention string as it has been deprecated. +- feature: Support the new unique username system with global display names. ([`#497`](https://github.com/nyxx-discord/nyxx/pull/497)) - ([`9e7b2e8`](https://github.com/nyxx-discord/nyxx/commit/9e7b2e89d5b29b1bed241dab9f458a2f3526f781)) +- bug: remove the `!` in the mention string as it has been deprecated. ([`#497`](https://github.com/nyxx-discord/nyxx/pull/497)) - ([`9e7b2e8`](https://github.com/nyxx-discord/nyxx/commit/9e7b2e89d5b29b1bed241dab9f458a2f3526f781)) ## 5.0.4 __04.06.2023__ -- bug: Fix invalid casts +- bug: Fix invalid casts. ## 5.0.3 __11.04.2023__ -- bug: Always initialize guild caches +- bug: Always initialize guild caches. ([`b982753`](https://github.com/nyxx-discord/nyxx/commit/b9827532c9d83c2cd99efb5003df1f5cb23ef306)) ## 5.0.2 __08.04.2023__ -- bug: TextChannelBuilder and VoiceChannel builder had rateLimitPerUser and videoQualityMode swapped (#471) -- documentation: Guild members (#470) +- bug: TextChannelBuilder and VoiceChannel builder had rateLimitPerUser and videoQualityMode swapped +- documentation: Guild members ([`#470`](https://github.com/nyxx-discord/nyxx/pull/470)) - ([`e165414`](https://github.com/nyxx-discord/nyxx/commit/e16541442e376a9c017cd1d9b69326fa86319427)) ## 5.0.1 __18.03.2023__ -- documentation: Channel invites (#448) -- bug: Correctly dispose all resources on bot stop (#451) +- documentation: Channel invites ([`#448`](https://github.com/nyxx-discord/nyxx/pull/448)) - ([`f54390d`](https://github.com/nyxx-discord/nyxx/commit/f54390d26099b864c006b39b35663bd8239b3030)) +- bug: Correctly dispose all resources on bot stop ([`#451`](https://github.com/nyxx-discord/nyxx/pull/451)) - ([`3d79b2b`](https://github.com/nyxx-discord/nyxx/commit/3d79b2b4eaf11c608b45a49d5249109a47e447b7)) ## 4.5.1 __19.03.2023__ -- bug: Correctly dispose all resources on bot stop (#451) +- bug: Correctly dispose all resources on bot stop ([`#451`](https://github.com/nyxx-discord/nyxx/pull/451)) - ([`3d79b2b`](https://github.com/nyxx-discord/nyxx/commit/3d79b2b4eaf11c608b45a49d5249109a47e447b7)) ## 5.0.0 __04.03.2023__ -- feature: Add named arguments anywhere we can (#396) -- feature: Make CDN urls more reliable (#373) -- feature: Dispatch raw events (#447) -- feature: Implement missing thread features (#429) -- feature: Add avatar decorations to cdn endpoints (#410) +- feature: Add named arguments anywhere we can ([`#396`](https://github.com/nyxx-discord/nyxx/pull/396)) - ([`3ec4562`](https://github.com/nyxx-discord/nyxx/commit/3ec4562f525fde7acda9c3e89b287576110b73ab)) +- feature: Make CDN urls more reliable ([`#373`](https://github.com/nyxx-discord/nyxx/pull/373)) - ([`1f68eeb`](https://github.com/nyxx-discord/nyxx/commit/1f68eeb6f2aad0ea99de1b41e8edd208fedace96)) +- feature: Dispatch raw events ([`#447`](https://github.com/nyxx-discord/nyxx/pull/447)) - ([`d9b373a`](https://github.com/nyxx-discord/nyxx/commit/d9b373a54e97bc7e0b00ba00bda6e6b94c6fdcf9)) +- feature: Implement missing thread features ([`#433`](https://github.com/nyxx-discord/nyxx/pull/433)) - ([`cdb95a7`](https://github.com/nyxx-discord/nyxx/commit/cdb95a7c876f0f27b908ed51f1a14c9ff8043bb6)) +- feature: Add avatar decorations to cdn endpoints ([`#410`](https://github.com/nyxx-discord/nyxx/pull/410)) - ([`e9256a4`](https://github.com/nyxx-discord/nyxx/commit/e9256a45c09f271afd09b18c851cef7fbaf7ae46)) ## 4.5.0 __18.02.2023__ -- feature: New message types (#431) -- feature: Thread members details (#432) -- feature: Implement guild active threads endpoint (#434) -- feature: Implement missing forum features (#433) -- feature: ETF Encoding (#420) -- feature: ETF encoding stability and payload compression (#421) -- feature: Implement @silent messages (#442) -- feature: Implement newly created and member fields in thread create event (#441) -- feature: Add flags property to member (#437) -- feature: Audit log create event (#436) -- bug: hasMore is optional for guild.fetchActiveThreads() (#443) -- bug: Mirror fix #352 to multipart request (#445) +- feature: New message types ([`#431`](https://github.com/nyxx-discord/nyxx/pull/431)) - ([`3e7d33a`](https://github.com/nyxx-discord/nyxx/commit/3e7d33ad7bf7536cf636da12b35f7413c99dfd30)) +- feature: Thread members details ([`#432`](https://github.com/nyxx-discord/nyxx/pull/432)) - ([`8377c8a`](https://github.com/nyxx-discord/nyxx/commit/8377c8a4159e42851d46314cc5c7ec6eac05aac7)) +- feature: Implement guild active threads endpoint ([`#434`](https://github.com/nyxx-discord/nyxx/pull/434)) - ([`ee5f7f2`](https://github.com/nyxx-discord/nyxx/commit/ee5f7f219b47565149966ce60c654e6be178a430)) +- feature: Implement missing forum features ([`#433`](https://github.com/nyxx-discord/nyxx/pull/433)) - ([`cdb95a7`](https://github.com/nyxx-discord/nyxx/commit/cdb95a7c876f0f27b908ed51f1a14c9ff8043bb6)) +- feature: ETF Encoding ([`#420`](https://github.com/nyxx-discord/nyxx/pull/420)) - ([`c7d3816`](https://github.com/nyxx-discord/nyxx/commit/c7d38160c0a4601dc5a06cc1ecc5df82acb3a819)) +- feature: ETF encoding stability and payload compression ([`#421`](https://github.com/nyxx-discord/nyxx/pull/421)) - ([`440b6cd`](https://github.com/nyxx-discord/nyxx/commit/440b6cd917f191366888674058902fe8631068cb)) +- feature: Implement @silent messages ([`#442`](https://github.com/nyxx-discord/nyxx/pull/442)) - ([`44b172d`](https://github.com/nyxx-discord/nyxx/commit/44b172d215131c3d6f5153d1624058f9b1850526)) +- feature: Implement newly created and member fields in thread create event ([`#441`](https://github.com/nyxx-discord/nyxx/pull/441)) - ([`10c36c5`](https://github.com/nyxx-discord/nyxx/commit/10c36c5facb407ae15e2fd441b55b32b0f630c4a)) +- feature: Add flags property to member ([`#437`](https://github.com/nyxx-discord/nyxx/pull/437)) - ([`1da4c0d`](https://github.com/nyxx-discord/nyxx/commit/1da4c0d0b8a1488e4d1646c8a5060508e72b2185)) +- feature: Audit log create event ([`#436`](https://github.com/nyxx-discord/nyxx/pull/436)) - ([`b55e85d`](https://github.com/nyxx-discord/nyxx/commit/b55e85de22d0b6311e8ba921f56c1acbe40a2aa6)) +- bug: hasMore is optional for guild.fetchActiveThreads() ([`#443`](https://github.com/nyxx-discord/nyxx/pull/443)) - ([`8895684`](https://github.com/nyxx-discord/nyxx/commit/8895684a339363e00057f6b5fbeb6aae8ca978f4)) +- bug: Mirror fix #352 to multipart request ([`#445`](https://github.com/nyxx-discord/nyxx/pull/445)) - ([`7121638`](https://github.com/nyxx-discord/nyxx/commit/7121638968cbad5e686c65034c946b394414415c)) - bug: bug: Fix forum channel available tags deserialization -- bug: Fix update member roles equality (#438) -- documentation: Fix comments and nullability in examples (#416) -- documentation: Add message intent to readme (#428) +- bug: Fix update member roles equality ([`#438`](https://github.com/nyxx-discord/nyxx/pull/438)) - ([`01f29c7`](https://github.com/nyxx-discord/nyxx/commit/01f29c780b96fd1b06085557879af446f1df1517)) +- documentation: Fix comments and nullability in examples ([`#416`](https://github.com/nyxx-discord/nyxx/pull/416)) - ([`24a85ec`](https://github.com/nyxx-discord/nyxx/commit/24a85ec90323c034f4591d7c2dd0afdd32022bd6)) +- documentation: Add message intent to readme ([`#428`](https://github.com/nyxx-discord/nyxx/pull/428)) - ([`e3a6730`](https://github.com/nyxx-discord/nyxx/commit/e3a67305c6e2ea154b58d49565a01dd9a40a5a7b)) ## 5.0.0-dev.2 __26.01.2023__ -- sync with dev branch (up to 4.5.0-dev.0) +- sync with dev branch (up to 4.5.0-dev.0) - ## 4.5.0-dev.0 __26.01.2023__ -- feature: New message types (#431) -- feature: Thread members details (#432) -- feature: Implement guild active threads endpoint (#434) -- feature: Implement missing forum features (#433) -- feature: ETF Encoding (#420) -- feature: ETF encoding stability and payload compression (#421) -- documentation: Fix comments and nullability in examples (#416) -- documentation: Add message intent to readme (#428) +- feature: New message types ([`#431`](https://github.com/nyxx-discord/nyxx/pull/431)) - ([`3e7d33a`](https://github.com/nyxx-discord/nyxx/commit/3e7d33ad7bf7536cf636da12b35f7413c99dfd30)) +- feature: Thread members details ([`#432`](https://github.com/nyxx-discord/nyxx/pull/432)) - ([`8377c8a`](https://github.com/nyxx-discord/nyxx/commit/8377c8a4159e42851d46314cc5c7ec6eac05aac7)) +- feature: Implement guild active threads endpoint ([`#434`](https://github.com/nyxx-discord/nyxx/pull/434)) - ([`ee5f7f2`](https://github.com/nyxx-discord/nyxx/commit/ee5f7f219b47565149966ce60c654e6be178a430)) +- feature: Implement missing forum features ([`#433`](https://github.com/nyxx-discord/nyxx/pull/433)) - ([`cdb95a7`](https://github.com/nyxx-discord/nyxx/commit/cdb95a7c876f0f27b908ed51f1a14c9ff8043bb6)) +- feature: ETF Encoding ([`#420`](https://github.com/nyxx-discord/nyxx/pull/420)) - ([`c7d3816`](https://github.com/nyxx-discord/nyxx/commit/c7d38160c0a4601dc5a06cc1ecc5df82acb3a819)) +- feature: ETF encoding stability and payload compression ([`#421`](https://github.com/nyxx-discord/nyxx/pull/421)) - ([`440b6cd`](https://github.com/nyxx-discord/nyxx/commit/440b6cd917f191366888674058902fe8631068cb)) +- documentation: Fix comments and nullability in examples ([`#416`](https://github.com/nyxx-discord/nyxx/pull/416)) - ([`24a85ec`](https://github.com/nyxx-discord/nyxx/commit/24a85ec90323c034f4591d7c2dd0afdd32022bd6)) +- documentation: Add message intent to readme ([`#428`](https://github.com/nyxx-discord/nyxx/pull/428)) - ([`e3a6730`](https://github.com/nyxx-discord/nyxx/commit/e3a67305c6e2ea154b58d49565a01dd9a40a5a7b)) ## 4.4.1 __22.01.2023__ -- hotfix: Fix ratelimit handling +- hotfix: Fix ratelimit handling ([`#435`](https://github.com/nyxx-discord/nyxx/pull/435)) - ([`80b0ac9`](https://github.com/nyxx-discord/nyxx/commit/80b0ac9856807b074dca55d73e588f873713c702)) ## 4.4.0 __12.12.2022__ -- feature: Improve error handling and logging (#403) +- feature: Improve error handling and logging ([`#403`](https://github.com/nyxx-discord/nyxx/pull/403)) - ([`8b7e67b`](https://github.com/nyxx-discord/nyxx/commit/8b7e67b209dd7c685d3ffae11030342a77afed5f)) - bug: Fix build() for GuildEventBuilder - bug: Update exports ## 4.4.0-dev.0 __20.11.2022__ -- feature: Improve error handling and logging (#403) +- feature: Improve error handling and logging ([`#403`](https://github.com/nyxx-discord/nyxx/pull/403)) - ([`8b7e67b`](https://github.com/nyxx-discord/nyxx/commit/8b7e67b209dd7c685d3ffae11030342a77afed5f)) ## 4.3.0 __19.11.2022__ -- feature: Add retry with backoff to network operations (gateway and http) (#395) -- feature: automoderation regexes (#393) -- feature: add support for interaction webhooks (#397) -- feature: Forward `RetryOptions` -- bug: Fixed bug when getting IInviteWithMeta (#398) -- bug: Emit bot start to plugins only when ready -- bug: fix builder not building when editing a guild member (#405) +- feature: Add retry with backoff to network operations (gateway and http) ([`#395`](https://github.com/nyxx-discord/nyxx/pull/395)) - ([`0c666f1`](https://github.com/nyxx-discord/nyxx/commit/0c666f19c591bda6d15b372711a4345b4a880e90)) +- feature: automoderation regexes ([`#393`](https://github.com/nyxx-discord/nyxx/pull/393)) - ([`6f27937`](https://github.com/nyxx-discord/nyxx/commit/6f279372b28fe35866d0713de1acc5887c46881d)) +- feature: add support for interaction webhooks ([`#397`](https://github.com/nyxx-discord/nyxx/pull/397)) - ([`a255e3a`](https://github.com/nyxx-discord/nyxx/commit/a255e3a67d94c03483a60d3a94315c8f0c932960)) +- feature: Forward `RetryOptions` ([`#402`](https://github.com/nyxx-discord/nyxx/pull/402)) - ([`fb6a902`](https://github.com/nyxx-discord/nyxx/commit/fb6a90254e4de349a6a376102b97ab944943680a)) +- bug: Fixed bug when getting IInviteWithMeta - +- bug: Emit bot start to plugins only when ready ([`#392`](https://github.com/nyxx-discord/nyxx/pull/392)) - ([`8b47436`](https://github.com/nyxx-discord/nyxx/commit/8b47436878f64fbc91469aa5811e9569bf298965)) +- bug: fix builder not building when editing a guild member ([`#405`](https://github.com/nyxx-discord/nyxx/pull/405)) - ([`ffbfadf`](https://github.com/nyxx-discord/nyxx/commit/ffbfadf6142328ab70ca6dbac82ec1d1e14054d3)) ## 5.0.0-dev.1 __15.11.2022__ -- feature: Add named arguments anywhere we can (#396) +- feature: Add named arguments anywhere we can ([`#396`](https://github.com/nyxx-discord/nyxx/pull/396)) - ([`3ec4562`](https://github.com/nyxx-discord/nyxx/commit/3ec4562f525fde7acda9c3e89b287576110b73ab)) This version also includes fixes from 4.2.1 ## 4.3.0-dev.1 __15.11.2022__ -- feature: add support for interaction webhooks (#397) -- bug: Fixed bug when getting IInviteWithMeta (#398) +- feature: add support for interaction webhooks ([`#397`](https://github.com/nyxx-discord/nyxx/pull/397)) - ([`a255e3a`](https://github.com/nyxx-discord/nyxx/commit/a255e3a67d94c03483a60d3a94315c8f0c932960)) This version also includes fixes from 4.2.1 @@ -196,26 +333,25 @@ __15.11.2022__ ## 4.3.0-dev.0 __14.11.2022__ -- feature: Add retry with backoff to network operations (gateway and http) (#395) -- feature: automoderation regexes (#393) -- bug: Emit bot start to plugins only when ready +- feature: Add retry with backoff to network operations (gateway and http) ([`#395`](https://github.com/nyxx-discord/nyxx/pull/395)) - ([`0c666f1`](https://github.com/nyxx-discord/nyxx/commit/0c666f19c591bda6d15b372711a4345b4a880e90)) +- feature: automoderation regexes ([`#393`](https://github.com/nyxx-discord/nyxx/pull/393)) - ([`6f27937`](https://github.com/nyxx-discord/nyxx/commit/6f279372b28fe35866d0713de1acc5887c46881d)) +- bug: Emit bot start to plugins only when ready ([`#392`](https://github.com/nyxx-discord/nyxx/pull/392)) - ([`8b47436`](https://github.com/nyxx-discord/nyxx/commit/8b47436878f64fbc91469aa5811e9569bf298965)) ## 4.2.0 __13.11.2022__ -- feature: missing forum channel features (#387) -- feature: Add `activeDeveloper` flag (#388) -- feature: Add support for new select menus components (#380 -- feature: Prefer using throw over returning Future.error -- bug: Fix null-assert error on shard disposal; don't reconnect shard after disposing -- bug: Cache user when fetching (#384) -- bug: add message content to client (#389) +- feature: missing forum channel features ([`#433`](https://github.com/nyxx-discord/nyxx/pull/433)) - ([`cdb95a7`](https://github.com/nyxx-discord/nyxx/commit/cdb95a7c876f0f27b908ed51f1a14c9ff8043bb6)) +- feature: Add `activeDeveloper` flag ([`#388`](https://github.com/nyxx-discord/nyxx/pull/388)) - ([`d6f692e`](https://github.com/nyxx-discord/nyxx/commit/d6f692eea72566ddcef7549e498de095ff3fc983)) +- feature: Add support for new select menus components feature: Prefer using throw over returning Future.error - +- bug: Fix null-assert error on shard disposal; don't reconnect shard after disposing ([`#386`](https://github.com/nyxx-discord/nyxx/pull/386)) - ([`ac38200`](https://github.com/nyxx-discord/nyxx/commit/ac3820070b00591ae5d137e2bc776db42ef962fa)) +- bug: Cache user when fetching ([`#384`](https://github.com/nyxx-discord/nyxx/pull/384)) - ([`2bca86e`](https://github.com/nyxx-discord/nyxx/commit/2bca86e160ed813c94d981d126f1049009bcbf8d)) +- bug: add message content to client ([`#389`](https://github.com/nyxx-discord/nyxx/pull/389)) - ([`6cb667a`](https://github.com/nyxx-discord/nyxx/commit/6cb667afb117ce606bcfaed685d79b77fc4c1f4e)) ## 4.2.0-dev.0 __11.11.2022__ -- feature: missing forum channel features (#387) -- bug: Cache user when fetching (#384) +- feature: missing forum channel features ([`#433`](https://github.com/nyxx-discord/nyxx/pull/433)) - ([`cdb95a7`](https://github.com/nyxx-discord/nyxx/commit/cdb95a7c876f0f27b908ed51f1a14c9ff8043bb6)) +- bug: Cache user when fetching ([`#384`](https://github.com/nyxx-discord/nyxx/pull/384)) - ([`2bca86e`](https://github.com/nyxx-discord/nyxx/commit/2bca86e160ed813c94d981d126f1049009bcbf8d)) ## 4.1.3 __01.11.2022__ @@ -225,41 +361,41 @@ __01.11.2022__ ## 4.1.2 __30.10.2022__ -- bug: Correctly emit connected event in `ShardManager` +- bug: Correctly emit connected event in `ShardManager` ([`#381`](https://github.com/nyxx-discord/nyxx/pull/381)) - ([`95b7179`](https://github.com/nyxx-discord/nyxx/commit/95b717910d561f2c6e275cf0b71a122a53c98f5c)) ## 4.1.1 __23.10.2022__ -- bug: Fix deserialize the emoji id of the forum tag (#378) +- bug: Fix deserialize the emoji id of the forum tag ([`#378`](https://github.com/nyxx-discord/nyxx/pull/378)) - ([`9a5df7b`](https://github.com/nyxx-discord/nyxx/commit/9a5df7bcb36422f7bfbde4bd1b55ab8e8950b84a)) ## 4.1.0 __25.09.2022__ -- feature: Add `invitesDisabled` feature (#370) -- feature: Add pending for member screening (#371) -- feature: member screening events (#372) -- feature: Cache guild events (#369) -- feature: Refactor internal shard system (#368) -- feature: Event to notify change of connection status (#364) -- feature: feature: auto moderation (#353) -- bug: Fixup shard disconnect event +- feature: Add `invitesDisabled` feature ([`#370`](https://github.com/nyxx-discord/nyxx/pull/370)) - ([`93140c0`](https://github.com/nyxx-discord/nyxx/commit/93140c0ed13630d71c927ebd937561935fdf31b9)) +- feature: Add pending for member screening ([`#371`](https://github.com/nyxx-discord/nyxx/pull/371)) - ([`ef9adef`](https://github.com/nyxx-discord/nyxx/commit/ef9adef7c14e39164eb8a16b3774c837aac9fa3d)) +- feature: member screening events ([`#372`](https://github.com/nyxx-discord/nyxx/pull/372)) - ([`5a5af0e`](https://github.com/nyxx-discord/nyxx/commit/5a5af0e34d57b0a17a9f7bc923b28c3b0b19453c)) +- feature: Cache guild events ([`#369`](https://github.com/nyxx-discord/nyxx/pull/369)) - ([`b5cdb98`](https://github.com/nyxx-discord/nyxx/commit/b5cdb982d8a7b65208c52e788635983a2271b1f0)) +- feature: Refactor internal shard system ([`#368`](https://github.com/nyxx-discord/nyxx/pull/368)) - ([`6ea3bcf`](https://github.com/nyxx-discord/nyxx/commit/6ea3bcf242baf97ae54c4016987b04261c24218a)) +- feature: Event to notify change of connection status ([`#364`](https://github.com/nyxx-discord/nyxx/pull/364)) - ([`e3a35ff`](https://github.com/nyxx-discord/nyxx/commit/e3a35ffebb7fbe9df08234bb3616aad57289bcf6)) +- feature: feature: auto moderation ([`#393`](https://github.com/nyxx-discord/nyxx/pull/393)) - ([`6f27937`](https://github.com/nyxx-discord/nyxx/commit/6f279372b28fe35866d0713de1acc5887c46881d)) +- bug: Fixup shard disconnect event ## 5.0.0-dev.0 __20.09.2022__ -- refactor: Make CDN urls more reliable (#373) +- refactor: Make CDN urls more reliable ([`#373`](https://github.com/nyxx-discord/nyxx/pull/373)) - ([`1f68eeb`](https://github.com/nyxx-discord/nyxx/commit/1f68eeb6f2aad0ea99de1b41e8edd208fedace96)) ## 4.1.0-dev.4 __15.09.2022__ -- feature: Add `invitesDisabled` feature (#370) -- feature: Add pending for member screening (#371) -- feature: member screening events (#372) +- feature: Add `invitesDisabled` feature ([`#370`](https://github.com/nyxx-discord/nyxx/pull/370)) - ([`93140c0`](https://github.com/nyxx-discord/nyxx/commit/93140c0ed13630d71c927ebd937561935fdf31b9)) +- feature: Add pending for member screening ([`#371`](https://github.com/nyxx-discord/nyxx/pull/371)) - ([`ef9adef`](https://github.com/nyxx-discord/nyxx/commit/ef9adef7c14e39164eb8a16b3774c837aac9fa3d)) +- feature: member screening events ([`#372`](https://github.com/nyxx-discord/nyxx/pull/372)) - ([`5a5af0e`](https://github.com/nyxx-discord/nyxx/commit/5a5af0e34d57b0a17a9f7bc923b28c3b0b19453c)) ## 4.1.0-dev.3 __03.09.2022__ -- feature: Cache guild events (#369) +- feature: Cache guild events ([#682](https://github.com/nyxx-discord/nyxx/pull/682)) - ([71b4424](https://github.com/nyxx-discord/nyxx/commit/71b4424c850a94fd8808f901462583c830a70cb5)) ## 4.1.0-dev.2 __28.08.2022__ @@ -269,27 +405,26 @@ __28.08.2022__ ## 4.1.0-dev.1 __28.08.2022__ -- feature: Refactor internal shard system (#368) -- feature: Event to notify change of connection status (#364) +- feature: Refactor internal shard system ([`#368`](https://github.com/nyxx-discord/nyxx/pull/368)) - ([`6ea3bcf`](https://github.com/nyxx-discord/nyxx/commit/6ea3bcf242baf97ae54c4016987b04261c24218a)) +- feature: Event to notify change of connection status ([`#364`](https://github.com/nyxx-discord/nyxx/pull/364)) - ([`e3a35ff`](https://github.com/nyxx-discord/nyxx/commit/e3a35ffebb7fbe9df08234bb3616aad57289bcf6)) ## 4.1.0-dev.0 __20.08.2022__ -- feature: feature: auto moderation (#353) - +- feature: feature: auto moderation ([`#393`](https://github.com/nyxx-discord/nyxx/pull/393)) - ([`6f27937`](https://github.com/nyxx-discord/nyxx/commit/6f279372b28fe35866d0713de1acc5887c46881d)) ## 4.0.0 __29.07.2022__ -- breaking: Fix typo in `IHttpResponseSucess` +- breaking: Fix typo in `IHttpResponseSucess` - breaking: Remove `threeDayThreadArchive` and `sevenDayThreadArchive` guild features -- breaking: Remove all deprecated members +- breaking: Remove all deprecated members - - bug: Fix ratelimiting - breaking: All calls to the API are now made via `IHttpRoute`s instead of `String`s. - Construct routes by creating an `IHttpRoute()` and `add`ing `HttpRoutePart`s or by calling the helper methods on the route. -- feature: Move to Gateway & API v10 +- feature: Move to Gateway & API v10 ([`#325`](https://github.com/nyxx-discord/nyxx/pull/325)) - ([`c134c64`](https://github.com/nyxx-discord/nyxx/commit/c134c645e6de4a9164e177ae15abb2e3502eecf6)) - Added the Message Content privileged intent - feature: Add guild Audit Log options -- feature: Implement forum channels +- feature: Implement forum channels ([`#332`](https://github.com/nyxx-discord/nyxx/pull/332)) - ([`4f58d70`](https://github.com/nyxx-discord/nyxx/commit/4f58d70032d33144556489580136afabbb18bfb0)) - feature: Implement guild Welcome Screen & Channel - feature: Add missing Audit log types - feature: Implement guild Banners @@ -298,44 +433,44 @@ __29.07.2022__ - feature: Add missing reaction endpoints - feature: Handle websocket disconnections - feature: Implement clean client shutdown -- feature: Add `limitLength` to `MessageBuilder` -- feature: Add paginated bans -- feature: Remove dollar prefix for identify payload (#361) -- bug: Fix mention string, and use a better approach to retrieve everyone role (#360) +- feature: Add `limitLength` to `MessageBuilder` ([`#356`](https://github.com/nyxx-discord/nyxx/pull/356)) - ([`763b054`](https://github.com/nyxx-discord/nyxx/commit/763b054a284984fa5f84e794f4d093be04d8154f)) +- feature: Add paginated bans ([`#326`](https://github.com/nyxx-discord/nyxx/pull/326)) - ([`6ebe590`](https://github.com/nyxx-discord/nyxx/commit/6ebe590a5e37928d18723a14ef2cc86b291a47c1)) +- feature: Remove dollar prefix for identify payload ([`#361`](https://github.com/nyxx-discord/nyxx/pull/361)) - ([`362adcb`](https://github.com/nyxx-discord/nyxx/commit/362adcb14b08a310d2ed43bca8178ebe74a77f1d)) +- bug: Fix mention string, and use a better approach to retrieve everyone role - - bug: Fix incorrect guild URLs - bug: Fix incorrect file encoding - bug: Fix member editing -- bug: Fix serialization issues +- bug: Fix serialization issues ([`#351`](https://github.com/nyxx-discord/nyxx/pull/351)) - ([`405cffb`](https://github.com/nyxx-discord/nyxx/commit/405cffb313a1fcb54c372e6f53158a09e442af66)) - bug: Fix uninitialized fields ## 4.0.0-dev.2 __12.06.2022__ -- feature: Add missing emoji endpoints (#346) -- feature: Add `threadName` on `IWebhook#execute()` (#348) -- feature: Implement graceful shutdown (#347) -- feature: Implement forum channels (#332) -- feature: Implement Dynamic Bucket Rate Limits (#316) -- feature: Implement paginated bans (#326) +- feature: Add missing emoji endpoints ([`#346`](https://github.com/nyxx-discord/nyxx/pull/346)) - ([`b9afdeb`](https://github.com/nyxx-discord/nyxx/commit/b9afdeb03a92137199b451b46550420ab8fa7a7a)) +- feature: Add `threadName` on `IWebhook#execute()` ([`#348`](https://github.com/nyxx-discord/nyxx/pull/348)) - ([`cfc7820`](https://github.com/nyxx-discord/nyxx/commit/cfc78201954c639378645dd7dacc4de151996edf)) +- feature: Implement graceful shutdown ([`#347`](https://github.com/nyxx-discord/nyxx/pull/347)) - ([`6444aae`](https://github.com/nyxx-discord/nyxx/commit/6444aaeb13f47b9ad9578b2545db3635581f5d4d)) +- feature: Implement forum channels ([`#332`](https://github.com/nyxx-discord/nyxx/pull/332)) - ([`4f58d70`](https://github.com/nyxx-discord/nyxx/commit/4f58d70032d33144556489580136afabbb18bfb0)) +- feature: Implement Dynamic Bucket Rate Limits ([`#316`](https://github.com/nyxx-discord/nyxx/pull/316)) - ([`866fa57`](https://github.com/nyxx-discord/nyxx/commit/866fa57f18e0a6ac2bf00ec2ed928236c10b759c)) +- feature: Implement paginated bans ([`#326`](https://github.com/nyxx-discord/nyxx/pull/326)) - ([`6ebe590`](https://github.com/nyxx-discord/nyxx/commit/6ebe590a5e37928d18723a14ef2cc86b291a47c1)) - feature: Implement missing guild properties - bug: Fixed disconnecting user from voice -- bug: failed to edit guild members (#328) -- bug: Invalid serialization of query params (#352) -- bug: Fix some serialization bugs (#351) +- bug: failed to edit guild members ([`#328`](https://github.com/nyxx-discord/nyxx/pull/328)) - ([`e242892`](https://github.com/nyxx-discord/nyxx/commit/e242892d686de30d155358273639107dd75335af)) +- bug: Invalid serialization of query params ([`#352`](https://github.com/nyxx-discord/nyxx/pull/352)) - ([`ed867d6`](https://github.com/nyxx-discord/nyxx/commit/ed867d665778010faa6d3705333ecc52367ec14a)) +- bug: Fix some serialization bugs ([`#351`](https://github.com/nyxx-discord/nyxx/pull/351)) - ([`405cffb`](https://github.com/nyxx-discord/nyxx/commit/405cffb313a1fcb54c372e6f53158a09e442af66)) ## 4.0.0-dev.1 __09.05.2022__ -- feature: Handle no internet on websocket (#321) -- bug: Remove Error form IHttpResponseError (#324) +- feature: Handle no internet on websocket +- bug: Remove Error form IHttpResponseError ([`#324`](https://github.com/nyxx-discord/nyxx/pull/324)) - ([`fc438e4`](https://github.com/nyxx-discord/nyxx/commit/fc438e4468bef1363ca1f6d94fbaefdfe92f36dc)) - Fixup field names on IHttpResponseError - Fixup IHttpResponseSuccess name -- feature: Move to API v10 (#325) +- feature: Move to API v10 ([`#325`](https://github.com/nyxx-discord/nyxx/pull/325)) - ([`c134c64`](https://github.com/nyxx-discord/nyxx/commit/c134c645e6de4a9164e177ae15abb2e3502eecf6)) ## 4.0.0-dev.0 __31.03.2022__ -- feature: Fix target id property and add guild audit logs options (#307) +- feature: Fix target id property and add guild audit logs options ([`#307`](https://github.com/nyxx-discord/nyxx/pull/307)) - ([`5683012`](https://github.com/nyxx-discord/nyxx/commit/56830125c79666a5e2d31588a175b8043075e609)) ## 3.4.2 __22.04.2022__ @@ -345,41 +480,41 @@ __22.04.2022__ ## 3.4.1 __10.04.2022__ -- bug: bugfix: failed to edit guild members (#328) +- bug: bugfix: failed to edit guild members ## 3.4.0 __09.04.2022__ -- feature: Add `@bannerUrl()` method (#318) -- feature: Implement paginated bans (#326) +- feature: Add `@bannerUrl()` method +- feature: Implement paginated bans ## 3.3.1 __30.03.2022__ -- bug: Fix member not being initialized in IMessage (#315) +- bug: Fix member not being initialized in IMessage ## 3.3.0 __15.03.2022__ -- feature: Guild emoji improvements (#305) +- feature: Guild emoji improvements - Added missing properties on `IBaseGuildEmoji`. - Partial emoji can be now resolved to it's full instance with `resolve()` method - Author of emoji can be now resolved with `fetchCreator()` -- feature: Allow editing messages to remove content (#313) -- feature: Add previous state to *UpdateEvents (#311) -- bug: fix: initialize name and format values for PartialSticker (#308) -- bug: Make IHttpResponseError subclass Exception (#303) -- bug: Update documentation (#302) +- feature: Allow editing messages to remove content +- feature: Add previous state to *UpdateEvents +- bug: fix: initialize name and format values for PartialSticker +- bug: Make IHttpResponseError subclass Exception +- bug: Update documentation ## 3.3.0-dev.1 __05.03.2022__ -- feature: Guild emoji improvements (#305) +- feature: Guild emoji improvements - Added missing properties on `IBaseGuildEmoji`. - Partial emoji can be now resolved to it's full instance with `resolve()` method - Author of emoji can be now resolved with `fetchCreator()` -- bug: Make IHttpResponseError subclass Exception (#303) -- bug: Update documentation (#302) +- bug: Make IHttpResponseError subclass Exception +- bug: Update documentation ## 3.3.0-dev.0 __08.02.2022__ @@ -412,13 +547,13 @@ __23.01.2022__ ## 3.2.3 __10.01.2022__ -- Fixup invalid formatting of emoji in BaseGuildEmoji.formatForMessage (#286) +- Fixup invalid formatting of emoji in BaseGuildEmoji.formatForMessage ## 3.2.2 __08.01.2022__ -- Fix message edit behavior (#283) -- Fix `addEmbed` behavior on message builder (#284) +- Fix message edit behavior +- Fix `addEmbed` behavior on message builder ## 3.2.1 __01.01.2022__ @@ -428,9 +563,9 @@ __01.01.2022__ ## 3.2.0 __31.12.2021__ -- Add missing ActivityTypes (#275) -- Fix deserialization of presence update event (#277) -- Implement voice channel region (#278) +- Add missing ActivityTypes +- Fix deserialization of presence update event +- Implement voice channel region ## 3.1.1 __29.12.2021__ @@ -440,21 +575,21 @@ __29.12.2021__ ## 3.1.0 __28.12.2021__ -- Implement patches needed for external sharding feature (#266) -- Implement boost progress bar (#266) -- Implement timeouts (#267) +- Implement patches needed for external sharding feature +- Implement boost progress bar +- Implement timeouts - deprecation of edit method parameters in favor of `MemberBuilder` class. In next major release all parameters except `builder` and `auditReason` will be removed -- Fix incorrectly initialised onDmReceived and onSelfMention streams (#270) +- Fix incorrectly initialised onDmReceived and onSelfMention streams ## 3.0.1 __21.12.2021__ -- Fix CliItegration plugin not working with IgnoreExceptions (#256) -- Use logger instead of print (#259) -- Fix typo in file name (#260) -- Nullable close code (#261) -- Missing ActivityBuilder (#262) +- Fix CliItegration plugin not working with IgnoreExceptions +- Use logger instead of print +- Fix typo in file name +- Nullable close code +- Missing ActivityBuilder ## 3.0.0 __19.12.2021__ @@ -536,8 +671,8 @@ __02.11.2021__ ## 2.1.0 __22.10.2021__ -- Add pending to member (#224) -- use case-insensitive name comparison in _registerCommandHandlers (#225) +- Add pending to member +- use case-insensitive name comparison in _registerCommandHandlers ## 2.0.5 _15.10.2021_ diff --git a/README.md b/README.md index 21947bbf4..7df5b0574 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A complete, robust and efficient wrapper around Discord's API for bots & applications. -To get started using nyxx, follow our [getting started guide](https://nyxx.l7ssha.xyz/docs/guides/writing_your_first_bot) to write your first bot. +To get started using nyxx, follow our [getting started guide](https://nyxx.l7ssha.xyz/docs/tutorials/writing_your_first_bot) to write your first bot. If you're already familiar with Discord's API, here's a quick example to get you started: ```dart @@ -31,9 +31,8 @@ void main() async { ## Other nyxx packages - [nyxx_commands](https://pub.dev/packages/nyxx_commands): A command framework for handling both simple & complex commands. -- [nyxx_pagination](https://pub.dev/packages/nyxx_pagination): Pagination support for nyxx. +- [nyxx_extensions](https://pub.dev/packages/nyxx_extensions): Pagination, emoji utilities and other miscellaneous helpers for developing bots using nyxx. - [nyxx_lavalink](https://pub.dev/packages/nyxx_lavalink): Lavalink support for playing audio in voice channels. -- [nyxx_extensions](https://pub.dev/packages/nyxx_extensions): Miscellaneous helpers for common situations when developing bots. ## More examples diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 000000000..991b6e67b --- /dev/null +++ b/dartdoc_options.yaml @@ -0,0 +1,4 @@ +dartdoc: + linkToSource: + root: . + uriTemplate: 'https://github.com/nyxx-discord/nyxx/blob/main/%f%#L%l%' diff --git a/example/simple_command.dart b/example/simple_command.dart index d112c6631..6b103c0f7 100644 --- a/example/simple_command.dart +++ b/example/simple_command.dart @@ -28,7 +28,7 @@ void main() async { // we received. await event.message.channel.sendMessage(MessageBuilder( content: 'Pong!', - replyId: event.message.id, + referencedMessage: MessageReferenceBuilder.reply(messageId: event.message.id), )); } }); diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 3c6582746..bc5cef62f 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -13,7 +13,10 @@ export 'src/errors.dart' OutOfRemainingSessionsError, IntegrationNotFoundException, AlreadyAcknowledgedError, - AlreadyRespondedError; + AlreadyRespondedError, + PluginError, + ClientClosedError, + SkuNotFoundException; export 'src/builders/builder.dart' show Builder, CreateBuilder, UpdateBuilder; export 'src/builders/image.dart' show ImageBuilder; @@ -21,7 +24,7 @@ export 'src/builders/user.dart' show UserUpdateBuilder; export 'src/builders/permission_overwrite.dart' show PermissionOverwriteBuilder; export 'src/builders/channel/channel_position.dart' show ChannelPositionBuilder; export 'src/builders/channel/forum_tag.dart' show ForumTagBuilder; -export 'src/builders/channel/group_dm.dart' show GroupDmUpdateBuilder; +export 'src/builders/channel/group_dm.dart' show GroupDmUpdateBuilder, DmRecipientBuilder; export 'src/builders/channel/guild_channel.dart' show ForumChannelUpdateBuilder, @@ -43,22 +46,25 @@ export 'src/builders/channel/thread.dart' show ThreadUpdateBuilder, ForumThreadB export 'src/builders/message/allowed_mentions.dart' show AllowedMentions; export 'src/builders/message/attachment.dart' show AttachmentBuilder; export 'src/builders/message/embed.dart' show EmbedBuilder, EmbedAuthorBuilder, EmbedFieldBuilder, EmbedFooterBuilder, EmbedImageBuilder, EmbedThumbnailBuilder; -export 'src/builders/message/message.dart' show MessageBuilder, MessageUpdateBuilder; +export 'src/builders/message/message.dart' show MessageBuilder, MessageUpdateBuilder, MessageReferenceBuilder; export 'src/builders/message/component.dart' - show ActionRowBuilder, ButtonBuilder, MessageComponentBuilder, SelectMenuBuilder, SelectMenuOptionBuilder, TextInputBuilder; + show ActionRowBuilder, ButtonBuilder, MessageComponentBuilder, SelectMenuBuilder, SelectMenuOptionBuilder, TextInputBuilder, DefaultValue; +export 'src/builders/message/poll.dart' show PollAnswerBuilder, PollBuilder, PollMediaBuilder; export 'src/builders/webhook.dart' show WebhookBuilder, WebhookUpdateBuilder; export 'src/builders/guild/guild.dart' show GuildBuilder, GuildUpdateBuilder; export 'src/builders/guild/member.dart' show CurrentMemberUpdateBuilder, MemberBuilder, MemberUpdateBuilder; export 'src/builders/guild/welcome_screen.dart' show WelcomeScreenUpdateBuilder; export 'src/builders/guild/widget.dart' show WidgetSettingsUpdateBuilder; -export 'src/builders/guild/scheduled_event.dart' show ScheduledEventBuilder, ScheduledEventUpdateBuilder; +export 'src/builders/guild/scheduled_event.dart' show ScheduledEventBuilder, ScheduledEventUpdateBuilder, RecurrenceRuleBuilder; export 'src/builders/guild/template.dart' show GuildTemplateBuilder, GuildTemplateUpdateBuilder; -export 'src/builders/guild/auto_moderation.dart' show AutoModerationRuleBuilder, AutoModerationRuleUpdateBuilder; +export 'src/builders/guild/auto_moderation.dart' + show AutoModerationRuleBuilder, AutoModerationRuleUpdateBuilder, ActionMetadataBuilder, AutoModerationActionBuilder, TriggerMetadataBuilder; +export 'src/builders/guild/onboarding.dart' show OnboardingPromptBuilder, OnboardingPromptOptionBuilder, OnboardingUpdateBuilder; export 'src/builders/role.dart' show RoleBuilder, RoleUpdateBuilder; export 'src/builders/voice.dart' show CurrentUserVoiceStateUpdateBuilder, VoiceStateUpdateBuilder, GatewayVoiceStateBuilder; export 'src/builders/presence.dart' show PresenceBuilder, CurrentUserStatus, ActivityBuilder; export 'src/builders/application_role_connection.dart' show ApplicationRoleConnectionUpdateBuilder; -export 'src/builders/emoji/emoji.dart' show EmojiBuilder, EmojiUpdateBuilder; +export 'src/builders/emoji/emoji.dart' show EmojiBuilder, EmojiUpdateBuilder, ApplicationEmojiBuilder, ApplicationEmojiUpdateBuilder; export 'src/builders/emoji/reaction.dart' show ReactionBuilder; export 'src/builders/invite.dart' show InviteBuilder; export 'src/builders/sticker.dart' show StickerBuilder, StickerUpdateBuilder; @@ -66,9 +72,9 @@ export 'src/builders/application_command.dart' show ApplicationCommandBuilder, ApplicationCommandUpdateBuilder, CommandOptionBuilder, CommandOptionChoiceBuilder; export 'src/builders/interaction_response.dart' show InteractionResponseBuilder, ModalBuilder, InteractionCallbackType; export 'src/builders/entitlement.dart' show TestEntitlementBuilder, TestEntitlementType; -export 'src/builders/application.dart' show ApplicationUpdateBuilder; +export 'src/builders/application.dart' show ApplicationUpdateBuilder, ApplicationIntegrationTypeConfigurationBuilder; -export 'src/cache/cache.dart' show Cache, CacheConfig; +export 'src/cache/cache.dart' show Cache, CacheConfig, CacheManager; export 'src/http/bucket.dart' show HttpBucket; export 'src/http/handler.dart' show HttpHandler, Oauth2HttpHandler, RateLimitInfo; @@ -92,15 +98,20 @@ export 'src/http/managers/gateway_manager.dart' show GatewayManager; export 'src/http/managers/scheduled_event_manager.dart' show ScheduledEventManager; export 'src/http/managers/auto_moderation_manager.dart' show AutoModerationManager; export 'src/http/managers/integration_manager.dart' show IntegrationManager; -export 'src/http/managers/emoji_manager.dart' show EmojiManager; +export 'src/http/managers/emoji_manager.dart' show EmojiManager, ApplicationEmojiManager, GuildEmojiManager; export 'src/http/managers/audit_log_manager.dart' show AuditLogManager; export 'src/http/managers/sticker_manager.dart' show GuildStickerManager, GlobalStickerManager; export 'src/http/managers/application_command_manager.dart' show ApplicationCommandManager, GlobalApplicationCommandManager, GuildApplicationCommandManager; export 'src/http/managers/interaction_manager.dart' show InteractionManager; export 'src/http/managers/entitlement_manager.dart' show EntitlementManager; +export 'src/http/managers/sku_manager.dart' show SkuManager; +export 'src/http/managers/subscription_manager.dart' show SubscriptionManager; export 'src/gateway/gateway.dart' show Gateway; -export 'src/gateway/message.dart' show Disconnecting, Dispose, ErrorReceived, EventReceived, GatewayMessage, Send, ShardData, ShardMessage; +export 'src/gateway/event_parser.dart' show EventParser; +export 'src/gateway/shard_runner.dart' show ShardRunner, ShardConnection; +export 'src/gateway/message.dart' + show Disconnecting, Dispose, ErrorReceived, EventReceived, GatewayMessage, Send, Sent, ShardData, ShardMessage, Identify, RequestingIdentify, StartShard; export 'src/gateway/shard.dart' show Shard; export 'src/models/discord_color.dart' show DiscordColor; @@ -139,10 +150,22 @@ export 'src/models/message/activity.dart' show MessageActivity, MessageActivityT export 'src/models/message/attachment.dart' show Attachment, AttachmentFlags; export 'src/models/message/author.dart' show MessageAuthor; export 'src/models/message/channel_mention.dart' show ChannelMention; -export 'src/models/message/embed.dart' show Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedProvider, EmbedThumbnail, EmbedVideo; -export 'src/models/message/message.dart' show Message, MessageFlags, PartialMessage, MessageType, MessageInteraction; +export 'src/models/message/embed.dart' show Embed, EmbedAuthor, EmbedField, EmbedFooter, EmbedImage, EmbedProvider, EmbedThumbnail, EmbedVideo, EmbedType; + +export 'src/models/message/message.dart' + show + Message, + MessageFlags, + PartialMessage, + MessageType, + // ignore: deprecated_member_use_from_same_package + MessageInteraction, + MessageInteractionMetadata, + MessageSnapshot, + MessageCall; +export 'src/models/message/poll.dart' show Poll, PollAnswer, PollAnswerCount, PollMedia, PollResults, PollLayoutType; export 'src/models/message/reaction.dart' show Reaction, ReactionCountDetails; -export 'src/models/message/reference.dart' show MessageReference; +export 'src/models/message/reference.dart' show MessageReference, MessageReferenceType; export 'src/models/message/role_subscription_data.dart' show RoleSubscriptionData; export 'src/models/message/component.dart' show @@ -151,14 +174,16 @@ export 'src/models/message/component.dart' MessageComponent, SelectMenuComponent, SelectMenuOption, + SelectMenuDefaultValue, + SelectMenuDefaultValueType, TextInputComponent, ButtonStyle, MessageComponentType, TextInputStyle; -export 'src/models/invite/invite.dart' show Invite, TargetType; +export 'src/models/invite/invite.dart' show Invite, TargetType, InviteType; export 'src/models/invite/invite_metadata.dart' show InviteWithMetadata; export 'src/models/webhook.dart' show PartialWebhook, Webhook, WebhookType, WebhookAuthor; -export 'src/models/guild/ban.dart' show Ban; +export 'src/models/guild/ban.dart' show Ban, BulkBanResponse; export 'src/models/guild/guild_preview.dart' show GuildPreview; export 'src/models/guild/guild_widget.dart' show GuildWidget, WidgetSettings, WidgetImageStyle; export 'src/models/guild/guild.dart' @@ -172,15 +197,36 @@ export 'src/models/guild/guild.dart' MfaLevel, NsfwLevel, PremiumTier, - VerificationLevel; + VerificationLevel, + UserGuild; export 'src/models/guild/integration.dart' show PartialIntegration, Integration, IntegrationAccount, IntegrationApplication, IntegrationExpireBehavior; export 'src/models/guild/member.dart' show Member, MemberFlags, PartialMember; -export 'src/models/guild/onboarding.dart' show Onboarding, OnboardingPrompt, OnboardingPromptOption, OnboardingPromptType; +export 'src/models/guild/onboarding.dart' show Onboarding, OnboardingPrompt, OnboardingPromptOption, OnboardingPromptType, OnboardingMode; export 'src/models/guild/welcome_screen.dart' show WelcomeScreen, WelcomeScreenChannel; -export 'src/models/guild/scheduled_event.dart' show EntityMetadata, PartialScheduledEvent, ScheduledEvent, ScheduledEventUser, EventStatus, ScheduledEntityType; +export 'src/models/guild/scheduled_event.dart' + show + EntityMetadata, + PartialScheduledEvent, + ScheduledEvent, + ScheduledEventUser, + EventStatus, + ScheduledEntityType, + RecurrenceRule, + RecurrenceRuleFrequency, + RecurrenceRuleMonth, + RecurrenceRuleNWeekday, + RecurrenceRuleWeekday; export 'src/models/guild/audit_log.dart' show AuditLogChange, AuditLogEntry, AuditLogEntryInfo, PartialAuditLogEntry, AuditLogEvent; export 'src/models/application.dart' - show Application, ApplicationFlags, InstallationParameters, PartialApplication, ApplicationRoleConnectionMetadata, ConnectionMetadataType; + show + Application, + ApplicationFlags, + InstallationParameters, + PartialApplication, + ApplicationRoleConnectionMetadata, + ConnectionMetadataType, + ApplicationIntegrationType, + ApplicationIntegrationTypeConfiguration; export 'src/models/guild/template.dart' show GuildTemplate; export 'src/models/guild/auto_moderation.dart' show @@ -260,7 +306,9 @@ export 'src/models/gateway/events/message.dart' MessageReactionRemoveAllEvent, MessageReactionRemoveEmojiEvent, MessageReactionRemoveEvent, - MessageUpdateEvent; + MessageUpdateEvent, + MessagePollVoteAddEvent, + MessagePollVoteRemoveEvent; export 'src/models/gateway/events/presence.dart' show PresenceUpdateEvent, TypingStartEvent, UserUpdateEvent; export 'src/models/gateway/events/ready.dart' show ReadyEvent, ResumedEvent; export 'src/models/gateway/events/stage_instance.dart' show StageInstanceCreateEvent, StageInstanceDeleteEvent, StageInstanceUpdateEvent; @@ -293,11 +341,15 @@ export 'src/models/interaction.dart' ApplicationCommandInteraction, MessageComponentInteraction, ModalSubmitInteraction, - PingInteraction; + PingInteraction, + InteractionContextType; export 'src/models/entitlement.dart' show Entitlement, PartialEntitlement, EntitlementType; -export 'src/models/sku.dart' show Sku, SkuType; +export 'src/models/sku.dart' show Sku, SkuType, SkuFlags, PartialSku; +export 'src/models/oauth2.dart' show OAuth2Information; +export 'src/models/subscription.dart' show PartialSubscription, Subscription, SubscriptionStatus; export 'src/utils/flags.dart' show Flag, Flags; +export 'src/utils/enum_like.dart' show EnumLike; export 'src/intents.dart' show GatewayIntents; export 'src/plugin/plugin.dart' show NyxxPlugin, NyxxPluginState; @@ -316,3 +368,4 @@ export 'package:http/http.dart' StreamedResponse; export 'package:logging/logging.dart' show Logger, Level; export 'package:runtime_type/runtime_type.dart' show RuntimeType; +export 'package:oauth2/oauth2.dart' show Credentials; diff --git a/lib/src/api_options.dart b/lib/src/api_options.dart index 8938a6075..ae8cf7c3f 100644 --- a/lib/src/api_options.dart +++ b/lib/src/api_options.dart @@ -6,13 +6,13 @@ import 'package:oauth2/oauth2.dart'; /// Options for connecting to the Discord API. abstract class ApiOptions { /// The version of nyxx used in [defaultUserAgent]. - static const nyxxVersion = '6.0.0'; + static const nyxxVersion = '6.4.3'; /// The URL to the nyxx repository used in [defaultUserAgent]. static const nyxxRepositoryUrl = 'https://github.com/nyxx-discord/nyxx'; /// The default value for the `User-Agent` header for bots made with nyxx. - static const defaultUserAgent = 'Nyxx ($nyxxRepositoryUrl, $nyxxVersion)'; + static const defaultUserAgent = 'DiscordBot ($nyxxRepositoryUrl, $nyxxVersion)'; /// The host at which the API can be found. /// diff --git a/lib/src/builders/application.dart b/lib/src/builders/application.dart index 03fc247d4..268046f96 100644 --- a/lib/src/builders/application.dart +++ b/lib/src/builders/application.dart @@ -4,6 +4,22 @@ import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/utils/flags.dart'; +class ApplicationIntegrationTypeConfigurationBuilder extends CreateBuilder { + /// Install params for each installation context's default in-app authorization link. + final InstallationParameters? oauth2InstallParameters; + + ApplicationIntegrationTypeConfigurationBuilder({this.oauth2InstallParameters}); + + @override + Map build() => { + if (oauth2InstallParameters != null) + 'oauth2_install_params': { + 'scopes': oauth2InstallParameters!.scopes, + 'permissions': oauth2InstallParameters!.permissions.value.toString(), + }, + }; +} + class ApplicationUpdateBuilder extends UpdateBuilder { Uri? customInstallUrl; @@ -23,31 +39,39 @@ class ApplicationUpdateBuilder extends UpdateBuilder { List? tags; + Map? integrationTypesConfig; + ApplicationUpdateBuilder({ this.customInstallUrl, this.description, - this.roleConnectionsVerificationUrl, + this.roleConnectionsVerificationUrl = sentinelUri, this.installationParameters, this.flags, this.icon = sentinelImageBuilder, this.coverImage = sentinelImageBuilder, - this.interactionsEndpointUrl, + this.interactionsEndpointUrl = sentinelUri, this.tags, + this.integrationTypesConfig, }); @override Map build() => { if (customInstallUrl != null) 'custom_install_url': customInstallUrl!.toString(), if (description != null) 'description': description, - if (roleConnectionsVerificationUrl != null) 'role_connections_verification_url': roleConnectionsVerificationUrl!.toString(), + if (!identical(roleConnectionsVerificationUrl, sentinelUri)) 'role_connections_verification_url': roleConnectionsVerificationUrl?.toString(), if (installationParameters != null) 'install_params': { 'scopes': installationParameters!.scopes, - 'permissions': installationParameters!.permissions.toString(), + 'permissions': installationParameters!.permissions.value.toString(), + }, + if (integrationTypesConfig != null) + 'integration_types_config': { + for (final MapEntry(:key, :value) in integrationTypesConfig!.entries) key.value.toString(): value.build(), }, if (flags != null) 'flags': flags!.value, if (!identical(icon, sentinelImageBuilder)) 'icon': icon?.buildDataString(), if (!identical(coverImage, sentinelImageBuilder)) 'cover_image': coverImage?.buildDataString(), + if (!identical(interactionsEndpointUrl, sentinelUri)) 'interactions_endpoint_url': interactionsEndpointUrl?.toString(), if (tags != null) 'tags': tags, }; } diff --git a/lib/src/builders/application_command.dart b/lib/src/builders/application_command.dart index 8687a06a0..86a067c7b 100644 --- a/lib/src/builders/application_command.dart +++ b/lib/src/builders/application_command.dart @@ -1,8 +1,10 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/builders/sentinels.dart'; +import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/commands/application_command_option.dart'; +import 'package:nyxx/src/models/interaction.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/utils/flags.dart'; @@ -20,12 +22,19 @@ class ApplicationCommandBuilder extends CreateBuilder { Flags? defaultMemberPermissions; + @Deprecated('Use `contexts`') bool? hasDmPermission; ApplicationCommandType type; bool? isNsfw; + /// Installation context(s) where the command is available, only for globally-scoped commands. Defaults to [ApplicationIntegrationType.guildInstall]. + List? integrationTypes; + + /// Interaction context(s) where the command can be used, only for globally-scoped commands. By default, all interaction context types included. + List? contexts; + ApplicationCommandBuilder({ required this.name, required this.type, @@ -36,6 +45,8 @@ class ApplicationCommandBuilder extends CreateBuilder { this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }); ApplicationCommandBuilder.chatInput({ @@ -47,6 +58,8 @@ class ApplicationCommandBuilder extends CreateBuilder { this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }) : type = ApplicationCommandType.chatInput; ApplicationCommandBuilder.message({ @@ -55,6 +68,8 @@ class ApplicationCommandBuilder extends CreateBuilder { this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }) : type = ApplicationCommandType.message, description = null, descriptionLocalizations = null, @@ -66,6 +81,8 @@ class ApplicationCommandBuilder extends CreateBuilder { this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }) : type = ApplicationCommandType.user, description = null, descriptionLocalizations = null, @@ -80,9 +97,12 @@ class ApplicationCommandBuilder extends CreateBuilder { 'description_localizations': {for (final MapEntry(:key, :value) in descriptionLocalizations!.entries) key.identifier: value}, if (options != null) 'options': options!.map((e) => e.build()).toList(), if (defaultMemberPermissions != null) 'default_member_permissions': defaultMemberPermissions!.value.toString(), + // ignore: deprecated_member_use_from_same_package if (hasDmPermission != null) 'dm_permission': hasDmPermission, 'type': type.value, if (isNsfw != null) 'nsfw': isNsfw, + if (integrationTypes != null) 'integration_types': integrationTypes!.map((type) => type.value).toList(), + if (contexts != null) 'contexts': contexts!.map((type) => type.value).toList(), }; } @@ -99,10 +119,17 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder Flags? defaultMemberPermissions; + @Deprecated('Use `contexts`') bool? hasDmPermission; bool? isNsfw; + /// Installation context(s) where the command is available, only for globally-scoped commands. Defaults to [ApplicationIntegrationType.guildInstall]. + List? integrationTypes; + + /// Interaction context(s) where the command can be used, only for globally-scoped commands. By default, all interaction context types included. + List? contexts; + ApplicationCommandUpdateBuilder({ this.name, this.nameLocalizations = sentinelMap, @@ -112,6 +139,8 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder this.defaultMemberPermissions = sentinelFlags, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }); ApplicationCommandUpdateBuilder.chatInput({ @@ -123,6 +152,8 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }); ApplicationCommandUpdateBuilder.message({ @@ -131,6 +162,8 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }) : description = null, descriptionLocalizations = null, options = null; @@ -141,6 +174,8 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder this.defaultMemberPermissions, this.hasDmPermission, this.isNsfw, + this.integrationTypes, + this.contexts, }) : description = null, descriptionLocalizations = null, options = null; @@ -154,8 +189,11 @@ class ApplicationCommandUpdateBuilder extends UpdateBuilder 'description_localizations': descriptionLocalizations?.map((key, value) => MapEntry(key.toString(), value)), if (options != null) 'options': options!.map((e) => e.build()).toList(), if (!identical(defaultMemberPermissions, sentinelFlags)) 'default_member_permissions': defaultMemberPermissions?.value.toString(), + // ignore: deprecated_member_use_from_same_package if (hasDmPermission != null) 'dm_permission': hasDmPermission, if (isNsfw != null) 'nsfw': isNsfw, + if (integrationTypes != null) 'integration_types': integrationTypes!.map((type) => type.value).toList(), + if (contexts != null) 'contexts': contexts!.map((type) => type.value).toList(), }; } @@ -388,7 +426,7 @@ class CommandOptionBuilder extends CreateBuilder { if (nameLocalizations != null) 'name_localizations': {for (final MapEntry(:key, :value) in nameLocalizations!.entries) key.identifier: value}, 'description': description, if (descriptionLocalizations != null) - 'description_localizations': {for (final MapEntry(:key, :value) in nameLocalizations!.entries) key.identifier: value}, + 'description_localizations': {for (final MapEntry(:key, :value) in descriptionLocalizations!.entries) key.identifier: value}, if (isRequired != null) 'required': isRequired, if (choices != null) 'choices': choices!.map((e) => e.build()).toList(), if (options != null) 'options': options!.map((e) => e.build()).toList(), diff --git a/lib/src/builders/channel/group_dm.dart b/lib/src/builders/channel/group_dm.dart index 99a40ee89..d60ee009b 100644 --- a/lib/src/builders/channel/group_dm.dart +++ b/lib/src/builders/channel/group_dm.dart @@ -16,3 +16,17 @@ class GroupDmUpdateBuilder extends UpdateBuilder { if (icon != null) 'icon': base64Encode(icon!), }; } + +class DmRecipientBuilder extends CreateBuilder { + String accessToken; + + String nick; + + DmRecipientBuilder({required this.accessToken, required this.nick}); + + @override + Map build() => { + 'access_token': accessToken, + 'nick': nick, + }; +} diff --git a/lib/src/builders/emoji/emoji.dart b/lib/src/builders/emoji/emoji.dart index a0bee4642..c9589ae07 100644 --- a/lib/src/builders/emoji/emoji.dart +++ b/lib/src/builders/emoji/emoji.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/builders/image.dart'; +import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/snowflake.dart'; @@ -36,12 +37,45 @@ class EmojiUpdateBuilder implements UpdateBuilder { EmojiUpdateBuilder({ this.name, - this.roles, + this.roles = sentinelList, + }); + + @override + Map build() => { + if (name != null) 'name': name, + if (!identical(roles, sentinelList)) 'roles': roles?.map((s) => s.toString()).toList(), + }; +} + +class ApplicationEmojiBuilder implements CreateBuilder { + /// The name of the emoji. + String name; + + /// The 128x128 emoji image. + ImageBuilder image; + + ApplicationEmojiBuilder({ + required this.name, + required this.image, + }); + + @override + Map build() => { + 'name': name, + 'image': image.buildDataString(), + }; +} + +class ApplicationEmojiUpdateBuilder implements UpdateBuilder { + /// The name of the emoji. + String? name; + + ApplicationEmojiUpdateBuilder({ + this.name, }); @override Map build() => { if (name != null) 'name': name, - if (roles != null) 'roles': roles!.map((s) => s.toString()).toList(), }; } diff --git a/lib/src/builders/guild/auto_moderation.dart b/lib/src/builders/guild/auto_moderation.dart index 43eee516b..6a7458eba 100644 --- a/lib/src/builders/guild/auto_moderation.dart +++ b/lib/src/builders/guild/auto_moderation.dart @@ -9,9 +9,9 @@ class AutoModerationRuleBuilder extends CreateBuilder { TriggerType triggerType; - TriggerMetadata? metadata; + TriggerMetadataBuilder? metadata; - List actions; + List actions; bool? isEnabled; @@ -30,32 +30,87 @@ class AutoModerationRuleBuilder extends CreateBuilder { this.exemptChannelIds, }); + AutoModerationRuleBuilder.keyword({ + required this.name, + required this.eventType, + required this.actions, + this.isEnabled, + this.exemptRoleIds, + this.exemptChannelIds, + List? keywordFilter, + List? regexPatterns, + List? allowList, + }) : triggerType = TriggerType.keyword, + metadata = TriggerMetadataBuilder( + keywordFilter: keywordFilter, + regexPatterns: regexPatterns, + allowList: allowList, + ); + + AutoModerationRuleBuilder.spam({ + required this.name, + required this.eventType, + required this.actions, + this.isEnabled, + this.exemptRoleIds, + this.exemptChannelIds, + }) : triggerType = TriggerType.spam, + metadata = null; + + AutoModerationRuleBuilder.keywordPreset({ + required this.name, + required this.eventType, + required this.actions, + this.isEnabled, + this.exemptRoleIds, + this.exemptChannelIds, + required List? presets, + List? allowList, + }) : triggerType = TriggerType.keywordPreset, + metadata = TriggerMetadataBuilder( + presets: presets, + allowList: allowList, + ); + + AutoModerationRuleBuilder.mentionSpam({ + required this.name, + required this.eventType, + required this.actions, + this.isEnabled, + this.exemptRoleIds, + this.exemptChannelIds, + required int mentionTotalLimit, + bool? isMentionRaidProtectionEnabled, + }) : triggerType = TriggerType.mentionSpam, + metadata = TriggerMetadataBuilder( + mentionTotalLimit: mentionTotalLimit, + isMentionRaidProtectionEnabled: isMentionRaidProtectionEnabled, + ); + + AutoModerationRuleBuilder.memberProfile({ + required this.name, + required this.eventType, + required this.actions, + this.isEnabled, + this.exemptRoleIds, + this.exemptChannelIds, + List? keywordFilter, + List? regexPatterns, + List? allowList, + }) : triggerType = TriggerType.memberProfile, + metadata = TriggerMetadataBuilder( + keywordFilter: keywordFilter, + regexPatterns: regexPatterns, + allowList: allowList, + ); + @override Map build() => { 'name': name, 'event_type': eventType.value, 'trigger_type': triggerType.value, - if (metadata != null) - 'trigger_metadata': { - 'keyword_filter': metadata!.keywordFilter, - 'regex_patterns': metadata!.regexPatterns, - 'presets': metadata!.presets?.map((type) => type.value).toList(), - 'allow_list': metadata!.allowList, - 'mention_total_limit': metadata!.mentionTotalLimit, - 'mention_raid_protection_enabled': metadata!.isMentionRaidProtectionEnabled, - }, - 'actions': [ - for (final action in actions) - { - 'type': action.type.value, - if (action.metadata != null) - 'metadata': { - 'channel_id': action.metadata!.channelId?.toString(), - 'duration_seconds': action.metadata!.duration?.inSeconds, - 'custom_message': action.metadata!.customMessage, - } - } - ], + if (metadata != null) 'trigger_metadata': metadata!.build(), + 'actions': actions.map((a) => a.build()).toList(), if (isEnabled != null) 'enabled': isEnabled, if (exemptRoleIds != null) 'exempt_roles': exemptRoleIds!.map((id) => id.toString()).toList(), if (exemptChannelIds != null) 'exempt_channels': exemptChannelIds!.map((id) => id.toString()).toList(), @@ -67,9 +122,9 @@ class AutoModerationRuleUpdateBuilder extends UpdateBuilder AutoModerationEventType? eventType; - TriggerMetadata? metadata; + TriggerMetadataBuilder? metadata; - List? actions; + List? actions; bool? isEnabled; @@ -91,30 +146,101 @@ class AutoModerationRuleUpdateBuilder extends UpdateBuilder Map build() => { if (name != null) 'name': name, if (eventType != null) 'event_type': eventType!.value, - if (metadata != null) - 'trigger_metadata': { - 'keyword_filter': metadata!.keywordFilter, - 'regex_patterns': metadata!.regexPatterns, - 'presets': metadata!.presets?.map((type) => type.value).toList(), - 'allow_list': metadata!.allowList, - 'mention_total_limit': metadata!.mentionTotalLimit, - 'mention_raid_protection_enabled': metadata!.isMentionRaidProtectionEnabled, - }, - if (actions != null) - 'actions': [ - for (final action in actions!) - { - 'type': action.type.value, - if (action.metadata != null) - 'metadata': { - 'channel_id': action.metadata!.channelId?.toString(), - 'duration_seconds': action.metadata!.duration?.inSeconds, - 'custom_message': action.metadata!.customMessage, - } - } - ], + if (metadata != null) 'trigger_metadata': metadata!.build(), + if (actions != null) 'actions': actions!.map((a) => a.build()).toList(), if (isEnabled != null) 'enabled': isEnabled, if (exemptRoleIds != null) 'exempt_roles': exemptRoleIds!.map((id) => id.toString()).toList(), if (exemptChannelIds != null) 'exempt_channels': exemptChannelIds!.map((id) => id.toString()).toList(), }; } + +class TriggerMetadataBuilder extends CreateBuilder { + /// A list of words that trigger the rule. + final List? keywordFilter; + + /// A list of regex patterns that trigger the rule. + final List? regexPatterns; + + /// A list of preset keyword types that trigger the rule. + final List? presets; + + /// A list of words allowed to bypass the rule. + final List? allowList; + + /// The maximum number of mentions in a message. + final int? mentionTotalLimit; + + /// Whether mention raid protection is enabled. + final bool? isMentionRaidProtectionEnabled; + + TriggerMetadataBuilder({ + this.keywordFilter, + this.regexPatterns, + this.presets, + this.allowList, + this.mentionTotalLimit, + this.isMentionRaidProtectionEnabled, + }); + + @override + Map build() => { + 'keyword_filter': keywordFilter, + 'regex_patterns': regexPatterns, + 'presets': presets?.map((type) => type.value).toList(), + 'allow_list': allowList, + 'mention_total_limit': mentionTotalLimit, + 'mention_raid_protection_enabled': isMentionRaidProtectionEnabled, + }; +} + +class AutoModerationActionBuilder extends CreateBuilder { + /// The type of action to perform. + final ActionType type; + + /// Metadata needed to perform the action. + final ActionMetadataBuilder? metadata; + + AutoModerationActionBuilder({required this.type, this.metadata}); + + AutoModerationActionBuilder.blockMessage({String? customMessage}) + : type = ActionType.blockMessage, + metadata = customMessage == null ? null : ActionMetadataBuilder(customMessage: customMessage); + + AutoModerationActionBuilder.sendAlertMessage({required Snowflake channelId}) + : type = ActionType.sendAlertMessage, + metadata = ActionMetadataBuilder(channelId: channelId); + + AutoModerationActionBuilder.timeout({required Duration duration}) + : type = ActionType.timeout, + metadata = ActionMetadataBuilder(duration: duration); + + @override + Map build() => { + 'type': type.value, + if (metadata != null) 'metadata': metadata!.build(), + }; +} + +class ActionMetadataBuilder extends CreateBuilder { + /// The ID of the channel to send the alert message to. + final Snowflake? channelId; + + /// The duration of time to time the user out for. + final Duration? duration; + + /// A custom message to send to the user. + final String? customMessage; + + ActionMetadataBuilder({ + this.channelId, + this.duration, + this.customMessage, + }); + + @override + Map build() => { + if (channelId != null) 'channel_id': channelId!.toString(), + if (duration != null) 'duration_seconds': duration!.inSeconds, + if (customMessage != null) 'custom_message': customMessage, + }; +} diff --git a/lib/src/builders/guild/guild.dart b/lib/src/builders/guild/guild.dart index 374a45939..2722e2be0 100644 --- a/lib/src/builders/guild/guild.dart +++ b/lib/src/builders/guild/guild.dart @@ -51,8 +51,8 @@ class GuildBuilder extends CreateBuilder { 'name': name, if (icon != null) 'icon': icon!.buildDataString(), if (verificationLevel != null) 'verification_level': verificationLevel!.value, - if (defaultMessageNotificationLevel != null) 'default_message_notification_level': defaultMessageNotificationLevel!.value, - if (explicitContentFilterLevel != null) 'explicit_content_filter_level': explicitContentFilterLevel!.value, + if (defaultMessageNotificationLevel != null) 'default_message_notifications': defaultMessageNotificationLevel!.value, + if (explicitContentFilterLevel != null) 'explicit_content_filter': explicitContentFilterLevel!.value, if (roles != null) 'roles': roles!.map((b) => b.build()).toList(), if (channels != null) 'channels': channels!.map((b) => b.build()).toList(), if (afkChannelId != null) 'afk_channel_id': afkChannelId!.toString(), @@ -101,6 +101,8 @@ class GuildUpdateBuilder extends UpdateBuilder { bool? premiumProgressBarEnabled; + Snowflake? safetyAlertsChannelId; + GuildUpdateBuilder({ this.name, this.verificationLevel, @@ -113,36 +115,38 @@ class GuildUpdateBuilder extends UpdateBuilder { this.splash = sentinelImageBuilder, this.discoverySplash = sentinelImageBuilder, this.banner = sentinelImageBuilder, - this.systemChannelId, + this.systemChannelId = sentinelSnowflake, this.systemChannelFlags, - this.rulesChannelId, - this.publicUpdatesChannelId, + this.rulesChannelId = sentinelSnowflake, + this.publicUpdatesChannelId = sentinelSnowflake, this.preferredLocale, this.features, this.description = sentinelString, this.premiumProgressBarEnabled, + this.safetyAlertsChannelId = sentinelSnowflake, }); @override Map build() => { if (name != null) 'name': name, - if (verificationLevel != null) 'verificationLevel': verificationLevel!.value, - if (defaultMessageNotificationLevel != null) 'defaultMessageNotificationLevel': defaultMessageNotificationLevel!.value, - if (explicitContentFilterLevel != null) 'explicitContentFilterLevel': explicitContentFilterLevel!.value, - if (!identical(afkChannelId, sentinelSnowflake)) 'afkChannelId': afkChannelId?.toString(), - if (afkTimeout != null) 'afkTimeout': afkTimeout!.inSeconds, + if (verificationLevel != null) 'verification_level': verificationLevel!.value, + if (defaultMessageNotificationLevel != null) 'default_message_notifications': defaultMessageNotificationLevel!.value, + if (explicitContentFilterLevel != null) 'explicit_content_filter': explicitContentFilterLevel!.value, + if (!identical(afkChannelId, sentinelSnowflake)) 'afk_channel_id': afkChannelId?.toString(), + if (afkTimeout != null) 'afk_timeout': afkTimeout!.inSeconds, if (!identical(icon, sentinelImageBuilder)) 'icon': icon?.buildDataString(), - if (newOwnerId != null) 'newOwnerId': newOwnerId!.toString(), + if (newOwnerId != null) 'owner_id': newOwnerId!.toString(), if (!identical(splash, sentinelImageBuilder)) 'splash': splash?.buildDataString(), - if (!identical(discoverySplash, sentinelImageBuilder)) 'discoverySplash': discoverySplash?.buildDataString(), + if (!identical(discoverySplash, sentinelImageBuilder)) 'discovery_splash': discoverySplash?.buildDataString(), if (!identical(banner, sentinelImageBuilder)) 'banner': banner?.buildDataString(), - if (systemChannelId != null) 'systemChannelId': systemChannelId!.toString(), - if (systemChannelFlags != null) 'systemChannelFlags': systemChannelFlags!.value, - if (rulesChannelId != null) 'rulesChannelId': rulesChannelId!.toString(), - if (publicUpdatesChannelId != null) 'publicUpdatesChannelId': publicUpdatesChannelId!.toString(), - if (preferredLocale != null) 'preferredLocale': preferredLocale!.identifier, + if (!identical(systemChannelId, sentinelSnowflake)) 'system_channel_id': systemChannelId?.toString(), + if (systemChannelFlags != null) 'system_channel_flags': systemChannelFlags!.value, + if (!identical(rulesChannelId, sentinelSnowflake)) 'rules_channel_id': rulesChannelId?.toString(), + if (!identical(publicUpdatesChannelId, sentinelSnowflake)) 'public_updates_channel_id': publicUpdatesChannelId?.toString(), + if (preferredLocale != null) 'preferred_locale': preferredLocale!.identifier, if (features != null) 'features': GuildManager.serializeGuildFeatures(features!), if (!identical(description, sentinelString)) 'description': description, - if (premiumProgressBarEnabled != null) 'premiumProgressBarEnabled': premiumProgressBarEnabled, + if (premiumProgressBarEnabled != null) 'premium_progress_bar_enabled': premiumProgressBarEnabled, + if (!identical(safetyAlertsChannelId, sentinelSnowflake)) 'safety_alerts_channel_id': safetyAlertsChannelId?.toString(), }; } diff --git a/lib/src/builders/guild/onboarding.dart b/lib/src/builders/guild/onboarding.dart new file mode 100644 index 000000000..ab7249233 --- /dev/null +++ b/lib/src/builders/guild/onboarding.dart @@ -0,0 +1,95 @@ +import 'package:nyxx/src/builders/builder.dart'; +import 'package:nyxx/src/models/emoji.dart'; +import 'package:nyxx/src/models/guild/onboarding.dart'; +import 'package:nyxx/src/models/snowflake.dart'; + +class OnboardingUpdateBuilder extends UpdateBuilder { + List prompts; + + List defaultChannelIds; + + bool isEnabled; + + OnboardingMode mode; + + OnboardingUpdateBuilder({ + required this.prompts, + required this.defaultChannelIds, + required this.isEnabled, + required this.mode, + }); + + @override + Map build() => { + 'prompts': prompts.map((prompt) => prompt.build()).toList(), + 'default_channel_ids': defaultChannelIds.map((channelId) => channelId.toString()).toList(), + 'enabled': isEnabled, + 'mode': mode.value, + }; +} + +class OnboardingPromptBuilder extends CreateBuilder { + OnboardingPromptType type; + + List options; + + String title; + + bool isSingleSelect; + + bool isRequired; + + bool isInOnboarding; + + OnboardingPromptBuilder({ + required this.type, + required this.options, + required this.title, + required this.isSingleSelect, + required this.isRequired, + required this.isInOnboarding, + }); + + @override + Map build() => { + 'type': type.value, + 'options': options.map((option) => option.build()).toList(), + 'title': title, + 'single_select': isSingleSelect, + 'required': isRequired, + 'in_onboarding': isInOnboarding, + }; +} + +class OnboardingPromptOptionBuilder extends CreateBuilder { + List channelIds; + + List roleIds; + + Emoji? emoji; + + String title; + + String? description; + + OnboardingPromptOptionBuilder({ + required this.channelIds, + required this.roleIds, + this.emoji, + required this.title, + this.description, + }); + + @override + Map build() => { + 'channel_ids': channelIds.map((id) => id.toString()).toList(), + 'role_ids': roleIds.map((id) => id.toString()).toList(), + if (emoji case final emoji?) 'emoji_name': emoji.name, + if (emoji case GuildEmoji emoji) ...{ + 'emoji_id': emoji.id, + 'emoji_animated': emoji.isAnimated, + }, + 'title': title, + 'description': description, + }; +} diff --git a/lib/src/builders/guild/scheduled_event.dart b/lib/src/builders/guild/scheduled_event.dart index 0a94e9ffb..6b6c1597e 100644 --- a/lib/src/builders/guild/scheduled_event.dart +++ b/lib/src/builders/guild/scheduled_event.dart @@ -24,22 +24,59 @@ class ScheduledEventBuilder extends CreateBuilder { ImageBuilder? image; + RecurrenceRuleBuilder? recurrenceRule; + ScheduledEventBuilder({ - required this.channelId, + this.channelId, this.metadata, required this.name, required this.privacyLevel, required this.scheduledStartTime, - required this.scheduledEndTime, + this.scheduledEndTime, this.description, required this.type, this.image, + this.recurrenceRule, }); + ScheduledEventBuilder.stageInstance({ + required Snowflake this.channelId, + required this.name, + required this.privacyLevel, + required this.scheduledStartTime, + this.scheduledEndTime, + this.description, + this.image, + this.recurrenceRule, + }) : type = ScheduledEntityType.stageInstance; + + ScheduledEventBuilder.voice({ + required Snowflake this.channelId, + required this.name, + required this.privacyLevel, + required this.scheduledStartTime, + this.scheduledEndTime, + this.description, + this.image, + this.recurrenceRule, + }) : type = ScheduledEntityType.voice; + + ScheduledEventBuilder.external({ + required this.name, + required this.privacyLevel, + required this.scheduledStartTime, + required DateTime this.scheduledEndTime, + required String location, + this.description, + this.image, + this.recurrenceRule, + }) : type = ScheduledEntityType.external, + metadata = EntityMetadata(location: location); + @override Map build() => { if (channelId != null) 'channel_id': channelId.toString(), - if (metadata != null) 'metadata': {'location': metadata!.location}, + if (metadata != null) 'entity_metadata': {'location': metadata!.location}, 'name': name, 'privacy_level': privacyLevel.value, 'scheduled_start_time': scheduledStartTime.toIso8601String(), @@ -47,6 +84,7 @@ class ScheduledEventBuilder extends CreateBuilder { if (description != null) 'description': description, 'entity_type': type.value, if (image != null) 'image': image!.buildDataString(), + if (recurrenceRule != null) 'recurrence_rule': recurrenceRule!.build(), }; } @@ -71,6 +109,8 @@ class ScheduledEventUpdateBuilder extends UpdateBuilder { ImageBuilder? image; + RecurrenceRuleBuilder? recurrenceRule; + ScheduledEventUpdateBuilder({ this.channelId = sentinelSnowflake, this.metadata = sentinelEntityMetadata, @@ -82,6 +122,7 @@ class ScheduledEventUpdateBuilder extends UpdateBuilder { this.type, this.status, this.image, + this.recurrenceRule, }); @override @@ -96,5 +137,61 @@ class ScheduledEventUpdateBuilder extends UpdateBuilder { if (type != null) 'entity_type': type!.value, if (status != null) 'status': status!.value, if (image != null) 'image': image!.buildDataString(), + if (recurrenceRule != null) 'recurrence_rule': recurrenceRule!.build(), + }; +} + +class RecurrenceRuleBuilder extends CreateBuilder { + DateTime start; + RecurrenceRuleFrequency frequency; + int interval; + List? byWeekday; + List? byNWeekday; + List? byMonth; + List? byMonthDay; + + RecurrenceRuleBuilder({ + required this.start, + required this.frequency, + required this.interval, + this.byWeekday, + this.byNWeekday, + this.byMonth, + this.byMonthDay, + }); + + RecurrenceRuleBuilder.daily({required this.start, this.byWeekday}) + : frequency = RecurrenceRuleFrequency.daily, + interval = 1; + + RecurrenceRuleBuilder.weekly({ + required this.start, + required this.interval, + RecurrenceRuleWeekday? day, + }) : frequency = RecurrenceRuleFrequency.weekly, + byWeekday = day == null ? null : [day]; + + RecurrenceRuleBuilder.monthly({ + required this.start, + RecurrenceRuleNWeekday? day, + }) : frequency = RecurrenceRuleFrequency.monthly, + interval = 1, + byNWeekday = day == null ? null : [day]; + + RecurrenceRuleBuilder.yearly({required this.start, (RecurrenceRuleMonth, int)? day}) + : frequency = RecurrenceRuleFrequency.yearly, + interval = 1, + byMonth = day == null ? null : [day.$1], + byMonthDay = day == null ? null : [day.$2]; + + @override + Map build() => { + 'start': start.toIso8601String(), + 'frequency': frequency.value, + 'interval': interval, + 'by_weekday': byWeekday?.map((weekday) => weekday.value).toList(), + 'by_n_weekday': byNWeekday?.map((nWeekday) => {'n': nWeekday.n, 'day': nWeekday.day.value}).toList(), + 'by_month': byMonth?.map((month) => month.value).toList(), + 'by_month_day': byMonthDay, }; } diff --git a/lib/src/builders/guild/template.dart b/lib/src/builders/guild/template.dart index 346cd5e75..1e51fed1c 100644 --- a/lib/src/builders/guild/template.dart +++ b/lib/src/builders/guild/template.dart @@ -7,12 +7,12 @@ class GuildTemplateBuilder extends CreateBuilder { String? description; - GuildTemplateBuilder({required this.name, this.description}); + GuildTemplateBuilder({required this.name, this.description = sentinelString}); @override Map build() => { 'name': name, - if (description != null) 'description': description, + if (!identical(description, sentinelString)) 'description': description, }; } diff --git a/lib/src/builders/interaction_response.dart b/lib/src/builders/interaction_response.dart index 1b9efdc7e..d75c8dbb5 100644 --- a/lib/src/builders/interaction_response.dart +++ b/lib/src/builders/interaction_response.dart @@ -21,13 +21,14 @@ class InteractionResponseBuilder extends CreateBuilder> choices) => InteractionResponseBuilder( type: InteractionCallbackType.applicationCommandAutocompleteResult, - data: choices, + data: {'choices': choices.map((e) => e.build()).toList()}, ); factory InteractionResponseBuilder.modal(ModalBuilder modal) => InteractionResponseBuilder(type: InteractionCallbackType.modal, data: modal); + @Deprecated('Respond with ButtonStyle.premium button instead') factory InteractionResponseBuilder.premiumRequired() => InteractionResponseBuilder(type: InteractionCallbackType.premiumRequired, data: null); @override @@ -81,13 +83,14 @@ class _EphemeralMessageBuilder extends MessageBuilder { required super.tts, required super.embeds, required super.allowedMentions, - required super.replyId, - required super.requireReplyToExist, required super.components, required super.stickerIds, required super.attachments, required super.suppressEmbeds, required super.suppressNotifications, + required super.enforceNonce, + required super.poll, + required super.referencedMessage, required this.isEphemeral, }); @@ -111,6 +114,7 @@ enum InteractionCallbackType { updateMessage._(7), applicationCommandAutocompleteResult._(8), modal._(9), + @Deprecated('Respond with ButtonStyle.premium button instead') premiumRequired._(10); final int value; diff --git a/lib/src/builders/message/component.dart b/lib/src/builders/message/component.dart index 656d10fbf..75164781c 100644 --- a/lib/src/builders/message/component.dart +++ b/lib/src/builders/message/component.dart @@ -38,6 +38,8 @@ class ButtonBuilder extends MessageComponentBuilder { String? customId; + Snowflake? skuId; + Uri? url; bool? isDisabled; @@ -47,6 +49,7 @@ class ButtonBuilder extends MessageComponentBuilder { this.label, this.emoji, this.customId, + this.skuId, this.url, this.isDisabled, }) : super(type: MessageComponentType.button); @@ -54,7 +57,7 @@ class ButtonBuilder extends MessageComponentBuilder { ButtonBuilder.primary({ this.label, this.emoji, - required String customId, + required String this.customId, this.isDisabled, }) : style = ButtonStyle.primary, super(type: MessageComponentType.button); @@ -62,7 +65,7 @@ class ButtonBuilder extends MessageComponentBuilder { ButtonBuilder.secondary({ this.label, this.emoji, - required String customId, + required String this.customId, this.isDisabled, }) : style = ButtonStyle.secondary, super(type: MessageComponentType.button); @@ -70,7 +73,7 @@ class ButtonBuilder extends MessageComponentBuilder { ButtonBuilder.success({ this.label, this.emoji, - required String customId, + required String this.customId, this.isDisabled, }) : style = ButtonStyle.success, super(type: MessageComponentType.button); @@ -78,7 +81,7 @@ class ButtonBuilder extends MessageComponentBuilder { ButtonBuilder.danger({ this.label, this.emoji, - required String customId, + required String this.customId, this.isDisabled, }) : style = ButtonStyle.danger, super(type: MessageComponentType.button); @@ -91,6 +94,12 @@ class ButtonBuilder extends MessageComponentBuilder { }) : style = ButtonStyle.link, super(type: MessageComponentType.button); + ButtonBuilder.premium({ + required Snowflake this.skuId, + this.isDisabled, + }) : style = ButtonStyle.premium, + super(type: MessageComponentType.button); + @override Map build() => { ...super.build(), @@ -98,11 +107,12 @@ class ButtonBuilder extends MessageComponentBuilder { if (label != null) 'label': label, if (emoji != null) 'emoji': { - 'id': emoji!.id == Snowflake.zero ? null : emoji!.id, + 'id': emoji!.id == Snowflake.zero ? null : emoji!.id.toString(), 'name': emoji!.name, if (emoji is GuildEmoji) 'animated': (emoji as GuildEmoji).isAnimated == true, }, if (customId != null) 'custom_id': customId, + if (skuId != null) 'sku_id': skuId.toString(), if (url != null) 'url': url!.toString(), if (isDisabled != null) 'disabled': isDisabled, }; @@ -223,7 +233,7 @@ class SelectMenuOptionBuilder extends CreateBuilder { if (description != null) 'description': description, if (emoji != null) 'emoji': { - 'id': emoji!.id.value, + 'id': emoji!.id.toString(), 'name': emoji!.name, 'animated': emoji is GuildEmoji && (emoji as GuildEmoji).isAnimated == true, }, @@ -291,6 +301,7 @@ class TextInputBuilder extends MessageComponentBuilder { if (minLength != null) 'min_length': minLength, if (maxLength != null) 'max_length': maxLength, if (isRequired != null) 'required': isRequired, + if (value != null) 'value': value, if (placeholder != null) 'placeholder': placeholder, }; } diff --git a/lib/src/builders/message/message.dart b/lib/src/builders/message/message.dart index d2a121a79..8ef92ff4b 100644 --- a/lib/src/builders/message/message.dart +++ b/lib/src/builders/message/message.dart @@ -3,10 +3,13 @@ import 'package:nyxx/src/builders/message/allowed_mentions.dart'; import 'package:nyxx/src/builders/message/attachment.dart'; import 'package:nyxx/src/builders/message/component.dart'; import 'package:nyxx/src/builders/message/embed.dart'; +import 'package:nyxx/src/builders/message/poll.dart'; import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/models/message/message.dart'; +import 'package:nyxx/src/models/message/reference.dart'; import 'package:nyxx/src/models/snowflake.dart'; +// TODO(abitofevrything): Remove replyId and requireReplyToExist properties. class MessageBuilder extends CreateBuilder { String? content; @@ -18,9 +21,7 @@ class MessageBuilder extends CreateBuilder { AllowedMentions? allowedMentions; - Snowflake? replyId; - - bool? requireReplyToExist; + MessageReferenceBuilder? referencedMessage; List? components; @@ -32,20 +33,63 @@ class MessageBuilder extends CreateBuilder { bool? suppressNotifications; + /// If true and nonce is present, it will be checked for uniqueness in the past few minutes. If another message was created by the same author with the same nonce, + /// that message will be returned and no new message will be created. + bool? enforceNonce; + + PollBuilder? poll; + MessageBuilder({ this.content, this.nonce, this.tts, this.embeds, this.allowedMentions, - this.replyId, - this.requireReplyToExist, + this.referencedMessage, + @Deprecated('Use referencedMessage instead') Snowflake? replyId, + @Deprecated('Use referencedMessage instead') bool? requireReplyToExist, this.components, this.stickerIds, this.attachments, this.suppressEmbeds, this.suppressNotifications, - }); + this.enforceNonce, + this.poll, + }) { + if (replyId != null) { + assert(referencedMessage == null, 'Cannot set replyId if referencedMessage is non-null'); + referencedMessage = MessageReferenceBuilder( + type: MessageReferenceType.defaultType, + messageId: replyId, + failIfInexistent: requireReplyToExist, + ); + } + } + + @Deprecated('Use referencedMessage instead') + Snowflake? get replyId => referencedMessage?.messageId; + + @Deprecated('Use referencedMessage instead') + set replyId(Snowflake? replyId) { + if (replyId == null) { + referencedMessage = null; + } else { + referencedMessage = MessageReferenceBuilder( + type: MessageReferenceType.defaultType, + messageId: replyId, + ); + } + } + + @Deprecated('Use referencedMessage instead') + bool? get requireReplyToExist => referencedMessage?.failIfInexistent; + + @Deprecated('Use referencedMessage instead') + set requireReplyToExist(bool? requireReplyToExist) { + if (referencedMessage != null) { + referencedMessage!.failIfInexistent = requireReplyToExist; + } + } @override Map build() => { @@ -54,17 +98,15 @@ class MessageBuilder extends CreateBuilder { if (tts != null) 'tts': tts, if (embeds != null) 'embeds': embeds!.map((e) => e.build()).toList(), if (allowedMentions != null) 'allowed_mentions': allowedMentions!.build(), - if (replyId != null) - 'message_reference': { - 'message_id': replyId.toString(), - if (requireReplyToExist != null) 'fail_if_not_exists': requireReplyToExist, - }, + if (referencedMessage != null) 'message_reference': referencedMessage!.build(), if (components != null) 'components': components!.map((e) => e.build()).toList(), if (stickerIds != null) 'sticker_ids': stickerIds!.map((e) => e.toString()).toList(), if (attachments != null) 'attachments': attachments!.map((e) => e.build()).toList(), if (suppressEmbeds != null || suppressNotifications != null) 'flags': (suppressEmbeds == true ? MessageFlags.suppressEmbeds.value : 0) | (suppressNotifications == true ? MessageFlags.suppressNotifications.value : 0), + if (enforceNonce != null) 'enforce_nonce': enforceNonce, + if (poll != null) 'poll': poll!.build(), }; } @@ -100,3 +142,46 @@ class MessageUpdateBuilder extends UpdateBuilder { if (suppressEmbeds != null) 'flags': (suppressEmbeds == true ? MessageFlags.suppressEmbeds.value : 0), }; } + +class MessageReferenceBuilder extends CreateBuilder { + MessageReferenceType type; + + Snowflake messageId; + + Snowflake? channelId; + + Snowflake? guildId; + + bool? failIfInexistent; + + MessageReferenceBuilder({ + required this.type, + required this.messageId, + this.channelId, + this.guildId, + this.failIfInexistent, + }); + + MessageReferenceBuilder.reply({ + required this.messageId, + this.channelId, + this.guildId, + this.failIfInexistent, + }) : type = MessageReferenceType.defaultType; + + MessageReferenceBuilder.forward({ + required this.messageId, + required Snowflake this.channelId, + this.guildId, + this.failIfInexistent, + }) : type = MessageReferenceType.forward; + + @override + Map build() => { + 'type': type.value, + 'message_id': messageId.toString(), + if (channelId != null) 'channel_id': channelId!.toString(), + if (guildId != null) 'guild_id': guildId!.toString(), + if (failIfInexistent != null) 'fail_if_not_exists': failIfInexistent, + }; +} diff --git a/lib/src/builders/message/poll.dart b/lib/src/builders/message/poll.dart new file mode 100644 index 000000000..0c45e6289 --- /dev/null +++ b/lib/src/builders/message/poll.dart @@ -0,0 +1,74 @@ +import 'package:nyxx/src/builders/builder.dart'; +import 'package:nyxx/src/models/emoji.dart'; +import 'package:nyxx/src/models/message/poll.dart'; +import 'package:nyxx/src/models/snowflake.dart'; + +/// {@macro poll_media} +class PollMediaBuilder extends CreateBuilder { + /// The text of the field. + String? text; + + /// The emoji of the field. + Emoji? emoji; + + PollMediaBuilder({this.text, this.emoji}); + + @override + Map build() => { + if (text != null) 'text': text, + if (emoji != null) + 'emoji': { + if (emoji!.id != Snowflake.zero) 'id': emoji!.id.toString(), + if (emoji!.name != null) 'name': emoji!.name, + }, + }; +} + +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#poll-answer-object +class PollAnswerBuilder extends CreateBuilder { + /// The data of the answer. + PollMediaBuilder pollMedia; + + PollAnswerBuilder({required this.pollMedia}); + + PollAnswerBuilder.text(String text, [Emoji? emoji]) : pollMedia = PollMediaBuilder(text: text, emoji: emoji); + + @override + Map build() => {'poll_media': pollMedia.build()}; +} + +/// {@macro poll} +class PollBuilder extends CreateBuilder { + /// The question of the poll. Only [PollMediaBuilder.text] is supported. + PollMediaBuilder question; + + /// Each of the answers available in the poll. + List answers; + + /// Number of hours the poll should be open for, up to 7 days. + Duration duration; + + /// Whether a user can select multiple answers. + bool? allowMultiselect; + + /// The layout type of the poll. + PollLayoutType? layoutType; + + PollBuilder({ + required this.question, + required this.answers, + required this.duration, + this.allowMultiselect, + this.layoutType, + }); + + @override + Map build() => { + 'question': question.build(), + 'answers': answers.map((a) => a.build()).toList(), + 'duration': duration.inHours, + if (allowMultiselect != null) 'allow_multiselect': allowMultiselect, + if (layoutType != null) 'layout_type': layoutType!.value, + }; +} diff --git a/lib/src/builders/permission_overwrite.dart b/lib/src/builders/permission_overwrite.dart index 8f55073fe..e9f2be1f9 100644 --- a/lib/src/builders/permission_overwrite.dart +++ b/lib/src/builders/permission_overwrite.dart @@ -16,7 +16,8 @@ class PermissionOverwriteBuilder extends CreateBuilder { PermissionOverwriteBuilder({required this.id, required this.type, this.allow, this.deny}); @override - Map build() => { + Map build({bool includeId = true}) => { + if (includeId) 'id': id.toString(), 'type': type.value, if (allow != null) 'allow': allow!.value.toString(), if (deny != null) 'deny': deny!.value.toString(), diff --git a/lib/src/builders/role.dart b/lib/src/builders/role.dart index 017894a2b..bfc2e63a7 100644 --- a/lib/src/builders/role.dart +++ b/lib/src/builders/role.dart @@ -36,7 +36,7 @@ class RoleBuilder extends CreateBuilder { if (name != null) 'name': name, if (permissions != null) 'permissions': permissions!.value.toString(), if (color != null) 'color': color!.value, - if (isHoisted != null) 'hoisted': isHoisted, + if (isHoisted != null) 'hoist': isHoisted, if (icon != null) 'icon': icon!.buildDataString(), if (unicodeEmoji != null) 'unicode_emoji': unicodeEmoji, if (isMentionable != null) 'mentionable': isMentionable, @@ -46,7 +46,7 @@ class RoleBuilder extends CreateBuilder { class RoleUpdateBuilder extends UpdateBuilder { String? name; - Permissions? permissions; + Flags? permissions; DiscordColor? color; @@ -73,7 +73,7 @@ class RoleUpdateBuilder extends UpdateBuilder { if (name != null) 'name': name, if (permissions != null) 'permissions': permissions!.value.toString(), if (color != null) 'color': color!.value, - if (isHoisted != null) 'hoisted': isHoisted, + if (isHoisted != null) 'hoist': isHoisted, if (!identical(icon, sentinelImageBuilder)) 'icon': icon?.buildDataString(), if (!identical(unicodeEmoji, sentinelString)) 'unicode_emoji': unicodeEmoji, if (isMentionable != null) 'mentionable': isMentionable, diff --git a/lib/src/builders/sentinels.dart b/lib/src/builders/sentinels.dart index 79734da50..c8462e6c6 100644 --- a/lib/src/builders/sentinels.dart +++ b/lib/src/builders/sentinels.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/builders/image.dart'; import 'package:nyxx/src/models/channel/types/forum.dart'; +import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/utils/flags.dart'; @@ -90,3 +91,21 @@ class _SentinelFlags implements Flags { @override void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } + +const sentinelUri = _SentinelUri(); + +class _SentinelUri implements Uri { + const _SentinelUri(); + + @override + void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _SentinelEmoji implements Emoji { + const _SentinelEmoji(); + + @override + void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +const sentinelEmoji = _SentinelEmoji(); diff --git a/lib/src/builders/user.dart b/lib/src/builders/user.dart index 30fa322ec..459c7cf25 100644 --- a/lib/src/builders/user.dart +++ b/lib/src/builders/user.dart @@ -1,16 +1,24 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/builders/image.dart'; +import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/models/user/user.dart'; class UserUpdateBuilder extends UpdateBuilder { + /// New user's username. String? username; + + /// New user's avatar. ImageBuilder? avatar; - UserUpdateBuilder({this.username, this.avatar}); + /// New user's banner. + ImageBuilder? banner; + + UserUpdateBuilder({this.username, this.avatar = sentinelImageBuilder, this.banner = sentinelImageBuilder}); @override Map build() => { if (username != null) 'username': username!, - if (avatar != null) 'avatar': avatar!.buildDataString(), + if (!identical(avatar, sentinelImageBuilder)) 'avatar': avatar?.buildDataString(), + if (!identical(banner, sentinelImageBuilder)) 'banner': banner?.buildDataString(), }; } diff --git a/lib/src/cache/cache.dart b/lib/src/cache/cache.dart index 540fd2774..72d2de60e 100644 --- a/lib/src/cache/cache.dart +++ b/lib/src/cache/cache.dart @@ -5,6 +5,68 @@ import 'package:nyxx/src/client.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +/// A manager for all the caches associated with a client. +/// +/// Provides a way to obtain the [Cache] instance associated with a given cache +/// identifier. Also provides utilities to inspect all the caches in a client. +/// +/// Empty caches are automatically discarded. +class CacheManager { + /// The client this [CacheManager] is for. + final Nyxx client; + + final Map> _caches = {}; + + /// A map containing all the caches attached to [client]. + /// + /// Cache identifiers are mapped to their respective [Cache] instances. + Map> get caches => UnmodifiableMapView(_caches); + + final Map>> _emptyCaches = {}; + + /// Create a new cache manager for a client. + CacheManager(this.client); + + /// Get the cache associated with [identifier], or create it if it does not yet exist. + /// + /// [config] is only used if the cache does not exist and needs to be created. + Cache getCache(String identifier, CacheConfig config) { + if (_caches[identifier] case final cache?) { + if (cache is Cache) { + return cache; + } + + throw ArgumentError('Type of cache (${cache.runtimeType}) does not match type argument ($T) for $identifier'); + } + + if (_emptyCaches[identifier] case final reference?) { + if (reference.target case final cache?) { + if (cache is Cache) { + return cache; + } + + throw ArgumentError('Type of cache (${cache.runtimeType}) does not match type argument ($T) for $identifier'); + } else { + _emptyCaches.remove(identifier); + } + } + + final cache = Cache._(this, identifier, config); + _onEmpty(cache); // Cache starts out empty. Don't be afraid to discard it. + return cache; + } + + void _onNotEmpty(Cache cache) { + _emptyCaches.remove(cache.identifier); + _caches[cache.identifier] = cache; + } + + void _onEmpty(Cache cache) { + _caches.remove(cache.identifier); + _emptyCaches[cache.identifier] = WeakReference(cache); + } +} + /// The configuration for a [Cache] instance. class CacheConfig { /// The maximum amount of items allowed in the cache. @@ -22,131 +84,170 @@ class CacheConfig { const CacheConfig({this.maxSize, this.shouldCache}); } -typedef _CacheKey = ({String identifier, Snowflake key}); - -class _CacheEntry { - Object? value; - int accessCount; +final class _CacheEntry extends LinkedListEntry<_CacheEntry> { + final Snowflake id; + T value; - _CacheEntry(this.value) : accessCount = 0; + _CacheEntry({required this.id, required this.value}); } /// A simple cache for [SnowflakeEntity]s. -class Cache with MapMixin { - static final Expando> _stores = Expando('Cache store'); +/// +/// The underlying implementation is a basic LRU cache, limited by +/// [CacheConfig.maxSize]. Entities will not be added to the cache if +/// [CacheConfig.shouldCache] returns false. +/// +/// {@template cache_filtering} +/// When the cache size exceeds [CacheConfig.maxSize], items already in the +/// cache for which [CacheConfig.shouldCache] now returns `false` are removed +/// first, then items are removed starting from the least recently accessed or +/// updated until the cache size no longer exceeds the maximum size. +/// {@endtemplate} +/// +/// The underlying implementation is subject to change in future versions and +/// should not be relied on. +class Cache extends MapBase { + /// Return a mapping of identifier to cache contents for all caches associated with [client]. + @Deprecated('Use client.cache.caches') + static Map> cachesFor(Nyxx client) => client.cache.caches; - Map<_CacheKey, _CacheEntry> get _store => _stores[client] ??= {}; + /// A list containing the entries of this cache, with the most recently + /// accessed or updated entry first. + /// + /// Entries are ordered most recently used first so [filterItems] can first + /// try to remove elements based on [CacheConfig.shouldCache], before + /// removing entries that were not recently accessed without iterating + /// backwards. + final LinkedList<_CacheEntry> _mru = LinkedList(); - /// The configuration for this cache. - final CacheConfig config; + /// A mapping of entry IDs to the entries themselves. + /// + /// This must be kept in sync with [_mru] - items in this map must be present + /// in [_mru]. + /// This only serves to provide O(1) access to an entry given its key, + /// instead of iterating over [_mru]. + final Map> _entries = {}; /// An identifier for this cache. /// - /// Caches with the same identifier will use the same backing store, so this allows for multiple caches pointing to the same resource to exist. + /// Two caches with the same identifier belonging to the same client are guaranteed to contain the same items. final String identifier; - /// The client this cache is associated with. - final Nyxx client; + /// The configuration for this cache. + final CacheConfig config; - /// Create a new cache with the provided config. - Cache(this.client, this.identifier, this.config); + /// The manager for this cache. + final CacheManager manager; - /// Filter the items in the cache so that it obeys the [config]. - /// - /// Items are retained based on the number of accesses they have until the [CacheConfig.maxSize] - /// is respected. - void filterItems() { - final keys = List.of(_store.keys.where((element) => element.identifier == identifier)); + /// The client this cache is for. + Nyxx get client => manager.client; - if (config.maxSize != null && keys.length > config.maxSize!) { - keys.sort((a, b) => _store[a]!.accessCount.compareTo(_store[b]!.accessCount)); + Cache._(this.manager, this.identifier, this.config); - final overflow = keys.length - config.maxSize!; + @Deprecated('Use client.cache.getCache') + factory Cache(Nyxx client, String identifier, CacheConfig config) => client.cache.getCache(identifier, config); - for (final key in keys.take(overflow)) { - _store.remove(key); + @override + Iterable get keys => _entries.keys; + + /// Filter the items in this cache so that it obeys the [config]. + /// + /// {@macro cache_filtering} + void filterItems() { + final maxSize = config.maxSize; + if (maxSize == null) return; + final toRemoveCount = length - maxSize; + if (toRemoveCount <= 0) return; + + final toRemove = List.filled(toRemoveCount, _mru.last); + var count = 0; + var definitelyRemovedIndex = length - toRemoveCount; + + for (final (index, entry) in _mru.indexed) { + if (index >= definitelyRemovedIndex) { + toRemove[count++] = entry; + } else if (config.shouldCache?.call(entry.value) == false) { + definitelyRemovedIndex++; + toRemove[count++] = entry; } } + + for (final entry in toRemove) { + remove(entry.id); + } } - bool _resizeScheduled = false; + bool _filterScheduled = false; /// Schedule [filterItems] to be run, if it isn't already scheduled. /// /// This allows for bulk insertions to only trigger one [filterItems] call. void scheduleFilterItems() { - if (!_resizeScheduled) { - _resizeScheduled = true; - scheduleMicrotask(() { - filterItems(); - _resizeScheduled = false; - }); + if (_filterScheduled) return; + _filterScheduled = true; + scheduleMicrotask(() { + filterItems(); + _filterScheduled = false; + }); + } + + void _recordUse(_CacheEntry entry) { + _mru.remove(entry); + _mru.addFirst(entry); + } + + @override + T? operator [](Object? key) { + if (_entries[key] case final entry?) { + _recordUse(entry); + return entry.value; } + + return null; } @override void operator []=(Snowflake key, T value) { - assert(value is! ManagedSnowflakeEntity || value.id == key, 'Mismatched entity key in cache'); - if (config.shouldCache?.call(value) == false) { remove(key); return; } - _store.update( - (identifier: identifier, key: key), + // We will no longer be empty after adding the entry. + if (isEmpty) manager._onNotEmpty(this); + + final entry = _entries.update( + key, (entry) => entry..value = value, - ifAbsent: () => _CacheEntry(value), + ifAbsent: () => _CacheEntry(id: key, value: value), ); + _recordUse(entry); scheduleFilterItems(); } - @override - T? operator [](Object? key) { - if (key is! Snowflake) { - return null; - } - - final entry = _store[(identifier: identifier, key: key)]; - if (entry == null) { - return null; - } - - entry.accessCount++; - return entry.value as T; - } - @override void clear() { - _store.removeWhere((key, value) => key.identifier == identifier); + _mru.clear(); + _entries.clear(); + manager._onEmpty(this); } - @override - Iterable get keys => _store.keys.where((element) => element.identifier == identifier).map((e) => e.key); - @override T? remove(Object? key) { - return _store.remove((identifier: identifier, key: key))?.value as T?; - } - - /// Return a mapping of identifier to cache contents for all caches associated with [client]. - static Map> cachesFor(Nyxx client) { - final store = _stores[client]; - if (store == null) { - return {}; - } + final entry = _entries.remove(key); + if (entry == null) return null; - final result = >{}; + if (isEmpty) manager._onEmpty(this); + _mru.remove(entry); - for (final entry in store.entries) { - (result[entry.key.identifier] ??= {})[entry.key.key] = entry.value.value; - } - - return result; + return entry.value; } -} -extension SnowflakeCache> on Cache { - void addEntities(Iterable entities) => addEntries(entities.map((e) => MapEntry(e.id, e))); + // Implement a more efficient containsKey method than MapBase, which + // checks `keys.contains(key)`. In our case, this causes an iteration over + // all items of [_lru], which can be avoided simply by checking the + // [_entries] map. + @override + bool containsKey(Object? key) => _entries.containsKey(key); } diff --git a/lib/src/client.dart b/lib/src/client.dart index c13021bec..1ef0af8e8 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1,6 +1,10 @@ +import 'dart:async'; + import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:nyxx/src/builders/presence.dart'; import 'package:nyxx/src/builders/voice.dart'; +import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/client_options.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/event_mixin.dart'; @@ -29,10 +33,14 @@ Future _doConnect(ApiOptions apiOptions, ClientOptions client } } + final originalConnect = connect; + connect = plugins.fold( - connect, + () async => await originalConnect() + .._initializedCompleter.complete(), (previousConnect, plugin) => () async => actualClientType.castInstance(await plugin.doConnect(apiOptions, clientOptions, previousConnect)), ); + return connect(); } @@ -45,6 +53,13 @@ Future _doClose(Nyxx client, Future Function() close, List get initialized => _initializedCompleter.future; +} + /// The base class for clients interacting with the Discord API. abstract class Nyxx { /// The options this client will use when connecting to the API. @@ -59,6 +74,11 @@ abstract class Nyxx { /// The logger for this client. Logger get logger; + /// The cache manager for this client. + CacheManager get cache; + + Completer get _initializedCompleter; + /// Create an instance of [NyxxRest] that can perform requests to the HTTP API and is /// authenticated with a bot token. static Future connectRest(String token, {RestClientOptions options = const RestClientOptions()}) => @@ -82,10 +102,14 @@ abstract class Nyxx { /// Create an instance of [NyxxOAuth2] that can perform requests to the HTTP API and is /// authenticated with OAuth2 [Credentials]. + /// + /// Note that `client.user.id` will contain [Snowflake.zero] if there no `identify` scope. static Future connectOAuth2(Credentials credentials, {RestClientOptions options = const RestClientOptions()}) => connectOAuth2WithOptions(OAuth2ApiOptions(credentials: credentials), options); /// Create an instance of [NyxxOAuth2] using the provided options. + /// + /// Note that `client.user.id` will contain [Snowflake.zero] if there no `identify` scope. static Future connectOAuth2WithOptions(OAuth2ApiOptions apiOptions, [RestClientOptions clientOptions = const RestClientOptions()]) async { clientOptions.logger ..info('Connecting to the REST API via OAuth2') @@ -94,10 +118,11 @@ abstract class Nyxx { return _doConnect(apiOptions, clientOptions, () async { final client = NyxxOAuth2._(apiOptions, clientOptions); + final information = await client.users.fetchCurrentOAuth2Information(); return client - .._application = await client.applications.fetchCurrentApplication() - .._user = await client.users.fetchCurrentUser(); + .._application = information.application + .._user = information.user ?? PartialUser(id: Snowflake.zero, manager: client.users); }, clientOptions.plugins); } @@ -163,6 +188,12 @@ class NyxxRest with ManagerMixin implements Nyxx { @override Logger get logger => options.logger; + @override + final Completer _initializedCompleter = Completer(); + + @override + late final CacheManager cache = CacheManager(this); + NyxxRest._(this.apiOptions, this.options); /// Add the current user to the thread with the ID [id]. @@ -180,7 +211,7 @@ class NyxxRest with ManagerMixin implements Nyxx { Future leaveThread(Snowflake id) => channels.leaveThread(id); /// List the guilds the current user is a member of. - Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => + Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => users.listCurrentUserGuilds(before: before, after: after, limit: limit); @override @@ -215,6 +246,12 @@ class NyxxOAuth2 with ManagerMixin implements NyxxRest { @override late final PartialUser _user; + @override + final Completer _initializedCompleter = Completer(); + + @override + late final CacheManager cache = CacheManager(this); + NyxxOAuth2._(this.apiOptions, this.options); @override @@ -224,7 +261,7 @@ class NyxxOAuth2 with ManagerMixin implements NyxxRest { Future leaveThread(Snowflake id) => channels.leaveThread(id); @override - Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => + Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => users.listCurrentUserGuilds(before: before, after: after, limit: limit); @override @@ -265,6 +302,12 @@ class NyxxGateway with ManagerMixin, EventMixin implements NyxxRest { @override Logger get logger => options.logger; + @override + final Completer _initializedCompleter = Completer(); + + @override + late final CacheManager cache = CacheManager(this); + NyxxGateway._(this.apiOptions, this.options); @override @@ -274,7 +317,7 @@ class NyxxGateway with ManagerMixin, EventMixin implements NyxxRest { Future leaveThread(Snowflake id) => channels.leaveThread(id); @override - Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => + Future> listGuilds({Snowflake? before, Snowflake? after, int? limit}) => users.listCurrentUserGuilds(before: before, after: after, limit: limit); /// Update the client's voice state in the guild with the ID [guildId]. diff --git a/lib/src/client_options.dart b/lib/src/client_options.dart index 173a4bfa0..6c601f9c3 100644 --- a/lib/src/client_options.dart +++ b/lib/src/client_options.dart @@ -15,13 +15,45 @@ import 'package:nyxx/src/models/guild/member.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/message/message.dart'; import 'package:nyxx/src/models/role.dart'; +import 'package:nyxx/src/models/sku.dart'; import 'package:nyxx/src/models/sticker/global_sticker.dart'; import 'package:nyxx/src/models/sticker/guild_sticker.dart'; +import 'package:nyxx/src/models/subscription.dart'; import 'package:nyxx/src/models/user/user.dart'; import 'package:nyxx/src/models/voice/voice_state.dart'; import 'package:nyxx/src/models/webhook.dart'; import 'package:nyxx/src/plugin/plugin.dart'; +/// The default [CacheConfig]. +/// +/// 2500 is a relatively arbitrary value that corresponds to the maximum number +/// of guilds a single shard can handle. This doesn't mean a client can cache +/// all the guilds it is in as it may have more than one shard. However, 2500 +/// is a large enough value that the cache will contain the most important +/// entities to avoid making network requests: +/// - The most active guilds. +/// - The most active channels. +/// - The most commonly used entitlements. +/// - (etc) +const _defaultCacheConfig = CacheConfig(maxSize: 2500); + +/// A [CacheConfig] with a smaller maximum size than [_defaultCacheConfig], +/// ideal for certain caches. +/// +/// Notably, the following types of caches may want to default to a smaller +/// maximum size: +/// - Caches that are not global (e.g that belong to a specific guild), as the +/// total number of entities cached is multiplied by the number of "parent" +/// entities. For example, setting the maximum member cache size to 100 on a +/// client which is in 1000 guilds means the total number of cached members +/// may still approach 100000. +/// - Caches that see a lot of ephemeral entries, for example the users cache. +/// Users are generally not needed over a long period of time, but rather for +/// a short burst (e.g when processing a command) before being discarded. We +/// don't need to cache these entities longer than the operations they are +/// involved in. +const _smallCacheConfig = CacheConfig(maxSize: 100); + /// Options for controlling the behavior of a [Nyxx] client. abstract class ClientOptions { /// The plugins to use for this client. @@ -33,8 +65,20 @@ abstract class ClientOptions { /// The logger to use for this client. Logger get logger => Logger(loggerName); + /// The threshold after which a warning will be logged if a request is waiting for rate limits. + /// + /// If this value is `null`, no warnings are emitted when a long rate limit is encountered. + /// + /// This value is also used to prevent log spam. Requests will only emit a warning once per [rateLimitWarningThreshold], even if they are rate limited + /// multiple times during that period. + final Duration? rateLimitWarningThreshold; + /// Create a new [ClientOptions]. - const ClientOptions({this.plugins = const [], this.loggerName = 'Nyxx'}); + const ClientOptions({ + this.plugins = const [], + this.loggerName = 'Nyxx', + this.rateLimitWarningThreshold = const Duration(seconds: 10), + }); } /// Options for controlling the behavior of a [NyxxRest] client. @@ -84,7 +128,7 @@ class RestClientOptions extends ClientOptions { /// The [CacheConfig] to use for the [Guild.auditLogs] manager. final CacheConfig auditLogEntryConfig; - /// The [CacheConfig] to use for the [NyxxRest.voice] manager. + /// The [CacheConfig] to use for the [PartialGuild.voiceStates] cache. final CacheConfig voiceStateConfig; /// The [CacheConfig] to use for the [NyxxRest.commands] manager. @@ -96,33 +140,57 @@ class RestClientOptions extends ClientOptions { /// The [CacheConfig] to use for the [Application.entitlements] manager. final CacheConfig entitlementConfig; + /// The [CacheConfig] to use for the [Application.skus] manager. + final CacheConfig skuConfig; + + /// Tje [CacheConfig] to use for the [Sku.subscriptions] manager. + final CacheConfig subscriptionConfig; + /// Create a new [RestClientOptions]. const RestClientOptions({ super.plugins, super.loggerName, - this.userCacheConfig = const CacheConfig(), - this.channelCacheConfig = const CacheConfig(), - this.messageCacheConfig = const CacheConfig(), - this.webhookCacheConfig = const CacheConfig(), - this.guildCacheConfig = const CacheConfig(), - this.memberCacheConfig = const CacheConfig(), - this.roleCacheConfig = const CacheConfig(), - this.emojiCacheConfig = const CacheConfig(), - this.stageInstanceCacheConfig = const CacheConfig(), - this.scheduledEventCacheConfig = const CacheConfig(), - this.autoModerationRuleConfig = const CacheConfig(), - this.integrationConfig = const CacheConfig(), - this.auditLogEntryConfig = const CacheConfig(), - this.voiceStateConfig = const CacheConfig(), - this.stickerCacheConfig = const CacheConfig(), - this.globalStickerCacheConfig = const CacheConfig(), - this.applicationCommandConfig = const CacheConfig(), - this.commandPermissionsConfig = const CacheConfig(), - this.entitlementConfig = const CacheConfig(), + super.rateLimitWarningThreshold, + // Users are generally not needed over long periods of time; use a small + // cache. + this.userCacheConfig = _smallCacheConfig, + this.channelCacheConfig = _defaultCacheConfig, + // Messages are generally not needed over long periods of time and are + // cached per channel anyway; use a small cache. + this.messageCacheConfig = _smallCacheConfig, + this.webhookCacheConfig = _defaultCacheConfig, + this.guildCacheConfig = _defaultCacheConfig, + // Members are generally not needed over long periods of time and are + // cached per guild anyway; use a small cache. + this.memberCacheConfig = _smallCacheConfig, + // Guilds tend not to have a large amount of roles (relatively speaking), + // but we also want to access all of a guild's roles during e.g permission + // calculations; use a larger cache. + this.roleCacheConfig = _defaultCacheConfig, + // Bots don't tend to use emoji or sticker data. Don't bother caching too + // many. + this.emojiCacheConfig = _smallCacheConfig, + this.stickerCacheConfig = _smallCacheConfig, + // ...but there are not too many global stickers, and they are the most + // used, so use a larger cache for these. + this.globalStickerCacheConfig = _defaultCacheConfig, + this.stageInstanceCacheConfig = _defaultCacheConfig, + this.scheduledEventCacheConfig = _smallCacheConfig, + this.autoModerationRuleConfig = _smallCacheConfig, + this.integrationConfig = _smallCacheConfig, + this.auditLogEntryConfig = _smallCacheConfig, + // Voice states are saved per guild, so we don't need to store large amount + // of them at a time. + this.voiceStateConfig = _smallCacheConfig, + this.applicationCommandConfig = _defaultCacheConfig, + this.commandPermissionsConfig = _smallCacheConfig, + this.entitlementConfig = _defaultCacheConfig, + this.skuConfig = _defaultCacheConfig, + this.subscriptionConfig = _defaultCacheConfig, }); } -/// Options for controlling the behavior of a [NyxxWebsocket] client. +/// Options for controlling the behavior of a [NyxxGateway] client. class GatewayClientOptions extends RestClientOptions { /// The minimum number of session starts this client needs to connect. /// @@ -136,6 +204,7 @@ class GatewayClientOptions extends RestClientOptions { this.minimumSessionStarts = 10, super.plugins, super.loggerName, + super.rateLimitWarningThreshold, super.userCacheConfig, super.channelCacheConfig, super.messageCacheConfig, @@ -152,5 +221,9 @@ class GatewayClientOptions extends RestClientOptions { super.applicationCommandConfig, super.commandPermissionsConfig, super.entitlementConfig, + super.skuConfig, + super.emojiCacheConfig, + super.globalStickerCacheConfig, + super.stickerCacheConfig, }); } diff --git a/lib/src/errors.dart b/lib/src/errors.dart index c94dfef36..62d01fb8c 100644 --- a/lib/src/errors.dart +++ b/lib/src/errors.dart @@ -81,6 +81,18 @@ class EntitlementNotFoundException extends NyxxException { EntitlementNotFoundException(this.applicationId, this.entitlementId) : super('Entitlement $entitlementId not found for application $applicationId'); } +/// An exception thrown when an SKU is not found for an application. +class SkuNotFoundException extends NyxxException { + /// The ID of the application. + final Snowflake applicationId; + + /// The ID of the sku. + final Snowflake skuId; + + /// Create a new [skuNotFoundException]. + SkuNotFoundException(this.applicationId, this.skuId) : super('SKU $skuId not found for application $applicationId'); +} + /// An error thrown when a shard disconnects unexpectedly. class ShardDisconnectedError extends Error { /// The shard that was disconnected. @@ -139,3 +151,9 @@ class PluginError extends Error { @override String toString() => message; } + +/// An error thrown when the client is closed while an operation is pending, or when an already closed client is used. +class ClientClosedError extends Error { + @override + String toString() => 'Client is closed'; +} diff --git a/lib/src/event_mixin.dart b/lib/src/event_mixin.dart index 5d9c3e3dd..5912958e5 100644 --- a/lib/src/event_mixin.dart +++ b/lib/src/event_mixin.dart @@ -255,4 +255,10 @@ mixin EventMixin implements Nyxx { /// A [Stream] of [ApplicationCommandAutocompleteInteraction]s received by this client. Stream> get onApplicationCommandAutocompleteInteraction => onInteractionCreate.whereType>(); + + /// A [Stream] of [MessagePollVoteAddEvent]s received by this client. + Stream get onMessagePollVoteAdd => onEvent.whereType(); + + /// A [Stream] of [MessagePollVoteRemoveEvent]s received by this client. + Stream get onMessagePollVoteRemove => onEvent.whereType(); } diff --git a/lib/src/gateway/event_parser.dart b/lib/src/gateway/event_parser.dart index cc07aed1b..bf72767f3 100644 --- a/lib/src/gateway/event_parser.dart +++ b/lib/src/gateway/event_parser.dart @@ -11,7 +11,7 @@ mixin class EventParser { Opcode.reconnect.value: parseReconnect, Opcode.invalidSession.value: parseInvalidSession, Opcode.hello.value: parseHello, - Opcode.heartbeatAck.value: (Map raw) => parseHeartbeatAck(raw, heartbeatLatency: heartbeatLatency!), + Opcode.heartbeatAck.value: (Map raw) => parseHeartbeatAck(raw, heartbeatLatency: heartbeatLatency ?? Duration.zero), }; return mapping[raw['op'] as int]!(raw); diff --git a/lib/src/gateway/gateway.dart b/lib/src/gateway/gateway.dart index bce20be13..226a8ca0c 100644 --- a/lib/src/gateway/gateway.dart +++ b/lib/src/gateway/gateway.dart @@ -4,7 +4,6 @@ import 'package:logging/logging.dart'; import 'package:nyxx/src/api_options.dart'; import 'package:nyxx/src/builders/presence.dart'; import 'package:nyxx/src/builders/voice.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/client.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/gateway/event_parser.dart'; @@ -42,7 +41,7 @@ import 'package:nyxx/src/models/interaction.dart'; import 'package:nyxx/src/models/presence.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; -import 'package:nyxx/src/utils/iterable_extension.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// Handles the connection to Discord's Gateway with shards, manages the client's cache based on Gateway events and provides an interface to the Gateway. @@ -63,23 +62,29 @@ class Gateway extends GatewayManager with EventParser { final List shards; /// A stream of messages received from all shards. - Stream get messages => _messagesController.stream; + // Adapting _messagesController.stream to a broadcast stream instead of + // simply making _messagesController a broadcast controller means events will + // be buffered until this field is initialized, which prevents events from + // being dropped during the connection process. + late final Stream messages = _messagesController.stream.asBroadcastStream(); - final StreamController _messagesController = StreamController.broadcast(); + final StreamController _messagesController = StreamController(); /// A stream of dispatch events received from all shards. - Stream get events => messages.map((message) { - if (message is! EventReceived) { - return null; - } - - final event = message.event; - if (event is! RawDispatchEvent) { - return null; - } - - return parseDispatchEvent(event); - }).whereType(); + // Make this late instead of a getter so only a single subscription is made, which prevents events from being parsed multiple times. + late final Stream events = messages.transform(StreamTransformer.fromBind((messages) async* { + await for (final message in messages) { + if (message is! EventReceived) continue; + + final event = message.event; + if (event is! RawDispatchEvent) continue; + + final parsedEvent = parseDispatchEvent(event); + // Update the cache as needed. + client.updateCacheWith(parsedEvent); + yield parsedEvent; + } + })).asBroadcastStream(); bool _closing = false; @@ -88,16 +93,66 @@ class Gateway extends GatewayManager with EventParser { /// See [Shard.latency] for details on how the latency is calculated. Duration get latency => shards.fold(Duration.zero, (previousValue, element) => previousValue + (element.latency ~/ shards.length)); + final Set _startOrIdentifyTimers = {}; + /// Create a new [Gateway]. Gateway(this.client, this.gatewayBot, this.shards, this.totalShards, this.shardIds) : super.create() { + final logger = Logger('${client.options.loggerName}.Gateway'); + + // https://discord.com/developers/docs/topics/gateway#rate-limiting + const identifyDelay = Duration(seconds: 5); + final maxConcurrency = gatewayBot.sessionStartLimit.maxConcurrency; + var remainingIdentifyRequests = gatewayBot.sessionStartLimit.remaining; + + // A mapping of rateLimitId (shard.id % maxConcurrency) to Futures that complete when the identify lock for that rate_limit_key is no longer used. + final identifyLocks = >{}; + + // Handle messages from the shards and start them according to their rate limit key. for (final shard in shards) { + final rateLimitKey = shard.id % maxConcurrency; + + // Delay the shard starting until it is (approximately) also ready to identify. + // This avoids opening many websocket connections simultaneously just to have most + // of them wait for their identify request. + late final Timer startTimer; + startTimer = Timer(identifyDelay * (shard.id ~/ maxConcurrency), () { + logger.fine('Starting shard ${shard.id}'); + shard.add(StartShard()); + _startOrIdentifyTimers.remove(startTimer); + }); + _startOrIdentifyTimers.add(startTimer); + shard.listen( - (message) { - if (message is ErrorReceived) { - shard.logger.warning('Received error: ${message.error}', message.error, message.stackTrace); - } + (event) { + _messagesController.add(event); + + if (event is RequestingIdentify) { + final currentLock = identifyLocks[rateLimitKey] ?? Future.value(); + identifyLocks[rateLimitKey] = currentLock.then((_) async { + if (_closing) return; + + if (remainingIdentifyRequests < client.options.minimumSessionStarts * 5) { + logger.warning('$remainingIdentifyRequests session starts remaining'); + } + + if (remainingIdentifyRequests < client.options.minimumSessionStarts) { + await client.close(); + throw OutOfRemainingSessionsError(gatewayBot); + } - _messagesController.add(message); + remainingIdentifyRequests--; + shard.add(Identify()); + + // Don't use Future.delayed so that we can exit early if close() is called. + // If we use Future.delayed, the program will remain alive until it is complete, even if nothing is waiting on it. + // This code is roughly equivalent to `await Future.delayed(identifyDelay)` + final delayCompleter = Completer(); + final delayTimer = Timer(identifyDelay, delayCompleter.complete); + _startOrIdentifyTimers.add(delayTimer); + await delayCompleter.future; + _startOrIdentifyTimers.remove(delayTimer); + }); + } }, onError: _messagesController.addError, onDone: () async { @@ -109,87 +164,9 @@ class Gateway extends GatewayManager with EventParser { throw ShardDisconnectedError(shard); }, + cancelOnError: false, ); } - - // Handle all events which should update cache. - events.listen((event) => switch (event) { - ReadyEvent(:final user) => client.users.cache[user.id] = user, - ChannelCreateEvent(:final channel) || ChannelUpdateEvent(:final channel) => client.channels.cache[channel.id] = channel, - ChannelDeleteEvent(:final channel) => client.channels.cache.remove(channel.id), - ThreadCreateEvent(:final thread) || ThreadUpdateEvent(:final thread) => client.channels.cache[thread.id] = thread, - ThreadDeleteEvent(:final thread) => client.channels.cache.remove(thread.id), - ThreadListSyncEvent(:final threads) => client.channels.cache..addEntities(threads), - final GuildCreateEvent event => () { - client.guilds.cache[event.guild.id] = event.guild; - - event.guild.members.cache.addEntities(event.members); - client.channels.cache.addEntities(event.channels); - client.channels.cache.addEntities(event.threads); - client.channels.stageInstanceCache.addEntities(event.stageInstances); - event.guild.scheduledEvents.cache.addEntities(event.scheduledEvents); - client.voice.cache.addEntries(event.voiceStates.map((e) => MapEntry(e.cacheKey, e))); - }(), - GuildUpdateEvent(:final guild) => client.guilds.cache[guild.id] = guild, - GuildDeleteEvent(:final guild, isUnavailable: false) => client.guilds.cache.remove(guild.id), - GuildMemberAddEvent(:final guildId, :final member) || - GuildMemberUpdateEvent(:final guildId, :final member) => - client.guilds[guildId].members.cache[member.id] = member, - GuildMemberRemoveEvent(:final guildId, :final user) => client.guilds[guildId].members.cache.remove(user.id), - GuildMembersChunkEvent(:final guildId, :final members) => client.guilds[guildId].members.cache..addEntities(members), - GuildRoleCreateEvent(:final guildId, :final role) || - GuildRoleUpdateEvent(:final guildId, :final role) => - client.guilds[guildId].roles.cache[role.id] = role, - GuildRoleDeleteEvent(:final guildId, :final roleId) => client.guilds[guildId].roles.cache.remove(roleId), - MessageCreateEvent(:final message) => (client.channels[message.channelId] as PartialTextChannel).messages.cache[message.id] = message, - MessageDeleteEvent(id: final messageId, :final channelId) => - MessageManager(client.options.messageCacheConfig, client, channelId: channelId).cache.remove(messageId), - MessageBulkDeleteEvent(ids: final messageIds, :final channelId) => - // ignore: avoid_function_literals_in_foreach_calls - messageIds..forEach((messageId) => MessageManager(client.options.messageCacheConfig, client, channelId: channelId).cache.remove(messageId)), - UserUpdateEvent(:final user) => client.users.cache[user.id] = user, - StageInstanceCreateEvent(:final instance) || StageInstanceUpdateEvent(:final instance) => client.channels.stageInstanceCache[instance.channelId] = - instance, - StageInstanceDeleteEvent(:final instance) => client.channels.stageInstanceCache.remove(instance.channelId), - GuildScheduledEventCreateEvent(:final event) || - GuildScheduledEventUpdateEvent(:final event) => - client.guilds[event.guildId].scheduledEvents.cache[event.id] = event, - GuildScheduledEventDeleteEvent(:final event) => client.guilds[event.guildId].scheduledEvents.cache.remove(event.id), - AutoModerationRuleCreateEvent(:final rule) || - AutoModerationRuleUpdateEvent(:final rule) => - client.guilds[rule.guildId].autoModerationRules.cache[rule.id] = rule, - AutoModerationRuleDeleteEvent(:final rule) => client.guilds[rule.guildId].autoModerationRules.cache.remove(rule.id), - IntegrationCreateEvent(:final guildId, :final integration) || - IntegrationUpdateEvent(:final guildId, :final integration) => - client.guilds[guildId].integrations.cache[integration.id] = integration, - IntegrationDeleteEvent(:final id, :final guildId) => client.guilds[guildId].integrations.cache.remove(id), - GuildAuditLogCreateEvent(:final entry, :final guildId) => client.guilds[guildId].auditLogs.cache[entry.id] = entry, - VoiceStateUpdateEvent(:final state) => client.voice.cache[state.cacheKey] = state, - GuildEmojisUpdateEvent(:final guildId, :final emojis) => client.guilds[guildId].emojis.cache - ..clear() - ..addEntities(emojis), - GuildStickersUpdateEvent(:final guildId, :final stickers) => client.guilds[guildId].stickers.cache.addEntities(stickers), - ApplicationCommandPermissionsUpdateEvent(:final permissions) => client.guilds[permissions.guildId].commands.permissionsCache[permissions.id] = - permissions, - InteractionCreateEvent(interaction: Interaction(:final guildId, data: ApplicationCommandInteractionData(resolved: final data?))) => () { - if (data.users != null) { - client.users.cache.addAll(data.users!); - } - - if (data.members != null && guildId != null) { - client.guilds[guildId].members.cache.addAll(data.members!); - } - - if (data.roles != null && guildId != null) { - client.guilds[guildId].roles.cache.addAll(data.roles!); - } - }(), - EntitlementCreateEvent(:final entitlement) || - EntitlementUpdateEvent(:final entitlement) => - client.applications[entitlement.applicationId].entitlements.cache[entitlement.id] = entitlement, - EntitlementDeleteEvent() => null, // TODO - _ => null, - }); } /// Connect to the gateway using the provided [client] and [gatewayBot] configuration. @@ -207,14 +184,6 @@ class Gateway extends GatewayManager with EventParser { ' Remaining Session Starts: ${gatewayBot.sessionStartLimit.remaining}, Reset After: ${gatewayBot.sessionStartLimit.resetAfter}', ); - if (gatewayBot.sessionStartLimit.remaining < 50) { - logger.warning('${gatewayBot.sessionStartLimit.remaining} session starts remaining'); - } - - if (gatewayBot.sessionStartLimit.remaining < client.options.minimumSessionStarts) { - throw OutOfRemainingSessionsError(gatewayBot); - } - assert( shardIds.every((element) => element < totalShards), 'Shard ID exceeds total shard count', @@ -235,23 +204,17 @@ class Gateway extends GatewayManager with EventParser { 'Cannot enable payload compression when using the ETF payload format', ); - const identifyDelay = Duration(seconds: 5); - - final shards = shardIds.indexed.map(((int, int) info) { - final (index, id) = info; - - return Future.delayed( - identifyDelay * (index ~/ gatewayBot.sessionStartLimit.maxConcurrency), - () => Shard.connect(id, totalShards, client.apiOptions, gatewayBot.url, client), - ); - }); - + final shards = shardIds.map((id) => Shard.connect(id, totalShards, client.apiOptions, gatewayBot.url, client)); return Gateway(client, gatewayBot, await Future.wait(shards), totalShards, shardIds); } /// Close this [Gateway] instance, disconnecting all shards and closing the event streams. Future close() async { _closing = true; + // Make sure we don't start any shards after we have closed. + for (final timer in _startOrIdentifyTimers) { + timer.cancel(); + } await Future.wait(shards.map((shard) => shard.close())); _messagesController.close(); } @@ -331,6 +294,8 @@ class Gateway extends GatewayManager with EventParser { 'ENTITLEMENT_CREATE': parseEntitlementCreate, 'ENTITLEMENT_UPDATE': parseEntitlementUpdate, 'ENTITLEMENT_DELETE': parseEntitlementDelete, + 'MESSAGE_POLL_VOTE_ADD': parseMessagePollVoteAdd, + 'MESSAGE_POLL_VOTE_REMOVE': parseMessagePollVoteRemove, }; return mapping[raw.name]?.call(raw.payload) ?? UnknownDispatchEvent(gateway: this, raw: raw); @@ -417,7 +382,7 @@ class Gateway extends GatewayManager with EventParser { guildId: guildId, action: client.guilds[guildId].autoModerationRules.parseAutoModerationAction(raw['action'] as Map), ruleId: Snowflake.parse(raw['rule_id']!), - triggerType: TriggerType.parse(raw['rule_trigger_type'] as int), + triggerType: TriggerType(raw['rule_trigger_type'] as int), userId: Snowflake.parse(raw['user_id']!), channelId: maybeParse(raw['channel_id'], Snowflake.parse), messageId: maybeParse(raw['message_id'], Snowflake.parse), @@ -476,12 +441,15 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [ThreadDeleteEvent] from [raw]. ThreadDeleteEvent parseThreadDelete(Map raw) { + final thread = PartialChannel( + id: Snowflake.parse(raw['id']!), + manager: client.channels, + ); + return ThreadDeleteEvent( gateway: this, - thread: PartialChannel( - id: Snowflake.parse(raw['id']!), - manager: client.channels, - ), + thread: thread, + deletedThread: client.channels.cache[thread.id] as Thread?, ); } @@ -497,26 +465,31 @@ class Gateway extends GatewayManager with EventParser { raw['threads'] as List, (Map raw) => client.channels.parse(raw, guildId: guildId) as Thread, ), - members: parseMany(raw['members'] as List, client.channels.parseThreadMember), + members: parseMany(raw['members'] as List, (Map raw) => client.channels.parseThreadMember(raw, guildId: guildId)), ); } /// Parse a [ThreadMemberUpdateEvent] from [raw]. ThreadMemberUpdateEvent parseThreadMemberUpdate(Map raw) { + final guildId = Snowflake.parse(raw['guild_id']!); + return ThreadMemberUpdateEvent( gateway: this, - member: client.channels.parseThreadMember(raw), + member: client.channels.parseThreadMember(raw, guildId: guildId), + guildId: guildId, ); } /// Parse a [ThreadMembersUpdateEvent] from [raw]. ThreadMembersUpdateEvent parseThreadMembersUpdate(Map raw) { + final guildId = Snowflake.parse(raw['guild_id']!); + return ThreadMembersUpdateEvent( gateway: this, id: Snowflake.parse(raw['id']!), - guildId: Snowflake.parse(raw['guild_id']!), + guildId: guildId, memberCount: raw['member_count'] as int, - addedMembers: maybeParseMany(raw['added_members'], client.channels.parseThreadMember), + addedMembers: maybeParseMany(raw['added_members'], (Map raw) => client.channels.parseThreadMember(raw, guildId: guildId)), removedMemberIds: maybeParseMany(raw['removed_member_ids'], Snowflake.parse), ); } @@ -545,7 +518,10 @@ class Gateway extends GatewayManager with EventParser { joinedAt: DateTime.parse(raw['joined_at'] as String), isLarge: raw['large'] as bool, memberCount: raw['member_count'] as int, - voiceStates: parseMany(raw['voice_states'] as List, client.voice.parseVoiceState), + voiceStates: parseMany( + raw['voice_states'] as List, + (Map raw) => client.voice.parseVoiceState(raw, guildId: guild.id), + ), members: parseMany(raw['members'] as List, client.guilds[guild.id].members.parse), channels: parseMany(raw['channels'] as List, (Map raw) => client.channels.parse(raw, guildId: guild.id) as GuildChannel), threads: parseMany(raw['threads'] as List, (Map raw) => client.channels.parse(raw, guildId: guild.id) as Thread), @@ -568,10 +544,13 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [GuildDeleteEvent] from [raw]. GuildDeleteEvent parseGuildDelete(Map raw) { + final id = Snowflake.parse(raw['id']!); + return GuildDeleteEvent( gateway: this, - guild: PartialGuild(id: Snowflake.parse(raw['id']!), manager: client.guilds), - isUnavailable: raw['unavailable'] as bool, + guild: PartialGuild(id: id, manager: client.guilds), + isUnavailable: raw['unavailable'] as bool? ?? false, + deletedGuild: client.guilds.cache[id], ); } @@ -647,10 +626,14 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [GuildMemberRemoveEvent] from [raw]. GuildMemberRemoveEvent parseGuildMemberRemove(Map raw) { + final guildId = Snowflake.parse(raw['guild_id']!); + final user = client.users.parse(raw['user'] as Map); + return GuildMemberRemoveEvent( gateway: this, - guildId: Snowflake.parse(raw['guild_id']!), - user: client.users.parse(raw['user'] as Map), + guildId: guildId, + user: user, + removedMember: client.guilds[guildId].members.cache[user.id], ); } @@ -709,10 +692,14 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [GuildRoleDeleteEvent] from [raw]. GuildRoleDeleteEvent parseGuildRoleDelete(Map raw) { + final roleId = Snowflake.parse(raw['role_id']!); + final guildId = Snowflake.parse(raw['guild_id']!); + return GuildRoleDeleteEvent( gateway: this, - roleId: Snowflake.parse(raw['role_id']!), - guildId: Snowflake.parse(raw['guild_id']!), + roleId: roleId, + guildId: guildId, + deletedRole: client.guilds[guildId].roles.cache[roleId], ); } @@ -794,11 +781,15 @@ class Gateway extends GatewayManager with EventParser { /// Parse an [IntegrationDeleteEvent] from [raw]. IntegrationDeleteEvent parseIntegrationDelete(Map raw) { + final guildId = Snowflake.parse(raw['guild_id']!); + final id = Snowflake.parse(raw['id']!); + return IntegrationDeleteEvent( gateway: this, - id: Snowflake.parse(raw['id']!), - guildId: Snowflake.parse(raw['guild_id']!), + id: id, + guildId: guildId, applicationId: maybeParse(raw['application_id'], Snowflake.parse), + deletedIntegration: client.guilds[guildId].integrations.cache[id], ); } @@ -861,7 +852,7 @@ class Gateway extends GatewayManager with EventParser { raw['member'], (Map _) => PartialMember( id: Snowflake.parse((raw['author'] as Map)['id']!), - manager: client.guilds[guildId ?? Snowflake.zero].members, + manager: client.guilds[guildId!].members, ), ), mentions: maybeParseMany(raw['mentions'], client.users.parse), @@ -872,20 +863,28 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [MessageDeleteEvent] from [raw]. MessageDeleteEvent parseMessageDelete(Map raw) { + final id = Snowflake.parse(raw['id']!); + final channelId = Snowflake.parse(raw['channel_id']!); + return MessageDeleteEvent( gateway: this, - id: Snowflake.parse(raw['id']!), - channelId: Snowflake.parse(raw['channel_id']!), + id: id, + channelId: channelId, guildId: maybeParse(raw['guild_id'], Snowflake.parse), + deletedMessage: (client.channels[channelId] as PartialTextChannel).messages.cache[id], ); } /// Parse a [MessageBulkDeleteEvent] from [raw]. MessageBulkDeleteEvent parseMessageBulkDelete(Map raw) { + final ids = parseMany(raw['ids'] as List, Snowflake.parse); + final channelId = Snowflake.parse(raw['channel_id']!); + return MessageBulkDeleteEvent( gateway: this, - ids: parseMany(raw['ids'] as List, Snowflake.parse), - channelId: Snowflake.parse(raw['channel_id']!), + ids: ids, + deletedMessages: ids.map((id) => (client.channels[channelId] as PartialTextChannel).messages.cache[id]).nonNulls.toList(), + channelId: channelId, guildId: maybeParse(raw['guild_id'], Snowflake.parse), ); } @@ -893,16 +892,19 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [MessageReactionAddEvent] from [raw]. MessageReactionAddEvent parseMessageReactionAdd(Map raw) { final guildId = maybeParse(raw['guild_id'], Snowflake.parse); + final userId = Snowflake.parse(raw['user_id']!); return MessageReactionAddEvent( - gateway: this, - userId: Snowflake.parse(raw['user_id']!), - channelId: Snowflake.parse(raw['channel_id']!), - messageId: Snowflake.parse(raw['message_id']!), - guildId: guildId, - member: maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse), - emoji: client.guilds[Snowflake.zero].emojis.parse(raw['emoji'] as Map), - messageAuthorId: maybeParse(raw['message_author_id'], Snowflake.parse)); + gateway: this, + userId: userId, + channelId: Snowflake.parse(raw['channel_id']!), + messageId: Snowflake.parse(raw['message_id']!), + guildId: guildId, + // Don't use a tearoff so we don't evaluate `guildId!` unless member is set. + member: maybeParse(raw['member'], (Map raw) => client.guilds[guildId!].members.parse(raw, userId: userId)), + emoji: client.guilds[Snowflake.zero].emojis.parse(raw['emoji'] as Map), + messageAuthorId: maybeParse(raw['message_author_id'], Snowflake.parse), + ); } /// Parse a [MessageReactionRemoveEvent] from [raw]. @@ -949,7 +951,7 @@ class Gateway extends GatewayManager with EventParser { (Map raw) => PartialUser(id: Snowflake.parse(raw['id']!), manager: client.users), ), guildId: maybeParse(raw['guild_id'], Snowflake.parse), - status: maybeParse(raw['status'], UserStatus.parse), + status: maybeParse(raw['status'], UserStatus.new), activities: maybeParseMany(raw['activities'], parseActivity), clientStatus: maybeParse(raw['client_status'], parseClientStatus), ); @@ -958,14 +960,16 @@ class Gateway extends GatewayManager with EventParser { /// Parse a [TypingStartEvent] from [raw]. TypingStartEvent parseTypingStart(Map raw) { var guildId = maybeParse(raw['guild_id'], Snowflake.parse); + final userId = Snowflake.parse(raw['user_id']!); return TypingStartEvent( gateway: this, channelId: Snowflake.parse(raw['channel_id']!), guildId: guildId, - userId: Snowflake.parse(raw['user_id']!), + userId: userId, timestamp: DateTime.fromMillisecondsSinceEpoch((raw['timestamp'] as int) * Duration.millisecondsPerSecond), - member: maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse), + // Don't use a tearoff so we don't evaluate `guildId!` unless member is set. + member: maybeParse(raw['member'], (Map raw) => client.guilds[guildId!].members.parse(raw, userId: userId)), ); } @@ -986,7 +990,8 @@ class Gateway extends GatewayManager with EventParser { return VoiceStateUpdateEvent( gateway: this, - oldState: client.voice.cache[voiceState.cacheKey], + // guildId should never be null in VOICE_STATE_UPDATE. + oldState: client.guilds[voiceState.guildId!].voiceStates[voiceState.userId], state: voiceState, ); } @@ -1024,6 +1029,7 @@ class Gateway extends GatewayManager with EventParser { InteractionType.modalSubmit => InteractionCreateEvent(gateway: this, interaction: interaction as ModalSubmitInteraction), InteractionType.applicationCommandAutocomplete => InteractionCreateEvent(gateway: this, interaction: interaction as ApplicationCommandAutocompleteInteraction), + InteractionType() => throw StateError('Unknown interaction type: ${interaction.type}'), } as InteractionCreateEvent>; } @@ -1078,7 +1084,38 @@ class Gateway extends GatewayManager with EventParser { /// Parse an [EntitlementDeleteEvent] from [raw]. EntitlementDeleteEvent parseEntitlementDelete(Map raw) { - return EntitlementDeleteEvent(gateway: this); + final applicationId = Snowflake.parse(raw['application_id']!); + final entitlement = client.applications[applicationId].entitlements.parse(raw); + + return EntitlementDeleteEvent( + gateway: this, + entitlement: entitlement, + deletedEntitlement: client.applications[applicationId].entitlements.cache[entitlement.id], + ); + } + + /// Parse an [MessagePollVoteAddEvent] from [raw]. + MessagePollVoteAddEvent parseMessagePollVoteAdd(Map raw) { + return MessagePollVoteAddEvent( + gateway: this, + userId: Snowflake.parse(raw['user_id']!), + channelId: Snowflake.parse(raw['channel_id']!), + messageId: Snowflake.parse(raw['message_id']!), + guildId: maybeParse(raw['guild_id'], Snowflake.parse), + answerId: raw['answer_id'] as int, + ); + } + + /// Parse an [MessagePollVoteRemoveEvent] from [raw]. + MessagePollVoteRemoveEvent parseMessagePollVoteRemove(Map raw) { + return MessagePollVoteRemoveEvent( + gateway: this, + userId: Snowflake.parse(raw['user_id']!), + channelId: Snowflake.parse(raw['channel_id']!), + messageId: Snowflake.parse(raw['message_id']!), + guildId: maybeParse(raw['guild_id'], Snowflake.parse), + answerId: raw['answer_id'] as int, + ); } /// Stream all members in a guild that match [query] or [userIds]. diff --git a/lib/src/gateway/message.dart b/lib/src/gateway/message.dart index 1da7da02e..a86858e6a 100644 --- a/lib/src/gateway/message.dart +++ b/lib/src/gateway/message.dart @@ -61,6 +61,18 @@ class Disconnecting extends ShardMessage { Disconnecting({required this.reason}); } +/// A shard message sent when the shard adds a payload to the connection. +class Sent extends ShardMessage { + /// The payload that was sent. + final Send payload; + + /// Create a new [Sent]. + Sent({required this.payload}); +} + +/// A shard message sent when the shard is waiting to identify on the Gateway. +class RequestingIdentify extends ShardMessage {} + /// The base class for all control messages sent from the client to the shard. abstract class GatewayMessage with ToStringHelper {} @@ -76,6 +88,12 @@ class Send extends GatewayMessage { Send({required this.opcode, required this.data}); } +/// A gateway message sent when the [Gateway] instance is ready for the shard to start. +class StartShard extends GatewayMessage {} + +/// A gateway message sent as a response to [RequestingIdentify] to allow the shard to identify. +class Identify extends GatewayMessage {} + /// A gateway message sent to instruct the shard to disconnect & stop handling any further messages. /// /// The shard can no longer be used after this is sent. diff --git a/lib/src/gateway/shard.dart b/lib/src/gateway/shard.dart index f70697f8a..46f3333f9 100644 --- a/lib/src/gateway/shard.dart +++ b/lib/src/gateway/shard.dart @@ -24,9 +24,14 @@ class Shard extends Stream implements StreamSink { /// The stream on which events from the runner are received. final Stream receiveStream; + final StreamController _rawReceiveController = StreamController(); + final StreamController _transformedReceiveController = StreamController.broadcast(); + /// The port on which events are sent to the runner. final SendPort sendPort; + final StreamController _sendController = StreamController(); + /// The client this [Shard] is for. final NyxxGateway client; @@ -44,8 +49,28 @@ class Shard extends Stream implements StreamSink { /// Create a new [Shard]. Shard(this.id, this.isolate, this.receiveStream, this.sendPort, this.client) { + client.initialized.then((_) { + final sendStream = client.options.plugins.fold( + _sendController.stream, + (previousValue, plugin) => plugin.interceptGatewayMessages(this, previousValue), + ); + sendStream.listen(sendPort.send, cancelOnError: false, onDone: close); + + final transformedReceiveStream = client.options.plugins.fold( + _rawReceiveController.stream, + (previousValue, plugin) => plugin.interceptShardMessages(this, previousValue), + ); + transformedReceiveStream.pipe(_transformedReceiveController); + }); + + receiveStream.cast().pipe(_rawReceiveController); + final subscription = listen((message) { - if (message is ErrorReceived) { + if (message is Sent) { + logger + ..fine('Sent payload: ${message.payload.opcode.name}') + ..finer('Opcode: ${message.payload.opcode.value}, Data: ${message.payload.data}'); + } else if (message is ErrorReceived) { logger.warning('Error: ${message.error}', message.error, message.stackTrace); } else if (message is Disconnecting) { logger.info('Disconnecting: ${message.reason}'); @@ -61,7 +86,7 @@ class Shard extends Stream implements StreamSink { if (isResumable) { logger.info('Reconnecting: invalid session'); } else { - logger.severe('Unresumable invalid session, disconnecting'); + logger.warning('Reconnecting: unresumable invalid session'); } case HelloEvent(:final heartbeatInterval): logger.finest('Heartbeat Interval: $heartbeatInterval'); @@ -83,6 +108,8 @@ class Shard extends Stream implements StreamSink { logger.info('Reconnected to Gateway'); } } + } else if (message is RequestingIdentify) { + logger.fine('Ready to identify'); } }); @@ -99,13 +126,14 @@ class Shard extends Stream implements StreamSink { static Future connect(int id, int totalShards, GatewayApiOptions apiOptions, Uri connectionUri, NyxxGateway client) async { final logger = Logger('${client.options.loggerName}.Shards[$id]'); - logger.info('Connecting to Gateway'); - final receivePort = ReceivePort('Shard #$id message stream (main)'); final receiveStream = receivePort.asBroadcastStream(); + logger.fine('Spawning shard runner'); + final isolate = await Isolate.spawn( _isolateMain, + debugName: 'Shard #$id runner', _IsolateSpawnData( totalShards: totalShards, id: id, @@ -126,6 +154,8 @@ class Shard extends Stream implements StreamSink { final sendPort = await receiveStream.first as SendPort; + logger.fine('Shard runner ready'); + return Shard(id, isolate, receiveStream, sendPort, client); } @@ -141,12 +171,15 @@ class Shard extends Stream implements StreamSink { void add(GatewayMessage event) { if (event is Send) { logger - ..fine('Send: ${event.opcode.name}') + ..fine('Sending: ${event.opcode.name}') ..finer('Opcode: ${event.opcode.value}, Data: ${event.data}'); } else if (event is Dispose) { logger.info('Disposing'); + } else if (event is Identify) { + logger.info('Connecting to Gateway'); } - sendPort.send(event); + + _sendController.add(event); } @override @@ -156,7 +189,7 @@ class Shard extends Stream implements StreamSink { void Function()? onDone, bool? cancelOnError, }) { - return receiveStream.cast().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); + return _transformedReceiveController.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override @@ -168,12 +201,14 @@ class Shard extends Stream implements StreamSink { Future doClose() async { add(Dispose()); - // Wait for disconnection confirmation - await firstWhere((message) => message is Disconnecting); + _sendController.close(); + // _rawReceiveController and _transformedReceiveController are closed by the piped + // receive port stream being closed. // Give the isolate time to shut down cleanly, but kill it if it takes too long. try { - await drain().timeout(const Duration(seconds: 5)); + // Wait for disconnection confirmation. + await firstWhere((message) => message is Disconnecting).then(drain).timeout(const Duration(seconds: 5)); } on TimeoutException { logger.warning('Isolate took too long to shut down, killing it'); isolate.kill(priority: Isolate.immediate); diff --git a/lib/src/gateway/shard_runner.dart b/lib/src/gateway/shard_runner.dart index 80655bdab..756e08577 100644 --- a/lib/src/gateway/shard_runner.dart +++ b/lib/src/gateway/shard_runner.dart @@ -36,6 +36,9 @@ class ShardRunner { /// The stopwatch timing the interval between a heartbeat being sent and a heartbeat ACK being received. Stopwatch? heartbeatStopwatch; + /// The interval between two heartbeats. + Duration? heartbeatInterval; + /// Whether the current connection can be resumed. bool canResume = false; @@ -54,22 +57,68 @@ class ShardRunner { ShardRunner(this.data); /// Run the shard runner. - Stream run(Stream messages) { + Stream run(Stream messages) async* { + // Add messages to this controller for them to be sent back to the main isolate. final controller = StreamController(); - // The subscription to the control messages stream. - // This subscription is paused whenever the shard is not successfully connected,. - final controlSubscription = messages.listen((message) { - if (message is Send) { - connection!.add(message); + // sendHandler is responsible for handling requests for this shard to send messages to the Gateway. + // It is paused whenever this shard isn't ready to send messages. + final sendController = StreamController(); + final sendHandler = sendController.stream.listen((e) async { + try { + await connection!.add(e); + } catch (error, s) { + controller.add(ErrorReceived(error: error, stackTrace: s)); + + // Prevent the recursive call to add() from looping too often. + await Future.delayed(Duration(milliseconds: 100)); + // Try to send the event again, unless we are disposing (in which case the controller will be closed). + if (!sendController.isClosed) { + sendController.add(e); + } } + }) + ..pause(); + + // identifyController serves as a notification system for Identify messages. + // Any Identify messages received are added to this stream. + final identifyController = StreamController.broadcast(); + + // startCompleter is completed when the Gateway instance is ready for this shard to start. + final startCompleter = Completer(); - if (message is Dispose) { + final messageHandler = messages.listen((message) { + if (message is Send) { + sendController.add(message); + } else if (message is Identify) { + identifyController.add(message); + } else if (message is Dispose) { disposing = true; - connection!.close(); + connection?.close(); + + // We might get a dispose request while we are waiting to identify. + // Add an error to the identify stream so we break out of the wait. + identifyController.addError( + Exception('Out of remaining session starts'), + StackTrace.current, + ); + + // We need to start the shard to jump ahead to the check for exiting the shard. + if (!startCompleter.isCompleted) { + startCompleter.complete(StartShard()); + } + } else if (message is StartShard) { + if (startCompleter.isCompleted) { + controller.add(ErrorReceived( + error: StateError('Received StartShard when shard was already started'), + stackTrace: StackTrace.current, + )); + return; + } + + startCompleter.complete(message); } - }) - ..pause(); + }); /// The main connection loop. /// @@ -77,16 +126,20 @@ class ShardRunner { Future asyncRun() async { while (true) { try { + // Check for dispose requests. If we should be disposing, exit the loop. + // Do this now instead of after the connection is closed in case we get + // a dispose request before the shard is even started. + if (disposing) { + controller.add(Disconnecting(reason: 'Dispose requested')); + return; + } + // Initialize lastHeartbeatAcked to `true` so we don't immediately disconnect in heartbeat(). lastHeartbeatAcked = true; - // Pause the control subscription until we are connected. - if (!controlSubscription.isPaused) { - controlSubscription.pause(); - } - // Open the websocket connection. connection = await ShardConnection.connect(gatewayUri.toString(), this); + connection!.onSent.listen(controller.add); // Obtain the heartbeat interval from the HELLO event and start heartbeating. final hello = await connection!.first; @@ -95,23 +148,25 @@ class ShardRunner { } controller.add(EventReceived(event: hello)); - startHeartbeat(hello.heartbeatInterval); + heartbeatInterval = hello.heartbeatInterval; + startHeartbeat(); // If we can resume (the connection loop was restarted) and we have the information needed, try to resume. // Otherwise, identify. if (canResume && seq != null && sessionId != null) { - sendResume(); + await sendResume(); } else { - sendIdentify(); - } + // Request to identify and wait for the confirmation. + controller.add(RequestingIdentify()); + await identifyController.stream.first; - canResume = false; + await sendIdentify(); + } - // We are connected, start handling control messages. - controlSubscription.resume(); + canResume = true; // Handle events from the connection & forward them to the result controller. - final subscription = connection!.listen((event) { + final subscription = connection!.listen((event) async { if (event is RawDispatchEvent) { seq = event.seq; @@ -124,24 +179,30 @@ class ShardRunner { }); sessionId = event.payload['session_id'] as String; + + // We are connected, start handling send requests. + sendHandler.resume(); + } else if (event.name == 'RESUMED') { + sendHandler.resume(); } } else if (event is ReconnectEvent) { - canResume = true; - connection!.close(); + connection!.close(4000); } else if (event is InvalidSessionEvent) { - if (event.isResumable) { - canResume = true; - } else { + if (!event.isResumable) { canResume = false; gatewayUri = originalGatewayUri; } - connection!.close(); + connection!.close(4000); } else if (event is HeartbeatAckEvent) { lastHeartbeatAcked = true; heartbeatStopwatch = null; } else if (event is HeartbeatEvent) { - connection!.add(Send(opcode: Opcode.heartbeat, data: seq)); + try { + await connection!.add(Send(opcode: Opcode.heartbeat, data: seq)); + } on StateError { + // ignore: Connection closed while adding event. + } } controller.add(EventReceived(event: event)); @@ -150,44 +211,55 @@ class ShardRunner { // Wait for the current connection to end, either due to a remote close or due to us disconnecting. await subscription.asFuture(); - // If the disconnect was triggered by a dispose, don't try to reconnect. Exit the loop. - if (disposing) { - controller.add(Disconnecting(reason: 'Dispose requested')); - return; - } + // Check if we can resume based on close code if the connection was closed by Discord. + if (connection!.localCloseCode == null) { + // https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes + const resumableCodes = [null, 4000, 4001, 4002, 4003, 4005, 4008]; + const errorCodes = [4004, 4010, 4011, 4012, 4013, 4014]; - // Check if we can resume based on close code. - // A manual close where we set closeCode earlier would have a close code of 1000, so this - // doesn't change closeCode if we set it manually. - // 1001 is the close code used for a ping failure, so include it in the resumable codes. - const resumableCodes = [null, 1001, 4000, 4001, 4002, 4003, 4007, 4008, 4009]; - final closeCode = connection!.websocket.closeCode; - canResume = canResume || resumableCodes.contains(closeCode); - - // If we encounter a fatal error, exit the shard. - if (!canResume && (closeCode ?? 0) >= 4000) { - controller.add(Disconnecting(reason: 'Received error close code: $closeCode')); - return; + if (errorCodes.contains(connection!.remoteCloseCode)) { + controller.add(Disconnecting(reason: 'Received error close code: ${connection!.remoteCloseCode}')); + return; + } + + canResume = resumableCodes.contains(connection!.remoteCloseCode); + + controller.add(ErrorReceived( + error: 'Connection was closed with code ${connection!.remoteCloseCode}', + stackTrace: StackTrace.current, + )); } } catch (error, stackTrace) { controller.add(ErrorReceived(error: error, stackTrace: stackTrace)); + // Prevents the while-true loop from looping too often when no internet is available. + await Future.delayed(Duration(milliseconds: 100)); } finally { + // Pause the send subscription until we are connected again. + // The handler may already be paused if the error occurred before we had identified. + if (!sendHandler.isPaused) { + sendHandler.pause(); + } + // Reset connection properties. - connection?.close(); + await connection?.close(4000); connection = null; heartbeatTimer?.cancel(); heartbeatTimer = null; heartbeatStopwatch = null; + heartbeatInterval = null; } } } + await startCompleter.future; asyncRun().then((_) { controller.close(); - controlSubscription.cancel(); + sendController.close(); + identifyController.close(); + messageHandler.cancel(); }); - return controller.stream; + yield* controller.stream; } void heartbeat() { @@ -198,19 +270,18 @@ class ShardRunner { connection!.add(Send(opcode: Opcode.heartbeat, data: seq)); lastHeartbeatAcked = false; - heartbeatStopwatch = Stopwatch()..start(); } - void startHeartbeat(Duration heartbeatInterval) { - heartbeatTimer = Timer(heartbeatInterval * Random().nextDouble(), () { + void startHeartbeat() { + heartbeatTimer = Timer(heartbeatInterval! * Random().nextDouble(), () { heartbeat(); - heartbeatTimer = Timer.periodic(heartbeatInterval, (_) => heartbeat()); + heartbeatTimer = Timer.periodic(heartbeatInterval!, (_) => heartbeat()); }); } - void sendIdentify() { - connection!.add(Send( + Future sendIdentify() async { + await connection!.add(Send( opcode: Opcode.identify, data: { 'token': data.apiOptions.token, @@ -228,9 +299,9 @@ class ShardRunner { )); } - void sendResume() { + Future sendResume() async { assert(sessionId != null && seq != null); - connection!.add(Send( + await connection!.add(Send( opcode: Opcode.resume, data: { 'token': data.apiOptions.token, @@ -241,16 +312,59 @@ class ShardRunner { } } +/// Handles parsing/encoding & compression/decompression of events on a [WebSocket] connection to the Gateway. class ShardConnection extends Stream implements StreamSink { + /// The number of messages that can be sent per [rateLimitDuration]. + // https://discord.com/developers/docs/topics/gateway#rate-limiting + static const rateLimitCount = 120; + + /// The duration after which the rate limit resets. + static const rateLimitDuration = Duration(seconds: 60); + + /// The connection to the Gateway. final WebSocket websocket; + + /// A stream of parsed events received from the Gateway. final Stream events; + + /// The [ShardRunner] that created this connection. final ShardRunner runner; - ShardConnection(this.websocket, this.events, this.runner); + /// The code used to close this connection, or `null` if this connection is open or was closed by the remote server. + int? localCloseCode; + + /// The code used to close this connection by the remote server, or `null` if this connection is open or was closed by calling [close]. + int? get remoteCloseCode => localCloseCode == null ? websocket.closeCode : null; + + /// A stream on which [Sent] events are added. + Stream get onSent => _sentController.stream; + final StreamController _sentController = StreamController(); + + /// The predicted number of heartbeats per [rateLimitDuration]. + /// + /// The [rateLimitCount] is reduced by this value for any non heartbeat event so heartbeats can always be sent immediately. + int get rateLimitHeartbeatReservation => (rateLimitDuration.inMicroseconds / runner.heartbeatInterval!.inMicroseconds).ceil(); + + /// The number of events sent in the current [rateLimitDuration]. + int _currentRateLimitCount = 0; + + /// A completer that completes once the current [rateLimitDuration] has passed. + Completer _currentRateLimitEnd = Completer(); + + /// Handles resetting [_currentRateLimitCount] and [_currentRateLimitEnd]. + late final Timer _rateLimitResetTimer; + + ShardConnection(this.websocket, this.events, this.runner) { + _rateLimitResetTimer = Timer.periodic(rateLimitDuration, (timer) { + _currentRateLimitCount = 0; + _currentRateLimitEnd.complete(); + _currentRateLimitEnd = Completer(); + }); + websocket.done.then((_) => close()); + } static Future connect(String gatewayUri, ShardRunner runner) async { final connection = await WebSocket.connect(gatewayUri); - connection.pingInterval = const Duration(seconds: 20); final uncompressedStream = switch (runner.data.apiOptions.compression) { GatewayCompression.transport => decompressTransport(connection.cast>()), @@ -281,7 +395,7 @@ class ShardConnection extends Stream implements StreamSink { } @override - void add(Send event) { + Future add(Send event) async { final payload = { 'op': event.opcode.value, 'd': event.data, @@ -292,7 +406,23 @@ class ShardConnection extends Stream implements StreamSink { GatewayPayloadFormat.etf => eterl.pack(payload), }; + final rateLimitLimit = event.opcode == Opcode.heartbeat ? 0 : rateLimitHeartbeatReservation; + while (rateLimitCount - _currentRateLimitCount <= rateLimitLimit) { + try { + await _currentRateLimitEnd.future; + } catch (e) { + // Swap out stack trace so the error message makes more sense. + Error.throwWithStackTrace(e, StackTrace.current); + } + } + + if (event.opcode == Opcode.heartbeat) { + runner.heartbeatStopwatch = Stopwatch()..start(); + } + + _currentRateLimitCount++; websocket.add(encoded); + _sentController.add(Sent(payload: event)); } @override @@ -302,10 +432,22 @@ class ShardConnection extends Stream implements StreamSink { Future addStream(Stream stream) => stream.forEach(add); @override - Future close([int? code]) => websocket.close(code ?? 1000); + Future close([int code = 1000]) async { + localCloseCode ??= code; + + _rateLimitResetTimer.cancel(); + if (!_currentRateLimitEnd.isCompleted) { + _currentRateLimitEnd + // Install an error handler so the error is not counted as uncaught. + ..future.catchError((e) {}) + ..completeError(StateError('Connection is closed'), StackTrace.current); + } + await websocket.close(code); + await _sentController.close(); + } @override - Future get done => websocket.done; + Future get done => websocket.done.then((_) => _sentController.done); } Stream decompressTransport(Stream> raw) { diff --git a/lib/src/http/cdn/cdn_request.dart b/lib/src/http/cdn/cdn_request.dart index 15ee02180..50d9c23ca 100644 --- a/lib/src/http/cdn/cdn_request.dart +++ b/lib/src/http/cdn/cdn_request.dart @@ -10,6 +10,6 @@ class CdnRequest extends HttpRequest { @override BaseRequest prepare(Nyxx client) { - return Request(method, Uri.https(client.apiOptions.cdnHost, route.path)); + return Request(method, Uri.https(client.apiOptions.cdnHost, route.path, queryParameters)); } } diff --git a/lib/src/http/handler.dart b/lib/src/http/handler.dart index ae64923e3..2bbc34494 100644 --- a/lib/src/http/handler.dart +++ b/lib/src/http/handler.dart @@ -5,9 +5,11 @@ import 'package:http/http.dart' hide MultipartRequest; import 'package:logging/logging.dart'; import 'package:nyxx/src/api_options.dart'; import 'package:nyxx/src/client.dart'; +import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/bucket.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/response.dart'; +import 'package:nyxx/src/plugin/plugin.dart'; import 'package:nyxx/src/utils/iterable_extension.dart'; extension on HttpRequest { @@ -93,10 +95,39 @@ class HttpHandler { /// If no requests have been completed, this getter returns [Duration.zero]. Duration get realLatency => _realLatencies.isEmpty ? Duration.zero : (_realLatencies.reduce((a, b) => a + b) ~/ _realLatencies.length); + final Set> _pendingRateLimits = {}; + /// Create a new [HttpHandler]. /// /// {@macro http_handler} - HttpHandler(this.client); + HttpHandler(this.client) { + if (client.options.rateLimitWarningThreshold case final threshold?) { + onRateLimit.listen((info) { + final (:request, :delay, :isGlobal, :isAnticipated) = info; + final requestStopwatch = _latencyStopwatches[request]; + if (requestStopwatch == null) return; + + final totalDelay = requestStopwatch.elapsed + delay; + + // Prevent warnings being emitted too often. This limits warnings to once per [threshold]. + if (totalDelay.inMicroseconds ~/ threshold.inMicroseconds <= requestStopwatch.elapsedMicroseconds ~/ threshold.inMicroseconds) return; + + if (totalDelay > threshold) { + logger.warning( + '${request.loggingId} has been pending for ${requestStopwatch.elapsed} and will be sent in $delay due to rate limiting.' + ' The request will have been pending for $totalDelay.', + ); + if (isAnticipated) { + logger.info('This is a predicted rate limit and was anticipated based on previous responses'); + } else if (isGlobal) { + logger.info('This is a global rate limit and will apply to all requests for the next $delay'); + } else { + logger.info('This rate limit was returned by the API'); + } + } + }); + } + } /// Send [request] to the API and return the response. /// @@ -111,7 +142,17 @@ class HttpHandler { /// that of the second request. /// /// Otherwise, this method returns a [HttpResponseError]. + /// + /// This method calls [NyxxPlugin.interceptRequest] on all plugins registered to the [client] which may intercept the [request]. Future execute(HttpRequest request) async { + final executeFn = client.options.plugins.fold( + _execute, + (previousValue, plugin) => (request) => plugin.interceptRequest(client, request, previousValue), + ); + return await executeFn(request); + } + + Future _execute(HttpRequest request) async { logger ..fine(request.loggingId) ..finer( @@ -166,7 +207,19 @@ class HttpHandler { if (waitTime > Duration.zero) { logger.finer('Holding ${request.loggingId} for $waitTime'); _onRateLimitController.add((request: request, delay: waitTime, isGlobal: isGlobal, isAnticipated: true)); - await Future.delayed(waitTime); + + // Don't use Future.delayed so that we can exit early if close() is called. + // If we use Future.delayed, the program will remain alive until it is complete, even if nothing is waiting on it. + // This is roughly equivalent to `await Future.delayed(waitTime)` + final completer = Completer(); + final timer = Timer(waitTime, completer.complete); + _pendingRateLimits.add(completer); + try { + await completer.future; + } finally { + _pendingRateLimits.remove(completer); + timer.cancel(); + } } } while (waitTime > Duration.zero); @@ -243,7 +296,20 @@ class HttpHandler { } _onRateLimitController.add((request: request, delay: retryAfter, isGlobal: isGlobal, isAnticipated: false)); - return Future.delayed(retryAfter, () => execute(request)); + + // Don't use Future.delayed so that we can exit early if close() is called. + // If we use Future.delayed, the program will remain alive until it is complete, even if nothing is waiting on it. + // This is roughly equivalent to `return Future.delayed(retryAfter, () => execute(request))` + final completer = Completer(); + final timer = Timer(retryAfter, completer.complete); + _pendingRateLimits.add(completer); + try { + await completer.future; + return execute(request); + } finally { + _pendingRateLimits.remove(completer); + timer.cancel(); + } } on TypeError { logger.shout('Invalid rate limit body for ${request.loggingId}! Your client is probably cloudflare banned!'); } @@ -264,9 +330,19 @@ class HttpHandler { } void close() { + // Timers associated with these completers will be cancelled in + // the finally block from the try/catch that the completer is awaited in. + for (final completer in _pendingRateLimits) { + completer.completeError( + ClientClosedError(), + StackTrace.current, + ); + } + httpClient.close(); _onRequestController.close(); _onResponseController.close(); + _onRateLimitController.close(); } } @@ -279,11 +355,11 @@ class Oauth2HttpHandler extends HttpHandler { Oauth2HttpHandler(NyxxOAuth2 super.client) : apiOptions = client.apiOptions; @override - Future execute(HttpRequest request) async { + Future _execute(HttpRequest request) async { if (apiOptions.credentials.isExpired && request.authenticated) { apiOptions.credentials = await apiOptions.credentials.refresh(); } - return await super.execute(request); + return await super._execute(request); } } diff --git a/lib/src/http/managers/application_command_manager.dart b/lib/src/http/managers/application_command_manager.dart index d0a589c6d..4e919cb7a 100644 --- a/lib/src/http/managers/application_command_manager.dart +++ b/lib/src/http/managers/application_command_manager.dart @@ -4,14 +4,18 @@ import 'package:nyxx/src/builders/application_command.dart'; import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; +import 'package:nyxx/src/http/response.dart'; import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/commands/application_command_option.dart'; import 'package:nyxx/src/models/commands/application_command_permissions.dart'; +import 'package:nyxx/src/models/interaction.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A [Manager] for [ApplicationCommand]s. @@ -34,7 +38,7 @@ abstract class ApplicationCommandManager extends Manager { return ApplicationCommand( id: Snowflake.parse(raw['id']!), manager: this, - type: ApplicationCommandType.parse(raw['type'] as int? ?? 1), + type: ApplicationCommandType(raw['type'] as int? ?? 1), applicationId: Snowflake.parse(raw['application_id']!), guildId: maybeParse(raw['guild_id'], Snowflake.parse), name: raw['name'] as String, @@ -55,6 +59,8 @@ abstract class ApplicationCommandManager extends Manager { defaultMemberPermissions: maybeParse(raw['default_member_permissions'], (String raw) => Permissions(int.parse(raw))), hasDmPermission: raw['dm_permission'] as bool?, isNsfw: raw['nsfw'] as bool?, + integrationTypes: maybeParseMany(raw['integration_types'], ApplicationIntegrationType.new) ?? [ApplicationIntegrationType.guildInstall], + contexts: maybeParseMany(raw['contexts'], InteractionContextType.new), version: Snowflake.parse(raw['version']!), ); } @@ -62,7 +68,7 @@ abstract class ApplicationCommandManager extends Manager { /// Parse a [CommandOption] from [raw]. CommandOption parseApplicationCommandOption(Map raw) { return CommandOption( - type: CommandOptionType.parse(raw['type'] as int), + type: CommandOptionType(raw['type'] as int), name: raw['name'] as String, nameLocalizations: maybeParse( raw['name_localizations'], @@ -80,7 +86,7 @@ abstract class ApplicationCommandManager extends Manager { isRequired: raw['required'] as bool?, choices: maybeParseMany(raw['choices'], parseOptionChoice), options: maybeParseMany(raw['options'], parseApplicationCommandOption), - channelTypes: maybeParseMany(raw['channel_types'], ChannelType.parse), + channelTypes: maybeParseMany(raw['channel_types'], ChannelType.new), minValue: raw['min_value'] as num?, maxValue: raw['max_value'] as num?, minLength: raw['min_length'] as int?, @@ -114,7 +120,7 @@ abstract class ApplicationCommandManager extends Manager { final response = await client.httpHandler.executeSafe(request); final commands = parseMany(response.jsonBody as List, parse); - cache.addEntities(commands); + commands.forEach(client.updateCacheWith); return commands; } @@ -129,7 +135,7 @@ abstract class ApplicationCommandManager extends Manager { final response = await client.httpHandler.executeSafe(request); final command = parse(response.jsonBody as Map); - cache[command.id] = command; + client.updateCacheWith(command); return command; } @@ -144,7 +150,7 @@ abstract class ApplicationCommandManager extends Manager { final response = await client.httpHandler.executeSafe(request); final command = parse(response.jsonBody as Map); - cache[command.id] = command; + client.updateCacheWith(command); return command; } @@ -159,7 +165,7 @@ abstract class ApplicationCommandManager extends Manager { final response = await client.httpHandler.executeSafe(request); final command = parse(response.jsonBody as Map); - cache[command.id] = command; + client.updateCacheWith(command); return command; } @@ -186,9 +192,8 @@ abstract class ApplicationCommandManager extends Manager { final response = await client.httpHandler.executeSafe(request); final commands = parseMany(response.jsonBody as List, parse); - cache - ..clear() - ..addEntities(commands); + cache.clear(); + commands.forEach(client.updateCacheWith); return commands; } } @@ -211,7 +216,7 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { required super.applicationId, required this.guildId, required CacheConfig permissionsConfig, - }) : permissionsCache = Cache(client, '$guildId.commandPermissions', permissionsConfig), + }) : permissionsCache = client.cache.getCache('$guildId.commandPermissions', permissionsConfig), super(identifier: '$guildId.commands'); /// Parse a [CommandPermissions] from [raw]. @@ -229,7 +234,7 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { CommandPermission parseCommandPermission(Map raw) { return CommandPermission( id: Snowflake.parse(raw['id']!), - type: CommandPermissionType.parse(raw['type'] as int), + type: CommandPermissionType(raw['type'] as int), hasPermission: raw['permission'] as bool, ); } @@ -246,7 +251,7 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { final response = await client.httpHandler.executeSafe(request); final permissions = parseMany(response.jsonBody as List, parseCommandPermissions); - permissionsCache.addEntities(permissions); + permissions.forEach(client.updateCacheWith); return permissions; } @@ -259,11 +264,21 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { ..permissions(); final request = BasicRequest(route); - final response = await client.httpHandler.executeSafe(request); - final permissions = parseCommandPermissions(response.jsonBody as Map); - - permissionsCache[permissions.id] = permissions; - return permissions; + try { + final response = await client.httpHandler.executeSafe(request); + final permissions = parseCommandPermissions(response.jsonBody as Map); + + client.updateCacheWith(permissions); + return permissions; + } on HttpResponseError catch (e) { + // 10066 = Unknown application command permissions + // Means there are no overrides for this command... why is this an error, Discord? + if (e.errorCode == 10066) { + return CommandPermissions(manager: this, id: id, applicationId: applicationId, guildId: guildId, permissions: []); + } + + rethrow; + } } // TODO: Do we add the command permission endpoints? diff --git a/lib/src/http/managers/application_manager.dart b/lib/src/http/managers/application_manager.dart index b66eef8ec..74343b7c7 100644 --- a/lib/src/http/managers/application_manager.dart +++ b/lib/src/http/managers/application_manager.dart @@ -61,10 +61,19 @@ class ApplicationManager { tags: maybeParseMany(raw['tags']), installationParameters: maybeParse(raw['install_params'], parseInstallationParameters), customInstallUrl: maybeParse(raw['custom_install_url'], Uri.parse), + integrationTypesConfig: maybeParse( + raw['integration_types_config'], + (Map config) => { + for (final MapEntry(:key, :value) in config.entries) + ApplicationIntegrationType(int.parse(key)): parseApplicationIntegrationTypeConfiguration(value as Map) + }, + ), roleConnectionsVerificationUrl: maybeParse(raw['role_connections_verification_url'], Uri.parse), + approximateUserInstallCount: raw['approximate_user_install_count'] as int?, ); } + /// Parse a [Team] from [raw]. Team parseTeam(Map raw) { return Team( manager: this, @@ -76,15 +85,17 @@ class ApplicationManager { ); } + /// Parse a [TeamMember] from [raw]. TeamMember parseTeamMember(Map raw) { return TeamMember( - membershipState: TeamMembershipState.parse(raw['membership_state'] as int), + membershipState: TeamMembershipState(raw['membership_state'] as int), teamId: Snowflake.parse(raw['team_id']!), user: PartialUser(id: Snowflake.parse((raw['user'] as Map)['id']!), manager: client.users), role: TeamMemberRole.parse(raw['role'] as String), ); } + /// Parse a [InstallationParameters] from [raw]. InstallationParameters parseInstallationParameters(Map raw) { return InstallationParameters( scopes: parseMany(raw['scopes'] as List), @@ -92,9 +103,17 @@ class ApplicationManager { ); } + /// Parse a [ApplicationIntegrationTypeConfiguration] from [raw]. + ApplicationIntegrationTypeConfiguration parseApplicationIntegrationTypeConfiguration(Map raw) { + return ApplicationIntegrationTypeConfiguration( + oauth2InstallParameters: maybeParse(raw['oauth2_install_params'], parseInstallationParameters), + ); + } + + /// Parse a [ApplicationRoleConnectionMetadata] from [raw]. ApplicationRoleConnectionMetadata parseApplicationRoleConnectionMetadata(Map raw) { return ApplicationRoleConnectionMetadata( - type: ConnectionMetadataType.parse(raw['type'] as int), + type: ConnectionMetadataType(raw['type'] as int), key: raw['key'] as String, name: raw['name'] as String, localizedNames: maybeParse( @@ -109,15 +128,12 @@ class ApplicationManager { ); } + /// Parse a [Sku] from [raw]. + @Deprecated('Use SkuManager.parse') Sku parseSku(Map raw) { - return Sku( - manager: this, - id: Snowflake.parse(raw['id']!), - type: SkuType.parse(raw['type'] as int), - applicationId: Snowflake.parse(raw['application_id']!), - name: raw['name'] as String, - slug: raw['slug'] as String, - ); + final applicationId = Snowflake.parse(raw['application_id']!); + + return client.applications[applicationId].skus.parse(raw); } /// Fetch an application's role connection metadata. @@ -144,6 +160,7 @@ class ApplicationManager { return parseMany(response.jsonBody as List, parseApplicationRoleConnectionMetadata); } + /// Fetch the current application. Future fetchCurrentApplication() async { final route = HttpRoute()..applications(id: '@me'); final request = BasicRequest(route); @@ -152,21 +169,27 @@ class ApplicationManager { return parse(response.jsonBody as Map); } - Future updateCurrentApplication(ApplicationUpdateBuilder builder) async { - final route = HttpRoute()..applications(id: '@me'); - final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + /// Fetch the current OAuth2 application. + Future fetchOAuth2CurrentApplication() async { + final route = HttpRoute() + ..oauth2() + ..applications(id: '@me'); + final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); return parse(response.jsonBody as Map); } - Future> listSkus(Snowflake id) async { - final route = HttpRoute() - ..applications(id: id.toString()) - ..skus(); - final request = BasicRequest(route); + /// Update the current application. + Future updateCurrentApplication(ApplicationUpdateBuilder builder) async { + final route = HttpRoute()..applications(id: '@me'); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, parseSku); + return parse(response.jsonBody as Map); } + + /// List this application's SKUs. + @Deprecated('Use SkuManager.list') + Future> listSkus(Snowflake id) => client.applications[id].skus.list(); } diff --git a/lib/src/http/managers/audit_log_manager.dart b/lib/src/http/managers/audit_log_manager.dart index 47703e5c5..ae8d533c2 100644 --- a/lib/src/http/managers/audit_log_manager.dart +++ b/lib/src/http/managers/audit_log_manager.dart @@ -1,4 +1,3 @@ -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; @@ -6,6 +5,7 @@ import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/guild/audit_log.dart'; import 'package:nyxx/src/models/permission_overwrite.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; class AuditLogManager extends ReadOnlyManager { @@ -24,12 +24,13 @@ class AuditLogManager extends ReadOnlyManager { targetId: maybeParse(raw['target_id'], Snowflake.parse), changes: maybeParseMany(raw['changes'], parseAuditLogChange), userId: maybeParse(raw['user_id'], Snowflake.parse), - actionType: AuditLogEvent.parse(raw['action_type'] as int), + actionType: AuditLogEvent(raw['action_type'] as int), options: maybeParse(raw['options'], parseAuditLogEntryInfo), reason: raw['reason'] as String?, ); } + /// Parse a [AuditLogChange] from [raw]. AuditLogChange parseAuditLogChange(Map raw) { return AuditLogChange( oldValue: raw['old_value'], @@ -38,6 +39,7 @@ class AuditLogManager extends ReadOnlyManager { ); } + /// Parse a [AuditLogEntryInfo] from [raw]. AuditLogEntryInfo parseAuditLogEntryInfo(Map raw) { return AuditLogEntryInfo( manager: this, @@ -51,7 +53,7 @@ class AuditLogManager extends ReadOnlyManager { membersRemoved: raw['members_removed'] as String?, messageId: maybeParse(raw['message_id'], Snowflake.parse), roleName: raw['role_name'] as String?, - overwriteType: maybeParse(raw['type'], (String raw) => PermissionOverwriteType.parse(int.parse(raw))), + overwriteType: maybeParse(raw['type'], (String raw) => PermissionOverwriteType(int.parse(raw))), integrationType: raw['integration_type'] as String?, ); } @@ -67,6 +69,7 @@ class AuditLogManager extends ReadOnlyManager { ); } + // List the audit log in the guild. Future> list({Snowflake? userId, AuditLogEvent? type, Snowflake? before, Snowflake? after, int? limit}) async { final route = HttpRoute() ..guilds(id: guildId.toString()) @@ -92,30 +95,24 @@ class AuditLogManager extends ReadOnlyManager { return client.guilds[guildId].commands.parse(raw); }); - for (final command in applicationCommands) { - if (command.guild == null) { - client.commands.cache[command.id] = command; - } else { - client.guilds[command.guildId!].commands.cache[command.id] = command; - } - } + applicationCommands.forEach(client.updateCacheWith); final autoModerationRules = parseMany(responseBody['auto_moderation_rules'] as List, client.guilds[guildId].autoModerationRules.parse); - client.guilds[guildId].autoModerationRules.cache.addEntities(autoModerationRules); + autoModerationRules.forEach(client.updateCacheWith); final scheduledEvents = parseMany(responseBody['guild_scheduled_events'] as List, client.guilds[guildId].scheduledEvents.parse); - client.guilds[guildId].scheduledEvents.cache.addEntities(scheduledEvents); + scheduledEvents.forEach(client.updateCacheWith); final threads = parseMany(responseBody['threads'] as List, client.channels.parse); - client.channels.cache.addEntities(threads); + threads.forEach(client.updateCacheWith); final users = parseMany(responseBody['users'] as List, client.users.parse); - client.users.cache.addEntities(users); + users.forEach(client.updateCacheWith); final webhooks = parseMany(responseBody['webhooks'] as List, client.webhooks.parse); - client.webhooks.cache.addEntities(webhooks); + webhooks.forEach(client.updateCacheWith); - cache.addEntities(entries); + entries.forEach(client.updateCacheWith); return entries; } } diff --git a/lib/src/http/managers/auto_moderation_manager.dart b/lib/src/http/managers/auto_moderation_manager.dart index 48fc56c19..1e074b9d2 100644 --- a/lib/src/http/managers/auto_moderation_manager.dart +++ b/lib/src/http/managers/auto_moderation_manager.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:nyxx/src/builders/guild/auto_moderation.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/guild/auto_moderation.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; class AutoModerationManager extends Manager { @@ -25,8 +25,8 @@ class AutoModerationManager extends Manager { guildId: Snowflake.parse(raw['guild_id']!), name: raw['name'] as String, creatorId: Snowflake.parse(raw['creator_id']!), - eventType: AutoModerationEventType.parse(raw['event_type'] as int), - triggerType: TriggerType.parse(raw['trigger_type'] as int), + eventType: AutoModerationEventType(raw['event_type'] as int), + triggerType: TriggerType(raw['trigger_type'] as int), metadata: parseTriggerMetadata(raw['trigger_metadata'] as Map), actions: parseMany(raw['actions'] as List, parseAutoModerationAction), isEnabled: raw['enabled'] as bool, @@ -35,24 +35,27 @@ class AutoModerationManager extends Manager { ); } + /// Parse a [TriggerMetadata] from [raw]. TriggerMetadata parseTriggerMetadata(Map raw) { return TriggerMetadata( keywordFilter: maybeParseMany(raw['keyword_filter']), regexPatterns: maybeParseMany(raw['regex_patterns']), - presets: maybeParseMany(raw['presets'], KeywordPresetType.parse), + presets: maybeParseMany(raw['presets'], KeywordPresetType.new), allowList: maybeParseMany(raw['allow_list']), mentionTotalLimit: raw['mention_total_limit'] as int?, isMentionRaidProtectionEnabled: raw['mention_raid_protection_enabled'] as bool?, ); } + /// Parse a [AutoModerationAction] from [raw]. AutoModerationAction parseAutoModerationAction(Map raw) { return AutoModerationAction( - type: ActionType.parse(raw['type'] as int), + type: ActionType(raw['type'] as int), metadata: maybeParse(raw['metadata'], parseActionMetadata), ); } + /// Parse a [ActionMetadata] from [raw]. ActionMetadata parseActionMetadata(Map raw) { return ActionMetadata( manager: this, @@ -73,7 +76,7 @@ class AutoModerationManager extends Manager { final response = await client.httpHandler.executeSafe(request); final rule = parse(response.jsonBody as Map); - cache[rule.id] = rule; + client.updateCacheWith(rule); return rule; } @@ -87,7 +90,7 @@ class AutoModerationManager extends Manager { final response = await client.httpHandler.executeSafe(request); final rules = parseMany(response.jsonBody as List, parse); - cache.addEntities(rules); + rules.forEach(client.updateCacheWith); return rules; } @@ -102,7 +105,7 @@ class AutoModerationManager extends Manager { final response = await client.httpHandler.executeSafe(request); final rule = parse(response.jsonBody as Map); - cache[rule.id] = rule; + client.updateCacheWith(rule); return rule; } @@ -117,7 +120,7 @@ class AutoModerationManager extends Manager { final response = await client.httpHandler.executeSafe(request); final rule = parse(response.jsonBody as Map); - cache[rule.id] = rule; + client.updateCacheWith(rule); return rule; } diff --git a/lib/src/http/managers/channel_manager.dart b/lib/src/http/managers/channel_manager.dart index 01a9dc993..c51df2f9a 100644 --- a/lib/src/http/managers/channel_manager.dart +++ b/lib/src/http/managers/channel_manager.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:http/http.dart' show MultipartFile; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/builders/builder.dart'; +import 'package:nyxx/src/builders/channel/group_dm.dart'; import 'package:nyxx/src/builders/channel/stage_instance.dart'; import 'package:nyxx/src/builders/channel/thread.dart'; import 'package:nyxx/src/builders/invite.dart'; @@ -24,6 +24,7 @@ import 'package:nyxx/src/models/channel/types/forum.dart'; import 'package:nyxx/src/models/channel/types/group_dm.dart'; import 'package:nyxx/src/models/channel/types/guild_announcement.dart'; import 'package:nyxx/src/models/channel/types/guild_category.dart'; +import 'package:nyxx/src/models/channel/types/guild_media.dart'; import 'package:nyxx/src/models/channel/types/guild_stage.dart'; import 'package:nyxx/src/models/channel/types/guild_text.dart'; import 'package:nyxx/src/models/channel/types/guild_voice.dart'; @@ -35,6 +36,7 @@ import 'package:nyxx/src/models/invite/invite_metadata.dart'; import 'package:nyxx/src/models/permission_overwrite.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; @@ -44,7 +46,7 @@ class ChannelManager extends ReadOnlyManager { /// Create a new [ChannelManager]. ChannelManager(super.config, super.client, {required CacheConfig stageInstanceConfig}) - : stageInstanceCache = Cache(client, 'channels.stageInstances', stageInstanceConfig), + : stageInstanceCache = client.cache.getCache('channels.stageInstances', stageInstanceConfig), super(identifier: 'channels'); /// Return a partial instance of the entity with ID [id] containing no data. @@ -62,7 +64,7 @@ class ChannelManager extends ReadOnlyManager { @override Channel parse(Map raw, {Snowflake? guildId}) { - final type = ChannelType.parse(raw['type'] as int); + final type = ChannelType(raw['type'] as int); final parsers = { ChannelType.guildText: parseGuildTextChannel, @@ -137,7 +139,7 @@ class ChannelManager extends ReadOnlyManager { rateLimitPerUser: maybeParse(raw['rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), rtcRegion: raw['rtc_region'] as String?, userLimit: raw['user_limit'] == 0 ? null : raw['user_limit'] as int?, - videoQualityMode: maybeParse(raw['video_quality_mode'], VideoQualityMode.parse) ?? VideoQualityMode.auto, + videoQualityMode: maybeParse(raw['video_quality_mode'], VideoQualityMode.new) ?? VideoQualityMode.auto, ); } @@ -303,7 +305,7 @@ class ChannelManager extends ReadOnlyManager { rateLimitPerUser: maybeParse(raw['rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), rtcRegion: raw['rtc_region'] as String?, userLimit: raw['user_limit'] == 0 ? null : raw['user_limit'] as int?, - videoQualityMode: maybeParse(raw['video_quality_mode'], VideoQualityMode.parse) ?? VideoQualityMode.auto, + videoQualityMode: maybeParse(raw['video_quality_mode'], VideoQualityMode.new) ?? VideoQualityMode.auto, ); } @@ -322,7 +324,7 @@ class ChannelManager extends ReadOnlyManager { return ForumChannel( id: Snowflake.parse(raw['id']!), manager: this, - defaultLayout: maybeParse(raw['default_forum_layout'], ForumLayout.parse), + defaultLayout: maybeParse(raw['default_forum_layout'], ForumLayout.new), topic: raw['topic'] as String?, rateLimitPerUser: maybeParse(raw['rate_limit_per_user'], (value) => value == 0 ? null : Duration(seconds: value)), lastThreadId: maybeParse(raw['last_message_id'], Snowflake.parse), @@ -330,7 +332,7 @@ class ChannelManager extends ReadOnlyManager { flags: ChannelFlags(raw['flags'] as int), availableTags: parseMany(raw['available_tags'] as List, parseForumTag), defaultReaction: maybeParse(raw['default_reaction_emoji'], parseDefaultReaction), - defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.parse), + defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.new), // Discord doesn't seem to include this field if the default 3 day expiration is used (3 days = 4320 minutes) defaultAutoArchiveDuration: Duration(minutes: raw['default_auto_archive_duration'] as int? ?? 4320), defaultThreadRateLimitPerUser: @@ -357,7 +359,7 @@ class ChannelManager extends ReadOnlyManager { flags: ChannelFlags(raw['flags'] as int), availableTags: parseMany(raw['available_tags'] as List, parseForumTag), defaultReaction: maybeParse(raw['default_reaction_emoji'], parseDefaultReaction), - defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.parse), + defaultSortOrder: maybeParse(raw['default_sort_order'], ForumSort.new), // Discord doesn't seem to include this field if the default 3 day expiration is used (3 days = 4320 minutes) defaultAutoArchiveDuration: Duration(minutes: raw['default_auto_archive_duration'] as int? ?? 4320), defaultThreadRateLimitPerUser: @@ -374,7 +376,7 @@ class ChannelManager extends ReadOnlyManager { PermissionOverwrite parsePermissionOverwrite(Map raw) { return PermissionOverwrite( id: Snowflake.parse(raw['id']!), - type: PermissionOverwriteType.parse(raw['type'] as int), + type: PermissionOverwriteType(raw['type'] as int), allow: Permissions(int.parse(raw['allow'] as String)), deny: Permissions(int.parse(raw['deny'] as String)), ); @@ -405,25 +407,28 @@ class ChannelManager extends ReadOnlyManager { ); } - ThreadMember parseThreadMember(Map raw) { + ThreadMember parseThreadMember(Map raw, {Snowflake? guildId}) { + final userId = Snowflake.parse(raw['user_id']!); + return ThreadMember( manager: this, joinTimestamp: DateTime.parse(raw['join_timestamp'] as String), flags: Flags(raw['flags'] as int), threadId: Snowflake.parse(raw['id']!), - userId: Snowflake.parse(raw['user_id']!), - member: maybeParse(raw['member'], client.guilds[Snowflake.zero].members.parse), + userId: userId, + member: maybeParse(raw['member'], (Map raw) => client.guilds[guildId ?? Snowflake.zero].members.parse(raw, userId: userId)), ); } - ThreadList parseThreadList(Map raw) { + ThreadList parseThreadList(Map raw, {Snowflake? guildId}) { return ThreadList( threads: parseMany(raw['threads'] as List, parse).cast(), - members: parseMany(raw['members'] as List, parseThreadMember), + members: parseMany(raw['members'] as List, (Map raw) => parseThreadMember(raw, guildId: guildId)), hasMore: raw['has_more'] as bool? ?? false, ); } + /// Parse a [StageInstance] from [raw]. StageInstance parseStageInstance(Map raw) { return StageInstance( id: Snowflake.parse(raw['id']!), @@ -431,7 +436,7 @@ class ChannelManager extends ReadOnlyManager { guildId: Snowflake.parse(raw['guild_id']!), channelId: Snowflake.parse(raw['channel_id']!), topic: raw['topic'] as String, - privacyLevel: PrivacyLevel.parse(raw['privacy_level'] as int), + privacyLevel: PrivacyLevel(raw['privacy_level'] as int), scheduledEventId: maybeParse(raw['guild_scheduled_event_id'], Snowflake.parse), ); } @@ -444,7 +449,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final channel = parse(response.jsonBody as Map); - cache[channel.id] = channel; + client.updateCacheWith(channel); return channel; } @@ -461,7 +466,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final channel = parse(response.jsonBody as Map); - cache[channel.id] = channel; + client.updateCacheWith(channel); return channel; } @@ -486,7 +491,7 @@ class ChannelManager extends ReadOnlyManager { final route = HttpRoute() ..channels(id: id.toString()) ..permissions(id: builder.id.toString()); - final request = BasicRequest(route, method: 'PUT', body: jsonEncode(builder.build())); + final request = BasicRequest(route, method: 'PUT', body: jsonEncode(builder.build(includeId: false))); await client.httpHandler.executeSafe(request); } @@ -509,7 +514,10 @@ class ChannelManager extends ReadOnlyManager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, client.invites.parseWithMetadata); + final invites = parseMany(response.jsonBody as List, client.invites.parseWithMetadata); + + invites.forEach(client.updateCacheWith); + return invites; } /// Create an invite in a guild channel. @@ -520,15 +528,23 @@ class ChannelManager extends ReadOnlyManager { final request = BasicRequest(route, method: 'POST', auditLogReason: auditLogReason, body: jsonEncode(builder.build())); final response = await client.httpHandler.executeSafe(request); - return client.invites.parse(response.jsonBody as Map); + final invite = client.invites.parse(response.jsonBody as Map); + + client.updateCacheWith(invite); + return invite; } /// Add a channel to another channel's followers. - Future followChannel(Snowflake id, Snowflake toFollow) async { + Future followChannel(Snowflake id, Snowflake toFollow, {String? auditLogReason}) async { final route = HttpRoute() ..channels(id: toFollow.toString()) ..followers(); - final request = BasicRequest(route, method: 'POST', body: jsonEncode({'webhook_channel_id': id.toString()})); + final request = BasicRequest( + route, + method: 'POST', + body: jsonEncode({'webhook_channel_id': id.toString()}), + auditLogReason: auditLogReason, + ); final response = await client.httpHandler.executeSafe(request); @@ -556,7 +572,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final thread = parse(response.jsonBody as Map) as Thread; - cache[thread.id] = thread; + client.updateCacheWith(thread); return thread; } @@ -570,7 +586,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final thread = parse(response.jsonBody as Map) as Thread; - cache[thread.id] = thread; + client.updateCacheWith(thread); return thread; } @@ -598,7 +614,7 @@ class ChannelManager extends ReadOnlyManager { request = MultipartRequest( route, - method: 'PATCH', + method: 'POST', jsonPayload: jsonEncode(payload), files: files, ); @@ -609,7 +625,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final thread = parse(response.jsonBody as Map) as Thread; - cache[thread.id] = thread; + client.updateCacheWith(thread); return thread; } @@ -666,6 +682,8 @@ class ChannelManager extends ReadOnlyManager { ); final response = await client.httpHandler.executeSafe(request); + // TODO: Can we provide the guildId? + // Don't update the cache since the guildId for the member will be Snowflake.zero return parseThreadMember(response.jsonBody as Map); } @@ -684,6 +702,8 @@ class ChannelManager extends ReadOnlyManager { ); final response = await client.httpHandler.executeSafe(request); + // TODO: Can we provide the guildId? + // Don't update the cache since the guildId for the member will be Snowflake.zero return parseMany(response.jsonBody as List, parseThreadMember); } @@ -703,7 +723,11 @@ class ChannelManager extends ReadOnlyManager { ); final response = await client.httpHandler.executeSafe(request); - return parseThreadList(response.jsonBody as Map); + // TODO: Can we provide the guild ID? + final threadList = parseThreadList(response.jsonBody as Map); + + client.updateCacheWith(threadList); + return threadList; } /// List the private archived threads in a channel. @@ -722,7 +746,11 @@ class ChannelManager extends ReadOnlyManager { ); final response = await client.httpHandler.executeSafe(request); - return parseThreadList(response.jsonBody as Map); + // TODO: Can we provide the guild ID? + final threadList = parseThreadList(response.jsonBody as Map); + + client.updateCacheWith(threadList); + return threadList; } /// List the private archived threads the current user has joined in a channel. @@ -742,7 +770,11 @@ class ChannelManager extends ReadOnlyManager { ); final response = await client.httpHandler.executeSafe(request); - return parseThreadList(response.jsonBody as Map); + // TODO: Can we provide the guild ID? + final threadList = parseThreadList(response.jsonBody as Map); + + client.updateCacheWith(threadList); + return threadList; } /// Start a stage instance in a channel. @@ -758,7 +790,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final stageInstance = parseStageInstance(response.jsonBody as Map); - stageInstanceCache[stageInstance.channelId] = stageInstance; + client.updateCacheWith(stageInstance); return stageInstance; } @@ -770,7 +802,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final stageInstance = parseStageInstance(response.jsonBody as Map); - stageInstanceCache[stageInstance.channelId] = stageInstance; + client.updateCacheWith(stageInstance); return stageInstance; } @@ -787,7 +819,7 @@ class ChannelManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final stageInstance = parseStageInstance(response.jsonBody as Map); - stageInstanceCache[stageInstance.channelId] = stageInstance; + client.updateCacheWith(stageInstance); return stageInstance; } @@ -800,4 +832,22 @@ class ChannelManager extends ReadOnlyManager { stageInstanceCache.remove(channelId); } + + Future addRecipient(Snowflake channelId, Snowflake userId, DmRecipientBuilder builder) async { + final route = HttpRoute() + ..channels(id: channelId.toString()) + ..recipients(id: userId.toString()); + final request = BasicRequest(route, method: 'PUT', body: jsonEncode(builder.build())); + + await client.httpHandler.executeSafe(request); + } + + Future removeRecipient(Snowflake channelId, Snowflake userId) async { + final route = HttpRoute() + ..channels(id: channelId.toString()) + ..recipients(id: userId.toString()); + final request = BasicRequest(route, method: 'DELETE'); + + await client.httpHandler.executeSafe(request); + } } diff --git a/lib/src/http/managers/emoji_manager.dart b/lib/src/http/managers/emoji_manager.dart index 825988cd6..e889a0587 100644 --- a/lib/src/http/managers/emoji_manager.dart +++ b/lib/src/http/managers/emoji_manager.dart @@ -2,19 +2,18 @@ import 'dart:async'; import 'dart:convert'; import 'package:nyxx/src/builders/emoji/emoji.dart'; -import 'package:nyxx/src/cache/cache.dart'; +import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; import 'manager.dart'; -class EmojiManager extends Manager { - final Snowflake guildId; - - EmojiManager(super.config, super.client, {required this.guildId}) : super(identifier: '$guildId.emojis'); +abstract class EmojiManager extends Manager { + EmojiManager(super.config, super.client, {required super.identifier}); @override PartialEmoji operator [](Snowflake id) => PartialEmoji(id: id, manager: this); @@ -31,6 +30,128 @@ class EmojiManager extends Manager { ); } + return sentinelEmoji; + } + + /// List the emojis. + Future> list(); +} + +class ApplicationEmojiManager extends EmojiManager { + final Snowflake applicationId; + + ApplicationEmojiManager(super.config, super.client, {required this.applicationId}) : super(identifier: 'applications.$applicationId.emojis'); + + @override + Emoji parse(Map raw) { + final emoji = super.parse(raw); + + if (!identical(emoji, sentinelEmoji)) { + return emoji; + } + + return ApplicationEmoji( + id: Snowflake.parse(raw['id']!), + manager: this, + isAnimated: raw['animated'] as bool, + isAvailable: raw['available'] as bool, + isManaged: raw['managed'] as bool, + requiresColons: raw['require_colons'] as bool, + name: raw['name'] as String, + user: maybeParse(raw['user'], client.users.parse), + ); + } + + @override + Future get(Snowflake id) async => await super.get(id) as ApplicationEmoji; + + @override + Future fetch(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + /// List the emojis in the application. + @override + Future> list() async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + + final emojis = parseMany( + response.jsonBody['items'] as List, + (raw) => parse(raw as Map) as ApplicationEmoji, + ); + + emojis.forEach(client.updateCacheWith); + + return emojis; + } + + @override + Future create(ApplicationEmojiBuilder builder) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(); + final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + @override + Future update(Snowflake id, ApplicationEmojiUpdateBuilder builder) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + + final response = await client.httpHandler.executeSafe(request); + final emoji = parse(response.jsonBody as Map) as ApplicationEmoji; + + client.updateCacheWith(emoji); + return emoji; + } + + @override + Future delete(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..emojis(id: id.toString()); + final request = BasicRequest(route, method: 'DELETE'); + + await client.httpHandler.executeSafe(request); + cache.remove(id); + } +} + +class GuildEmojiManager extends EmojiManager { + final Snowflake guildId; + + GuildEmojiManager(super.config, super.client, {required this.guildId}) : super(identifier: 'guilds.$guildId.emojis'); + + @override + Emoji parse(Map raw) { + final emoji = super.parse(raw); + + if (!identical(emoji, sentinelEmoji)) { + return emoji; + } + return GuildEmoji( id: Snowflake.parse(raw['id']!), manager: this, @@ -69,10 +190,12 @@ class EmojiManager extends Manager { final response = await client.httpHandler.executeSafe(request); final emoji = parse(response.jsonBody as Map) as GuildEmoji; - cache[emoji.id] = emoji; + client.updateCacheWith(emoji); return emoji; } + /// List the emojis in the guild. + @override Future> list() async { _checkIsConcrete(); @@ -84,50 +207,50 @@ class EmojiManager extends Manager { final response = await client.httpHandler.executeSafe(request); final emojis = parseMany(response.jsonBody as List, (Map raw) => parse(raw) as GuildEmoji); - cache.addEntities(emojis); + emojis.forEach(client.updateCacheWith); return emojis; } @override - Future create(EmojiBuilder builder, {String? audiReason}) async { + Future create(EmojiBuilder builder, {String? auditLogReason}) async { _checkIsConcrete(); final route = HttpRoute() ..guilds(id: guildId.toString()) ..emojis(); - final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); + final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build()), auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); final emoji = parse(response.jsonBody as Map) as GuildEmoji; - cache[emoji.id] = emoji; + client.updateCacheWith(emoji); return emoji; } @override - Future update(Snowflake id, EmojiUpdateBuilder builder, {String? auditReason}) async { + Future update(Snowflake id, EmojiUpdateBuilder builder, {String? auditLogReason}) async { _checkIsConcrete(id); final route = HttpRoute() ..guilds(id: guildId.toString()) ..emojis(id: id.toString()); - final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build()), auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); final emoji = parse(response.jsonBody as Map) as GuildEmoji; - cache[emoji.id] = emoji; + client.updateCacheWith(emoji); return emoji; } @override - Future delete(Snowflake id, {String? auditReason}) async { + Future delete(Snowflake id, {String? auditLogReason}) async { _checkIsConcrete(id); final route = HttpRoute() ..guilds(id: guildId.toString()) ..emojis(id: id.toString()); - final request = BasicRequest(route, method: 'DELETE'); + final request = BasicRequest(route, method: 'DELETE', auditLogReason: auditLogReason); await client.httpHandler.executeSafe(request); cache.remove(id); diff --git a/lib/src/http/managers/entitlement_manager.dart b/lib/src/http/managers/entitlement_manager.dart index f383df7b2..44b6c4ac4 100644 --- a/lib/src/http/managers/entitlement_manager.dart +++ b/lib/src/http/managers/entitlement_manager.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'package:nyxx/src/builders/entitlement.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/entitlement.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A [Manager] for [Entitlement]s. @@ -30,8 +30,9 @@ class EntitlementManager extends ReadOnlyManager { userId: maybeParse(raw['user_id'], Snowflake.parse), guildId: maybeParse(raw['guild_id'], Snowflake.parse), applicationId: Snowflake.parse(raw['application_id']!), - type: EntitlementType.parse(raw['type'] as int), - isConsumed: raw['consumed'] as bool, + type: EntitlementType(raw['type'] as int), + isConsumed: raw['consumed'] as bool? ?? false, + isDeleted: raw['deleted'] as bool? ?? false, startsAt: maybeParse(raw['starts_at'], DateTime.parse), endsAt: maybeParse(raw['ends_at'], DateTime.parse), ); @@ -63,7 +64,7 @@ class EntitlementManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final entitlements = parseMany(response.jsonBody as List, parse); - cache.addEntities(entitlements); + entitlements.forEach(client.updateCacheWith); return entitlements; } @@ -87,7 +88,7 @@ class EntitlementManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final entitlement = parse(response.jsonBody as Map); - cache[entitlement.id] = entitlement; + client.updateCacheWith(entitlement); return entitlement; } @@ -101,4 +102,15 @@ class EntitlementManager extends ReadOnlyManager { await client.httpHandler.executeSafe(request); cache.remove(id); } + + /// Marks a entitlement for the user as consumed. + Future consume(Snowflake id) async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..entitlements(id: id.toString()) + ..consume(); + final request = BasicRequest(route, method: 'POST'); + + await client.httpHandler.executeSafe(request); + } } diff --git a/lib/src/http/managers/gateway_manager.dart b/lib/src/http/managers/gateway_manager.dart index 2ee4ad8a1..84e776c1e 100644 --- a/lib/src/http/managers/gateway_manager.dart +++ b/lib/src/http/managers/gateway_manager.dart @@ -45,7 +45,7 @@ abstract class GatewayManager { // No fields are validated server-side. Expect errors. return Activity( name: raw['name'] as String, - type: ActivityType.parse(raw['type'] as int), + type: ActivityType(raw['type'] as int), url: tryParse(raw['url'], Uri.parse), createdAt: tryParse(raw['created_at'], DateTime.fromMillisecondsSinceEpoch), timestamps: tryParse(raw['timestamps'], parseActivityTimestamps), @@ -103,9 +103,9 @@ abstract class GatewayManager { ClientStatus parseClientStatus(Map raw) { return ClientStatus( - desktop: maybeParse(raw['desktop'], UserStatus.parse), - mobile: maybeParse(raw['mobile'], UserStatus.parse), - web: maybeParse(raw['web'], UserStatus.parse), + desktop: maybeParse(raw['desktop'], UserStatus.new), + mobile: maybeParse(raw['mobile'], UserStatus.new), + web: maybeParse(raw['web'], UserStatus.new), ); } diff --git a/lib/src/http/managers/guild_manager.dart b/lib/src/http/managers/guild_manager.dart index f53373de4..280574366 100644 --- a/lib/src/http/managers/guild_manager.dart +++ b/lib/src/http/managers/guild_manager.dart @@ -4,12 +4,12 @@ import 'dart:typed_data'; import 'package:nyxx/src/builders/channel/channel_position.dart'; import 'package:nyxx/src/builders/channel/guild_channel.dart'; import 'package:nyxx/src/builders/guild/guild.dart'; +import 'package:nyxx/src/builders/guild/onboarding.dart'; import 'package:nyxx/src/builders/guild/template.dart'; import 'package:nyxx/src/builders/guild/welcome_screen.dart'; import 'package:nyxx/src/builders/guild/widget.dart'; import 'package:nyxx/src/builders/image.dart'; import 'package:nyxx/src/builders/voice.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; @@ -30,6 +30,7 @@ import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; import 'package:nyxx/src/models/voice/voice_region.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; @@ -59,12 +60,12 @@ class GuildManager extends Manager { afkTimeout: Duration(seconds: raw['afk_timeout'] as int), isWidgetEnabled: raw['widget_enabled'] as bool? ?? false, widgetChannelId: maybeParse(raw['widget_channel_id'], Snowflake.parse), - verificationLevel: VerificationLevel.parse(raw['verification_level'] as int), - defaultMessageNotificationLevel: MessageNotificationLevel.parse(raw['default_message_notifications'] as int), - explicitContentFilterLevel: ExplicitContentFilterLevel.parse(raw['explicit_content_filter'] as int), + verificationLevel: VerificationLevel(raw['verification_level'] as int), + defaultMessageNotificationLevel: MessageNotificationLevel(raw['default_message_notifications'] as int), + explicitContentFilterLevel: ExplicitContentFilterLevel(raw['explicit_content_filter'] as int), roleList: parseMany(raw['roles'] as List, this[id].roles.parse), features: parseGuildFeatures(raw['features'] as List), - mfaLevel: MfaLevel.parse(raw['mfa_level'] as int), + mfaLevel: MfaLevel(raw['mfa_level'] as int), applicationId: maybeParse(raw['application_id'], Snowflake.parse), systemChannelId: maybeParse(raw['system_channel_id'], Snowflake.parse), systemChannelFlags: SystemChannelFlags(raw['system_channel_flags'] as int), @@ -74,7 +75,7 @@ class GuildManager extends Manager { vanityUrlCode: raw['vanity_url_code'] as String?, description: raw['description'] as String?, bannerHash: raw['banner'] as String?, - premiumTier: PremiumTier.parse(raw['premium_tier'] as int), + premiumTier: PremiumTier(raw['premium_tier'] as int), premiumSubscriptionCount: raw['premium_subscription_count'] as int?, preferredLocale: Locale.parse(raw['preferred_locale'] as String), publicUpdatesChannelId: maybeParse(raw['public_updates_channel_id'], Snowflake.parse), @@ -83,7 +84,7 @@ class GuildManager extends Manager { approximateMemberCount: raw['approximate_member_count'] as int?, approximatePresenceCount: raw['approximate_presence_count'] as int?, welcomeScreen: maybeParse(raw['welcome_screen'], parseWelcomeScreen), - nsfwLevel: NsfwLevel.parse(raw['nsfw_level'] as int), + nsfwLevel: NsfwLevel(raw['nsfw_level'] as int), hasPremiumProgressBarEnabled: raw['premium_progress_bar_enabled'] as bool, emojiList: parseMany(raw['emojis'] as List, this[id].emojis.parse), stickerList: parseMany(raw['stickers'] as List? ?? [], this[id].stickers.parse), @@ -91,6 +92,22 @@ class GuildManager extends Manager { ); } + /// Parse [UserGuild] from [raw]. + UserGuild parseUserGuild(Map raw) { + final id = Snowflake.parse(raw['id']!); + return UserGuild( + id: id, + manager: this, + name: raw['name'] as String, + iconHash: raw['icon'] as String?, + isOwnedByCurrentUser: raw['owner'] as bool, + currentUserPermissions: Permissions(int.parse(raw['permissions'] as String)), + features: parseGuildFeatures(raw['features'] as List), + approximateMemberCount: raw['approximate_member_count'] as int?, + approximatePresenceCount: raw['approximate_presence_count'] as int?, + ); + } + static final Map> _nameToGuildFeature = { 'ANIMATED_BANNER': GuildFeatures.animatedBanner, 'ANIMATED_ICON': GuildFeatures.animatedIcon, @@ -125,7 +142,7 @@ class GuildManager extends Manager { for (final entry in _nameToGuildFeature.entries) entry.value: entry.key, }; - /// Parse an [GuildFeatures] from [raw]./// Parse [GuildFeatures] from [raw]. + /// Parse an [GuildFeatures] from [raw]. GuildFeatures parseGuildFeatures(List raw) { final featureFlags = parseMany(raw, parseGuildFeature); @@ -194,6 +211,14 @@ class GuildManager extends Manager { ); } + /// Parse a [BulkBanResponse] from [raw]. + BulkBanResponse parseBulkBanResponse(Map raw) { + return BulkBanResponse( + bannedUsers: parseMany(raw['banned_users'] as List, Snowflake.parse), + failedUsers: parseMany(raw['failed_users'] as List, Snowflake.parse), + ); + } + /// Parse a [WidgetSettings] from [raw]. WidgetSettings parseWidgetSettings(Map raw) { return WidgetSettings( @@ -232,6 +257,7 @@ class GuildManager extends Manager { prompts: parseMany(raw['prompts'] as List, (Map raw) => parseOnboardingPrompt(raw, guildId: guildId)), defaultChannelIds: parseMany(raw['default_channel_ids'] as List, Snowflake.parse), isEnabled: raw['enabled'] as bool, + mode: OnboardingMode(raw['mode'] as int), ); } @@ -239,7 +265,7 @@ class GuildManager extends Manager { OnboardingPrompt parseOnboardingPrompt(Map raw, {Snowflake? guildId}) { return OnboardingPrompt( id: Snowflake.parse(raw['id']!), - type: OnboardingPromptType.parse(raw['type'] as int), + type: OnboardingPromptType(raw['type'] as int), options: parseMany(raw['options'] as List, (Map raw) => parseOnboardingPromptOption(raw, guildId: guildId)), title: raw['title'] as String, isSingleSelect: raw['single_select'] as bool, @@ -255,7 +281,7 @@ class GuildManager extends Manager { // Discord passes an "empty" emoji object when unset instead of null if (rawEmoji['id'] != null || rawEmoji['name'] != null) { - emoji = this[guildId ?? Snowflake.zero].emojis.parse(raw['emoji'] as Map); + emoji = client.guilds[guildId ?? Snowflake.zero].emojis.parse(raw['emoji'] as Map); } return OnboardingPromptOption( @@ -269,6 +295,7 @@ class GuildManager extends Manager { ); } + /// Parse a [GuildTemplate] from [raw]. GuildTemplate parseGuildTemplate(Map raw) { final sourceGuildId = Snowflake.parse(raw['source_guild_id']!); @@ -298,6 +325,7 @@ class GuildManager extends Manager { for (final role in ((raw['serialized_source_guild'] as Map)['roles'] as List).cast>()) { 'position': 0, + 'flags': 0, ...role, }, ], @@ -314,7 +342,7 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final guild = parse(response.jsonBody as Map); - cache[guild.id] = guild; + client.updateCacheWith(guild); return guild; } @@ -326,7 +354,7 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final guild = parse(response.jsonBody as Map); - cache[guild.id] = guild; + client.updateCacheWith(guild); return guild; } @@ -338,7 +366,7 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final guild = parse(response.jsonBody as Map); - cache[guild.id] = guild; + client.updateCacheWith(guild); return guild; } @@ -359,7 +387,10 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseGuildPreview(response.jsonBody as Map); + final preview = parseGuildPreview(response.jsonBody as Map); + + client.updateCacheWith(preview); + return preview; } /// Fetch the channels in a guild. @@ -372,7 +403,7 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final channels = parseMany(response.jsonBody as List, client.channels.parse).cast(); - client.channels.cache.addEntities(channels); + channels.forEach(client.updateCacheWith); return channels; } @@ -386,11 +417,11 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final channel = client.channels.parse(response.jsonBody as Map) as T; - client.channels.cache[channel.id] = channel; + client.updateCacheWith(channel); return channel; } - ///Update the positions of channels in a guild. + /// Update the positions of channels in a guild. Future updateChannelPositions(Snowflake id, List positions) async { final route = HttpRoute() ..guilds(id: id.toString()) @@ -409,9 +440,9 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - final list = client.channels.parseThreadList(response.jsonBody as Map); + final list = client.channels.parseThreadList(response.jsonBody as Map, guildId: id); - client.channels.cache.addEntities(list.threads); + client.updateCacheWith(list); return list; } @@ -420,10 +451,17 @@ class GuildManager extends Manager { final route = HttpRoute() ..guilds(id: id.toString()) ..bans(); - final request = BasicRequest(route); + final request = BasicRequest(route, queryParameters: { + if (limit != null) 'limit': limit.toString(), + if (after != null) 'after': after.toString(), + if (before != null) 'before': before.toString(), + }); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, parseBan); + final bans = parseMany(response.jsonBody as List, parseBan); + + bans.forEach(client.updateCacheWith); + return bans; } /// Fetch a ban in a guild. @@ -434,7 +472,10 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseBan(response.jsonBody as Map); + final ban = parseBan(response.jsonBody as Map); + + client.updateCacheWith(ban); + return ban; } /// Create a ban in a guild. @@ -454,6 +495,24 @@ class GuildManager extends Manager { await client.httpHandler.executeSafe(request); } + /// Ban up to 200 users from a guild, and optionally delete previous messages sent by the banned users. + Future bulkBan(Snowflake id, List userIds, {Duration? deleteMessages, String? auditLogReason}) async { + final route = HttpRoute() + ..guilds(id: id.toString()) + ..bulkBan(); + final request = BasicRequest( + route, + method: 'POST', + auditLogReason: auditLogReason, + body: jsonEncode({ + 'user_ids': userIds.map((s) => s.toString()).toList(), + if (deleteMessages != null) 'delete_message_seconds': deleteMessages.inSeconds, + }), + ); + final response = await client.httpHandler.executeSafe(request); + return parseBulkBanResponse(response.jsonBody as Map); + } + /// Delete a ban in a guild. Future deleteBan(Snowflake id, Snowflake userId, {String? auditLogReason}) async { final route = HttpRoute() @@ -477,7 +536,7 @@ class GuildManager extends Manager { ); final response = await client.httpHandler.executeSafe(request); - return MfaLevel.parse(response.jsonBody as int); + return MfaLevel((response.jsonBody as Map)['level'] as int); } /// Fetch the prune count in a guild. @@ -539,7 +598,10 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, client.invites.parse); + final invites = parseMany(response.jsonBody as List, client.invites.parse); + + invites.forEach(client.updateCacheWith); + return invites; } /// Fetch a guild's widget settings. @@ -634,6 +696,17 @@ class GuildManager extends Manager { return parseOnboarding(response.jsonBody as Map); } + /// Update a guild's onboarding. + Future updateOnboarding(Snowflake id, OnboardingUpdateBuilder builder, {String? auditLogReason}) async { + final route = HttpRoute() + ..guilds(id: id.toString()) + ..onboarding(); + final request = BasicRequest(route, method: 'PUT', body: jsonEncode(builder.build()), auditLogReason: auditLogReason); + + final response = await client.httpHandler.executeSafe(request); + return parseOnboarding(response.jsonBody as Map); + } + /// Update the current user's voice state in a guild. Future updateCurrentUserVoiceState(Snowflake id, CurrentUserVoiceStateUpdateBuilder builder) async { final route = HttpRoute() @@ -662,7 +735,10 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseGuildTemplate(response.jsonBody as Map); + final template = parseGuildTemplate(response.jsonBody as Map); + + client.updateCacheWith(template); + return template; } /// Create a guild from a guild template. @@ -675,7 +751,7 @@ class GuildManager extends Manager { final response = await client.httpHandler.executeSafe(request); final guild = parse(response.jsonBody as Map); - cache[guild.id] = guild; + client.updateCacheWith(guild); return guild; } @@ -687,10 +763,13 @@ class GuildManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, parseGuildTemplate); + final templates = parseMany(response.jsonBody as List, parseGuildTemplate); + + templates.forEach(client.updateCacheWith); + return templates; } - /// Create a guild template from a guild. + /// Create a guild template. Future createGuildTemplate(Snowflake id, GuildTemplateBuilder builder) async { final route = HttpRoute() ..guilds(id: id.toString()) @@ -698,7 +777,10 @@ class GuildManager extends Manager { final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); final response = await client.httpHandler.executeSafe(request); - return parseGuildTemplate(response.jsonBody as Map); + final template = parseGuildTemplate(response.jsonBody as Map); + + client.updateCacheWith(template); + return template; } /// Sync a guild template to the source guild. @@ -709,7 +791,10 @@ class GuildManager extends Manager { final request = BasicRequest(route, method: 'PUT'); final response = await client.httpHandler.executeSafe(request); - return parseGuildTemplate(response.jsonBody as Map); + final template = parseGuildTemplate(response.jsonBody as Map); + + client.updateCacheWith(template); + return template; } /// Update a guild template. @@ -720,7 +805,10 @@ class GuildManager extends Manager { final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); final response = await client.httpHandler.executeSafe(request); - return parseGuildTemplate(response.jsonBody as Map); + final template = parseGuildTemplate(response.jsonBody as Map); + + client.updateCacheWith(template); + return template; } /// Delete a guild template. @@ -731,6 +819,10 @@ class GuildManager extends Manager { final request = BasicRequest(route, method: 'DELETE'); final response = await client.httpHandler.executeSafe(request); - return parseGuildTemplate(response.jsonBody as Map); + final template = parseGuildTemplate(response.jsonBody as Map); + + // Templates aren't cached, so we don't need to remove it from any cache, but it still contains a nested user object we can cache. + client.updateCacheWith(template); + return template; } } diff --git a/lib/src/http/managers/integration_manager.dart b/lib/src/http/managers/integration_manager.dart index 149ca6164..51b3fe69c 100644 --- a/lib/src/http/managers/integration_manager.dart +++ b/lib/src/http/managers/integration_manager.dart @@ -1,10 +1,10 @@ -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/guild/integration.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A [Manager] for [Integration]s. @@ -29,7 +29,7 @@ class IntegrationManager extends ReadOnlyManager { isSyncing: raw['syncing'] as bool?, roleId: maybeParse(raw['role_id'], Snowflake.parse), enableEmoticons: raw['enable_emoticons'] as bool?, - expireBehavior: maybeParse(raw['expire_behavior'], IntegrationExpireBehavior.parse), + expireBehavior: maybeParse(raw['expire_behavior'], IntegrationExpireBehavior.new), expireGracePeriod: maybeParse(raw['expire_grace_period'], (int value) => Duration(days: value)), user: maybeParse(raw['user'], client.users.parse), account: parseIntegrationAccount(raw['account'] as Map), @@ -80,7 +80,7 @@ class IntegrationManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final integrations = parseMany(response.jsonBody as List, parse); - cache.addEntities(integrations); + integrations.forEach(client.updateCacheWith); return integrations; } diff --git a/lib/src/http/managers/interaction_manager.dart b/lib/src/http/managers/interaction_manager.dart index 88b46637f..d6ecb89f8 100644 --- a/lib/src/http/managers/interaction_manager.dart +++ b/lib/src/http/managers/interaction_manager.dart @@ -7,6 +7,7 @@ import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/client.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/channel/text_channel.dart'; import 'package:nyxx/src/models/commands/application_command.dart'; @@ -17,6 +18,7 @@ import 'package:nyxx/src/models/message/component.dart'; import 'package:nyxx/src/models/message/message.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A [Manager] for [Interaction]s. @@ -31,22 +33,36 @@ class InteractionManager { InteractionManager(this.client, {required this.applicationId}); Interaction parse(Map raw) { - final type = InteractionType.parse(raw['type'] as int); + final type = InteractionType(raw['type'] as int); + final guildId = maybeParse(raw['guild_id'], Snowflake.parse); final channelId = maybeParse(raw['channel_id'], Snowflake.parse); final id = Snowflake.parse(raw['id']!); final applicationId = Snowflake.parse(raw['application_id']!); final channel = maybeParse(raw['channel'], (Map raw) => client.channels[Snowflake.parse(raw['id']!)]); - final member = maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse); + // Don't use a tearoff so we don't evaluate `guildId!` unless member is set. + final member = maybeParse(raw['member'], (Map raw) => client.guilds[guildId!].members.parse(raw)); final user = maybeParse(raw['user'], client.users.parse); final token = raw['token'] as String; final version = raw['version'] as int; - final message = maybeParse(raw['message'], (client.channels[channelId ?? Snowflake.zero] as PartialTextChannel).messages.parse); - final appPermissions = maybeParse(raw['app_permissions'], (String raw) => Permissions(int.parse(raw))); + // Don't use a tearoff so we don't evaluate `channelId!` unless message is set. + final message = maybeParse( + raw['message'], + (Map raw) => (client.channels[channelId!] as PartialTextChannel).messages.parse(raw, guildId: guildId), + ); + final appPermissions = Permissions(int.parse(raw['app_permissions'] as String)); final locale = maybeParse(raw['locale'], Locale.parse); final guildLocale = maybeParse(raw['guild_locale'], Locale.parse); final entitlements = parseMany(raw['entitlements'] as List, client.applications[applicationId].entitlements.parse); + final authorizingIntegrationOwners = maybeParse( + raw['authorizing_integration_owners'], + (Map map) => { + for (final MapEntry(:key, :value) in map.entries) ApplicationIntegrationType(int.parse(key)): Snowflake.parse(value!), + }, + ); + final context = maybeParse(raw['context'], InteractionContextType.new); + return switch (type) { InteractionType.ping => PingInteraction( manager: this, @@ -65,6 +81,8 @@ class InteractionManager { locale: locale, guildLocale: guildLocale, entitlements: entitlements, + authorizingIntegrationOwners: authorizingIntegrationOwners, + context: context, ), InteractionType.applicationCommand => ApplicationCommandInteraction( manager: this, @@ -84,13 +102,15 @@ class InteractionManager { locale: locale, guildLocale: guildLocale, entitlements: entitlements, + authorizingIntegrationOwners: authorizingIntegrationOwners, + context: context, ), InteractionType.messageComponent => MessageComponentInteraction( manager: this, id: id, applicationId: applicationId, type: type, - data: parseMessageComponentInteractionData(raw['data'] as Map), + data: parseMessageComponentInteractionData(raw['data'] as Map, guildId: guildId, channelId: channelId), guildId: guildId, channel: channel, channelId: channelId, @@ -103,6 +123,8 @@ class InteractionManager { locale: locale, guildLocale: guildLocale, entitlements: entitlements, + authorizingIntegrationOwners: authorizingIntegrationOwners, + context: context, ), InteractionType.modalSubmit => ModalSubmitInteraction( manager: this, @@ -122,6 +144,8 @@ class InteractionManager { locale: locale, guildLocale: guildLocale, entitlements: entitlements, + authorizingIntegrationOwners: authorizingIntegrationOwners, + context: context, ), InteractionType.applicationCommandAutocomplete => ApplicationCommandAutocompleteInteraction( manager: this, @@ -141,7 +165,10 @@ class InteractionManager { locale: locale, guildLocale: guildLocale, entitlements: entitlements, + authorizingIntegrationOwners: authorizingIntegrationOwners, + context: context, ), + InteractionType() => throw StateError('Unknown interaction type: $type'), } as Interaction; } @@ -149,9 +176,11 @@ class InteractionManager { return ApplicationCommandInteractionData( id: Snowflake.parse(raw['id']!), name: raw['name'] as String, - type: ApplicationCommandType.parse(raw['type'] as int), + type: ApplicationCommandType(raw['type'] as int), resolved: maybeParse(raw['resolved'], (Map raw) => parseResolvedData(raw, guildId: guildId, channelId: channelId)), options: maybeParseMany(raw['options'], parseInteractionOption), + // This guild_id is the ID of the guild the command is registered in, so it may be null even if the command was executed in a guild. + // Because of this, we still need the guildId parameter for the actual guild the command was executed in. guildId: maybeParse(raw['guild_id'], Snowflake.parse), targetId: maybeParse(raw['target_id'], Snowflake.parse), ); @@ -212,19 +241,19 @@ class InteractionManager { InteractionOption parseInteractionOption(Map raw) { return InteractionOption( name: raw['name'] as String, - type: CommandOptionType.parse(raw['type'] as int), + type: CommandOptionType(raw['type'] as int), value: raw['value'], options: maybeParseMany(raw['options'], parseInteractionOption), isFocused: raw['focused'] as bool?, ); } - MessageComponentInteractionData parseMessageComponentInteractionData(Map raw) { + MessageComponentInteractionData parseMessageComponentInteractionData(Map raw, {Snowflake? guildId, Snowflake? channelId}) { return MessageComponentInteractionData( customId: raw['custom_id'] as String, - type: MessageComponentType.parse(raw['component_type'] as int), + type: MessageComponentType(raw['component_type'] as int), values: maybeParseMany(raw['values']), - resolved: maybeParse(raw['resolved'], parseResolvedData), + resolved: maybeParse(raw['resolved'], (Map raw) => parseResolvedData(raw, guildId: guildId, channelId: channelId)), ); } @@ -330,7 +359,10 @@ class InteractionManager { final response = await client.httpHandler.executeSafe(request); final channelId = Snowflake.parse((response.jsonBody as Map)['channel_id']!); - return (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + final message = (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + + client.updateCacheWith(message); + return message; } /// Fetch a followup to an interaction. @@ -351,7 +383,10 @@ class InteractionManager { final response = await client.httpHandler.executeSafe(request); final channelId = Snowflake.parse((response.jsonBody as Map)['channel_id']!); - return (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + final message = (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + + client.updateCacheWith(message); + return message; } Future _updateResponse(String token, String messageId, MessageUpdateBuilder builder) async { @@ -393,7 +428,10 @@ class InteractionManager { final response = await client.httpHandler.executeSafe(request); final channelId = Snowflake.parse((response.jsonBody as Map)['channel_id']!); - return (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + final message = (client.channels[channelId] as PartialTextChannel).messages.parse(response.jsonBody as Map); + + client.updateCacheWith(message); + return message; } Future _deleteResponse(String token, String messageId) async { diff --git a/lib/src/http/managers/invite_manager.dart b/lib/src/http/managers/invite_manager.dart index a43c3923d..b5e4a0076 100644 --- a/lib/src/http/managers/invite_manager.dart +++ b/lib/src/http/managers/invite_manager.dart @@ -7,6 +7,7 @@ import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/invite/invite.dart'; import 'package:nyxx/src/models/invite/invite_metadata.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [Invite]s. @@ -25,11 +26,12 @@ class InviteManager { ); return Invite( + type: InviteType(raw['type'] as int), code: raw['code'] as String, guild: guild, channel: PartialChannel(id: Snowflake.parse((raw['channel'] as Map)['id']!), manager: client.channels), inviter: maybeParse(raw['inviter'], client.users.parse), - targetType: maybeParse(raw['target_type'], TargetType.parse), + targetType: maybeParse(raw['target_type'], TargetType.new), targetUser: maybeParse(raw['target_user'], client.users.parse), targetApplication: maybeParse( raw['target_application'], @@ -38,14 +40,17 @@ class InviteManager { approximatePresenceCount: raw['approximate_presence_count'] as int?, approximateMemberCount: raw['approximate_member_count'] as int?, expiresAt: maybeParse(raw['expires_at'], DateTime.parse), - guildScheduledEvent: maybeParse(raw['guild_scheduled_event'], client.guilds[guild?.id ?? Snowflake.zero].scheduledEvents.parse), + // Don't use a tearoff so we don't evaluate `guild!.id` unless guild_scheduled_event is set. + guildScheduledEvent: maybeParse(raw['guild_scheduled_event'], (Map raw) => client.guilds[guild!.id].scheduledEvents.parse(raw)), ); } + /// Parse an [InviteWithMetadata] from [raw]. InviteWithMetadata parseWithMetadata(Map raw) { final invite = parse(raw); return InviteWithMetadata( + type: invite.type, code: invite.code, guild: invite.guild, channel: invite.channel, @@ -65,7 +70,6 @@ class InviteManager { ); } - /// Fetch an invite. Future fetch(String code, {bool? withCounts, bool? withExpiration, Snowflake? scheduledEventId}) async { final route = HttpRoute()..invites(id: code); final request = BasicRequest(route, queryParameters: { @@ -75,15 +79,22 @@ class InviteManager { }); final response = await client.httpHandler.executeSafe(request); - return parse(response.jsonBody as Map); + final invite = parse(response.jsonBody as Map); + + client.updateCacheWith(invite); + return invite; } /// Delete an invite. - Future delete(String code) async { + Future delete(String code, {String? auditLogReason}) async { final route = HttpRoute()..invites(id: code); - final request = BasicRequest(route, method: 'DELETE'); + final request = BasicRequest(route, method: 'DELETE', auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); - return parse(response.jsonBody as Map); + final invite = parse(response.jsonBody as Map); + + // Invites aren't cached, so we don't need to remove it, but it still contains nested objects we can cache. + client.updateCacheWith(invite); + return invite; } } diff --git a/lib/src/http/managers/manager.dart b/lib/src/http/managers/manager.dart index 33cd534af..3293ba5b1 100644 --- a/lib/src/http/managers/manager.dart +++ b/lib/src/http/managers/manager.dart @@ -15,7 +15,7 @@ abstract class ReadOnlyManager> { final NyxxRest client; /// Create a new read-only manager. - ReadOnlyManager(CacheConfig config, this.client, {required String identifier}) : cache = Cache(client, identifier, config); + ReadOnlyManager(CacheConfig config, this.client, {required String identifier}) : cache = client.cache.getCache(identifier, config); /// Parse the [raw] data received from the API into an instance of the type of this manager. T parse(Map raw); diff --git a/lib/src/http/managers/member_manager.dart b/lib/src/http/managers/member_manager.dart index c238e5ee0..a083c39b9 100644 --- a/lib/src/http/managers/member_manager.dart +++ b/lib/src/http/managers/member_manager.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:nyxx/src/builders/guild/member.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; @@ -9,6 +8,7 @@ import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/guild/member.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [Member]s. @@ -24,7 +24,7 @@ class MemberManager extends Manager { @override Member parse(Map raw, {Snowflake? userId}) { return Member( - id: userId ?? Snowflake.parse((raw['user'] as Map)['id']!), + id: maybeParse((raw['user'] as Map?)?['id'], Snowflake.parse) ?? userId ?? Snowflake.zero, manager: this, user: maybeParse(raw['user'], client.users.parse), nick: raw['nick'] as String?, @@ -49,9 +49,9 @@ class MemberManager extends Manager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - final member = parse(response.jsonBody as Map); + final member = parse(response.jsonBody as Map, userId: id); - cache[member.id] = member; + client.updateCacheWith(member); return member; } @@ -68,7 +68,7 @@ class MemberManager extends Manager { final response = await client.httpHandler.executeSafe(request); final members = parseMany(response.jsonBody as List, parse); - cache.addEntities(members); + members.forEach(client.updateCacheWith); return members; } @@ -84,9 +84,9 @@ class MemberManager extends Manager { throw MemberAlreadyExistsException(guildId, builder.userId); } - final member = parse(response.jsonBody as Map); + final member = parse(response.jsonBody as Map, userId: builder.userId); - cache[member.id] = member; + client.updateCacheWith(member); return member; } @@ -98,12 +98,13 @@ class MemberManager extends Manager { final request = BasicRequest(route, method: 'PATCH', auditLogReason: auditLogReason, body: jsonEncode(builder.build())); final response = await client.httpHandler.executeSafe(request); - final member = parse(response.jsonBody as Map); + final member = parse(response.jsonBody as Map, userId: id); - cache[member.id] = member; + client.updateCacheWith(member); return member; } + /// Kick a member. @override Future delete(Snowflake id, {String? auditLogReason}) async { final route = HttpRoute() @@ -124,20 +125,23 @@ class MemberManager extends Manager { final request = BasicRequest(route, queryParameters: {'query': query, if (limit != null) 'limit': limit.toString()}); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, parse); + final members = parseMany(response.jsonBody as List, parse); + + members.forEach(client.updateCacheWith); + return members; } /// Update the current member in the guild. - Future updateCurrentMember(CurrentMemberUpdateBuilder builder) async { + Future updateCurrentMember(CurrentMemberUpdateBuilder builder, {String? auditLogReason}) async { final route = HttpRoute() ..guilds(id: guildId.toString()) ..members(id: '@me'); - final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build())); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build()), auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); - final member = parse(response.jsonBody as Map); + final member = parse(response.jsonBody as Map, userId: client.user.id); - cache[member.id] = member; + client.updateCacheWith(member); return member; } diff --git a/lib/src/http/managers/message_manager.dart b/lib/src/http/managers/message_manager.dart index 1a3eb54bd..b26203714 100644 --- a/lib/src/http/managers/message_manager.dart +++ b/lib/src/http/managers/message_manager.dart @@ -4,12 +4,12 @@ import 'package:http/http.dart' show MultipartFile; import 'package:nyxx/src/builders/emoji/reaction.dart'; import 'package:nyxx/src/builders/message/message.dart'; import 'package:nyxx/src/builders/sentinels.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/channel.dart'; +import 'package:nyxx/src/models/channel/guild_channel.dart'; import 'package:nyxx/src/models/channel/thread.dart'; import 'package:nyxx/src/models/discord_color.dart'; import 'package:nyxx/src/models/interaction.dart'; @@ -23,8 +23,10 @@ import 'package:nyxx/src/models/message/message.dart'; import 'package:nyxx/src/models/message/reaction.dart'; import 'package:nyxx/src/models/message/reference.dart'; import 'package:nyxx/src/models/message/role_subscription_data.dart'; +import 'package:nyxx/src/models/message/poll.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [Message]s in a [TextChannel]. @@ -39,30 +41,37 @@ class MessageManager extends Manager { PartialMessage operator [](Snowflake id) => PartialMessage(id: id, manager: this); @override - Message parse(Map raw) { + Message parse(Map raw, {Snowflake? guildId}) { + if (client.channels.cache[channelId] case GuildChannel(guildId: final guildIdFromChannel)) { + guildId ??= guildIdFromChannel; + } + final webhookId = maybeParse(raw['webhook_id'], Snowflake.parse); + final snapshot = parseMessageSnapshot(raw); + return Message( id: Snowflake.parse(raw['id']!), manager: this, + content: snapshot.content, + timestamp: snapshot.timestamp, + editedTimestamp: snapshot.editedTimestamp, + attachments: snapshot.attachments, + embeds: snapshot.embeds, + flags: snapshot.flags, + mentions: snapshot.mentions, + roleMentionIds: snapshot.roleMentionIds, + type: snapshot.type, author: (webhookId == null ? client.users.parse(raw['author'] as Map) : client.webhooks.parseWebhookAuthor(raw['author'] as Map)) as MessageAuthor, - content: raw['content'] as String, - timestamp: DateTime.parse(raw['timestamp'] as String), - editedTimestamp: maybeParse(raw['edited_timestamp'], DateTime.parse), isTts: raw['tts'] as bool, mentionsEveryone: raw['mention_everyone'] as bool, - mentions: parseMany(raw['mentions'] as List, client.users.parse), - roleMentionIds: parseMany(raw['mention_roles'] as List, Snowflake.parse), channelMentions: maybeParseMany(raw['mention_channels'], parseChannelMention) ?? [], - attachments: parseMany(raw['attachments'] as List, parseAttachment), - embeds: parseMany(raw['embeds'] as List, parseEmbed), reactions: maybeParseMany(raw['reactions'], parseReaction) ?? [], nonce: raw['nonce'] /* as int | String */, isPinned: raw['pinned'] as bool, webhookId: webhookId, - type: MessageType.parse(raw['type'] as int), activity: maybeParse(raw['activity'], parseMessageActivity), application: maybeParse( raw['application'], @@ -70,18 +79,27 @@ class MessageManager extends Manager { ), applicationId: maybeParse(raw['application_id'], Snowflake.parse), reference: maybeParse(raw['message_reference'], parseMessageReference), - flags: MessageFlags(raw['flags'] as int? ?? 0), + messageSnapshots: maybeParseMany( + raw['message_snapshots'], + (Map raw) => parseMessageSnapshot(raw['message'] as Map), + ), referencedMessage: maybeParse(raw['referenced_message'], parse), interaction: maybeParse( raw['interaction'], - (Map raw) => parseMessageInteraction(raw), + (Map raw) => parseMessageInteraction(raw, guildId: guildId), + ), + interactionMetadata: maybeParse( + raw['interaction_metadata'], + parseMessageInteractionMetadata, ), thread: maybeParse(raw['thread'], client.channels.parse) as Thread?, components: maybeParseMany(raw['components'], parseMessageComponent), position: raw['position'] as int?, roleSubscriptionData: maybeParse(raw['role_subscription_data'], parseRoleSubscriptionData), stickers: parseMany(raw['sticker_items'] as List? ?? [], client.stickers.parseStickerItem), - resolved: maybeParse(raw['resolved'], client.interactions.parseResolvedData), + resolved: maybeParse(raw['resolved'], (Map raw) => client.interactions.parseResolvedData(raw, guildId: guildId, channelId: channelId)), + poll: maybeParse(raw['poll'], parsePoll), + call: maybeParse(raw['call'], parseMessageCall), ); } @@ -90,7 +108,7 @@ class MessageManager extends Manager { id: Snowflake.parse(raw['id']!), manager: client.channels, guildId: Snowflake.parse(raw['guild_id']!), - type: ChannelType.parse(raw['type'] as int), + type: ChannelType(raw['type'] as int), name: raw['name'] as String, ); } @@ -117,6 +135,7 @@ class MessageManager extends Manager { Embed parseEmbed(Map raw) { return Embed( title: raw['title'] as String?, + type: EmbedType(raw['type'] as String), description: raw['description'] as String?, url: maybeParse(raw['url'], Uri.parse), timestamp: maybeParse(raw['timestamp'], DateTime.parse), @@ -210,13 +229,14 @@ class MessageManager extends Manager { MessageActivity parseMessageActivity(Map raw) { return MessageActivity( - type: MessageActivityType.parse(raw['type'] as int), + type: MessageActivityType(raw['type'] as int), partyId: raw['party_id'] as String?, ); } MessageReference parseMessageReference(Map raw) { return MessageReference( + type: maybeParse(raw['type'], MessageReferenceType.new) ?? MessageReferenceType.defaultType, manager: this, messageId: maybeParse(raw['message_id'], Snowflake.parse), channelId: Snowflake.parse(raw['channel_id']!), @@ -234,23 +254,24 @@ class MessageManager extends Manager { } MessageComponent parseMessageComponent(Map raw) { - final type = MessageComponentType.parse(raw['type'] as int); + final type = MessageComponentType(raw['type'] as int); return switch (type) { MessageComponentType.actionRow => ActionRowComponent( components: parseMany(raw['components'] as List, parseMessageComponent), ), MessageComponentType.button => ButtonComponent( - style: ButtonStyle.parse(raw['style'] as int), - label: raw['label'] as String, + style: ButtonStyle(raw['style'] as int), + label: raw['label'] as String?, emoji: maybeParse(raw['emoji'], client.guilds[Snowflake.zero].emojis.parse), customId: raw['custom_id'] as String?, + skuId: maybeParse(raw['sku_id'], Snowflake.parse), url: maybeParse(raw['url'], Uri.parse), isDisabled: raw['disabled'] as bool?, ), MessageComponentType.textInput => TextInputComponent( customId: raw['custom_id'] as String, - style: maybeParse(raw['style'], TextInputStyle.parse), + style: maybeParse(raw['style'], TextInputStyle.new), label: raw['label'] as String?, minLength: raw['min_length'] as int?, maxLength: raw['max_length'] as int?, @@ -267,12 +288,14 @@ class MessageManager extends Manager { type: type, customId: raw['custom_id'] as String, options: maybeParseMany(raw['options'], parseSelectMenuOption), - channelTypes: maybeParseMany(raw['channel_types'], ChannelType.parse), + channelTypes: maybeParseMany(raw['channel_types'], ChannelType.new), placeholder: raw['placeholder'] as String?, + defaultValues: maybeParseMany(raw['default_values'], parseSelectMenuDefaultValue), minValues: raw['min_values'] as int?, maxValues: raw['max_values'] as int?, isDisabled: raw['disabled'] as bool?, ), + MessageComponentType() => throw StateError('Unknown message component type: $type'), }; } @@ -286,22 +309,114 @@ class MessageManager extends Manager { ); } - MessageInteraction parseMessageInteraction(Map raw) { + SelectMenuDefaultValue parseSelectMenuDefaultValue(Map raw) { + return SelectMenuDefaultValue( + id: Snowflake.parse(raw['id']!), + type: SelectMenuDefaultValueType(raw['type'] as String), + ); + } + + // ignore: deprecated_member_use_from_same_package + MessageInteraction parseMessageInteraction(Map raw, {Snowflake? guildId}) { final user = client.users.parse(raw['user'] as Map); + // ignore: deprecated_member_use_from_same_package return MessageInteraction( id: Snowflake.parse(raw['id']!), - type: InteractionType.parse(raw['type'] as int), + type: InteractionType(raw['type'] as int), name: raw['name'] as String, user: user, - // TODO: Find a way to get the guild ID. member: maybeParse( raw['member'], - (Map raw) => client.guilds[Snowflake.zero].members[user.id], + (Map raw) => client.guilds[guildId ?? Snowflake.zero].members[user.id], ), ); } + MessageInteractionMetadata parseMessageInteractionMetadata(Map raw) { + final user = client.users.parse(raw['user'] as Map); + + return MessageInteractionMetadata( + id: Snowflake.parse(raw['id']!), + type: InteractionType(raw['type'] as int), + user: user, + authorizingIntegrationOwners: { + for (final MapEntry(:key, :value) in (raw['authorizing_integration_owners'] as Map).entries) + ApplicationIntegrationType(int.parse(key)): Snowflake.parse(value!), + }, + originalResponseMessageId: maybeParse(raw['original_response_message_id'], Snowflake.parse), + interactedMessageId: maybeParse(raw['interacted_message_id'], Snowflake.parse), + triggeringInteractionMetadata: maybeParse(raw['triggering_interaction_metadata'], parseMessageInteractionMetadata), + ); + } + + PollMedia parsePollMedia(Map raw) { + return PollMedia( + text: raw['text'] as String?, + emoji: maybeParse(raw['emoji'], client.guilds[Snowflake.zero].emojis.parse), + ); + } + + PollAnswer parsePollAnswer(Map raw) { + return PollAnswer( + id: raw['answer_id'] as int, + pollMedia: parsePollMedia(raw['poll_media'] as Map), + ); + } + + PollAnswerCount parsePollAnswerCount(Map raw) { + return PollAnswerCount( + answerId: raw['id'] as int, + count: raw['count'] as int, + me: raw['me_voted'] as bool, + ); + } + + PollResults parsePollResults(Map raw) { + return PollResults( + isFinalized: raw['is_finalized'] as bool, + answerCounts: parseMany(raw['answer_counts'] as List, parsePollAnswerCount), + ); + } + + Poll parsePoll(Map raw) { + return Poll( + question: parsePollMedia(raw['question'] as Map), + answers: parseMany(raw['answers'] as List, parsePollAnswer), + endsAt: maybeParse(raw['expiry'] as String?, DateTime.parse), + allowsMultiselect: raw['allow_multiselect'] as bool, + layoutType: PollLayoutType(raw['layout_type'] as int), + results: maybeParse(raw['results'], parsePollResults), + ); + } + + /// Parse a [MessageSnapshot] from [raw]. + /// + /// [raw] must be the inner `message` field from the actual message snapshot + /// object. See the comment on [MessageReference] for why. + MessageSnapshot parseMessageSnapshot(Map raw) { + return MessageSnapshot( + content: raw['content'] as String, + timestamp: DateTime.parse(raw['timestamp'] as String), + editedTimestamp: maybeParse(raw['edited_timestamp'], DateTime.parse), + attachments: parseMany(raw['attachments'] as List, parseAttachment), + embeds: parseMany(raw['embeds'] as List, parseEmbed), + flags: MessageFlags(raw['flags'] as int? ?? 0), + mentions: parseMany(raw['mentions'] as List, client.users.parse), + // https://github.com/discord/discord-api-docs/issues/7193 + roleMentionIds: maybeParseMany(raw['mention_roles'] as List?, Snowflake.parse) ?? [], + type: MessageType(raw['type'] as int), + ); + } + + MessageCall parseMessageCall(Map raw) { + return MessageCall( + manager: this, + participantIds: parseMany(raw['participants'] as List, Snowflake.parse), + endedAt: maybeParse(raw['ended_at'], DateTime.parse), + ); + } + @override Future create(MessageBuilder builder) async { final route = HttpRoute() @@ -337,7 +452,7 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final message = parse(response.jsonBody as Map); - cache[message.id] = message; + client.updateCacheWith(message); return message; } @@ -351,7 +466,7 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final message = parse(response.jsonBody as Map); - cache[message.id] = message; + client.updateCacheWith(message); return message; } @@ -390,7 +505,7 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final message = parse(response.jsonBody as Map); - cache[message.id] = message; + client.updateCacheWith(message); return message; } @@ -424,7 +539,7 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final messages = parseMany(response.jsonBody as List, parse); - cache.addEntities(messages); + messages.forEach(client.updateCacheWith); return messages; } @@ -439,19 +554,24 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final message = parse(response.jsonBody as Map); - cache[message.id] = message; + client.updateCacheWith(message); return message; } /// Bulk delete many messages at once /// /// This will throw an error if any of [ids] is not a valid message ID or if any of the messages are from before [Snowflake.bulkDeleteLimit]. - Future bulkDelete(Iterable ids) async { + Future bulkDelete(Iterable ids, {String? auditLogReason}) async { final route = HttpRoute() ..channels(id: channelId.toString()) ..messages() ..bulkDelete(); - final request = BasicRequest(route, method: 'POST', body: jsonEncode(ids.map((e) => e.toString()).toList())); + final request = BasicRequest( + route, + method: 'POST', + body: jsonEncode({'messages': ids.map((e) => e.toString()).toList()}), + auditLogReason: auditLogReason, + ); await client.httpHandler.executeSafe(request); } @@ -466,7 +586,7 @@ class MessageManager extends Manager { final response = await client.httpHandler.executeSafe(request); final messages = parseMany(response.jsonBody as List, parse); - cache.addEntities(messages); + messages.forEach(client.updateCacheWith); return messages; } @@ -550,17 +670,59 @@ class MessageManager extends Manager { } /// Get a list of users that reacted with a given emoji on a message. - Future> fetchReactions(Snowflake id, ReactionBuilder emoji) async { + Future> fetchReactions(Snowflake id, ReactionBuilder emoji, {Snowflake? after, int? limit}) async { final route = HttpRoute() ..channels(id: channelId.toString()) ..messages(id: id.toString()) ..reactions(emoji: emoji.build()); - final request = BasicRequest(route); + final request = BasicRequest( + route, + queryParameters: { + if (after != null) 'after': after.toString(), + if (limit != null) 'limit': limit.toString(), + }, + ); final response = await client.httpHandler.executeSafe(request); + final users = parseMany(response.jsonBody as List, client.users.parse); - return parseMany(response.jsonBody as List, client.users.parse); + users.forEach(client.updateCacheWith); + return users; } - // TODO once oauth2 is implemented: Group DM control endpoints + /// Get a list of users that voted for this specific answer. + Future> fetchAnswerVoters(Snowflake id, int answerId, {Snowflake? after, int? limit}) async { + final route = HttpRoute() + ..channels(id: channelId.toString()) + ..polls(id: id.toString()) + ..answers(id: answerId); + final request = BasicRequest( + route, + queryParameters: { + if (after != null) 'after': after.toString(), + if (limit != null) 'limit': limit.toString(), + }, + ); + + final response = await client.httpHandler.executeSafe(request); + final users = parseMany((response.jsonBody as Map)['users'] as List, client.users.parse); + + users.forEach(client.updateCacheWith); + return users; + } + + /// Immediately ends the poll. + Future endPoll(Snowflake id) async { + final route = HttpRoute() + ..channels(id: channelId.toString()) + ..polls(id: id.toString()) + ..expire(); + final request = BasicRequest(route, method: 'POST'); + + final response = await client.httpHandler.executeSafe(request); + final message = parse(response.jsonBody as Map); + + client.updateCacheWith(message); + return message; + } } diff --git a/lib/src/http/managers/role_manager.dart b/lib/src/http/managers/role_manager.dart index 564d8de95..246319588 100644 --- a/lib/src/http/managers/role_manager.dart +++ b/lib/src/http/managers/role_manager.dart @@ -1,8 +1,6 @@ import 'dart:convert'; import 'package:nyxx/src/builders/role.dart'; -import 'package:nyxx/src/cache/cache.dart'; -import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; @@ -10,6 +8,7 @@ import 'package:nyxx/src/models/discord_color.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/role.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [Role]s. @@ -46,7 +45,10 @@ class RoleManager extends Manager { return RoleTags( botId: maybeParse(raw['bot_id'], Snowflake.parse), integrationId: maybeParse(raw['integration_id'], Snowflake.parse), + isPremiumSubscriber: raw.containsKey('premium_subscriber'), subscriptionListingId: maybeParse(raw['subscription_listing_id'], Snowflake.parse), + isAvailableForPurchase: raw.containsKey('available_for_purchase'), + isLinkedRole: raw.containsKey('guild_connections'), ); } @@ -60,20 +62,22 @@ class RoleManager extends Manager { final response = await client.httpHandler.executeSafe(request); final roles = parseMany(response.jsonBody as List, parse); - cache.addEntities(roles); + roles.forEach(client.updateCacheWith); return roles; } @override Future fetch(Snowflake id) async { - // There isn't an endpoint to fetch a single role. Re-fetch all the roles to ensure they are up to date, - // and return the matching role. - final roles = await list(); + final route = HttpRoute() + ..guilds(id: guildId.toString()) + ..roles(id: id.toString()); - return roles.firstWhere( - (role) => role.id == id, - orElse: () => throw RoleNotFoundException(guildId, id), - ); + final request = BasicRequest(route); + final response = await client.httpHandler.executeSafe(request); + final role = parse(response.jsonBody as Map); + + client.updateCacheWith(role); + return role; } @override @@ -86,7 +90,7 @@ class RoleManager extends Manager { final response = await client.httpHandler.executeSafe(request); final role = parse(response.jsonBody as Map); - cache[role.id] = role; + client.updateCacheWith(role); return role; } @@ -100,7 +104,7 @@ class RoleManager extends Manager { final response = await client.httpHandler.executeSafe(request); final role = parse(response.jsonBody as Map); - cache[role.id] = role; + client.updateCacheWith(role); return role; } @@ -130,7 +134,7 @@ class RoleManager extends Manager { final response = await client.httpHandler.executeSafe(request); final roles = parseMany(response.jsonBody as List, parse); - cache.addEntities(roles); + roles.forEach(client.updateCacheWith); return roles; } } diff --git a/lib/src/http/managers/scheduled_event_manager.dart b/lib/src/http/managers/scheduled_event_manager.dart index bf638e862..8ad7cacdc 100644 --- a/lib/src/http/managers/scheduled_event_manager.dart +++ b/lib/src/http/managers/scheduled_event_manager.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'package:nyxx/src/builders/guild/scheduled_event.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/channel/stage_instance.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A [Manager] for [ScheduledEvent]s. @@ -26,20 +26,21 @@ class ScheduledEventManager extends Manager { id: Snowflake.parse(raw['id']!), manager: this, guildId: Snowflake.parse(raw['guild_id']!), - channelId: Snowflake.parse(raw['channel_id']!), + channelId: maybeParse(raw['channel_id'], Snowflake.parse), creatorId: maybeParse(raw['creator_id'], Snowflake.parse), name: raw['name'] as String, description: raw['description'] as String?, scheduledStartTime: DateTime.parse(raw['scheduled_start_time'] as String), scheduledEndTime: maybeParse(raw['scheduled_end_time'], DateTime.parse), - privacyLevel: PrivacyLevel.parse(raw['privacy_level'] as int), - status: EventStatus.parse(raw['status'] as int), - type: ScheduledEntityType.parse(raw['entity_type'] as int), + privacyLevel: PrivacyLevel(raw['privacy_level'] as int), + status: EventStatus(raw['status'] as int), + type: ScheduledEntityType(raw['entity_type'] as int), entityId: maybeParse(raw['entity_id'], Snowflake.parse), metadata: maybeParse(raw['entity_metadata'], parseEntityMetadata), creator: maybeParse(raw['creator'], client.users.parse), userCount: raw['user_count'] as int?, coverImageHash: raw['image'] as String?, + recurrenceRule: maybeParse(raw['recurrence_rule'], parseRecurrenceRule), ); } @@ -50,11 +51,35 @@ class ScheduledEventManager extends Manager { } ScheduledEventUser parseScheduledEventUser(Map raw) { + final user = client.users.parse(raw['user'] as Map); + return ScheduledEventUser( manager: this, scheduledEventId: Snowflake.parse(raw['guild_scheduled_event_id']!), - user: client.users.parse(raw['user'] as Map), - member: maybeParse(raw['member'], client.guilds[guildId].members.parse), + user: user, + member: maybeParse(raw['member'], (Map raw) => client.guilds[guildId].members.parse(raw, userId: user.id)), + ); + } + + RecurrenceRule parseRecurrenceRule(Map raw) { + return RecurrenceRule( + start: DateTime.parse(raw['start'] as String), + end: maybeParse(raw['end'], DateTime.parse), + frequency: RecurrenceRuleFrequency(raw['frequency'] as int), + interval: raw['interval'] as int, + byWeekday: maybeParseMany(raw['by_weekday'], RecurrenceRuleWeekday.new), + byNWeekday: maybeParseMany(raw['by_n_weekday'], parseRecurrenceRuleNWeekday), + byMonth: maybeParseMany(raw['by_month'], RecurrenceRuleMonth.new), + byMonthDay: maybeParseMany(raw['by_month_day']), + byYearDay: maybeParseMany(raw['by_year_day']), + count: raw['count'] as int?, + ); + } + + RecurrenceRuleNWeekday parseRecurrenceRuleNWeekday(Map raw) { + return RecurrenceRuleNWeekday( + n: raw['n'] as int, + day: RecurrenceRuleWeekday(raw['day'] as int), ); } @@ -68,7 +93,7 @@ class ScheduledEventManager extends Manager { final response = await client.httpHandler.executeSafe(request); final event = parse(response.jsonBody as Map); - cache[event.id] = event; + client.updateCacheWith(event); return event; } @@ -82,7 +107,7 @@ class ScheduledEventManager extends Manager { final response = await client.httpHandler.executeSafe(request); final events = parseMany(response.jsonBody as List, parse); - cache.addEntities(events); + events.forEach(client.updateCacheWith); return events; } @@ -96,7 +121,7 @@ class ScheduledEventManager extends Manager { final response = await client.httpHandler.executeSafe(request); final event = parse(response.jsonBody as Map); - cache[event.id] = event; + client.updateCacheWith(event); return event; } @@ -110,7 +135,7 @@ class ScheduledEventManager extends Manager { final response = await client.httpHandler.executeSafe(request); final event = parse(response.jsonBody as Map); - cache[event.id] = event; + client.updateCacheWith(event); return event; } @@ -140,6 +165,9 @@ class ScheduledEventManager extends Manager { }); final response = await client.httpHandler.executeSafe(request); - return parseMany(response.jsonBody as List, parseScheduledEventUser); + final users = parseMany(response.jsonBody as List, parseScheduledEventUser); + + users.forEach(client.updateCacheWith); + return users; } } diff --git a/lib/src/http/managers/sku_manager.dart b/lib/src/http/managers/sku_manager.dart new file mode 100644 index 000000000..70d17d1e9 --- /dev/null +++ b/lib/src/http/managers/sku_manager.dart @@ -0,0 +1,53 @@ +import 'package:nyxx/src/errors.dart'; +import 'package:nyxx/src/http/managers/manager.dart'; +import 'package:nyxx/src/http/request.dart'; +import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/sku.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/parsing_helpers.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; + +class SkuManager extends ReadOnlyManager { + final Snowflake applicationId; + + SkuManager(super.config, super.client, {required this.applicationId}) : super(identifier: '$applicationId.skus'); + + @override + PartialSku operator [](Snowflake id) => PartialSku(manager: this, id: id); + + @override + Sku parse(Map raw) { + return Sku( + manager: this, + id: Snowflake.parse(raw['id']!), + type: SkuType(raw['type'] as int), + applicationId: Snowflake.parse(raw['application_id']!), + name: raw['name'] as String, + slug: raw['slug'] as String, + flags: SkuFlags(raw['flags'] as int), + ); + } + + Future> list() async { + final route = HttpRoute() + ..applications(id: applicationId.toString()) + ..skus(); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final skus = parseMany(response.jsonBody as List, parse); + + skus.forEach(client.updateCacheWith); + return skus; + } + + @override + Future fetch(Snowflake id) async { + final skus = await list(); + + return skus.firstWhere( + (sku) => sku.id == id, + orElse: () => throw SkuNotFoundException(applicationId, id), + ); + } +} diff --git a/lib/src/http/managers/sticker_manager.dart b/lib/src/http/managers/sticker_manager.dart index 9d9f4c77f..e4dd15c10 100644 --- a/lib/src/http/managers/sticker_manager.dart +++ b/lib/src/http/managers/sticker_manager.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:nyxx/src/builders/sticker.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; @@ -11,6 +10,7 @@ import 'package:nyxx/src/models/sticker/global_sticker.dart'; import 'package:nyxx/src/models/sticker/guild_sticker.dart'; import 'package:nyxx/src/models/sticker/sticker.dart'; import 'package:nyxx/src/models/sticker/sticker_pack.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; class GuildStickerManager extends Manager { @@ -22,18 +22,37 @@ class GuildStickerManager extends Manager { PartialGuildSticker operator [](Snowflake id) => PartialGuildSticker(id: id, manager: this); @override - Future create(StickerBuilder builder) async { + GuildSticker parse(Map raw) { + return GuildSticker( + manager: this, + id: Snowflake.parse(raw['id']!), + name: raw['name'] as String, + description: raw['description'] as String?, + tags: raw['tags'] as String, + type: StickerType.parse(raw['type'] as int), + formatType: StickerFormatType(raw['format_type'] as int), + available: raw['available'] as bool? ?? false, + guildId: Snowflake.parse(raw['guild_id']!), + user: ((raw['user'] ?? {}) as Map)['id'] != null ? client.users[Snowflake.parse((raw['user'] as Map)['id']!)] : null, + sortValue: raw['sort_value'] as int?, + ); + } + + @override + Future create(StickerBuilder builder, {String? auditLogReason}) async { final route = HttpRoute() ..guilds(id: guildId.toString()) ..stickers(); - final request = FormDataRequest(route, - method: 'POST', formParams: builder.build().cast(), files: [MultipartFile.fromBytes('file', builder.file.buildRawData())]); - final response = await client.httpHandler.executeSafe(request); + method: 'POST', + formParams: builder.build().cast(), + files: [MultipartFile.fromBytes('file', builder.file.buildRawData())], + auditLogReason: auditLogReason); + final response = await client.httpHandler.executeSafe(request); final sticker = parse(response.jsonBody as Map); - cache[sticker.id] = sticker; + client.updateCacheWith(sticker); return sticker; } @@ -41,22 +60,21 @@ class GuildStickerManager extends Manager { final route = HttpRoute() ..guilds(id: guildId.toString()) ..stickers(); - final request = BasicRequest(route); - final response = await client.httpHandler.executeSafe(request); + final response = await client.httpHandler.executeSafe(request); final stickers = parseMany(response.jsonBody as List, (Map raw) => parse(raw)); - cache.addEntities(stickers); + stickers.forEach(client.updateCacheWith); return stickers; } @override - Future delete(Snowflake id) async { + Future delete(Snowflake id, {String? auditLogReason}) async { final route = HttpRoute() ..guilds(id: guildId.toString()) ..stickers(id: id.toString()); - final request = BasicRequest(route, method: 'DELETE'); + final request = BasicRequest(route, method: 'DELETE', auditLogReason: auditLogReason); await client.httpHandler.executeSafe(request); cache.remove(id); @@ -73,39 +91,22 @@ class GuildStickerManager extends Manager { final sticker = parse(response.jsonBody as Map); - cache[sticker.id] = sticker; + client.updateCacheWith(sticker); return sticker; } @override - GuildSticker parse(Map raw) { - return GuildSticker( - manager: this, - id: Snowflake.parse(raw['id']!), - name: raw['name'] as String, - description: raw['description'] as String?, - tags: raw['tags'] as String, - type: StickerType.parse(raw['type'] as int), - formatType: StickerFormatType.parse(raw['format_type'] as int), - available: raw['available'] as bool? ?? false, - guildId: Snowflake.parse(raw['guild_id']!), - user: ((raw['user'] ?? {}) as Map)['id'] != null ? client.users[Snowflake.parse((raw['user'] as Map)['id']!)] : null, - sortValue: raw['sort_value'] as int?, - ); - } - - @override - Future update(Snowflake id, StickerUpdateBuilder builder) async { + Future update(Snowflake id, StickerUpdateBuilder builder, {String? auditLogReason}) async { final route = HttpRoute() ..guilds(id: guildId.toString()) ..stickers(id: id.toString()); - final request = BasicRequest(route, body: jsonEncode(builder.build()), method: 'PATCH'); + final request = BasicRequest(route, body: jsonEncode(builder.build()), method: 'PATCH', auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); final sticker = parse(response.jsonBody as Map); - cache[sticker.id] = sticker; + client.updateCacheWith(sticker); return sticker; } } @@ -117,41 +118,27 @@ class GlobalStickerManager extends ReadOnlyManager { PartialGlobalSticker operator [](Snowflake id) => PartialGlobalSticker(id: id, manager: this); @override - Future fetch(Snowflake id) async { - final route = HttpRoute()..stickers(id: id.toString()); - - final request = BasicRequest(route); - final response = await client.httpHandler.executeSafe(request); - - final sticker = parse(response.jsonBody as Map); - - cache[sticker.id] = sticker; - return sticker; - } - - Future fetchStickerPack(Snowflake id) async { - final route = HttpRoute()..stickerPacks(id: id.toString()); - - final request = BasicRequest(route); - final response = (await client.httpHandler.executeSafe(request)).jsonBody as Map; - - return parseStickerPack(response); - } - - Future> fetchNitroStickerPacks() async { - final route = HttpRoute()..stickerPacks(); - - final request = BasicRequest(route); - final response = (await client.httpHandler.executeSafe(request)).jsonBody as Map; - - return (response['sticker_packs'] as List).map((e) => parseStickerPack(e as Map)).toList(); + GlobalSticker parse(Map raw) { + return GlobalSticker( + manager: this, + id: Snowflake.parse(raw['id']!), + packId: Snowflake.parse(raw['pack_id']!), + name: raw['name'] as String, + description: raw['description'] as String?, + tags: raw['tags'] as String, + type: StickerType.parse(raw['type'] as int), + formatType: StickerFormatType(raw['format_type'] as int), + available: raw['available'] as bool? ?? false, + user: ((raw['user'] ?? {}) as Map)['id'] != null ? client.users[Snowflake.parse((raw['user'] as Map)['id']!)] : null, + sortValue: raw['sort_value'] as int?, + ); } StickerItem parseStickerItem(Map raw) { return StickerItem( id: Snowflake.parse(raw['id']!), name: raw['name'] as String, - formatType: StickerFormatType.parse(raw['format_type'] as int), + formatType: StickerFormatType(raw['format_type'] as int), ); } @@ -169,19 +156,37 @@ class GlobalStickerManager extends ReadOnlyManager { } @override - GlobalSticker parse(Map raw) { - return GlobalSticker( - manager: this, - id: Snowflake.parse(raw['id']!), - packId: Snowflake.parse(raw['pack_id']!), - name: raw['name'] as String, - description: raw['description'] as String?, - tags: raw['tags'] as String, - type: StickerType.parse(raw['type'] as int), - formatType: StickerFormatType.parse(raw['format_type'] as int), - available: raw['available'] as bool? ?? false, - user: ((raw['user'] ?? {}) as Map)['id'] != null ? client.users[Snowflake.parse((raw['user'] as Map)['id']!)] : null, - sortValue: raw['sort_value'] as int?, - ); + Future fetch(Snowflake id) async { + final route = HttpRoute()..stickers(id: id.toString()); + + final request = BasicRequest(route); + final response = await client.httpHandler.executeSafe(request); + + final sticker = parse(response.jsonBody as Map); + + client.updateCacheWith(sticker); + return sticker; + } + + Future fetchStickerPack(Snowflake id) async { + final route = HttpRoute()..stickerPacks(id: id.toString()); + final request = BasicRequest(route); + + final response = (await client.httpHandler.executeSafe(request)).jsonBody as Map; + final pack = parseStickerPack(response); + + client.updateCacheWith(pack); + return pack; + } + + Future> fetchNitroStickerPacks() async { + final route = HttpRoute()..stickerPacks(); + final request = BasicRequest(route); + + final response = (await client.httpHandler.executeSafe(request)).jsonBody as Map; + final packs = parseMany(response['sticker_packs'] as List, parseStickerPack); + + packs.forEach(client.updateCacheWith); + return packs; } } diff --git a/lib/src/http/managers/subscription_manager.dart b/lib/src/http/managers/subscription_manager.dart new file mode 100644 index 000000000..97c251478 --- /dev/null +++ b/lib/src/http/managers/subscription_manager.dart @@ -0,0 +1,66 @@ +import 'package:nyxx/src/http/managers/manager.dart'; +import 'package:nyxx/src/http/request.dart'; +import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/models/subscription.dart'; +import 'package:nyxx/src/utils/parsing_helpers.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; + +class SubscriptionManager extends ReadOnlyManager { + final Snowflake applicationId; + final Snowflake skuId; + + SubscriptionManager(super.config, super.client, {required this.applicationId, required this.skuId}) + : super(identifier: '$applicationId.$skuId.subscriptions'); + + @override + PartialSubscription operator [](Snowflake id) => PartialSubscription(manager: this, id: id); + + @override + Subscription parse(Map raw) { + return Subscription( + manager: this, + id: Snowflake.parse(raw['id']!), + userId: Snowflake.parse(raw['user_id']!), + skuIds: parseMany(raw['sku_ids'] as List, Snowflake.parse), + entitlementIds: parseMany(raw['entitlement_ids'] as List, Snowflake.parse), + currentPeriodStart: DateTime.parse(raw['current_period_start'] as String), + currentPeriodEnd: DateTime.parse(raw['current_period_end'] as String), + status: SubscriptionStatus(raw['status'] as int), + canceledAt: maybeParse(raw['canceled_at'], DateTime.parse), + countryCode: raw['country'] as String?, + ); + } + + @override + Future fetch(Snowflake id) async { + final route = HttpRoute() + ..skus(id: skuId.toString()) + ..subscriptions(id: id.toString()); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final subscription = parse(response.jsonBody as Map); + + client.updateCacheWith(subscription); + return subscription; + } + + Future> list({Snowflake? before, Snowflake? after, int? limit, Snowflake? userId}) async { + final route = HttpRoute() + ..skus(id: skuId.toString()) + ..subscriptions(); + final request = BasicRequest(route, queryParameters: { + if (before != null) 'before': before.toString(), + if (after != null) 'after': after.toString(), + if (limit != null) 'limit': limit.toString(), + if (userId != null) 'user_id': userId.toString(), + }); + + final response = await client.httpHandler.executeSafe(request); + final subscriptions = parseMany(response.jsonBody as List, parse); + + subscriptions.forEach(client.updateCacheWith); + return subscriptions; + } +} diff --git a/lib/src/http/managers/user_manager.dart b/lib/src/http/managers/user_manager.dart index 62470bb67..ce903a67b 100644 --- a/lib/src/http/managers/user_manager.dart +++ b/lib/src/http/managers/user_manager.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/builders/user.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/types/dm.dart'; import 'package:nyxx/src/models/channel/types/group_dm.dart'; import 'package:nyxx/src/models/discord_color.dart'; @@ -13,10 +14,12 @@ import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/guild/integration.dart'; import 'package:nyxx/src/models/guild/member.dart'; import 'package:nyxx/src/models/locale.dart'; +import 'package:nyxx/src/models/oauth2.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/application_role_connection.dart'; import 'package:nyxx/src/models/user/connection.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [User]s. @@ -49,12 +52,13 @@ class UserManager extends ReadOnlyManager { accentColor: hasAccentColor ? DiscordColor(raw['accent_color'] as int) : null, locale: hasLocale ? Locale.parse(raw['locale'] as String) : null, flags: hasFlags ? UserFlags(raw['flags'] as int) : null, - nitroType: hasPremiumType ? NitroType.parse(raw['premium_type'] as int) : NitroType.none, + nitroType: hasPremiumType ? NitroType(raw['premium_type'] as int) : NitroType.none, publicFlags: hasPublicFlags ? UserFlags(raw['public_flags'] as int) : null, avatarDecorationHash: raw['avatar_decoration'] as String?, ); } + /// Parse a [Connection] from [raw]. Connection parseConnection(Map raw) { return Connection( id: raw['id'] as String, @@ -73,10 +77,11 @@ class UserManager extends ReadOnlyManager { isFriendSyncEnabled: raw['friend_sync'] as bool, showActivity: raw['show_activity'] as bool, isTwoWayLink: raw['two_way_link'] as bool, - visibility: ConnectionVisibility.parse(raw['visibility'] as int), + visibility: ConnectionVisibility(raw['visibility'] as int), ); } + /// Parse a [ApplicationRoleConnection] from [raw]. ApplicationRoleConnection parseApplicationRoleConnection(Map raw) { return ApplicationRoleConnection( platformName: raw['platform_name'] as String?, @@ -93,7 +98,7 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final user = parse(response.jsonBody as Map); - cache[user.id] = user; + client.updateCacheWith(user); return user; } @@ -105,7 +110,7 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final user = parse(response.jsonBody as Map); - cache[user.id] = user; + client.updateCacheWith(user); return user; } @@ -121,12 +126,12 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final user = parse(response.jsonBody as Map); - cache[user.id] = user; + client.updateCacheWith(user); return user; } /// List the guilds the current user is a member of. - Future> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit, bool? withCounts}) async { + Future> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit, bool? withCounts}) async { final route = HttpRoute() ..users(id: '@me') ..guilds(); @@ -140,7 +145,7 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); return parseMany( response.jsonBody as List, - (Map raw) => PartialGuild(id: Snowflake.parse(raw['id']!), manager: client.guilds), + (Map raw) => client.guilds.parseUserGuild(raw), ); } @@ -153,7 +158,10 @@ class UserManager extends ReadOnlyManager { final request = BasicRequest(route); final response = await client.httpHandler.executeSafe(request); - return client.guilds[guildId].members.parse(response.jsonBody as Map); + final member = client.guilds[guildId].members.parse(response.jsonBody as Map, userId: client.user.id); + + client.updateCacheWith(member); + return member; } /// Leave a guild. @@ -176,7 +184,7 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final channel = client.channels.parse(response.jsonBody as Map) as DmChannel; - client.channels.cache[channel.id] = channel; + client.updateCacheWith(channel); return channel; } @@ -199,7 +207,7 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); final channel = client.channels.parse(response.jsonBody as Map) as GroupDmChannel; - client.channels.cache[channel.id] = channel; + client.updateCacheWith(channel); return channel; } @@ -242,4 +250,18 @@ class UserManager extends ReadOnlyManager { final response = await client.httpHandler.executeSafe(request); return parseApplicationRoleConnection(response.jsonBody as Map); } + + Future fetchCurrentOAuth2Information() async { + final route = HttpRoute() + ..oauth2() + ..add(HttpRoutePart('@me')); + final request = BasicRequest(route); + final response = await client.httpHandler.executeSafe(request); + final body = response.jsonBody as Map; + return OAuth2Information( + application: PartialApplication(manager: client.applications, id: Snowflake.parse((body['application'] as Map)['id']!)), + scopes: (body['scopes'] as List).cast(), + expiresOn: DateTime.parse(body['expires'] as String), + user: maybeParse(body['user'], client.users.parse)); + } } diff --git a/lib/src/http/managers/voice_manager.dart b/lib/src/http/managers/voice_manager.dart index 0ebca9413..f4d0dcebf 100644 --- a/lib/src/http/managers/voice_manager.dart +++ b/lib/src/http/managers/voice_manager.dart @@ -5,6 +5,7 @@ import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/voice/voice_region.dart'; import 'package:nyxx/src/models/voice/voice_state.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [VoiceState]s. @@ -13,21 +14,24 @@ class VoiceManager { final NyxxRest client; /// The cache for voice states. + @Deprecated('Use PartialGuild.voiceStates instead') final Cache cache; /// Create a new [VoiceManager]. - VoiceManager(this.client) : cache = Cache(client, 'voiceStates', client.options.voiceStateConfig); + // ignore: deprecated_member_use_from_same_package + VoiceManager(this.client) : cache = client.cache.getCache('voiceStates', client.options.voiceStateConfig); /// Parse a [VoiceState] from a [Map]. - VoiceState parseVoiceState(Map raw) { - final guildId = maybeParse(raw['guild_id'], Snowflake.parse); + VoiceState parseVoiceState(Map raw, {Snowflake? guildId}) { + guildId ??= maybeParse(raw['guild_id'], Snowflake.parse); + final userId = Snowflake.parse(raw['user_id']!); return VoiceState( manager: this, guildId: guildId, channelId: maybeParse(raw['channel_id'], Snowflake.parse), - userId: Snowflake.parse(raw['user_id']!), - member: maybeParse(raw['member'], client.guilds[guildId ?? Snowflake.zero].members.parse), + userId: userId, + member: maybeParse(raw['member'], (Map raw) => client.guilds[guildId ?? Snowflake.zero].members.parse(raw, userId: userId)), sessionId: raw['session_id'] as String, isServerDeafened: raw['deaf'] as bool, isServerMuted: raw['mute'] as bool, @@ -61,4 +65,30 @@ class VoiceManager { final response = await client.httpHandler.executeSafe(request); return parseMany(response.jsonBody as List, parseVoiceRegion); } + + Future fetchCurrentUserVoiceState(Snowflake guildId) async { + final route = HttpRoute() + ..guilds(id: guildId.toString()) + ..voiceStates(id: '@me'); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final state = parseVoiceState(response.jsonBody as Map); + + client.updateCacheWith(state); + return state; + } + + Future fetchVoiceState(Snowflake guildId, Snowflake userId) async { + final route = HttpRoute() + ..guilds(id: guildId.toString()) + ..voiceStates(id: userId.toString()); + final request = BasicRequest(route); + + final response = await client.httpHandler.executeSafe(request); + final state = parseVoiceState(response.jsonBody as Map); + + client.updateCacheWith(state); + return state; + } } diff --git a/lib/src/http/managers/webhook_manager.dart b/lib/src/http/managers/webhook_manager.dart index 491e9fbea..e96b45243 100644 --- a/lib/src/http/managers/webhook_manager.dart +++ b/lib/src/http/managers/webhook_manager.dart @@ -5,7 +5,6 @@ import 'package:http/http.dart' hide MultipartRequest; import 'package:nyxx/src/builders/message/message.dart'; import 'package:nyxx/src/builders/sentinels.dart'; import 'package:nyxx/src/builders/webhook.dart'; -import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/managers/manager.dart'; import 'package:nyxx/src/http/request.dart'; import 'package:nyxx/src/http/route.dart'; @@ -15,6 +14,7 @@ import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/message/message.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/webhook.dart'; +import 'package:nyxx/src/utils/cache_helpers.dart'; import 'package:nyxx/src/utils/parsing_helpers.dart'; /// A manager for [Webhook]s. @@ -30,7 +30,7 @@ class WebhookManager extends Manager { return Webhook( id: Snowflake.parse(raw['id']!), manager: this, - type: WebhookType.parse(raw['type'] as int), + type: WebhookType(raw['type'] as int), guildId: maybeParse(raw['guild_id'], Snowflake.parse), channelId: maybeParse(raw['channel_id'], Snowflake.parse), user: maybeParse(raw['user'], client.users.parse), @@ -56,6 +56,7 @@ class WebhookManager extends Manager { ); } + /// Parse a [WebhookAuthor] from [raw]. WebhookAuthor parseWebhookAuthor(Map raw) { return WebhookAuthor( id: Snowflake.parse(raw['id']!), @@ -76,36 +77,36 @@ class WebhookManager extends Manager { final response = await client.httpHandler.executeSafe(request); final webhook = parse(response.jsonBody as Map); - cache[webhook.id] = webhook; + client.updateCacheWith(webhook); return webhook; } @override - Future create(WebhookBuilder builder) async { + Future create(WebhookBuilder builder, {String? auditLogReason}) async { final route = HttpRoute() ..channels(id: builder.channelId.toString()) ..webhooks(); - final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build())); + final request = BasicRequest(route, method: 'POST', body: jsonEncode(builder.build()), auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); final webhook = parse(response.jsonBody as Map); - cache[webhook.id] = webhook; + client.updateCacheWith(webhook); return webhook; } @override - Future update(Snowflake id, WebhookUpdateBuilder builder, {String? token}) async { + Future update(Snowflake id, WebhookUpdateBuilder builder, {String? token, String? auditLogReason}) async { final route = HttpRoute()..webhooks(id: id.toString()); if (token != null) { route.add(HttpRoutePart(token)); } - final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build()), authenticated: token == null); + final request = BasicRequest(route, method: 'PATCH', body: jsonEncode(builder.build()), authenticated: token == null, auditLogReason: auditLogReason); final response = await client.httpHandler.executeSafe(request); final webhook = parse(response.jsonBody as Map); - cache[webhook.id] = webhook; + client.updateCacheWith(webhook); return webhook; } @@ -132,7 +133,7 @@ class WebhookManager extends Manager { final response = await client.httpHandler.executeSafe(request); final webhooks = parseMany(response.jsonBody as List, parse); - cache.addEntities(webhooks); + webhooks.forEach(client.updateCacheWith); return webhooks; } @@ -146,12 +147,13 @@ class WebhookManager extends Manager { final response = await client.httpHandler.executeSafe(request); final webhooks = parseMany(response.jsonBody as List, parse); - cache.addEntities(webhooks); + webhooks.forEach(client.updateCacheWith); return webhooks; } /// Execute a webhook. - Future execute(Snowflake id, MessageBuilder builder, {required String token, bool? wait, Snowflake? threadId}) async { + Future execute(Snowflake id, MessageBuilder builder, + {required String token, bool? wait, Snowflake? threadId, String? threadName, List? appliedTags, String? username, String? avatarUrl}) async { final route = HttpRoute() ..webhooks(id: id.toString()) ..add(HttpRoutePart(token)); @@ -160,7 +162,13 @@ class WebhookManager extends Manager { final HttpRequest request; if (!identical(builder.attachments, sentinelList) && builder.attachments?.isNotEmpty == true) { final attachments = builder.attachments!; - final payload = builder.build(); + final payload = { + ...builder.build(), + if (threadName != null) 'thread_name': threadName, + if (appliedTags != null) 'applied_tags': appliedTags.map((e) => e.toString()), + if (username != null) 'username': username, + if (avatarUrl != null) 'avatar_url': avatarUrl, + }; final files = []; for (int i = 0; i < attachments.length; i++) { @@ -185,7 +193,13 @@ class WebhookManager extends Manager { request = BasicRequest( route, method: 'POST', - body: jsonEncode(builder.build()), + body: jsonEncode({ + ...builder.build(), + if (threadName != null) 'thread_name': threadName, + if (appliedTags != null) 'applied_tags': appliedTags.map((e) => e.toString()), + if (username != null) 'username': username, + if (avatarUrl != null) 'avatar_url': avatarUrl, + }), queryParameters: queryParameters, authenticated: false, ); @@ -201,7 +215,7 @@ class WebhookManager extends Manager { final messageManager = (client.channels[channelId] as PartialTextChannel).messages; final message = messageManager.parse(response.jsonBody as Map); - messageManager.cache[message.id] = message; + client.updateCacheWith(message); return message; } @@ -222,7 +236,7 @@ class WebhookManager extends Manager { final messageManager = (client.channels[channelId] as PartialTextChannel).messages; final message = messageManager.parse(response.jsonBody as Map); - messageManager.cache[message.id] = message; + client.updateCacheWith(message); return message; } @@ -278,7 +292,7 @@ class WebhookManager extends Manager { final messageManager = (client.channels[channelId] as PartialTextChannel).messages; final message = messageManager.parse(response.jsonBody as Map); - messageManager.cache[message.id] = message; + client.updateCacheWith(message); return message; } diff --git a/lib/src/http/request.dart b/lib/src/http/request.dart index 0f7fa795c..c07eb3765 100644 --- a/lib/src/http/request.dart +++ b/lib/src/http/request.dart @@ -74,7 +74,7 @@ abstract class HttpRequest { Map _getHeaders(Nyxx client) => { userAgent: client.apiOptions.userAgent, - if (auditLogReason != null) xAuditLogReason: auditLogReason!, + if (auditLogReason != null) xAuditLogReason: Uri.encodeComponent(auditLogReason!), if (authenticated) authorization: client.apiOptions.authorizationHeader, ...headers, }; diff --git a/lib/src/http/route.dart b/lib/src/http/route.dart index 16d91b545..b7cebaf49 100644 --- a/lib/src/http/route.dart +++ b/lib/src/http/route.dart @@ -67,7 +67,7 @@ class HttpRoutePart { /// A parameter in a [HttpRoutePart]. /// -/// {@template http_route_part} +/// {@template http_route_param} /// This is not a query parameter, it is a parameter encoded in the path of the request itself, such /// as the id of a guild in `/guilds/0123456789`. /// {@endtemplate} @@ -307,8 +307,29 @@ extension RouteHelpers on HttpRoute { void entitlements({String? id}) => add(HttpRoutePart('entitlements', [if (id != null) HttpRouteParam(id)])); /// Adds the [`skus`](https://discord.com/developers/docs/monetization/skus#list-skus) part to this [HttpRoute]. - void skus() => add(HttpRoutePart('skus')); + void skus({String? id}) => add(HttpRoutePart('skus', [if (id != null) HttpRouteParam(id)])); + + /// Adds the [`consume`](https://discord.com/developers/docs/monetization/entitlements#consume-an-entitlement) part to this [HttpRoute]. + void consume() => add(HttpRoutePart('consume')); /// Adds the [`avatar-decorations`](https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints) part to this [HttpRoute]. void avatarDecorations({String? id}) => add(HttpRoutePart('avatar-decorations', [if (id != null) HttpRouteParam(id)])); + + /// Adds the [`recipients`](https://discord.com/developers/docs/resources/channel#group-dm-add-recipient) part to this [HttpRoute]. + void recipients({String? id}) => add(HttpRoutePart('recipients', [if (id != null) HttpRouteParam(id)])); + + /// Adds the [`polls`](https://discord.com/developers/docs/resources/poll#get-answer-voters) part to this [HttpRoute]. + void polls({String? id}) => add(HttpRoutePart('polls', [if (id != null) HttpRouteParam(id)])); + + /// Adds the [`answers`](https://discord.com/developers/docs/resources/poll#get-answer-voters) part to this [HttpRoute]. + void answers({int? id}) => add(HttpRoutePart('answers', [if (id != null) HttpRouteParam(id.toString())])); + + /// Adds the [`expire`](https://discord.com/developers/docs/resources/poll#expire-poll) part to this [HttpRoute]. + void expire() => add(HttpRoutePart('expire')); + + /// Adds the [`bulk-ban`](https://discord.com/developers/docs/resources/guild#bulk-guild-ban) part to this [HttpRoute]. + void bulkBan() => add(HttpRoutePart('bulk-ban')); + + /// Adds the [`subscriptions`](https://discord.com/developers/docs/resources/subscription#list-sku-subscriptions) part to this [HttpRoute]. + void subscriptions({String? id}) => add(HttpRoutePart('subscriptions', [if (id != null) HttpRouteParam(id)])); } diff --git a/lib/src/intents.dart b/lib/src/intents.dart index 786a40607..87d381f4a 100644 --- a/lib/src/intents.dart +++ b/lib/src/intents.dart @@ -21,12 +21,14 @@ class GatewayIntents extends Flags { static const guildScheduledEvents = Flag.fromOffset(16); static const autoModerationConfiguration = Flag.fromOffset(20); static const autoModerationExecution = Flag.fromOffset(21); + static const guildMessagePolls = Flag.fromOffset(24); + static const directMessagePolls = Flag.fromOffset(25); /// A [GatewayIntents] with all intents enabled. - static const all = GatewayIntents(0x1fffff); + static const all = GatewayIntents(0x331ffff); /// A [GatewayIntents] with all unprivileged intents enabled. - static const allUnprivileged = GatewayIntents(0x317efd); + static const allUnprivileged = GatewayIntents(0x3317efd); /// A [GatewayIntents] with all privileged intents enabled. static const allPrivileged = GatewayIntents(0x8102); diff --git a/lib/src/models/application.dart b/lib/src/models/application.dart index 4cbe2c51f..8cd8eb0c7 100644 --- a/lib/src/models/application.dart +++ b/lib/src/models/application.dart @@ -1,7 +1,10 @@ import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/application_manager.dart'; +import 'package:nyxx/src/http/managers/emoji_manager.dart'; import 'package:nyxx/src/http/managers/entitlement_manager.dart'; +import 'package:nyxx/src/http/managers/sku_manager.dart'; import 'package:nyxx/src/http/route.dart'; +import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; @@ -9,6 +12,7 @@ import 'package:nyxx/src/models/sku.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/team.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; @@ -25,7 +29,14 @@ class PartialApplication with ToStringHelper { /// An [EntitlementManager] for this application's [Entitlement]s. EntitlementManager get entitlements => EntitlementManager(manager.client.options.entitlementConfig, manager.client, applicationId: id); + /// An [ApplicationEmojiManager] for this application's [Emoji]s. + ApplicationEmojiManager get emojis => ApplicationEmojiManager(manager.client.options.emojiCacheConfig, manager.client, applicationId: id); + + /// An [SkuManager] for this application's [Sku]s. + SkuManager get skus => SkuManager(manager.client.options.skuConfig, manager.client, applicationId: id); + /// Create a new [PartialApplication]. + /// @nodoc PartialApplication({required this.id, required this.manager}); /// Fetch this application's role connection metadata. @@ -38,6 +49,14 @@ class PartialApplication with ToStringHelper { Future> listSkus() => manager.listSkus(id); } +class ApplicationIntegrationTypeConfiguration { + /// Install params for each installation context's default in-app authorization link. + final InstallationParameters? oauth2InstallParameters; + + /// @nodoc + ApplicationIntegrationTypeConfiguration({required this.oauth2InstallParameters}); +} + /// {@template application} /// An OAuth2 application. /// {@endtemplate} @@ -111,6 +130,9 @@ class Application extends PartialApplication { /// Settings for this application's default authorization link. final InstallationParameters? installationParameters; + /// Default scopes and permissions for each supported installation context. + final Map? integrationTypesConfig; + /// The custom authorization link for this application. final Uri? customInstallUrl; @@ -119,7 +141,11 @@ class Application extends PartialApplication { /// When configured, this will render the app as a verification method in the guild role verification configuration. final Uri? roleConnectionsVerificationUrl; + /// The approximate number of users that have installed this application. + final int? approximateUserInstallCount; + /// {@macro application} + /// @nodoc Application({ required super.id, required super.manager, @@ -146,8 +172,10 @@ class Application extends PartialApplication { required this.interactionsEndpointUrl, required this.tags, required this.installationParameters, + required this.integrationTypesConfig, required this.customInstallUrl, required this.roleConnectionsVerificationUrl, + required this.approximateUserInstallCount, }); /// This application's icon. @@ -169,6 +197,20 @@ class Application extends PartialApplication { ); } +final class ApplicationIntegrationType extends EnumLike { + /// App is installable to servers. + static const guildInstall = ApplicationIntegrationType(0); + + /// App is installable to users. + static const userInstall = ApplicationIntegrationType(1); + + /// @nodoc + const ApplicationIntegrationType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ApplicationIntegrationType.parse(int value) : this(value); +} + /// Flags for an [Application]. class ApplicationFlags extends Flags { /// Indicates if an app uses the Auto Moderation API. @@ -246,6 +288,7 @@ class InstallationParameters with ToStringHelper { final Permissions permissions; /// {@macro installation_parameters} + /// @nodoc InstallationParameters({ required this.scopes, required this.permissions, @@ -275,6 +318,7 @@ class ApplicationRoleConnectionMetadata with ToStringHelper { final Map? localizedDescriptions; /// {@macro application_role_connection_metadata} + /// @nodoc ApplicationRoleConnectionMetadata({ required this.type, required this.key, @@ -286,29 +330,19 @@ class ApplicationRoleConnectionMetadata with ToStringHelper { } /// The type of an [ApplicationRoleConnectionMetadata]. -enum ConnectionMetadataType { - integerLessThanOrEqual._(1), - integerGreaterThanOrEqual._(2), - integerEqual._(3), - integerNotEqual._(4), - dateTimeLessThanOrEqual._(5), - dateTimeGreaterThanOrEqual._(6), - booleanEqual._(7), - booleanNotEqual._(8); - - /// The value of this [ConnectionMetadataType]. - final int value; - - const ConnectionMetadataType._(this.value); - - /// Parse a [ConnectionMetadataType] from an [int]. - /// - /// The [value] must be a valid connection metadata type. - factory ConnectionMetadataType.parse(int value) => ConnectionMetadataType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown connection metadata type', value), - ); - - @override - String toString() => 'ConnectionMetadataType($value)'; +final class ConnectionMetadataType extends EnumLike { + static const integerLessThanOrEqual = ConnectionMetadataType(1); + static const integerGreaterThanOrEqual = ConnectionMetadataType(2); + static const integerEqual = ConnectionMetadataType(3); + static const integerNotEqual = ConnectionMetadataType(4); + static const dateTimeLessThanOrEqual = ConnectionMetadataType(5); + static const dateTimeGreaterThanOrEqual = ConnectionMetadataType(6); + static const booleanEqual = ConnectionMetadataType(7); + static const booleanNotEqual = ConnectionMetadataType(8); + + /// @nodoc + const ConnectionMetadataType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ConnectionMetadataType.parse(int value) : this(value); } diff --git a/lib/src/models/channel/channel.dart b/lib/src/models/channel/channel.dart index 8321c4606..05f34b3d9 100644 --- a/lib/src/models/channel/channel.dart +++ b/lib/src/models/channel/channel.dart @@ -2,6 +2,7 @@ import 'package:nyxx/src/builders/builder.dart'; import 'package:nyxx/src/http/managers/channel_manager.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; /// A partial [Channel] object. @@ -10,6 +11,7 @@ class PartialChannel extends ManagedSnowflakeEntity { final ChannelManager manager; /// Create a new [PartialChannel]. + /// @nodoc PartialChannel({required super.id, required this.manager}); /// Update this channel. @@ -31,7 +33,7 @@ class PartialChannel extends ManagedSnowflakeEntity { /// External references: /// * [ChannelManager.followChannel] /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#follow-announcement-channel - Future follow(Snowflake id) => manager.followChannel(this.id, id); + Future follow(Snowflake id, {String? auditLogReason}) => manager.followChannel(this.id, id, auditLogReason: auditLogReason); } /// {@template channel} @@ -42,65 +44,56 @@ abstract class Channel extends PartialChannel { ChannelType get type; /// {@macro channel} + /// @nodoc Channel({required super.id, required super.manager}); } /// The type of a channel. -enum ChannelType { +final class ChannelType extends EnumLike { /// A text channel in a [Guild]. - guildText._(0), + static const guildText = ChannelType(0); /// A DM channel with a single other recipient. - dm._(1), + static const dm = ChannelType(1); /// A voice channel in a [Guild]. - guildVoice._(2), + static const guildVoice = ChannelType(2); /// A DM channel with multiple recipients. - groupDm._(3), + static const groupDm = ChannelType(3); /// A category in a [Guild]. - guildCategory._(4), + static const guildCategory = ChannelType(4); /// An announcement channel in a [Guild]. - guildAnnouncement._(5), + static const guildAnnouncement = ChannelType(5); /// A [Thread] in an announcement channel. - announcementThread._(10), + static const announcementThread = ChannelType(10); /// A public thread. - publicThread._(11), + static const publicThread = ChannelType(11); /// A private thread. - privateThread._(12), + static const privateThread = ChannelType(12); /// A stage channel in a [Guild]. - guildStageVoice._(13), + static const guildStageVoice = ChannelType(13); /// A [Guild] directory. - guildDirectory._(14), + static const guildDirectory = ChannelType(14); /// A forum channel in a [Guild]. - guildForum._(15), + static const guildForum = ChannelType(15); /// A media channel in a [Guild]. - guildMedia._(16); + static const guildMedia = ChannelType(16); - /// The value of this [ChannelType]. - final int value; + /// @nodoc + const ChannelType(super.value); - const ChannelType._(this.value); - - /// Parse a [ChannelType] from a [value]. - /// - /// The [value] must be a valid channel type. - factory ChannelType.parse(int value) => ChannelType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown channel type', value), - ); - - @override - String toString() => 'ChannelType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ChannelType.parse(int value) : this(value); } /// A set of flags applied to channels. diff --git a/lib/src/models/channel/followed_channel.dart b/lib/src/models/channel/followed_channel.dart index 55228001a..1f8a91cc6 100644 --- a/lib/src/models/channel/followed_channel.dart +++ b/lib/src/models/channel/followed_channel.dart @@ -18,6 +18,7 @@ class FollowedChannel with ToStringHelper { final Snowflake webhookId; /// {@macro followed_channel} + /// @nodoc FollowedChannel({required this.manager, required this.channelId, required this.webhookId}); /// The followed channel. diff --git a/lib/src/models/channel/stage_instance.dart b/lib/src/models/channel/stage_instance.dart index 10a1c7706..a67170738 100644 --- a/lib/src/models/channel/stage_instance.dart +++ b/lib/src/models/channel/stage_instance.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; /// {@template stage_instance} /// Information about a live stage. @@ -30,6 +31,7 @@ class StageInstance extends SnowflakeEntity { final Snowflake? scheduledEventId; /// {@macro stage_instance} + /// @nodoc StageInstance({ required super.id, required this.manager, @@ -57,19 +59,13 @@ class StageInstance extends SnowflakeEntity { } /// The privacy level of a [StageInstance]. -enum PrivacyLevel { - public._(1), - guildOnly._(2); +final class PrivacyLevel extends EnumLike { + static const public = PrivacyLevel(1); + static const guildOnly = PrivacyLevel(2); - final int value; + /// @nodoc + const PrivacyLevel(super.value); - const PrivacyLevel._(this.value); - - /// Parse a [PrivacyLevel] from an [int]. - /// - /// The [value] must be a valid privacy level. - factory PrivacyLevel.parse(int value) => PrivacyLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Unknown privacy level', value), - ); + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + PrivacyLevel.parse(int value) : this(value); } diff --git a/lib/src/models/channel/text_channel.dart b/lib/src/models/channel/text_channel.dart index a9afa3f18..d8d89f392 100644 --- a/lib/src/models/channel/text_channel.dart +++ b/lib/src/models/channel/text_channel.dart @@ -10,6 +10,7 @@ class PartialTextChannel extends PartialChannel { MessageManager get messages => MessageManager(manager.client.options.messageCacheConfig, manager.client, channelId: id); /// Create a new [PartialTextChannel]. + /// @nodoc PartialTextChannel({required super.id, required super.manager}); /// Send a message to this channel. @@ -46,6 +47,7 @@ abstract class TextChannel extends PartialTextChannel implements Channel { /// The time at which the last message was pinned, or `null` if no messages have been pinned. DateTime? get lastPinTimestamp; + /// @nodoc TextChannel({required super.id, required super.manager}); /// The last message sent in this channel, or `null` if no messages have been sent. diff --git a/lib/src/models/channel/thread.dart b/lib/src/models/channel/thread.dart index 03ad2a23b..f1c5cefae 100644 --- a/lib/src/models/channel/thread.dart +++ b/lib/src/models/channel/thread.dart @@ -92,6 +92,7 @@ class PartialThreadMember { final Flags flags; /// {@macro partial_thread_member} + /// @nodoc PartialThreadMember({required this.joinTimestamp, required this.flags}); } @@ -111,6 +112,7 @@ class ThreadMember extends PartialThreadMember { final Member? member; /// {@macro thread_member} + /// @nodoc ThreadMember({ required super.joinTimestamp, required super.flags, diff --git a/lib/src/models/channel/thread_list.dart b/lib/src/models/channel/thread_list.dart index 6ccda21ee..7b52c183f 100644 --- a/lib/src/models/channel/thread_list.dart +++ b/lib/src/models/channel/thread_list.dart @@ -15,6 +15,7 @@ class ThreadList with ToStringHelper { final bool hasMore; /// {@macro thread_list} + /// @nodoc ThreadList({ required this.threads, required this.members, diff --git a/lib/src/models/channel/types/announcement_thread.dart b/lib/src/models/channel/types/announcement_thread.dart index 4664fb52a..dfe61e867 100644 --- a/lib/src/models/channel/types/announcement_thread.dart +++ b/lib/src/models/channel/types/announcement_thread.dart @@ -77,6 +77,7 @@ class AnnouncementThread extends TextChannel implements Thread { @override ChannelType get type => ChannelType.announcementThread; + /// @nodoc AnnouncementThread({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/directory.dart b/lib/src/models/channel/types/directory.dart index 118aa1e0a..9f1fa4d0c 100644 --- a/lib/src/models/channel/types/directory.dart +++ b/lib/src/models/channel/types/directory.dart @@ -8,5 +8,6 @@ class DirectoryChannel extends Channel { ChannelType get type => ChannelType.guildDirectory; /// {@macro directory_channel} + /// @nodoc DirectoryChannel({required super.id, required super.manager}); } diff --git a/lib/src/models/channel/types/dm.dart b/lib/src/models/channel/types/dm.dart index 514b55e3c..7140fbbfa 100644 --- a/lib/src/models/channel/types/dm.dart +++ b/lib/src/models/channel/types/dm.dart @@ -24,6 +24,7 @@ class DmChannel extends TextChannel { ChannelType get type => ChannelType.dm; /// {@macro dm_channel} + /// @nodoc DmChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/forum.dart b/lib/src/models/channel/types/forum.dart index 2f65bf1d4..57a1c3f56 100644 --- a/lib/src/models/channel/types/forum.dart +++ b/lib/src/models/channel/types/forum.dart @@ -12,6 +12,7 @@ import 'package:nyxx/src/models/invite/invite_metadata.dart'; import 'package:nyxx/src/models/permission_overwrite.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/webhook.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template forum_channel} @@ -73,6 +74,7 @@ class ForumChannel extends Channel implements GuildChannel, ThreadsOnlyChannel { ChannelType get type => ChannelType.guildForum; /// {@macro forum_channel} + /// @nodoc ForumChannel({ required super.id, required super.manager, @@ -157,6 +159,7 @@ class ForumTag with ToStringHelper { final String? emojiName; /// {@macro forum_tag} + /// @nodoc ForumTag({ required this.id, required this.name, @@ -177,50 +180,31 @@ class DefaultReaction with ToStringHelper { final String? emojiName; /// {@macro default_reaction} + /// @nodoc DefaultReaction({required this.emojiId, required this.emojiName}); } /// The sorting order in a [ForumChannel]. -enum ForumSort { - latestActivity._(0), - creationDate._(1); +final class ForumSort extends EnumLike { + static const latestActivity = ForumSort(0); + static const creationDate = ForumSort(1); - /// The value of this forum sort. - final int value; + /// @nodoc + const ForumSort(super.value); - const ForumSort._(this.value); - - /// Parse a [ForumSort] from an [int]. - /// - /// The [value] must be a valid forum sort. - factory ForumSort.parse(int value) => ForumSort.values.firstWhere( - (sort) => sort.value == value, - orElse: () => throw FormatException('Unknown forum sort', value), - ); - - @override - String toString() => 'ForumSort($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ForumSort.parse(int value) : this(value); } /// The layout in a [ForumChannel]. -enum ForumLayout { - notSet._(0), - listView._(1), - galleryView._(2); +final class ForumLayout extends EnumLike { + static const notSet = ForumLayout(0); + static const listView = ForumLayout(1); + static const galleryView = ForumLayout(2); - /// The value of this forum layout. - final int value; + /// @nodoc + const ForumLayout(super.value); - const ForumLayout._(this.value); - - /// Parse a [ForumLayout] from an [int]. - /// - /// The [value] must be a valid forum layout. - factory ForumLayout.parse(int value) => ForumLayout.values.firstWhere( - (layout) => layout.value == value, - orElse: () => throw FormatException('Unknown forum layout', value), - ); - - @override - String toString() => 'ForumLayout($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ForumLayout.parse(int value) : this(value); } diff --git a/lib/src/models/channel/types/group_dm.dart b/lib/src/models/channel/types/group_dm.dart index 4d3823bcd..9077d5906 100644 --- a/lib/src/models/channel/types/group_dm.dart +++ b/lib/src/models/channel/types/group_dm.dart @@ -40,6 +40,7 @@ class GroupDmChannel extends TextChannel { ChannelType get type => ChannelType.groupDm; /// {@macro group_dm_channel} + /// @nodoc GroupDmChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_announcement.dart b/lib/src/models/channel/types/guild_announcement.dart index afcb40308..cfec61a90 100644 --- a/lib/src/models/channel/types/guild_announcement.dart +++ b/lib/src/models/channel/types/guild_announcement.dart @@ -59,6 +59,7 @@ class GuildAnnouncementChannel extends TextChannel implements GuildChannel, HasT ChannelType get type => ChannelType.guildAnnouncement; /// {@macro guild_announcement_channel} + /// @nodoc GuildAnnouncementChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_category.dart b/lib/src/models/channel/types/guild_category.dart index 54816fc10..5e381fd17 100644 --- a/lib/src/models/channel/types/guild_category.dart +++ b/lib/src/models/channel/types/guild_category.dart @@ -35,6 +35,7 @@ class GuildCategory extends Channel implements GuildChannel { ChannelType get type => ChannelType.guildCategory; /// {@macro guild_category} + /// @nodoc GuildCategory({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_media.dart b/lib/src/models/channel/types/guild_media.dart index 0a885cece..1704dc307 100644 --- a/lib/src/models/channel/types/guild_media.dart +++ b/lib/src/models/channel/types/guild_media.dart @@ -70,6 +70,7 @@ class GuildMediaChannel extends Channel implements GuildChannel, ThreadsOnlyChan ChannelType get type => ChannelType.guildForum; /// {@macro guild_media_channel} + /// @nodoc GuildMediaChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_stage.dart b/lib/src/models/channel/types/guild_stage.dart index ca7d1ba87..c86901ca5 100644 --- a/lib/src/models/channel/types/guild_stage.dart +++ b/lib/src/models/channel/types/guild_stage.dart @@ -59,6 +59,7 @@ class GuildStageChannel extends TextChannel implements VoiceChannel, GuildChanne ChannelType get type => ChannelType.guildStageVoice; /// {@macro guild_stage_channel} + /// @nodoc GuildStageChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_text.dart b/lib/src/models/channel/types/guild_text.dart index 0991deaae..9f68c81e7 100644 --- a/lib/src/models/channel/types/guild_text.dart +++ b/lib/src/models/channel/types/guild_text.dart @@ -59,6 +59,7 @@ class GuildTextChannel extends TextChannel implements GuildChannel, HasThreadsCh ChannelType get type => ChannelType.guildText; /// {@macro guild_text_channel} + /// @nodoc GuildTextChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/guild_voice.dart b/lib/src/models/channel/types/guild_voice.dart index 40e9a6e45..96b5a5ae9 100644 --- a/lib/src/models/channel/types/guild_voice.dart +++ b/lib/src/models/channel/types/guild_voice.dart @@ -59,6 +59,7 @@ class GuildVoiceChannel extends TextChannel implements GuildChannel, VoiceChanne ChannelType get type => ChannelType.guildVoice; /// {@macro guild_voice_channel} + /// @nodoc GuildVoiceChannel({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/private_thread.dart b/lib/src/models/channel/types/private_thread.dart index 63b5e98a8..37cf09c60 100644 --- a/lib/src/models/channel/types/private_thread.dart +++ b/lib/src/models/channel/types/private_thread.dart @@ -83,6 +83,7 @@ class PrivateThread extends TextChannel implements Thread { ChannelType get type => ChannelType.privateThread; /// {@macro private_thread} + /// @nodoc PrivateThread({ required super.id, required super.manager, diff --git a/lib/src/models/channel/types/public_thread.dart b/lib/src/models/channel/types/public_thread.dart index e2f897c0b..b09038500 100644 --- a/lib/src/models/channel/types/public_thread.dart +++ b/lib/src/models/channel/types/public_thread.dart @@ -81,6 +81,7 @@ class PublicThread extends TextChannel implements Thread { ChannelType get type => ChannelType.publicThread; /// {@macro public_thread} + /// @nodoc PublicThread({ required super.id, required super.manager, diff --git a/lib/src/models/channel/voice_channel.dart b/lib/src/models/channel/voice_channel.dart index aed347644..e83202ced 100644 --- a/lib/src/models/channel/voice_channel.dart +++ b/lib/src/models/channel/voice_channel.dart @@ -1,4 +1,5 @@ import 'package:nyxx/src/models/channel/channel.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; /// A voice channel. abstract class VoiceChannel implements Channel { @@ -16,26 +17,16 @@ abstract class VoiceChannel implements Channel { } /// The quality mode of cameras in a [VoiceChannel]. -enum VideoQualityMode { +final class VideoQualityMode extends EnumLike { /// Automatic. - auto._(1), + static const auto = VideoQualityMode(1); /// 720p. - full._(2); + static const full = VideoQualityMode(2); - /// The value of this [VideoQualityMode]. - final int value; + /// @nodoc + const VideoQualityMode(super.value); - const VideoQualityMode._(this.value); - - /// Parse a [VideoQualityMode] from an [int]. - /// - /// [value] must be a valid [VideoQualityMode]. - factory VideoQualityMode.parse(int value) => VideoQualityMode.values.firstWhere( - (mode) => mode.value == value, - orElse: () => throw FormatException('Unknown VideoQualityMode', value), - ); - - @override - String toString() => 'VideoQualityMode($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + VideoQualityMode.parse(int value) : this(value); } diff --git a/lib/src/models/commands/application_command.dart b/lib/src/models/commands/application_command.dart index c9a915b9a..878c2cd50 100644 --- a/lib/src/models/commands/application_command.dart +++ b/lib/src/models/commands/application_command.dart @@ -3,10 +3,12 @@ import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/commands/application_command_option.dart'; import 'package:nyxx/src/models/commands/application_command_permissions.dart'; import 'package:nyxx/src/models/guild/guild.dart'; +import 'package:nyxx/src/models/interaction.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; /// A partial [ApplicationCommand]. class PartialApplicationCommand extends WritableSnowflakeEntity { @@ -14,6 +16,7 @@ class PartialApplicationCommand extends WritableSnowflakeEntity integrationTypes; + + /// Interaction context(s) where the command can be used, only for globally-scoped commands. By default, all interaction context types included for new commands. + final List? contexts; + /// An auto-incrementing version number. final Snowflake version; /// {@macro application_command} + /// @nodoc ApplicationCommand({ required super.id, required super.manager, @@ -77,6 +88,8 @@ class ApplicationCommand extends PartialApplicationCommand { required this.defaultMemberPermissions, required this.hasDmPermission, required this.isNsfw, + required this.integrationTypes, + required this.contexts, required this.version, }); @@ -88,24 +101,19 @@ class ApplicationCommand extends PartialApplicationCommand { } /// The type of an [ApplicationCommand]. -enum ApplicationCommandType { - chatInput._(1), - user._(2), - message._(3); +final class ApplicationCommandType extends EnumLike { + /// A chat input command. + static const chatInput = ApplicationCommandType(1); - /// The value of this [ApplicationCommandType]. - final int value; + /// A user command. + static const user = ApplicationCommandType(2); - const ApplicationCommandType._(this.value); + /// A message command. + static const message = ApplicationCommandType(3); - /// Parse an [ApplicationCommandType] from an [int]. - /// - /// The [value] must be a valid application command type. - factory ApplicationCommandType.parse(int value) => ApplicationCommandType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown application command type', value), - ); + /// @nodoc + const ApplicationCommandType(super.value); - @override - String toString() => 'ApplicationCommandType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ApplicationCommandType.parse(int value) : this(value); } diff --git a/lib/src/models/commands/application_command_option.dart b/lib/src/models/commands/application_command_option.dart index fe243e24a..4b7931068 100644 --- a/lib/src/models/commands/application_command_option.dart +++ b/lib/src/models/commands/application_command_option.dart @@ -1,6 +1,7 @@ import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template command_option} @@ -50,6 +51,7 @@ class CommandOption with ToStringHelper { final bool? hasAutocomplete; /// {@macro command_option} + /// @nodoc CommandOption({ required this.type, required this.name, @@ -69,34 +71,24 @@ class CommandOption with ToStringHelper { } /// The type of a [CommandOption]. -enum CommandOptionType { - subCommand._(1), - subCommandGroup._(2), - string._(3), - integer._(4), - boolean._(5), - user._(6), - channel._(7), - role._(8), - mentionable._(9), - number._(10), - attachment._(11); - - /// The value of this [CommandOptionType]. - final int value; - - const CommandOptionType._(this.value); - - /// Parse a [CommandOptionType] from an [int]. - /// - /// The [value] must be a valid command option type. - factory CommandOptionType.parse(int value) => CommandOptionType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown command option type', value), - ); - - @override - String toString() => 'CommandOptionType($value)'; +final class CommandOptionType extends EnumLike { + static const subCommand = CommandOptionType(1); + static const subCommandGroup = CommandOptionType(2); + static const string = CommandOptionType(3); + static const integer = CommandOptionType(4); + static const boolean = CommandOptionType(5); + static const user = CommandOptionType(6); + static const channel = CommandOptionType(7); + static const role = CommandOptionType(8); + static const mentionable = CommandOptionType(9); + static const number = CommandOptionType(10); + static const attachment = CommandOptionType(11); + + /// @nodoc + const CommandOptionType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + CommandOptionType.parse(int value) : this(value); } /// {@template command_option_choice} @@ -113,6 +105,7 @@ class CommandOptionChoice { final dynamic value; /// {@macro command_option_choice} + /// @nodoc CommandOptionChoice({required this.name, required this.nameLocalizations, required this.value}); } diff --git a/lib/src/models/commands/application_command_permissions.dart b/lib/src/models/commands/application_command_permissions.dart index e507c88a7..d149a54dc 100644 --- a/lib/src/models/commands/application_command_permissions.dart +++ b/lib/src/models/commands/application_command_permissions.dart @@ -4,6 +4,7 @@ import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template command_permissions} @@ -23,6 +24,7 @@ class CommandPermissions extends SnowflakeEntity { final List permissions; /// {@macro command_permissions} + /// @nodoc CommandPermissions({ required this.manager, required super.id, @@ -68,28 +70,24 @@ class CommandPermission with ToStringHelper { final bool hasPermission; /// {@macro command_permission} + /// @nodoc CommandPermission({required this.id, required this.type, required this.hasPermission}); } /// The type of a [CommandPermission]. -enum CommandPermissionType { - role._(1), - user._(2), - channel._(3); +final class CommandPermissionType extends EnumLike { + /// The permission applies to a role. + static const role = CommandPermissionType(1); - /// The value of this [CommandPermissionType]. - final int value; + /// The permission applies to a user. + static const user = CommandPermissionType(2); - const CommandPermissionType._(this.value); + /// The permission applies to a channel. + static const channel = CommandPermissionType(3); - /// Parse a [CommandPermissionType] from an [int]. - /// - /// The [value] must be a valid command permission type. - factory CommandPermissionType.parse(int value) => CommandPermissionType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown command permission type', value), - ); + /// @nodoc + const CommandPermissionType(super.value); - @override - String toString() => 'CommandPermissionType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + CommandPermissionType.parse(int value) : this(value); } diff --git a/lib/src/models/emoji.dart b/lib/src/models/emoji.dart index 1e709b02f..c8e14610b 100644 --- a/lib/src/models/emoji.dart +++ b/lib/src/models/emoji.dart @@ -12,14 +12,16 @@ class PartialEmoji extends WritableSnowflakeEntity { final EmojiManager manager; /// Create a new [PartialEmoji]. + /// @nodoc PartialEmoji({required super.id, required this.manager}); } -/// An emoji. Either a [TextEmoji] or a [GuildEmoji]. +/// An emoji. Either a [TextEmoji], an [ApplicationEmoji] or a [GuildEmoji]. abstract class Emoji extends PartialEmoji { /// The emoji's name. Can be `dartlang` for a custom emoji, or `❤️` for a text emoji. String? get name; + /// @nodoc Emoji({ required super.id, required super.manager, @@ -31,6 +33,7 @@ class TextEmoji extends Emoji { @override final String name; + /// @nodoc TextEmoji({ required super.id, required super.manager, @@ -42,6 +45,48 @@ class TextEmoji extends Emoji { Future fetch() async => this; } +// Apparently an ApplicationEmoji contains a `roles` field, but it's always an empty list, so we don't include it here. +/// A custom emoji created on the application's emoji tab. +class ApplicationEmoji extends Emoji { + @override + final String name; + + /// The user that created this emoji. `null` if it was the first time it was created. + final User? user; + + /// Whether this emoji must be wrapped in colons. + final bool requiresColons; + + /// Whether this emoji is managed. + final bool isManaged; + + /// Whether this emoji is animated. + final bool isAnimated; + + /// Whether this emoji can be used, always true for ApplicationEmojis. + final bool isAvailable; + + /// @nodoc + ApplicationEmoji({ + required super.id, + required ApplicationEmojiManager super.manager, + required this.name, + required this.user, + required this.requiresColons, + required this.isManaged, + required this.isAnimated, + required this.isAvailable, + }); + + /// This emoji's image. + CdnAsset get image => CdnAsset( + client: manager.client, + base: HttpRoute()..emojis(), + hash: id.toString(), + isAnimated: isAnimated, + ); +} + /// A custom guild emoji. class GuildEmoji extends Emoji { @override @@ -65,9 +110,10 @@ class GuildEmoji extends Emoji { /// Whether this emoji can be used, may be false due to loss of Server Boosts. final bool? isAvailable; + /// @nodoc GuildEmoji({ required super.id, - required super.manager, + required GuildEmojiManager super.manager, required this.name, required this.roleIds, required this.user, @@ -78,7 +124,7 @@ class GuildEmoji extends Emoji { }); /// The roles allowed to use this emoji. - List? get roles => roleIds?.map((e) => manager.client.guilds[manager.guildId].roles[e]).toList(); + List? get roles => roleIds?.map((e) => manager.client.guilds[(manager as GuildEmojiManager).guildId].roles[e]).toList(); /// This emoji's image. CdnAsset get image => CdnAsset( diff --git a/lib/src/models/entitlement.dart b/lib/src/models/entitlement.dart index 428a4b2fc..0bd042ba9 100644 --- a/lib/src/models/entitlement.dart +++ b/lib/src/models/entitlement.dart @@ -4,6 +4,7 @@ import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; /// A partial [Entitlement]. class PartialEntitlement extends ManagedSnowflakeEntity { @@ -11,7 +12,11 @@ class PartialEntitlement extends ManagedSnowflakeEntity { final EntitlementManager manager; /// Create a new [PartialEntitlement]. + /// @nodoc PartialEntitlement({required this.manager, required super.id}); + + /// Marks a entitlement for the user as consumed. + Future consume() => manager.consume(id); } /// {@template entitlement} @@ -33,6 +38,9 @@ class Entitlement extends PartialEntitlement { /// The type of this entitlement. final EntitlementType type; + /// Whether entitlement was deleted. + final bool isDeleted; + /// Whether this entitlement is consumed. final bool isConsumed; @@ -43,6 +51,7 @@ class Entitlement extends PartialEntitlement { final DateTime? endsAt; /// {@macro entitlement} + /// @nodoc Entitlement({ required super.manager, required super.id, @@ -52,6 +61,7 @@ class Entitlement extends PartialEntitlement { required this.applicationId, required this.type, required this.isConsumed, + required this.isDeleted, required this.startsAt, required this.endsAt, }); @@ -67,18 +77,33 @@ class Entitlement extends PartialEntitlement { } /// The type of an [Entitlement]. -enum EntitlementType { - applicationSubscription._(8); +final class EntitlementType extends EnumLike { + /// Entitlement was purchased by user. + static const EntitlementType purchase = EntitlementType(1); - final int value; + /// Entitlement was granted by Discord Nitro subscription. + static const EntitlementType premiumSubscription = EntitlementType(2); - const EntitlementType._(this.value); + /// Entitlement was gifted by developer. + static const EntitlementType developerGift = EntitlementType(3); - factory EntitlementType.parse(int value) => EntitlementType.values.firstWhere( - (element) => element.value == value, - orElse: () => throw FormatException('Unknown entitlement type', value), - ); + /// Entitlement was purchased by a dev in application test mode. + static const EntitlementType testModePurchase = EntitlementType(4); - @override - String toString() => 'EntitlementType($value)'; + /// Entitlement was granted when the SKU was free. + static const EntitlementType freePurchase = EntitlementType(5); + + /// Entitlement was gifted by another user. + static const EntitlementType userGift = EntitlementType(6); + + /// Entitlement was claimed by user for free as a Nitro Subscriber. + static const EntitlementType premiumPurchase = EntitlementType(7); + + /// Entitlement was purchased as an app subscription. + static const EntitlementType applicationSubscription = EntitlementType(8); + + const EntitlementType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + EntitlementType.parse(int value) : this(value); } diff --git a/lib/src/models/gateway/event.dart b/lib/src/models/gateway/event.dart index 1f969c45e..034ffa8b9 100644 --- a/lib/src/models/gateway/event.dart +++ b/lib/src/models/gateway/event.dart @@ -10,6 +10,7 @@ abstract class GatewayEvent with ToStringHelper { final Opcode opcode; /// {@macro gateway_event} + /// @nodoc GatewayEvent({required this.opcode}); } @@ -27,6 +28,7 @@ class RawDispatchEvent extends GatewayEvent { final Map payload; /// {@macro raw_dispatch_event} + /// @nodoc RawDispatchEvent({required this.seq, required this.name, required this.payload}) : super(opcode: Opcode.dispatch); } @@ -38,6 +40,7 @@ abstract class DispatchEvent extends GatewayEvent { final Gateway gateway; /// {@macro dispatch_event} + /// @nodoc DispatchEvent({required this.gateway}) : super(opcode: Opcode.dispatch); } @@ -51,6 +54,7 @@ class UnknownDispatchEvent extends DispatchEvent { final RawDispatchEvent raw; /// {@macro unknown_dispatch_event} + /// @nodoc UnknownDispatchEvent({required super.gateway, required this.raw}); } @@ -66,7 +70,7 @@ class HeartbeatEvent extends GatewayEvent { /// Emitted when the client receives a request to reconnect. /// {@endtemplate} class ReconnectEvent extends GatewayEvent { - /// {@macro reconnect_events} + /// {@macro reconnect_event} ReconnectEvent() : super(opcode: Opcode.reconnect); } @@ -78,6 +82,7 @@ class InvalidSessionEvent extends GatewayEvent { final bool isResumable; /// {@macro invalid_session_event} + /// @nodoc InvalidSessionEvent({required this.isResumable}) : super(opcode: Opcode.invalidSession); } @@ -89,6 +94,7 @@ class HelloEvent extends GatewayEvent { final Duration heartbeatInterval; /// {@macro hello_event} + /// @nodoc HelloEvent({required this.heartbeatInterval}) : super(opcode: Opcode.hello); } @@ -100,5 +106,6 @@ class HeartbeatAckEvent extends GatewayEvent { final Duration latency; /// {@macro heartbeat_ack_event} + /// @nodoc HeartbeatAckEvent({required this.latency}) : super(opcode: Opcode.heartbeatAck); } diff --git a/lib/src/models/gateway/events/application_command.dart b/lib/src/models/gateway/events/application_command.dart index 6422ed7d4..fce19c6d1 100644 --- a/lib/src/models/gateway/events/application_command.dart +++ b/lib/src/models/gateway/events/application_command.dart @@ -12,5 +12,6 @@ class ApplicationCommandPermissionsUpdateEvent extends DispatchEvent { final CommandPermissions? oldPermissions; /// {@macro application_command_permissions_update_event} + /// @nodoc ApplicationCommandPermissionsUpdateEvent({required super.gateway, required this.permissions, required this.oldPermissions}); } diff --git a/lib/src/models/gateway/events/auto_moderation.dart b/lib/src/models/gateway/events/auto_moderation.dart index d52965723..47710fd41 100644 --- a/lib/src/models/gateway/events/auto_moderation.dart +++ b/lib/src/models/gateway/events/auto_moderation.dart @@ -16,6 +16,7 @@ class AutoModerationRuleCreateEvent extends DispatchEvent { final AutoModerationRule rule; /// {@macro auto_moderation_rule_create_event} + /// @nodoc AutoModerationRuleCreateEvent({required super.gateway, required this.rule}); } @@ -30,6 +31,7 @@ class AutoModerationRuleUpdateEvent extends DispatchEvent { final AutoModerationRule rule; /// {@macro auto_moderation_rule_update_event} + /// @nodoc AutoModerationRuleUpdateEvent({required super.gateway, required this.oldRule, required this.rule}); } @@ -41,6 +43,7 @@ class AutoModerationRuleDeleteEvent extends DispatchEvent { final AutoModerationRule rule; /// {@macro auto_moderation_rule_delete_event} + /// @nodoc AutoModerationRuleDeleteEvent({required super.gateway, required this.rule}); } @@ -82,6 +85,7 @@ class AutoModerationActionExecutionEvent extends DispatchEvent { final String? matchedContent; /// {@macro auto_moderation_action_execution_event} + /// @nodoc AutoModerationActionExecutionEvent({ required super.gateway, required this.guildId, diff --git a/lib/src/models/gateway/events/channel.dart b/lib/src/models/gateway/events/channel.dart index 9fbd15d1d..b521d185c 100644 --- a/lib/src/models/gateway/events/channel.dart +++ b/lib/src/models/gateway/events/channel.dart @@ -13,6 +13,7 @@ class ChannelCreateEvent extends DispatchEvent { final Channel channel; /// {@macro channel_create_event} + /// @nodoc ChannelCreateEvent({required super.gateway, required this.channel}); } @@ -27,6 +28,7 @@ class ChannelUpdateEvent extends DispatchEvent { final Channel channel; /// {@macro channel_update_event} + /// @nodoc ChannelUpdateEvent({required super.gateway, required this.oldChannel, required this.channel}); } @@ -38,6 +40,7 @@ class ChannelDeleteEvent extends DispatchEvent { final Channel channel; /// {@macro channel_delete_event} + /// @nodoc ChannelDeleteEvent({required super.gateway, required this.channel}); } @@ -49,6 +52,7 @@ class ThreadCreateEvent extends DispatchEvent { final Thread thread; /// {@macro thread_create_event} + /// @nodoc ThreadCreateEvent({required super.gateway, required this.thread}); } @@ -63,6 +67,7 @@ class ThreadUpdateEvent extends DispatchEvent { final Thread thread; /// {@macro thread_update_event} + /// @nodoc ThreadUpdateEvent({required super.gateway, required this.oldThread, required this.thread}); } @@ -73,8 +78,12 @@ class ThreadDeleteEvent extends DispatchEvent { /// The thread which was deleted. final PartialChannel thread; + /// The thread as it was cached before it was deleted. + final Thread? deletedThread; + /// {@macro thread_delete_event} - ThreadDeleteEvent({required super.gateway, required this.thread}); + /// @nodoc + ThreadDeleteEvent({required super.gateway, required this.thread, required this.deletedThread}); } /// {@template thread_list_sync_event} @@ -94,6 +103,7 @@ class ThreadListSyncEvent extends DispatchEvent { final List members; /// {@macro thread_list_sync_event} + /// @nodoc ThreadListSyncEvent({ required super.gateway, required this.guildId, @@ -116,8 +126,15 @@ class ThreadMemberUpdateEvent extends DispatchEvent { /// The updated member. final ThreadMember member; + /// The ID of the guild in which the member was updated. + final Snowflake guildId; + /// {@macro thread_member_update_event} - ThreadMemberUpdateEvent({required super.gateway, required this.member}); + /// @nodoc + ThreadMemberUpdateEvent({required super.gateway, required this.member, required this.guildId}); + + /// The guild in which the member was updated. + PartialGuild get guild => gateway.client.guilds[guildId]; } /// {@template thread_members_update_event} @@ -140,6 +157,7 @@ class ThreadMembersUpdateEvent extends DispatchEvent { final List? removedMemberIds; /// {@macro thread_members_update_event} + /// @nodoc ThreadMembersUpdateEvent({ required super.gateway, required this.id, @@ -170,6 +188,7 @@ class ChannelPinsUpdateEvent extends DispatchEvent { final DateTime? lastPinTimestamp; /// {@macro channel_pins_update_event} + /// @nodoc ChannelPinsUpdateEvent({required super.gateway, required this.guildId, required this.channelId, required this.lastPinTimestamp}); /// The guild the channel is in. diff --git a/lib/src/models/gateway/events/entitlement.dart b/lib/src/models/gateway/events/entitlement.dart index 11fd7e6f8..4a4642900 100644 --- a/lib/src/models/gateway/events/entitlement.dart +++ b/lib/src/models/gateway/events/entitlement.dart @@ -9,6 +9,7 @@ class EntitlementCreateEvent extends DispatchEvent { final Entitlement entitlement; /// {@macro entitlement_create_event} + /// @nodoc EntitlementCreateEvent({required super.gateway, required this.entitlement}); } @@ -23,6 +24,7 @@ class EntitlementUpdateEvent extends DispatchEvent { final Entitlement? oldEntitlement; /// {@macro entitlement_update_event} + /// @nodoc EntitlementUpdateEvent({required super.gateway, required this.entitlement, required this.oldEntitlement}); } @@ -30,8 +32,13 @@ class EntitlementUpdateEvent extends DispatchEvent { /// Emitted when an entitlement is deleted. /// {@endtemplate} class EntitlementDeleteEvent extends DispatchEvent { - // TODO: What is the payload here? + /// The entitlement that was deleted. + final Entitlement entitlement; + + /// The entitlement as it was cached before it was deleted. + final Entitlement? deletedEntitlement; /// {@macro entitlement_delete_event} - EntitlementDeleteEvent({required super.gateway}); + /// @nodoc + EntitlementDeleteEvent({required super.gateway, required this.entitlement, required this.deletedEntitlement}); } diff --git a/lib/src/models/gateway/events/guild.dart b/lib/src/models/gateway/events/guild.dart index ca59d12ef..7584eccd0 100644 --- a/lib/src/models/gateway/events/guild.dart +++ b/lib/src/models/gateway/events/guild.dart @@ -22,6 +22,7 @@ class UnavailableGuildCreateEvent extends DispatchEvent { final PartialGuild guild; /// {@macro unavailable_guild_create_event} + /// @nodoc UnavailableGuildCreateEvent({required super.gateway, required this.guild}); } @@ -63,6 +64,7 @@ class GuildCreateEvent extends DispatchEvent implements UnavailableGuildCreateEv final List scheduledEvents; /// {@macro guild_create_event} + /// @nodoc GuildCreateEvent({ required super.gateway, required this.guild, @@ -90,6 +92,7 @@ class GuildUpdateEvent extends DispatchEvent { final Guild guild; /// {@macro guild_update_event} + /// @nodoc GuildUpdateEvent({required super.gateway, required this.oldGuild, required this.guild}); } @@ -103,8 +106,12 @@ class GuildDeleteEvent extends DispatchEvent { /// Whether the client was removed because the guild is unavailable. final bool isUnavailable; + /// The guild as it was cached before it was deleted. + final Guild? deletedGuild; + /// {@macro guild_delete_event} - GuildDeleteEvent({required super.gateway, required this.guild, required this.isUnavailable}); + /// @nodoc + GuildDeleteEvent({required super.gateway, required this.guild, required this.isUnavailable, required this.deletedGuild}); } /// {@template guild_audit_log_create_event} @@ -118,6 +125,7 @@ class GuildAuditLogCreateEvent extends DispatchEvent { final Snowflake guildId; /// {@macro guild_audit_log_create_event} + /// @nodoc GuildAuditLogCreateEvent({required super.gateway, required this.entry, required this.guildId}); /// The guild in which the entry was created. @@ -135,6 +143,7 @@ class GuildBanAddEvent extends DispatchEvent { final User user; /// {@macro guild_ban_add_event} + /// @nodoc GuildBanAddEvent({required super.gateway, required this.guildId, required this.user}); /// The guild in which the user was banned. @@ -152,6 +161,7 @@ class GuildBanRemoveEvent extends DispatchEvent { final User user; /// {@macro guild_ban_remove_event} + /// @nodoc GuildBanRemoveEvent({required super.gateway, required this.guildId, required this.user}); /// The guild in which the user was unbanned. @@ -169,6 +179,7 @@ class GuildEmojisUpdateEvent extends DispatchEvent { final List emojis; /// {@macro guild_emojis_update_event} + /// @nodoc GuildEmojisUpdateEvent({required super.gateway, required this.guildId, required this.emojis}); /// The guild in which emojis were updated. @@ -186,6 +197,7 @@ class GuildStickersUpdateEvent extends DispatchEvent { final List stickers; /// {@macro guild_stickers_update_event} + /// @nodoc GuildStickersUpdateEvent({required super.gateway, required this.guildId, required this.stickers}); /// The guild in which the stickers were updated. @@ -200,6 +212,7 @@ class GuildIntegrationsUpdateEvent extends DispatchEvent { final Snowflake guildId; /// {@macro guild_integrations_update_event} + /// @nodoc GuildIntegrationsUpdateEvent({required super.gateway, required this.guildId}); /// The guild in which the integrations were updated. @@ -217,6 +230,7 @@ class GuildMemberAddEvent extends DispatchEvent { final Member member; /// {@macro guild_member_add_event} + /// @nodoc GuildMemberAddEvent({required super.gateway, required this.guildId, required this.member}); /// The guild in which the member was added. @@ -233,8 +247,12 @@ class GuildMemberRemoveEvent extends DispatchEvent { /// The removed user. final User user; + /// The member as it was cached before being removed. + final Member? removedMember; + /// {@macro guild_member_remove_event} - GuildMemberRemoveEvent({required super.gateway, required this.guildId, required this.user}); + /// @nodoc + GuildMemberRemoveEvent({required super.gateway, required this.guildId, required this.user, required this.removedMember}); /// The guild in which the member was removed. PartialGuild get guild => gateway.client.guilds[guildId]; @@ -254,6 +272,7 @@ class GuildMemberUpdateEvent extends DispatchEvent { final Snowflake guildId; /// {@macro guild_member_update_event} + /// @nodoc GuildMemberUpdateEvent({required super.gateway, required this.oldMember, required this.member, required this.guildId}); /// The guild in which the member was updated. @@ -286,6 +305,7 @@ class GuildMembersChunkEvent extends DispatchEvent { final String? nonce; /// {@macro guild_members_chunk_event} + /// @nodoc GuildMembersChunkEvent({ required super.gateway, required this.guildId, @@ -312,6 +332,7 @@ class GuildRoleCreateEvent extends DispatchEvent { final Role role; /// {@macro guild_role_create_event} + /// @nodoc GuildRoleCreateEvent({required super.gateway, required this.guildId, required this.role}); /// The guild in which the role was created. @@ -332,6 +353,7 @@ class GuildRoleUpdateEvent extends DispatchEvent { final Role role; /// {@macro guild_role_update_event} + /// @nodoc GuildRoleUpdateEvent({required super.gateway, required this.guildId, required this.oldRole, required this.role}); /// The guild in which the role was updated. @@ -345,11 +367,15 @@ class GuildRoleDeleteEvent extends DispatchEvent { /// The ID of the guild. final Snowflake guildId; + /// The role as it was cached before being deleted. + final Role? deletedRole; + /// The ID of the deleted role. final Snowflake roleId; /// {@macro guild_role_delete_event} - GuildRoleDeleteEvent({required super.gateway, required this.roleId, required this.guildId}); + /// @nodoc + GuildRoleDeleteEvent({required super.gateway, required this.roleId, required this.guildId, required this.deletedRole}); /// The guild in which the role was deleted. PartialGuild get guild => gateway.client.guilds[guildId]; @@ -363,6 +389,7 @@ class GuildScheduledEventCreateEvent extends DispatchEvent { final ScheduledEvent event; /// {@macro guild_scheduled_event_create_event} + /// @nodoc GuildScheduledEventCreateEvent({required super.gateway, required this.event}); } @@ -377,6 +404,7 @@ class GuildScheduledEventUpdateEvent extends DispatchEvent { final ScheduledEvent event; /// {@macro guild_scheduled_event_update_event} + /// @nodoc GuildScheduledEventUpdateEvent({required super.gateway, required this.oldEvent, required this.event}); } @@ -388,6 +416,7 @@ class GuildScheduledEventDeleteEvent extends DispatchEvent { final ScheduledEvent event; /// {@macro guild_scheduled_event_delete_event} + /// @nodoc GuildScheduledEventDeleteEvent({required super.gateway, required this.event}); } @@ -405,6 +434,7 @@ class GuildScheduledEventUserAddEvent extends DispatchEvent { final Snowflake guildId; /// {@macro guild_scheduled_event_user_add_event} + /// @nodoc GuildScheduledEventUserAddEvent({required super.gateway, required this.scheduledEventId, required this.userId, required this.guildId}); /// The guild that the scheduled event is in. @@ -434,6 +464,7 @@ class GuildScheduledEventUserRemoveEvent extends DispatchEvent { final Snowflake guildId; /// {@macro guild_scheduled_event_user_remove_event} + /// @nodoc GuildScheduledEventUserRemoveEvent({required super.gateway, required this.scheduledEventId, required this.userId, required this.guildId}); /// The guild that the scheduled event is in. diff --git a/lib/src/models/gateway/events/integration.dart b/lib/src/models/gateway/events/integration.dart index ed0bb1133..c20669cc4 100644 --- a/lib/src/models/gateway/events/integration.dart +++ b/lib/src/models/gateway/events/integration.dart @@ -15,6 +15,7 @@ class IntegrationCreateEvent extends DispatchEvent { final Integration integration; /// {@macro integration_create_event} + /// @nodoc IntegrationCreateEvent({required super.gateway, required this.guildId, required this.integration}); /// The guild the integration was created in. @@ -35,6 +36,7 @@ class IntegrationUpdateEvent extends DispatchEvent { final Integration integration; /// {@macro integration_update_event} + /// @nodoc IntegrationUpdateEvent({required super.gateway, required this.guildId, required this.oldIntegration, required this.integration}); /// The guild the integration was updated in. @@ -54,8 +56,12 @@ class IntegrationDeleteEvent extends DispatchEvent { /// The ID of the application associated with the integration. final Snowflake? applicationId; + /// The integration as it was cached before being deleted. + final Integration? deletedIntegration; + /// {@macro integration_delete_event} - IntegrationDeleteEvent({required super.gateway, required this.id, required this.guildId, required this.applicationId}); + /// @nodoc + IntegrationDeleteEvent({required super.gateway, required this.id, required this.guildId, required this.applicationId, required this.deletedIntegration}); /// The guild the integration was deleted from. PartialGuild get guild => gateway.client.guilds[guildId]; diff --git a/lib/src/models/gateway/events/interaction.dart b/lib/src/models/gateway/events/interaction.dart index cc73280a6..e2abb9f4a 100644 --- a/lib/src/models/gateway/events/interaction.dart +++ b/lib/src/models/gateway/events/interaction.dart @@ -9,5 +9,6 @@ class InteractionCreateEvent> extends DispatchEve final T interaction; /// {@macro interaction_create_event} + /// @nodoc InteractionCreateEvent({required super.gateway, required this.interaction}); } diff --git a/lib/src/models/gateway/events/invite.dart b/lib/src/models/gateway/events/invite.dart index e0577816f..532595283 100644 --- a/lib/src/models/gateway/events/invite.dart +++ b/lib/src/models/gateway/events/invite.dart @@ -12,6 +12,7 @@ class InviteCreateEvent extends DispatchEvent { final InviteWithMetadata invite; /// {@macro invite_create_event} + /// @nodoc InviteCreateEvent({required super.gateway, required this.invite}); } @@ -29,6 +30,7 @@ class InviteDeleteEvent extends DispatchEvent { final String code; /// {@macro invite_delete_event} + /// @nodoc InviteDeleteEvent({required super.gateway, required this.channelId, required this.guildId, required this.code}); /// The channel the invite was for. diff --git a/lib/src/models/gateway/events/message.dart b/lib/src/models/gateway/events/message.dart index 3d445931d..a1b150e5f 100644 --- a/lib/src/models/gateway/events/message.dart +++ b/lib/src/models/gateway/events/message.dart @@ -24,6 +24,7 @@ class MessageCreateEvent extends DispatchEvent { final Message message; /// {@macro message_create_event} + /// @nodoc MessageCreateEvent({required super.gateway, required this.guildId, required this.member, required this.mentions, required this.message}); /// The guild the message was sent in. @@ -50,6 +51,7 @@ class MessageUpdateEvent extends DispatchEvent { final Message? oldMessage; /// {@macro message_update_event} + /// @nodoc MessageUpdateEvent({ required super.gateway, required this.guildId, @@ -76,8 +78,12 @@ class MessageDeleteEvent extends DispatchEvent { /// The ID of the guild the message was deleted in. final Snowflake? guildId; + /// The message as it was cached before being deleted. + final Message? deletedMessage; + /// {@macro message_delete_event} - MessageDeleteEvent({required super.gateway, required this.id, required this.channelId, required this.guildId}); + /// @nodoc + MessageDeleteEvent({required super.gateway, required this.id, required this.channelId, required this.guildId, required this.deletedMessage}); /// The guild the message was deleted in. PartialGuild? get guild => guildId == null ? null : gateway.client.guilds[guildId!]; @@ -93,6 +99,9 @@ class MessageBulkDeleteEvent extends DispatchEvent { /// A list of the IDs of the deleted messages. final List ids; + /// A list of the messages that were found in cache before being deleted. + final List deletedMessages; + /// The ID of the channel the messages were deleted in. final Snowflake channelId; @@ -100,7 +109,8 @@ class MessageBulkDeleteEvent extends DispatchEvent { final Snowflake? guildId; /// {@macro message_bulk_delete_event} - MessageBulkDeleteEvent({required super.gateway, required this.ids, required this.channelId, required this.guildId}); + /// @nodoc + MessageBulkDeleteEvent({required super.gateway, required this.ids, required this.deletedMessages, required this.channelId, required this.guildId}); /// The guild the messages were deleted in. PartialGuild? get guild => guildId == null ? null : gateway.client.guilds[guildId!]; @@ -135,6 +145,7 @@ class MessageReactionAddEvent extends DispatchEvent { final Snowflake? messageAuthorId; /// {@macro message_reaction_add_event} + /// @nodoc MessageReactionAddEvent({ required super.gateway, required this.userId, @@ -182,6 +193,7 @@ class MessageReactionRemoveEvent extends DispatchEvent { final Emoji emoji; /// {@macro message_reaction_remove_event} + /// @nodoc MessageReactionRemoveEvent({ required super.gateway, required this.userId, @@ -202,6 +214,9 @@ class MessageReactionRemoveEvent extends DispatchEvent { /// The message the reaction was removed from. PartialMessage get message => channel.messages[messageId]; + + /// The member that removed the reaction. + PartialMember? get member => guild?.members[userId]; } /// {@template message_reaction_remove_all_event} @@ -218,6 +233,7 @@ class MessageReactionRemoveAllEvent extends DispatchEvent { final Snowflake? guildId; /// {@macro message_reaction_remove_all_event} + /// @nodoc MessageReactionRemoveAllEvent({ required super.gateway, required this.channelId, @@ -251,6 +267,7 @@ class MessageReactionRemoveEmojiEvent extends DispatchEvent { final PartialEmoji emoji; /// {@macro message_reaction_remove_emoji_event} + /// @nodoc MessageReactionRemoveEmojiEvent({ required super.gateway, required this.channelId, @@ -268,3 +285,95 @@ class MessageReactionRemoveEmojiEvent extends DispatchEvent { /// The message the reactions were removed from. PartialMessage get message => channel.messages[messageId]; } + +/// {@template message_poll_vote_add_event} +/// Emitted when user votes on a poll. If the poll allows multiple selection, one event will be sent per answer. +/// {@endtemplate} +class MessagePollVoteAddEvent extends DispatchEvent { + /// The ID of the user that voted on a poll. + final Snowflake userId; + + /// The ID of the channel the message is in. + final Snowflake channelId; + + /// The ID of the message where vote added on a poll. + final Snowflake messageId; + + /// The ID of the guild the message is in. + final Snowflake? guildId; + + /// The ID of the answer on the poll. + final int answerId; + + /// {@macro message_poll_vote_add_event} + /// @nodoc + MessagePollVoteAddEvent({ + required super.gateway, + required this.userId, + required this.channelId, + required this.messageId, + required this.guildId, + required this.answerId, + }); + + /// The user that voted on a poll. + PartialUser get user => gateway.client.users[userId]; + + /// The channel the message is in. + PartialTextChannel get channel => gateway.client.channels[channelId] as PartialTextChannel; + + /// The message where vote added on a poll. + PartialMessage get message => channel.messages[messageId]; + + /// The guild the message is in. + PartialGuild? get guild => guildId == null ? null : gateway.client.guilds[guildId!]; + + /// The member that voted on a poll. + PartialMember? get member => guild?.members[userId]; +} + +/// {@template message_poll_vote_remove_event} +/// Emitted when user removes their vote on a poll. If the poll allows for multiple selections, one event will be sent per answer. +/// {@endtemplate} +class MessagePollVoteRemoveEvent extends DispatchEvent { + /// The ID of the user that removed their vote from a poll. + final Snowflake userId; + + /// The ID of the channel the message is in. + final Snowflake channelId; + + /// The ID of the message where vote removed from a poll. + final Snowflake messageId; + + /// The ID of the guild the message is in. + final Snowflake? guildId; + + /// The ID of the answer on the poll. + final int answerId; + + /// {@macro message_poll_vote_remove_event} + /// @nodoc + MessagePollVoteRemoveEvent({ + required super.gateway, + required this.userId, + required this.channelId, + required this.messageId, + required this.guildId, + required this.answerId, + }); + + /// The user that removed their vote from a poll. + PartialUser get user => gateway.client.users[userId]; + + /// The channel the message is in. + PartialTextChannel get channel => gateway.client.channels[channelId] as PartialTextChannel; + + /// The message where vote removed from a poll. + PartialMessage get message => channel.messages[messageId]; + + /// The guild the message is in. + PartialGuild? get guild => guildId == null ? null : gateway.client.guilds[guildId!]; + + /// The member that removed their vote from a poll. + PartialMember? get member => guild?.members[userId]; +} diff --git a/lib/src/models/gateway/events/presence.dart b/lib/src/models/gateway/events/presence.dart index 55dd2d9c7..ef2cc7344 100644 --- a/lib/src/models/gateway/events/presence.dart +++ b/lib/src/models/gateway/events/presence.dart @@ -26,6 +26,7 @@ class PresenceUpdateEvent extends DispatchEvent { final ClientStatus? clientStatus; /// {@macro presence_update_event} + /// @nodoc PresenceUpdateEvent({ required super.gateway, required this.user, @@ -59,6 +60,7 @@ class TypingStartEvent extends DispatchEvent { final Member? member; /// {@macro typing_start_event} + /// @nodoc TypingStartEvent({ required super.gateway, required this.channelId, @@ -89,5 +91,6 @@ class UserUpdateEvent extends DispatchEvent { final User user; /// {@macro user_update_event} + /// @nodoc UserUpdateEvent({required super.gateway, required this.oldUser, required this.user}); } diff --git a/lib/src/models/gateway/events/ready.dart b/lib/src/models/gateway/events/ready.dart index 96a5dae91..e91e04d26 100644 --- a/lib/src/models/gateway/events/ready.dart +++ b/lib/src/models/gateway/events/ready.dart @@ -32,6 +32,7 @@ class ReadyEvent extends DispatchEvent { final PartialApplication application; /// {@macro ready_event} + /// @nodoc ReadyEvent({ required super.gateway, required this.version, @@ -50,5 +51,6 @@ class ReadyEvent extends DispatchEvent { /// {@endtemplate} class ResumedEvent extends DispatchEvent { /// {@macro resumed_event} + /// @nodoc ResumedEvent({required super.gateway}); } diff --git a/lib/src/models/gateway/events/stage_instance.dart b/lib/src/models/gateway/events/stage_instance.dart index 8dac59389..627448027 100644 --- a/lib/src/models/gateway/events/stage_instance.dart +++ b/lib/src/models/gateway/events/stage_instance.dart @@ -9,6 +9,7 @@ class StageInstanceCreateEvent extends DispatchEvent { final StageInstance instance; /// {@macro stage_instance_create_event} + /// @nodoc StageInstanceCreateEvent({required super.gateway, required this.instance}); } @@ -23,6 +24,7 @@ class StageInstanceUpdateEvent extends DispatchEvent { final StageInstance instance; /// {@macro stage_instance_update_event} + /// @nodoc StageInstanceUpdateEvent({required super.gateway, required this.oldInstance, required this.instance}); } @@ -34,5 +36,6 @@ class StageInstanceDeleteEvent extends DispatchEvent { final StageInstance instance; /// {@macro stage_instance_delete_event} + /// @nodoc StageInstanceDeleteEvent({required super.gateway, required this.instance}); } diff --git a/lib/src/models/gateway/events/voice.dart b/lib/src/models/gateway/events/voice.dart index 455a3b378..b470dcd66 100644 --- a/lib/src/models/gateway/events/voice.dart +++ b/lib/src/models/gateway/events/voice.dart @@ -14,6 +14,7 @@ class VoiceStateUpdateEvent extends DispatchEvent { final VoiceState? oldState; /// {@macro voice_state_update_event} + /// @nodoc VoiceStateUpdateEvent({required super.gateway, required this.oldState, required this.state}); } @@ -31,6 +32,7 @@ class VoiceServerUpdateEvent extends DispatchEvent { final String? endpoint; /// {@macro voice_server_update_event} + /// @nodoc VoiceServerUpdateEvent({required super.gateway, required this.token, required this.guildId, required this.endpoint}); /// The guild. diff --git a/lib/src/models/gateway/events/webhook.dart b/lib/src/models/gateway/events/webhook.dart index 26f3943e3..f3b640a74 100644 --- a/lib/src/models/gateway/events/webhook.dart +++ b/lib/src/models/gateway/events/webhook.dart @@ -14,6 +14,7 @@ class WebhooksUpdateEvent extends DispatchEvent { final Snowflake channelId; /// {@macro webhooks_update_event} + /// @nodoc WebhooksUpdateEvent({required super.gateway, required this.guildId, required this.channelId}); /// The guild the webhook was updated in. diff --git a/lib/src/models/gateway/gateway.dart b/lib/src/models/gateway/gateway.dart index d6a61e488..d63ccf563 100644 --- a/lib/src/models/gateway/gateway.dart +++ b/lib/src/models/gateway/gateway.dart @@ -8,6 +8,7 @@ class GatewayConfiguration with ToStringHelper { final Uri url; /// {@macro gateway_configuration} + /// @nodoc GatewayConfiguration({required this.url}); } @@ -22,6 +23,7 @@ class GatewayBot extends GatewayConfiguration { final SessionStartLimit sessionStartLimit; /// {@macro gateway_bot} + /// @nodoc GatewayBot({ required super.url, required this.shards, @@ -46,6 +48,7 @@ class SessionStartLimit with ToStringHelper { final int maxConcurrency; /// {@macro session_start_limit} + /// @nodoc SessionStartLimit({ required this.total, required this.remaining, diff --git a/lib/src/models/guild/audit_log.dart b/lib/src/models/guild/audit_log.dart index 247e07746..bdd282039 100644 --- a/lib/src/models/guild/audit_log.dart +++ b/lib/src/models/guild/audit_log.dart @@ -7,6 +7,7 @@ import 'package:nyxx/src/models/permission_overwrite.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A partial [AuditLogEntry]. @@ -15,6 +16,7 @@ class PartialAuditLogEntry extends ManagedSnowflakeEntity { final AuditLogManager manager; /// Create a new [PartialAuditLogEntry]. + /// @nodoc PartialAuditLogEntry({required super.id, required this.manager}); } @@ -41,6 +43,7 @@ class AuditLogEntry extends PartialAuditLogEntry { final String? reason; /// {@macro audit_log_entry} + /// @nodoc AuditLogEntry({ required super.id, required super.manager, @@ -70,6 +73,7 @@ class AuditLogChange with ToStringHelper { final String key; /// {@macro audit_log_change} + /// @nodoc AuditLogChange({ required this.oldValue, required this.newValue, @@ -78,79 +82,76 @@ class AuditLogChange with ToStringHelper { } /// The type of event an [AuditLogEntry] represents. -enum AuditLogEvent { - guildUpdate._(1), - channelCreate._(10), - channelUpdate._(11), - channelDelete._(12), - channelOverwriteCreate._(13), - channelOverwriteUpdate._(14), - channelOverwriteDelete._(15), - memberKick._(20), - memberPrune._(21), - memberBanAdd._(22), - memberBanRemove._(23), - memberUpdate._(24), - memberRoleUpdate._(25), - memberMove._(26), - memberDisconnect._(27), - botAdd._(28), - roleCreate._(30), - roleUpdate._(31), - roleDelete._(32), - inviteCreate._(40), - inviteUpdate._(41), - inviteDelete._(42), - webhookCreate._(50), - webhookUpdate._(51), - webhookDelete._(52), - emojiCreate._(60), - emojiUpdate._(61), - emojiDelete._(62), - messageDelete._(72), - messageBulkDelete._(73), - messagePin._(74), - messageUnpin._(75), - integrationCreate._(80), - integrationUpdate._(81), - integrationDelete._(82), - stageInstanceCreate._(83), - stageInstanceUpdate._(84), - stageInstanceDelete._(85), - stickerCreate._(90), - stickerUpdate._(91), - stickerDelete._(92), - guildScheduledEventCreate._(100), - guildScheduledEventUpdate._(101), - guildScheduledEventDelete._(102), - threadCreate._(110), - threadUpdate._(111), - threadDelete._(112), - applicationCommandPermissionUpdate._(121), - autoModerationRuleCreate._(140), - autoModerationRuleUpdate._(141), - autoModerationRuleDelete._(142), - autoModerationBlockMessage._(143), - autoModerationFlagToChannel._(144), - autoModerationUserCommunicationDisabled._(145), - creatorMonetizationRequestCreated._(150), - creatorMonetizationTermsAccepted._(151); - - /// The value of this [AuditLogEvent]. - final int value; - - const AuditLogEvent._(this.value); - - /// Parse an [AuditLogEvent] from an [int]. - /// - /// The [value] must be a valid audit log event. - factory AuditLogEvent.parse(int value) => AuditLogEvent.values.firstWhere( - (event) => event.value == value, - orElse: () => throw FormatException('Unknown audit log event', value), - ); - - @override - String toString() => 'AuditLogEvent($value)'; +final class AuditLogEvent extends EnumLike { + static const guildUpdate = AuditLogEvent(1); + static const channelCreate = AuditLogEvent(10); + static const channelUpdate = AuditLogEvent(11); + static const channelDelete = AuditLogEvent(12); + static const channelOverwriteCreate = AuditLogEvent(13); + static const channelOverwriteUpdate = AuditLogEvent(14); + static const channelOverwriteDelete = AuditLogEvent(15); + static const memberKick = AuditLogEvent(20); + static const memberPrune = AuditLogEvent(21); + static const memberBanAdd = AuditLogEvent(22); + static const memberBanRemove = AuditLogEvent(23); + static const memberUpdate = AuditLogEvent(24); + static const memberRoleUpdate = AuditLogEvent(25); + static const memberMove = AuditLogEvent(26); + static const memberDisconnect = AuditLogEvent(27); + static const botAdd = AuditLogEvent(28); + static const roleCreate = AuditLogEvent(30); + static const roleUpdate = AuditLogEvent(31); + static const roleDelete = AuditLogEvent(32); + static const inviteCreate = AuditLogEvent(40); + static const inviteUpdate = AuditLogEvent(41); + static const inviteDelete = AuditLogEvent(42); + static const webhookCreate = AuditLogEvent(50); + static const webhookUpdate = AuditLogEvent(51); + static const webhookDelete = AuditLogEvent(52); + static const emojiCreate = AuditLogEvent(60); + static const emojiUpdate = AuditLogEvent(61); + static const emojiDelete = AuditLogEvent(62); + static const messageDelete = AuditLogEvent(72); + static const messageBulkDelete = AuditLogEvent(73); + static const messagePin = AuditLogEvent(74); + static const messageUnpin = AuditLogEvent(75); + static const integrationCreate = AuditLogEvent(80); + static const integrationUpdate = AuditLogEvent(81); + static const integrationDelete = AuditLogEvent(82); + static const stageInstanceCreate = AuditLogEvent(83); + static const stageInstanceUpdate = AuditLogEvent(84); + static const stageInstanceDelete = AuditLogEvent(85); + static const stickerCreate = AuditLogEvent(90); + static const stickerUpdate = AuditLogEvent(91); + static const stickerDelete = AuditLogEvent(92); + static const guildScheduledEventCreate = AuditLogEvent(100); + static const guildScheduledEventUpdate = AuditLogEvent(101); + static const guildScheduledEventDelete = AuditLogEvent(102); + static const threadCreate = AuditLogEvent(110); + static const threadUpdate = AuditLogEvent(111); + static const threadDelete = AuditLogEvent(112); + static const applicationCommandPermissionUpdate = AuditLogEvent(121); + static const autoModerationRuleCreate = AuditLogEvent(140); + static const autoModerationRuleUpdate = AuditLogEvent(141); + static const autoModerationRuleDelete = AuditLogEvent(142); + static const autoModerationBlockMessage = AuditLogEvent(143); + static const autoModerationFlagToChannel = AuditLogEvent(144); + static const autoModerationUserCommunicationDisabled = AuditLogEvent(145); + static const creatorMonetizationRequestCreated = AuditLogEvent(150); + static const creatorMonetizationTermsAccepted = AuditLogEvent(151); + static const onboardingPromptCreate = AuditLogEvent(163); + static const onboardingPromptUpdate = AuditLogEvent(164); + static const onboardingPromptDelete = AuditLogEvent(165); + static const onboardingCreate = AuditLogEvent(166); + static const onboardingUpdate = AuditLogEvent(167); + static const homeSettingsCreate = AuditLogEvent(190); + static const homeSettingsUpdate = AuditLogEvent(191); + + /// @nodoc + const AuditLogEvent(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + AuditLogEvent.parse(int value) : this(value); } /// {@template audit_log_entry_info} @@ -197,6 +198,7 @@ class AuditLogEntryInfo with ToStringHelper { final String? integrationType; /// {@macro audit_log_entry_info} + /// @nodoc AuditLogEntryInfo({ required this.manager, required this.applicationId, diff --git a/lib/src/models/guild/auto_moderation.dart b/lib/src/models/guild/auto_moderation.dart index a887a452f..99a9156eb 100644 --- a/lib/src/models/guild/auto_moderation.dart +++ b/lib/src/models/guild/auto_moderation.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/builders/guild/auto_moderation.dart'; import 'package:nyxx/src/http/managers/auto_moderation_manager.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/channel/text_channel.dart'; @@ -7,6 +8,7 @@ import 'package:nyxx/src/models/role.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A partial [AutoModerationRule]. @@ -15,6 +17,7 @@ class PartialAutoModerationRule extends WritableSnowflakeEntity exemptChannelIds; /// {@macro auto_moderation_rule} + /// @nodoc AutoModerationRule({ required super.id, required super.manager, @@ -80,74 +84,70 @@ class AutoModerationRule extends PartialAutoModerationRule { } /// The type of event on which an [AutoModerationRule] triggers. -enum AutoModerationEventType { - messageSend._(1); +final class AutoModerationEventType extends EnumLike { + /// When a member sends or edits a message in the guild. + static const messageSend = AutoModerationEventType(1); - /// The value of this [AutoModerationEventType]. - final int value; + /// When a member edits their profile. + static const memberUpdate = AutoModerationEventType(2); - const AutoModerationEventType._(this.value); + /// @nodoc + const AutoModerationEventType(super.value); - /// Parse an [AutoModerationEventType] from an [int]. - /// - /// The [value] must be a valid auto moderation event type. - factory AutoModerationEventType.parse(int value) => AutoModerationEventType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown auto moderation event type', value), - ); - - @override - String toString() => 'AutoModerationEventType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + AutoModerationEventType.parse(int value) : this(value); } /// The type of a trigger for an [AutoModerationRule] -enum TriggerType { - keyword._(1), - spam._(2), - keywordPreset._(4), - mentionSpam._(5); +final class TriggerType extends EnumLike { + /// Check if content contains words from a user defined list of keywords. + static const keyword = TriggerType(1); - /// The value of this [TriggerType]. - final int value; + /// Check if content represents generic spam. + static const spam = TriggerType(3); - const TriggerType._(this.value); + /// Check if content contains words from internal pre-defined wordsets. + static const keywordPreset = TriggerType(4); - /// Parse an [TriggerType] from an [int]. - /// - /// The [value] must be a valid trigger type. - factory TriggerType.parse(int value) => TriggerType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown trigger type', value), - ); + /// Check if content contains more unique mentions than allowed. + static const mentionSpam = TriggerType(5); - @override - String toString() => 'TriggerType($value)'; + /// Check if member profile contains words from a user defined list of keywords. + static const memberProfile = TriggerType(6); + + /// @nodoc + const TriggerType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + TriggerType.parse(int value) : this(value); } /// {@template trigger_metadata} /// Additional metadata associated with the trigger for an [AutoModerationRule]. /// {@endtemplate} -class TriggerMetadata with ToStringHelper { - /// A list of words that trigger the rule. +// TODO(abitofevrything): Remove `implements TriggerMetadataBuilder` +class TriggerMetadata with ToStringHelper implements TriggerMetadataBuilder { + @override final List? keywordFilter; - /// A list of regex patterns that trigger the rule. // TODO: Do we want to parse these as RegExp objects? + @override final List? regexPatterns; - /// A list of preset keyword types that trigger the rule. + @override final List? presets; - /// A list of words allowed to bypass the rule. + @override final List? allowList; - /// The maximum number of mentions in a message. + @override final int? mentionTotalLimit; - /// Whether mention raid protection is enabled. + @override final bool? isMentionRaidProtectionEnabled; /// {@macro trigger_metadata} + /// @nodoc TriggerMetadata({ required this.keywordFilter, required this.regexPatterns, @@ -156,87 +156,90 @@ class TriggerMetadata with ToStringHelper { required this.mentionTotalLimit, required this.isMentionRaidProtectionEnabled, }); + + @override + @Deprecated('Use TriggerMetadataBuilder instead') + Map build() => { + 'keyword_filter': keywordFilter, + 'regex_patterns': regexPatterns, + 'presets': presets?.map((type) => type.value).toList(), + 'allow_list': allowList, + 'mention_total_limit': mentionTotalLimit, + 'mention_raid_protection_enabled': isMentionRaidProtectionEnabled, + }; } /// A preset list of trigger keywords for an [AutoModerationRule]. -enum KeywordPresetType { - profanity._(1), - sexualContent._(2), - slurs._(3); - - /// The value of this [KeywordPresetType]. - final int value; +final class KeywordPresetType extends EnumLike { + static const profanity = KeywordPresetType(1); + static const sexualContent = KeywordPresetType(2); + static const slurs = KeywordPresetType(3); - const KeywordPresetType._(this.value); + /// @nodoc + const KeywordPresetType(super.value); - /// Parse an [KeywordPresetType] from an [int]. - /// - /// The [value] must be a valid keyword preset type. - factory KeywordPresetType.parse(int value) => KeywordPresetType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown keyword preset type', value), - ); - - @override - String toString() => 'KeywordPresetType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + KeywordPresetType.parse(int value) : this(value); } /// {@template auto_moderation_action} /// Describes an action to take when an [AutoModerationRule] is triggered. /// {@endtemplate} -class AutoModerationAction with ToStringHelper { - /// The type of action to perform. +// TODO(abitofevrything): Remove `implements AutoModerationActionBuilder` +class AutoModerationAction with ToStringHelper implements AutoModerationActionBuilder { + @override final ActionType type; - /// Metadata needed to perform the action. + @override final ActionMetadata? metadata; /// {@macro auto_moderation_action} + /// @nodoc AutoModerationAction({ required this.type, required this.metadata, }); + + @override + @Deprecated('Use AutoModerationActionBuilder instead') + Map build() => { + 'type': type.value, + if (metadata != null) 'metadata': metadata!.build(), + }; } /// The type of action for an [AutoModerationAction]. -enum ActionType { - blockMessage._(1), - sendAlertMessage._(2), - timeout._(3); - - /// The value of this [ActionType]. - final int value; +final class ActionType extends EnumLike { + static const blockMessage = ActionType(1); + static const sendAlertMessage = ActionType(2); + static const timeout = ActionType(3); + static const blockMemberInteraction = ActionType(4); - const ActionType._(this.value); + /// @nodoc + const ActionType(super.value); - /// Parse an [ActionType] from an [int]. - /// - /// The [value] must be a valid action type. - factory ActionType.parse(int value) => ActionType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown action type', value), - ); - - @override - String toString() => 'ActionType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ActionType.parse(int value) : this(value); } /// {@template action_metadata} /// Additional metadata associated with an [AutoModerationAction]. /// {@endtemplate} -class ActionMetadata with ToStringHelper { +// TODO(abitofevrything): Remove `implements ActionMetadataBuilder` +class ActionMetadata with ToStringHelper implements ActionMetadataBuilder { final AutoModerationManager manager; - /// The ID of the channel to send the alert message to. + @override final Snowflake? channelId; - /// The duration of time to time the user out for. + @override final Duration? duration; - /// A custom message to send to the user. + @override final String? customMessage; /// {@macro action_metadata} + /// @nodoc ActionMetadata({ required this.manager, required this.channelId, @@ -246,4 +249,12 @@ class ActionMetadata with ToStringHelper { /// The channel to send the alert message to. PartialTextChannel? get channel => channelId == null ? null : manager.client.channels[channelId!] as PartialTextChannel?; + + @override + @Deprecated('Use ActionMetadataBuilder instead') + Map build() => { + 'channel_id': channelId?.toString(), + 'duration_seconds': duration?.inSeconds, + 'custom_message': customMessage, + }; } diff --git a/lib/src/models/guild/ban.dart b/lib/src/models/guild/ban.dart index 0e035c7c3..442abae57 100644 --- a/lib/src/models/guild/ban.dart +++ b/lib/src/models/guild/ban.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; @@ -12,5 +13,17 @@ class Ban with ToStringHelper { final User user; /// {@macro ban} + /// @nodoc Ban({required this.reason, required this.user}); } + +class BulkBanResponse with ToStringHelper { + /// A list of user IDs, that were succesfully banned. + final List bannedUsers; + + /// A list of user IDs, that were not banned. + final List failedUsers; + + /// @nodoc + BulkBanResponse({required this.bannedUsers, required this.failedUsers}); +} diff --git a/lib/src/models/guild/guild.dart b/lib/src/models/guild/guild.dart index 4a54fab7a..28d3e2a62 100644 --- a/lib/src/models/guild/guild.dart +++ b/lib/src/models/guild/guild.dart @@ -2,10 +2,12 @@ import 'dart:typed_data'; import 'package:nyxx/src/builders/channel/channel_position.dart'; import 'package:nyxx/src/builders/channel/guild_channel.dart'; +import 'package:nyxx/src/builders/guild/onboarding.dart'; import 'package:nyxx/src/builders/guild/template.dart'; import 'package:nyxx/src/builders/guild/welcome_screen.dart'; import 'package:nyxx/src/builders/guild/widget.dart'; import 'package:nyxx/src/builders/voice.dart'; +import 'package:nyxx/src/cache/cache.dart'; import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/application_command_manager.dart'; import 'package:nyxx/src/http/managers/audit_log_manager.dart'; @@ -40,6 +42,8 @@ import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/sticker/guild_sticker.dart'; import 'package:nyxx/src/models/user/user.dart'; import 'package:nyxx/src/models/voice/voice_region.dart'; +import 'package:nyxx/src/models/voice/voice_state.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; /// A partial [Guild]. @@ -62,8 +66,8 @@ class PartialGuild extends WritableSnowflakeEntity { /// An [IntegrationManager] for the integrations of this guild. IntegrationManager get integrations => IntegrationManager(manager.client.options.integrationConfig, manager.client, guildId: id); - /// An [EmojiManager] for the emojis of this guild. - EmojiManager get emojis => EmojiManager(manager.client.options.emojiCacheConfig, manager.client, guildId: id); + /// A [GuildEmojiManager] for the emojis of this guild. + GuildEmojiManager get emojis => GuildEmojiManager(manager.client.options.emojiCacheConfig, manager.client, guildId: id); /// An [GuildStickerManager] for the stickers of this guild. GuildStickerManager get stickers => GuildStickerManager(manager.client.options.stickerCacheConfig, manager.client, guildId: id); @@ -71,6 +75,9 @@ class PartialGuild extends WritableSnowflakeEntity { /// An [AuditLogManager] for the audit log of this guild. AuditLogManager get auditLogs => AuditLogManager(manager.client.options.auditLogEntryConfig, manager.client, guildId: id); + /// A [Cache] for [VoiceState]s in this guild. + Cache get voiceStates => manager.client.cache.getCache('$id.voiceStates', manager.client.options.voiceStateConfig); + /// A [GuildApplicationCommandManager] for the application commands of this guild. GuildApplicationCommandManager get commands => GuildApplicationCommandManager( manager.client.options.applicationCommandConfig, @@ -81,6 +88,7 @@ class PartialGuild extends WritableSnowflakeEntity { ); /// Create a new [PartialGuild]. + /// @nodoc PartialGuild({required super.id, required this.manager}); @override @@ -103,17 +111,21 @@ class PartialGuild extends WritableSnowflakeEntity { Future listActiveThreads() => manager.listActiveThreads(id); /// List the bans in this guild. - Future> listBans() => manager.listBans(id); + Future> listBans({int? limit, Snowflake? after, Snowflake? before}) => manager.listBans(id, limit: limit, after: after, before: before); - /// Ban a member in this guild. + /// Ban a user in this guild. Future createBan(Snowflake userId, {Duration? deleteMessages, String? auditLogReason}) => manager.createBan(id, userId, auditLogReason: auditLogReason, deleteMessages: deleteMessages); - /// Unban a member in this guild. + /// Ban up to 200 users from a guild, and optionally delete previous messages sent by the banned users. + Future bulkBan(List userIds, {Duration? deleteMessages, String? auditLogReason}) => + manager.bulkBan(id, userIds, deleteMessages: deleteMessages, auditLogReason: auditLogReason); + + /// Unban a user in this guild. Future deleteBan(Snowflake userId, {String? auditLogReason}) => manager.deleteBan(id, userId, auditLogReason: auditLogReason); - /// Fetch the MFA level for this guild. - Future updateMfaLevel(MfaLevel level, {String? auditLogReason}) => manager.updateMfaLevel(id, level, auditLogReason: auditLogReason); + /// Update a guild's MFA level. + Future updateMfaLevel(MfaLevel level, {String? auditLogReason}) => manager.updateMfaLevel(id, level, auditLogReason: auditLogReason); /// Fetch the member prune count for the given [days] and [roleIds]. Future fetchPruneCount({int? days, List? roleIds}) => manager.fetchPruneCount(id, days: days, roleIds: roleIds); @@ -157,6 +169,10 @@ class PartialGuild extends WritableSnowflakeEntity { /// Fetch the onboarding information for this guild. Future fetchOnboarding() => manager.fetchOnboarding(id); + /// Update this guild's onboarding. + Future updateOnboarding(OnboardingUpdateBuilder builder, {String? auditLogReason}) => + manager.updateOnboarding(id, builder, auditLogReason: auditLogReason); + /// Update the current user's voice state in this guild. Future updateCurrentUserVoiceState(CurrentUserVoiceStateUpdateBuilder builder) => manager.updateCurrentUserVoiceState(id, builder); @@ -191,39 +207,74 @@ class PartialGuild extends WritableSnowflakeEntity { Future fetchVanityCode() => manager.fetchVanityCode(id); } -/// {@template guild} -/// A collection of channels & users. -/// -/// Guilds are often referred to as servers. -/// {@endtemplate} -class Guild extends PartialGuild { +/// {@macro guild} +class UserGuild extends PartialGuild { /// This guild's name. final String name; /// The hash of this guild's icon. final String? iconHash; + /// Whether this guild is owned by the current user. + final bool? isOwnedByCurrentUser; + + /// The current user's permissions. + final Permissions? currentUserPermissions; + + /// A set of features enabled in this guild. + final GuildFeatures features; + + /// An approximate number of members in this guild. + /// + /// {@template fetch_with_counts_only} + /// This is only returned when fetching this guild with `withCounts` set to `true`. + /// {@endtemplate} + final int? approximateMemberCount; + + /// An approximate number of presences in this guild. + /// + /// {@macro fetch_with_counts_only} + final int? approximatePresenceCount; + + /// {@macro guild} + /// @nodoc + UserGuild({ + required super.id, + required super.manager, + required this.name, + required this.iconHash, + required this.isOwnedByCurrentUser, + required this.currentUserPermissions, + required this.features, + required this.approximateMemberCount, + required this.approximatePresenceCount, + }); + + /// This guild's icon. + CdnAsset? get icon => iconHash == null + ? null + : CdnAsset( + client: manager.client, + base: HttpRoute()..icons(id: id.toString()), + hash: iconHash!, + ); +} + +/// {@template guild} +/// A collection of channels & users. +/// +/// Guilds are often referred to as servers. +/// {@endtemplate} +class Guild extends UserGuild { /// The hash of this guild's splash image. final String? splashHash; /// The hash of this guild's discovery splash image. final String? discoverySplashHash; - /// Whether this guild is owned by the current user. - /// - /// {@template get_current_user_guilds_only} - /// This field is only present when fetching the current user's guilds. - /// {@endtemplate} - final bool? isOwnedByCurrentUser; - /// The ID of this guild's owner. final Snowflake ownerId; - /// The current user's permissions. - /// - /// {@macro get_current_user_guilds_only} - final Permissions? currentUserPermissions; - /// The ID of this guild's AFK channel. final Snowflake? afkChannelId; @@ -253,9 +304,6 @@ class Guild extends PartialGuild { // Renamed to avoid conflict with the emojis manager. final List emojiList; - /// A set of features enabled in this guild. - final GuildFeatures features; - /// This guild's MFA level. final MfaLevel mfaLevel; @@ -304,18 +352,6 @@ class Guild extends PartialGuild { /// The maximum number of users in a stage video channel. final int? maxStageChannelUsers; - /// An approximate number of members in this guild. - /// - /// {@template fetch_with_counts_only} - /// This is only returned when fetching this guild with `withCounts` set to `true`. - /// {@endtemplate} - final int? approximateMemberCount; - - /// An approximate number of presences in this guild. - /// - /// {@macro fetch_with_counts_only} - final int? approximatePresenceCount; - /// This guild's welcome screen. final WelcomeScreen? welcomeScreen; @@ -333,16 +369,17 @@ class Guild extends PartialGuild { final Snowflake? safetyAlertsChannelId; /// {@macro guild} + /// @nodoc Guild({ required super.id, required super.manager, - required this.name, - required this.iconHash, + required super.name, + required super.iconHash, required this.splashHash, required this.discoverySplashHash, - required this.isOwnedByCurrentUser, + required super.isOwnedByCurrentUser, required this.ownerId, - required this.currentUserPermissions, + required super.currentUserPermissions, required this.afkChannelId, required this.afkTimeout, required this.isWidgetEnabled, @@ -351,7 +388,7 @@ class Guild extends PartialGuild { required this.defaultMessageNotificationLevel, required this.explicitContentFilterLevel, required this.roleList, - required this.features, + required super.features, required this.mfaLevel, required this.applicationId, required this.systemChannelId, @@ -368,8 +405,8 @@ class Guild extends PartialGuild { required this.publicUpdatesChannelId, required this.maxVideoChannelUsers, required this.maxStageChannelUsers, - required this.approximateMemberCount, - required this.approximatePresenceCount, + required super.approximateMemberCount, + required super.approximatePresenceCount, required this.welcomeScreen, required this.nsfwLevel, required this.hasPremiumProgressBarEnabled, @@ -406,15 +443,6 @@ class Guild extends PartialGuild { /// The channel safety alerts are sent to. PartialTextChannel? get safetyAlertsChannel => safetyAlertsChannelId == null ? null : manager.client.channels[safetyAlertsChannelId!] as PartialTextChannel; - /// This guild's icon. - CdnAsset? get icon => iconHash == null - ? null - : CdnAsset( - client: manager.client, - base: HttpRoute()..icons(id: id.toString()), - hash: iconHash!, - ); - /// This guild's splash image. CdnAsset? get splash => splashHash == null ? null @@ -444,73 +472,43 @@ class Guild extends PartialGuild { } /// The verification level for a guild. -enum VerificationLevel { - none._(0), - low._(1), - medium._(2), - high._(3), - veryHigh._(4); - - /// The value of this verification level. - final int value; - - const VerificationLevel._(this.value); - - /// Parses a [VerificationLevel] from an [int]. - /// - /// The [value] must be a valid verification level. - factory VerificationLevel.parse(int value) => VerificationLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid verification level', value), - ); - - @override - String toString() => 'VerificationLevel($value)'; +final class VerificationLevel extends EnumLike { + static const none = VerificationLevel(0); + static const low = VerificationLevel(1); + static const medium = VerificationLevel(2); + static const high = VerificationLevel(3); + static const veryHigh = VerificationLevel(4); + + /// @nodoc + const VerificationLevel(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + VerificationLevel.parse(int value) : this(value); } /// The level at which message notifications are sent in a guild. -enum MessageNotificationLevel { - allMessages._(0), - onlyMentions._(1); - - /// The value of this message notification level. - final int value; +final class MessageNotificationLevel extends EnumLike { + static const allMessages = MessageNotificationLevel(0); + static const onlyMentions = MessageNotificationLevel(1); - const MessageNotificationLevel._(this.value); + /// @nodoc + const MessageNotificationLevel(super.value); - /// Parses a [MessageNotificationLevel] from an [int]. - /// - /// The [value] must be a valid message notification level. - factory MessageNotificationLevel.parse(int value) => MessageNotificationLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid message notification level', value), - ); - - @override - String toString() => 'MessageNotificationLevel($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + MessageNotificationLevel.parse(int value) : this(value); } /// The level of explicit content filtering in a guild. -enum ExplicitContentFilterLevel { - disabled._(0), - membersWithoutRoles._(1), - allMembers._(2); +final class ExplicitContentFilterLevel extends EnumLike { + static const disabled = ExplicitContentFilterLevel(0); + static const membersWithoutRoles = ExplicitContentFilterLevel(1); + static const allMembers = ExplicitContentFilterLevel(2); - /// The value of this explicit content filter level. - final int value; - - const ExplicitContentFilterLevel._(this.value); - - /// Parses an [ExplicitContentFilterLevel] from an [int]. - /// - /// The [value] must be a valid explicit content filter level. - factory ExplicitContentFilterLevel.parse(int value) => ExplicitContentFilterLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid explicit content filter level', value), - ); + /// @nodoc + const ExplicitContentFilterLevel(super.value); - @override - String toString() => 'ExplicitContentFilterLevel($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ExplicitContentFilterLevel.parse(int value) : this(value); } /// Features that can be enabled in certain guilds. @@ -683,25 +681,15 @@ class GuildFeatures extends Flags { } /// The MFA level required for moderators of a guild. -enum MfaLevel { - none._(0), - elevated._(1); - - /// The value of this MFA level. - final int value; +final class MfaLevel extends EnumLike { + static const none = MfaLevel(0); + static const elevated = MfaLevel(1); - const MfaLevel._(this.value); - - /// Parses an [MfaLevel] from an [int]. - /// - /// The [value] must be a valid mfa level. - factory MfaLevel.parse(int value) => MfaLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid mfa level', value), - ); + /// @nodoc + const MfaLevel(super.value); - @override - String toString() => 'MfaLevel($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + MfaLevel.parse(int value) : this(value); } /// The configuration of a guild's system channel. @@ -747,49 +735,29 @@ class SystemChannelFlags extends Flags { } /// The premium tier of a guild. -enum PremiumTier { - none._(0), - one._(1), - two._(2), - three._(3); - - /// The value of this tier. - final int value; - - const PremiumTier._(this.value); +final class PremiumTier extends EnumLike { + static const none = PremiumTier(0); + static const one = PremiumTier(1); + static const two = PremiumTier(2); + static const three = PremiumTier(3); - /// Parses a [PremiumTier] from an [int]. - /// - /// The [value] must be a valid premium tier. - factory PremiumTier.parse(int value) => PremiumTier.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid premium tier', value), - ); + /// nodoc + const PremiumTier(super.value); - @override - String toString() => 'PremiumTier($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + PremiumTier.parse(int value) : this(value); } /// The NSFW level of a guild. -enum NsfwLevel { - unset._(0), - explicit._(1), - safe._(2), - ageRestricted._(3); - - /// The value of this NSFW level. - final int value; +final class NsfwLevel extends EnumLike { + static const unset = NsfwLevel(0); + static const explicit = NsfwLevel(1); + static const safe = NsfwLevel(2); + static const ageRestricted = NsfwLevel(3); - const NsfwLevel._(this.value); + /// nodoc + const NsfwLevel(super.value); - /// Parses an [NsfwLevel] from an [int]. - /// - /// The [value] must be a valid nsfw level. - factory NsfwLevel.parse(int value) => NsfwLevel.values.firstWhere( - (level) => level.value == value, - orElse: () => throw FormatException('Invalid nsfw level', value), - ); - - @override - String toString() => 'NsfwLevel($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + NsfwLevel.parse(int value) : this(value); } diff --git a/lib/src/models/guild/guild_preview.dart b/lib/src/models/guild/guild_preview.dart index 08af83ed9..97148e218 100644 --- a/lib/src/models/guild/guild_preview.dart +++ b/lib/src/models/guild/guild_preview.dart @@ -37,6 +37,7 @@ class GuildPreview extends PartialGuild { final List stickerList; /// {@macro guild_preview} + /// @nodoc GuildPreview({ required super.id, required super.manager, diff --git a/lib/src/models/guild/guild_widget.dart b/lib/src/models/guild/guild_widget.dart index f183dd690..590d298ee 100644 --- a/lib/src/models/guild/guild_widget.dart +++ b/lib/src/models/guild/guild_widget.dart @@ -31,6 +31,7 @@ class GuildWidget with ToStringHelper { final int presenceCount; /// {@macro guild_widget} + /// @nodoc GuildWidget({ required this.manager, required this.guildId, @@ -59,6 +60,7 @@ class WidgetSettings with ToStringHelper { final Snowflake? channelId; /// {@macro widget_settings} + /// @nodoc WidgetSettings({ required this.manager, required this.isEnabled, diff --git a/lib/src/models/guild/integration.dart b/lib/src/models/guild/integration.dart index ca514ba38..29dfc504f 100644 --- a/lib/src/models/guild/integration.dart +++ b/lib/src/models/guild/integration.dart @@ -3,6 +3,7 @@ import 'package:nyxx/src/models/role.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A partial [Integration]. @@ -11,6 +12,7 @@ class PartialIntegration extends ManagedSnowflakeEntity { final IntegrationManager manager; /// Create a new [PartialIntegration]. + /// @nodoc PartialIntegration({required super.id, required this.manager}); /// Delete this integration. @@ -67,6 +69,7 @@ class Integration extends PartialIntegration { final List? scopes; /// {@macro integration} + /// @nodoc Integration({ required super.id, required super.manager, @@ -92,25 +95,15 @@ class Integration extends PartialIntegration { } /// The behavior of an integration when a member's subscription expires. -enum IntegrationExpireBehavior { - removeRole._(0), - kick._(1); +final class IntegrationExpireBehavior extends EnumLike { + static const removeRole = IntegrationExpireBehavior(0); + static const kick = IntegrationExpireBehavior(1); - /// TThe value of this [IntegrationExpireBehavior]. - final int value; + /// @nodoc + const IntegrationExpireBehavior(super.value); - const IntegrationExpireBehavior._(this.value); - - /// Parse an [IntegrationExpireBehavior] from an [int]. - /// - /// The [value] must be a valid integration expire behavior. - factory IntegrationExpireBehavior.parse(int value) => IntegrationExpireBehavior.values.firstWhere( - (behavior) => behavior.value == value, - orElse: () => throw FormatException('Unknown integration expire behavior', value), - ); - - @override - String toString() => 'IntegrationExpireBehavior($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + IntegrationExpireBehavior.parse(int value) : this(value); } /// {@template integration_account} @@ -124,6 +117,7 @@ class IntegrationAccount with ToStringHelper { final String name; /// {@macro integration_account} + /// @nodoc IntegrationAccount({required this.id, required this.name}); } @@ -147,6 +141,7 @@ class IntegrationApplication with ToStringHelper { final User? bot; /// {@macro integration_application} + /// @nodoc IntegrationApplication({ required this.id, required this.name, diff --git a/lib/src/models/guild/member.dart b/lib/src/models/guild/member.dart index 25bf2e346..9c709d799 100644 --- a/lib/src/models/guild/member.dart +++ b/lib/src/models/guild/member.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/builders/guild/member.dart'; import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/member_manager.dart'; import 'package:nyxx/src/http/route.dart'; @@ -14,6 +15,7 @@ class PartialMember extends WritableSnowflakeEntity { final MemberManager manager; /// Create a new [PartialMember]. + /// @nodoc PartialMember({required super.id, required this.manager}); /// Add a role to this member. @@ -23,10 +25,27 @@ class PartialMember extends WritableSnowflakeEntity { Future removeRole(Snowflake roleId, {String? auditLogReason}) => manager.removeRole(id, roleId); /// Ban this member. - Future ban({String? auditLogReason}) => manager.client.guilds[manager.guildId].createBan(id, auditLogReason: auditLogReason); + Future ban({Duration? deleteMessages, String? auditLogReason}) => + manager.client.guilds[manager.guildId].createBan(id, auditLogReason: auditLogReason, deleteMessages: deleteMessages); /// Unban this member. Future unban({String? auditLogReason}) => manager.client.guilds[manager.guildId].deleteBan(id, auditLogReason: auditLogReason); + + /// Update this member, returning the updated member. + /// + /// External references: + /// * [MemberManager.update] + /// * Discord API Reference: https://discord.com/developers/docs/resources/guild#modify-guild-member + @override + Future update(MemberUpdateBuilder builder, {String? auditLogReason}) => manager.update(id, builder, auditLogReason: auditLogReason); + + /// Kick this member. + /// + /// External references: + /// * [MemberManager.delete] + /// * Discord API Reference: https://discord.com/developers/docs/resources/guild#remove-guild-member + @override + Future delete({String? auditLogReason}) => manager.delete(id, auditLogReason: auditLogReason); } /// {@template member} @@ -70,6 +89,7 @@ class Member extends PartialMember { final DateTime? communicationDisabledUntil; /// {@macro member} + /// @nodoc Member({ required super.id, required super.manager, diff --git a/lib/src/models/guild/onboarding.dart b/lib/src/models/guild/onboarding.dart index ec75336f3..45021f0b1 100644 --- a/lib/src/models/guild/onboarding.dart +++ b/lib/src/models/guild/onboarding.dart @@ -3,13 +3,14 @@ import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template onboarding} /// The configuration for a [Guild]'s onboarding process. /// {@endtemplate} class Onboarding with ToStringHelper { - /// The manager for this [onboarding]. + /// The manager for this [Onboarding]. final GuildManager manager; /// The ID of the guild this onboarding is for. @@ -24,13 +25,18 @@ class Onboarding with ToStringHelper { /// Whether onboarding is enabled for this guild. final bool isEnabled; + /// The current onboarding mode. + final OnboardingMode mode; + /// {@macro onboarding} + /// @nodoc Onboarding({ required this.manager, required this.guildId, required this.prompts, required this.defaultChannelIds, required this.isEnabled, + required this.mode, }); /// The guild this onboarding is for. @@ -68,6 +74,7 @@ class OnboardingPrompt with ToStringHelper { final bool isInOnboarding; /// {@macro onboarding_prompt} + /// @nodoc OnboardingPrompt({ required this.id, required this.type, @@ -80,25 +87,15 @@ class OnboardingPrompt with ToStringHelper { } /// The type of an [Onboarding] prompt. -enum OnboardingPromptType { - multipleChoice._(0), - dropdown._(1); - - /// The value of this [OnboardingPromptType]. - final int value; +final class OnboardingPromptType extends EnumLike { + static const multipleChoice = OnboardingPromptType(0); + static const dropdown = OnboardingPromptType(1); - const OnboardingPromptType._(this.value); + /// @nodoc + const OnboardingPromptType(super.value); - /// Parse an [OnboardingPromptType] from an [int]. - /// - /// The [value] must be a valid onboarding prompt type. - factory OnboardingPromptType.parse(int value) => OnboardingPromptType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown onboarding prompt type', value), - ); - - @override - String toString() => 'OnboardingPromptType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + OnboardingPromptType.parse(int value) : this(value); } /// {@template onboarding_prompt_option} @@ -118,6 +115,9 @@ class OnboardingPromptOption with ToStringHelper { final List roleIds; /// The emoji associated with this onboarding prompt. + // The `emoji_id`, `emoji_name`, and `emoji_animated` fields never seem to be + // included in this structure when it is returned from the API. Since the + // `emoji` object contains this information anyway, we don't include them. final Emoji? emoji; /// The title of this option. @@ -127,6 +127,7 @@ class OnboardingPromptOption with ToStringHelper { final String? description; /// {@macro onboarding_prompt_option} + /// @nodoc OnboardingPromptOption({ required this.manager, required this.id, @@ -140,3 +141,19 @@ class OnboardingPromptOption with ToStringHelper { /// The channels the user is granted access to. List get channels => channelIds.map((e) => manager.client.channels[e]).toList(); } + +/// The mode under which onboarding constraints operate when creating an +/// [Onboarding]. +/// +/// These constraints are that there must be at least 7 Default Channels and +/// at least 5 of them must allow sending messages to the @everyone role. +final class OnboardingMode extends EnumLike { + /// Only default channels count towards the constraints. + static const defaultMode = OnboardingMode(0); + + /// Both default channels and questions count towards the constraints, + static const advanced = OnboardingMode(1); + + /// @nodoc + const OnboardingMode(super.value); +} diff --git a/lib/src/models/guild/scheduled_event.dart b/lib/src/models/guild/scheduled_event.dart index 0fd73a2c3..b93e7d3ff 100644 --- a/lib/src/models/guild/scheduled_event.dart +++ b/lib/src/models/guild/scheduled_event.dart @@ -8,6 +8,7 @@ import 'package:nyxx/src/models/guild/member.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A partial [ScheduledEvent]. @@ -16,6 +17,7 @@ class PartialScheduledEvent extends WritableSnowflakeEntity { final ScheduledEventManager manager; /// Create a new [PartialScheduledEvent]. + /// @nodoc PartialScheduledEvent({required super.id, required this.manager}); /// List the users that have followed this event. @@ -74,7 +76,11 @@ class ScheduledEvent extends PartialScheduledEvent { /// The hash of this event's cover image. final String? coverImageHash; + /// The rule defining how often this event should recur. + final RecurrenceRule? recurrenceRule; + /// {@macro scheduled_event} + /// @nodoc ScheduledEvent({ required super.id, required super.manager, @@ -93,6 +99,7 @@ class ScheduledEvent extends PartialScheduledEvent { required this.creator, required this.userCount, required this.coverImageHash, + required this.recurrenceRule, }); /// The guild this event is in. @@ -115,50 +122,30 @@ class ScheduledEvent extends PartialScheduledEvent { } /// The status of a [ScheduledEvent]. -enum EventStatus { - scheduled._(1), - active._(2), - completed._(3), - cancelled._(4); - - /// TThe value of this [EventStatus]. - final int value; +final class EventStatus extends EnumLike { + static const scheduled = EventStatus(1); + static const active = EventStatus(2); + static const completed = EventStatus(3); + static const cancelled = EventStatus(4); - const EventStatus._(this.value); - - /// Parse an [EventStatus] from an [int]. - /// - /// The [value] must be a valid event status. - factory EventStatus.parse(int value) => EventStatus.values.firstWhere( - (status) => status.value == value, - orElse: () => throw FormatException('Unknown event status', value), - ); + /// @nodoc + const EventStatus(super.value); - @override - String toString() => 'EventStatus($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + EventStatus.parse(int value) : this(value); } /// The type of the entity associated with a [ScheduledEvent]. -enum ScheduledEntityType { - stageInstance._(1), - voice._(2), - external._(3); - - /// The value of this [ScheduledEntityType]. - final int value; - - const ScheduledEntityType._(this.value); +final class ScheduledEntityType extends EnumLike { + static const stageInstance = ScheduledEntityType(1); + static const voice = ScheduledEntityType(2); + static const external = ScheduledEntityType(3); - /// Parse a [ScheduledEntityType] from an [int]. - /// - /// The [value] must be a valid scheduled entity type. - factory ScheduledEntityType.parse(int value) => ScheduledEntityType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown scheduled entity type', value), - ); + /// @nodoc + const ScheduledEntityType(super.value); - @override - String toString() => 'ScheduledEntityType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ScheduledEntityType.parse(int value) : this(value); } /// {@template entity_metadata} @@ -169,6 +156,7 @@ class EntityMetadata with ToStringHelper { final String? location; /// {@macro entity_metadata} + /// @nodoc EntityMetadata({required this.location}); } @@ -188,6 +176,7 @@ class ScheduledEventUser with ToStringHelper { final Member? member; /// {@macro scheduled_event_user} + /// @nodoc ScheduledEventUser({ required this.manager, required this.scheduledEventId, @@ -198,3 +187,118 @@ class ScheduledEventUser with ToStringHelper { /// The event the user followed. PartialScheduledEvent get scheduledEvent => manager[scheduledEventId]; } + +/// Indicates how often a [ScheduledEvent] should recur. +/// +/// See also: +/// * https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-recurrence-rule-object +class RecurrenceRule with ToStringHelper { + /// The start of the interval within which the event recurs. + final DateTime start; + + /// The end of the interval within which the event recurs. + final DateTime? end; + + /// The frequency this rule applies to. + final RecurrenceRuleFrequency frequency; + + /// The spacing between each recurrence of the event, in multiples of [frequency]. + final int interval; + + /// The specific days within a week the event recurs on. + final List? byWeekday; + + /// The specific days within a specific week the event recurs on. + final List? byNWeekday; + + /// The specific months the event recurs on. + final List? byMonth; + + /// The specific days within a month the event recurs on. + final List? byMonthDay; + + /// The specific days within a year the event recurs on. + final List? byYearDay; + + /// The total number of times the event is allowed to recur before stopping. + final int? count; + + /// @nodoc + RecurrenceRule({ + required this.start, + required this.end, + required this.frequency, + required this.interval, + required this.byWeekday, + required this.byNWeekday, + required this.byMonth, + required this.byMonthDay, + required this.byYearDay, + required this.count, + }); +} + +/// The frequency with which a [ScheduledEvent] can recur. +final class RecurrenceRuleFrequency extends EnumLike { + /// The event recurs at an interval in years. + static const yearly = RecurrenceRuleFrequency(0); + + /// The event recurs at an interval in months. + static const monthly = RecurrenceRuleFrequency(1); + + /// The event recurs at an interval in weeks. + static const weekly = RecurrenceRuleFrequency(2); + + /// The event recurs at an interval in days. + static const daily = RecurrenceRuleFrequency(3); + + /// @nodoc + const RecurrenceRuleFrequency(super.value); +} + +/// The weekday on which a [ScheduledEvent] recurs. +final class RecurrenceRuleWeekday extends EnumLike { + static const monday = RecurrenceRuleWeekday(0); + static const tuesday = RecurrenceRuleWeekday(1); + static const wednesday = RecurrenceRuleWeekday(2); + static const thursday = RecurrenceRuleWeekday(3); + static const friday = RecurrenceRuleWeekday(4); + static const saturday = RecurrenceRuleWeekday(5); + static const sunday = RecurrenceRuleWeekday(6); + + /// @nodoc + const RecurrenceRuleWeekday(super.value); +} + +/// The week and weekday on which a [ScheduledEvent] recurs. +class RecurrenceRuleNWeekday with ToStringHelper { + /// The index of the week in which the event recurs. + /// + /// This will always be at least 1 and at most 5. + final int n; + + /// The day in the week on which the event recurs. + final RecurrenceRuleWeekday day; + + /// @nodoc + RecurrenceRuleNWeekday({required this.n, required this.day}); +} + +/// The month on which a [ScheduledEvent] recurs. +final class RecurrenceRuleMonth extends EnumLike { + static const january = RecurrenceRuleMonth(0); + static const february = RecurrenceRuleMonth(1); + static const march = RecurrenceRuleMonth(2); + static const april = RecurrenceRuleMonth(3); + static const may = RecurrenceRuleMonth(4); + static const june = RecurrenceRuleMonth(5); + static const july = RecurrenceRuleMonth(6); + static const august = RecurrenceRuleMonth(7); + static const september = RecurrenceRuleMonth(8); + static const october = RecurrenceRuleMonth(9); + static const november = RecurrenceRuleMonth(10); + static const december = RecurrenceRuleMonth(11); + + /// @nodoc + const RecurrenceRuleMonth(super.value); +} diff --git a/lib/src/models/guild/template.dart b/lib/src/models/guild/template.dart index 07a71cedd..39ea641e1 100644 --- a/lib/src/models/guild/template.dart +++ b/lib/src/models/guild/template.dart @@ -47,6 +47,7 @@ class GuildTemplate with ToStringHelper { final bool? isDirty; /// {@macro guild_template} + /// @nodoc GuildTemplate({ required this.code, required this.manager, diff --git a/lib/src/models/guild/welcome_screen.dart b/lib/src/models/guild/welcome_screen.dart index 360e6c32f..0f5949201 100644 --- a/lib/src/models/guild/welcome_screen.dart +++ b/lib/src/models/guild/welcome_screen.dart @@ -14,6 +14,7 @@ class WelcomeScreen with ToStringHelper { final List channels; /// {@macro welcome_screen} + /// @nodoc WelcomeScreen({required this.description, required this.channels}); } @@ -37,6 +38,7 @@ class WelcomeScreenChannel with ToStringHelper { final String? emojiName; /// {@macro welcome_screen_channel} + /// @nodoc WelcomeScreenChannel({ required this.manager, required this.channelId, diff --git a/lib/src/models/interaction.dart b/lib/src/models/interaction.dart index ddafecd95..c52246087 100644 --- a/lib/src/models/interaction.dart +++ b/lib/src/models/interaction.dart @@ -4,6 +4,7 @@ import 'package:nyxx/src/builders/interaction_response.dart'; import 'package:nyxx/src/builders/message/message.dart'; import 'package:nyxx/src/errors.dart'; import 'package:nyxx/src/http/managers/interaction_manager.dart'; +import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/commands/application_command.dart'; import 'package:nyxx/src/models/commands/application_command_option.dart'; @@ -18,8 +19,30 @@ import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/role.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; +/// A context indicating whether command can be used in DMs, groups, or guilds. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types +final class InteractionContextType extends EnumLike { + /// Interaction can be used within servers. + static const InteractionContextType guild = InteractionContextType(0); + + /// Interaction can be used within DMs with the app's bot user. + static const InteractionContextType botDm = InteractionContextType(1); + + /// Interaction can be used within Group DMs and DMs other than the app's bot user. + static const InteractionContextType privateChannel = InteractionContextType(2); + + /// @nodoc + const InteractionContextType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + InteractionContextType.parse(int value) : this(value); +} + /// {@template interaction} /// An interaction sent by Discord when a user interacts with an [ApplicationCommand], a [MessageComponent] /// or a [ModalBuilder]. @@ -65,7 +88,7 @@ abstract class Interaction with ToStringHelper { final Message? message; /// The permissions of the application that triggered this interaction. - final Permissions? appPermissions; + final Permissions appPermissions; /// The preferred locale of the user that triggered this interaction. final Locale? locale; @@ -76,7 +99,14 @@ abstract class Interaction with ToStringHelper { /// The entitlements for the user and guild of this interaction. final List entitlements; + /// Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. + final Map? authorizingIntegrationOwners; + + /// Context where the interaction was triggered from. + final InteractionContextType? context; + /// {@macro interaction} + /// @nodoc Interaction({ required this.manager, required this.id, @@ -95,6 +125,8 @@ abstract class Interaction with ToStringHelper { required this.locale, required this.guildLocale, required this.entitlements, + required this.authorizingIntegrationOwners, + required this.context, }); /// The guild in which this interaction was triggered. @@ -133,6 +165,7 @@ mixin MessageResponse on Interaction { await manager.createResponse(id, token, InteractionResponseBuilder.channelMessage(builder, isEphemeral: isEphemeral)); } else { assert(isEphemeral == _wasEphemeral || isEphemeral == null, 'Cannot change the value of isEphemeral between acknowledge and respond'); + _didRespond = true; await manager.createFollowup(token, builder); } @@ -180,6 +213,7 @@ mixin ModalResponse on Interaction { /// {@endtemplate} class PingInteraction extends Interaction { /// {@macro ping_interaction} + /// @nodoc PingInteraction({ required super.manager, required super.id, @@ -197,6 +231,8 @@ class PingInteraction extends Interaction { required super.locale, required super.guildLocale, required super.entitlements, + required super.authorizingIntegrationOwners, + required super.context, }) : super(data: null); /// Send a pong response to this interaction. @@ -209,6 +245,7 @@ class PingInteraction extends Interaction { class ApplicationCommandInteraction extends Interaction with MessageResponse, ModalResponse { /// {@macro application_command_interaction} + /// @nodoc ApplicationCommandInteraction({ required super.manager, required super.id, @@ -227,6 +264,8 @@ class ApplicationCommandInteraction extends Interaction with MessageResponse, ModalResponse { /// {@macro message_component_interaction} + /// @nodoc MessageComponentInteraction({ required super.manager, required super.id, @@ -254,18 +294,20 @@ class MessageComponentInteraction extends Interaction acknowledge({bool? updateMessage, bool? isEphemeral}) async { + assert(updateMessage != true || isEphemeral != true, 'Cannot set isEphemeral to true if updateMessage is set to true'); + if (_didAcknowledge) { throw AlreadyAcknowledgedError(this); } - assert(updateMessage != true || isEphemeral != true, 'Cannot set isEphemeral to true if updateMessage is set to true'); - _didAcknowledge = true; _didUpdateMessage = updateMessage; _wasEphemeral = isEphemeral; @@ -279,14 +321,15 @@ class MessageComponentInteraction extends Interaction respond(Builder builder, {bool? updateMessage, bool? isEphemeral}) async { - assert(updateMessage == null || type == InteractionType.messageComponent, 'Cannot set updateMessage for non-component interactions'); assert(updateMessage != true || isEphemeral != true, 'Cannot set isEphemeral to true if updateMessage is set to true'); - assert(builder is MessageUpdateBuilder == updateMessage, 'builder must be a MessageUpdateBuilder if updateMessage is true'); - assert(builder is MessageBuilder != updateMessage, 'builder must be a MessageBuilder if updateMessage is null or false'); + assert(updateMessage != true || builder is MessageUpdateBuilder, 'builder must be a MessageUpdateBuilder if updateMessage is true'); + assert(updateMessage == true || builder is MessageBuilder, 'builder must be a MessageBuilder if updateMessage is null or false'); - if (!_didAcknowledge) { - assert(updateMessage != true || isEphemeral != true, 'Cannot set isEphemeral to true if updateMessage is set to true'); + if (_didRespond) { + throw AlreadyRespondedError(this); + } + if (!_didAcknowledge) { _didAcknowledge = true; _didRespond = true; _didUpdateMessage = updateMessage; @@ -295,19 +338,15 @@ class MessageComponentInteraction extends Interaction with MessageResponse { /// {@macro modal_submit_interaction} + /// @nodoc ModalSubmitInteraction({ required super.manager, required super.id, @@ -339,6 +379,8 @@ class ModalSubmitInteraction extends Interaction wit required super.locale, required super.guildLocale, required super.entitlements, + required super.authorizingIntegrationOwners, + required super.context, }); } @@ -347,6 +389,7 @@ class ModalSubmitInteraction extends Interaction wit /// {@endtemplate} class ApplicationCommandAutocompleteInteraction extends Interaction { /// {@macro application_command_autocomplete_interaction} + /// @nodoc ApplicationCommandAutocompleteInteraction({ required super.manager, required super.id, @@ -365,6 +408,8 @@ class ApplicationCommandAutocompleteInteraction extends Interaction InteractionType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown interaction type', value), - ); - - @override - String toString() => 'InteractionType($value)'; +final class InteractionType extends EnumLike { + static const ping = InteractionType(1); + static const applicationCommand = InteractionType(2); + static const messageComponent = InteractionType(3); + static const applicationCommandAutocomplete = InteractionType(4); + static const modalSubmit = InteractionType(5); + + /// @nodoc + const InteractionType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + InteractionType.parse(int value) : this(value); } /// {@template application_command_interaction_data} @@ -416,13 +451,14 @@ class ApplicationCommandInteractionData with ToStringHelper { /// A list of provided options. final List? options; - /// The ID of the guild the command was invoked in. + /// The ID of the guild the command was registered in, or `null` if it was a global command. final Snowflake? guildId; /// The ID of the entity the command was invoked on. final Snowflake? targetId; /// {@macro application_command_interaction_data} + /// @nodoc ApplicationCommandInteractionData({ required this.id, required this.name, @@ -457,6 +493,7 @@ class ResolvedData with ToStringHelper { final Map? attachments; /// {@macro resolved_data} + /// @nodoc ResolvedData({ required this.users, required this.members, @@ -487,6 +524,7 @@ class InteractionOption with ToStringHelper { final bool? isFocused; /// {@macro interaction_option} + /// @nodoc InteractionOption({ required this.name, required this.type, @@ -513,6 +551,7 @@ class MessageComponentInteractionData with ToStringHelper { final ResolvedData? resolved; /// {@macro message_component_interaction_data} + /// @nodoc MessageComponentInteractionData({required this.customId, required this.type, required this.values, required this.resolved}); } @@ -527,5 +566,6 @@ class ModalSubmitInteractionData with ToStringHelper { final List components; /// {@macro modal_submit_interaction_data} + /// @nodoc ModalSubmitInteractionData({required this.customId, required this.components}); } diff --git a/lib/src/models/invite/invite.dart b/lib/src/models/invite/invite.dart index 7de37fb9f..747aaedaa 100644 --- a/lib/src/models/invite/invite.dart +++ b/lib/src/models/invite/invite.dart @@ -3,6 +3,7 @@ import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/guild/scheduled_event.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template invite} @@ -10,6 +11,9 @@ import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// If the invite is to a [Channel], this will be a [GroupDmChannel]. /// {@endtemplate} class Invite with ToStringHelper { + /// The type of this invite. + final InviteType type; + /// The invite's code. This is a unique identifier. final String code; @@ -52,7 +56,9 @@ class Invite with ToStringHelper { final ScheduledEvent? guildScheduledEvent; /// {@macro invite} + /// @nodoc Invite({ + required this.type, required this.code, required this.guild, required this.channel, @@ -68,26 +74,23 @@ class Invite with ToStringHelper { } /// The type of an [Invite]'s target. -enum TargetType { - /// The invite is targeting a stream. - stream._(1), - - /// The invite is targeting an embedded application. - embeddedApplication._(2); +final class TargetType extends EnumLike { + static const stream = TargetType(1); + static const embeddedApplication = TargetType(2); - /// The value of this [TargetType]. - final int value; + /// @nodoc + const TargetType(super.value); - /// Parse a [TargetType] from an [int]. - /// - /// The [value] must be a valid target type. - factory TargetType.parse(int value) => TargetType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown TargetType', value), - ); + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + TargetType.parse(int value) : this(value); +} - const TargetType._(this.value); +/// The type of an [Invite]. +final class InviteType extends EnumLike { + static const guild = InviteType(0); + static const groupDm = InviteType(1); + static const friend = InviteType(3); - @override - String toString() => 'TargetType($value)'; + /// @nodoc + const InviteType(super.value); } diff --git a/lib/src/models/invite/invite_metadata.dart b/lib/src/models/invite/invite_metadata.dart index 540eab1b3..6d127be79 100644 --- a/lib/src/models/invite/invite_metadata.dart +++ b/lib/src/models/invite/invite_metadata.dart @@ -16,7 +16,9 @@ class InviteWithMetadata extends Invite { /// When this invite was created. final DateTime createdAt; + /// @nodoc InviteWithMetadata({ + required super.type, required super.code, required super.guild, required super.channel, diff --git a/lib/src/models/locale.dart b/lib/src/models/locale.dart index 32f4dd03a..fde9b2623 100644 --- a/lib/src/models/locale.dart +++ b/lib/src/models/locale.dart @@ -9,6 +9,7 @@ enum Locale { enGb._('en-GB', 'English, UK', 'English, UK'), enUs._('en-US', 'English, US', 'English, US'), esEs._('es-ES', 'Spanish', 'Español'), + es419._('es-419', 'Spanish, LATAM', 'Español, LATAM'), fr._('fr', 'French', 'Français'), hr._('hr', 'Croatian', 'Hrvatski'), it._('it', 'Italian', 'Italiano'), @@ -50,11 +51,8 @@ enum Locale { /// /// [identifier] must be a string containing an identifier matching [Locale.identifier] for one of /// the listed locales. - factory Locale.parse(String identifier) => Locale.values.firstWhere( - (locale) => locale.identifier == identifier, + factory Locale.parse(String identifier) => values.firstWhere( + (loc) => loc.identifier == identifier, orElse: () => throw FormatException('Unknown Locale', identifier), ); - - @override - String toString() => 'Locale($identifier)'; } diff --git a/lib/src/models/message/activity.dart b/lib/src/models/message/activity.dart index 44da6e08d..173e823be 100644 --- a/lib/src/models/message/activity.dart +++ b/lib/src/models/message/activity.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template message_activity} @@ -14,6 +15,7 @@ class MessageActivity with ToStringHelper { final String? partyId; /// {@macro message_activity} + /// @nodoc MessageActivity({ required this.type, required this.partyId, @@ -24,25 +26,15 @@ class MessageActivity with ToStringHelper { /// /// External references: /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#message-object-message-activity-types -enum MessageActivityType { - join._(1), - spectate._(2), - listen._(3), - joinRequest._(5); +final class MessageActivityType extends EnumLike { + static const join = MessageActivityType(1); + static const spectate = MessageActivityType(2); + static const listen = MessageActivityType(3); + static const joinRequest = MessageActivityType(5); - /// The value of this [MessageActivityType]. - final int value; + /// @nodoc + const MessageActivityType(super.value); - const MessageActivityType._(this.value); - - /// Parse a [MessageActivityType] from an [int]. - /// - /// [value] must be a valid message activity type. - factory MessageActivityType.parse(int value) => MessageActivityType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown MessageActivityType', value), - ); - - @override - String toString() => 'MessageActivityType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + MessageActivityType.parse(int value) : this(value); } diff --git a/lib/src/models/message/attachment.dart b/lib/src/models/message/attachment.dart index 27fe26bf7..54533ea15 100644 --- a/lib/src/models/message/attachment.dart +++ b/lib/src/models/message/attachment.dart @@ -77,6 +77,7 @@ class Attachment with ToStringHelper implements CdnAsset { bool get isAnimated => false; /// {@macro attachment} + /// @nodoc Attachment({ required this.id, required this.manager, diff --git a/lib/src/models/message/channel_mention.dart b/lib/src/models/message/channel_mention.dart index 32eb6ebc2..179abc626 100644 --- a/lib/src/models/message/channel_mention.dart +++ b/lib/src/models/message/channel_mention.dart @@ -19,6 +19,7 @@ class ChannelMention extends PartialChannel { final String name; /// {@macro channel_mention} + /// @nodoc ChannelMention({ required super.id, required super.manager, diff --git a/lib/src/models/message/component.dart b/lib/src/models/message/component.dart index 3b6ab40d2..ae7698a92 100644 --- a/lib/src/models/message/component.dart +++ b/lib/src/models/message/component.dart @@ -1,130 +1,194 @@ import 'package:nyxx/src/models/channel/channel.dart'; import 'package:nyxx/src/models/emoji.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; -enum MessageComponentType { - actionRow._(1), - button._(2), - stringSelect._(3), - textInput._(4), - userSelect._(5), - roleSelect._(6), - mentionableSelect._(7), - channelSelect._(8); - - final int value; - - const MessageComponentType._(this.value); - - factory MessageComponentType.parse(int value) => MessageComponentType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown message component type', value), - ); - - @override - String toString() => 'MessageComponentType($value)'; +/// The type of a [MessageComponent]. +final class MessageComponentType extends EnumLike { + static const actionRow = MessageComponentType(1); + static const button = MessageComponentType(2); + static const stringSelect = MessageComponentType(3); + static const textInput = MessageComponentType(4); + static const userSelect = MessageComponentType(5); + static const roleSelect = MessageComponentType(6); + static const mentionableSelect = MessageComponentType(7); + static const channelSelect = MessageComponentType(8); + + /// @nodoc + const MessageComponentType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + MessageComponentType.parse(int value) : this(value); } +/// A component in a [Message]. abstract class MessageComponent with ToStringHelper { + /// The type of this component. MessageComponentType get type; } +/// A [MessageComponent] that contains multiple child [MessageComponent]s. class ActionRowComponent extends MessageComponent { @override MessageComponentType get type => MessageComponentType.actionRow; + /// The children of this [ActionRowComponent]. final List components; + /// Create a new [ActionRowComponent]. + /// @nodoc ActionRowComponent({required this.components}); } +/// A clickable button. class ButtonComponent extends MessageComponent { @override MessageComponentType get type => MessageComponentType.button; + /// The style of this button. final ButtonStyle style; + /// The label displayed on this button. final String? label; + /// The [Emoji] displayed on this button. final Emoji? emoji; + /// This component's custom ID. final String? customId; + /// The purchasable SKU ID, if this button has [ButtonStyle.premium] style. + final Snowflake? skuId; + + /// The URL this button redirects to, if this button is a URL button. final Uri? url; + /// Whether this button is disabled. final bool? isDisabled; + /// Create a new [ButtonComponent]. + /// @nodoc ButtonComponent({ required this.style, required this.label, required this.emoji, required this.customId, + required this.skuId, required this.url, required this.isDisabled, }); } -enum ButtonStyle { - primary._(1), - secondary._(2), - success._(3), - danger._(4), - link._(5); +/// The style of a [ButtonComponent]. +final class ButtonStyle extends EnumLike { + static const primary = ButtonStyle(1); + static const secondary = ButtonStyle(2); + static const success = ButtonStyle(3); + static const danger = ButtonStyle(4); + static const link = ButtonStyle(5); + static const premium = ButtonStyle(6); - final int value; + /// @nodoc + const ButtonStyle(super.value); - const ButtonStyle._(this.value); - - factory ButtonStyle.parse(int value) => ButtonStyle.values.firstWhere( - (style) => style.value == value, - orElse: () => throw FormatException('Unknown button style', value), - ); - - @override - String toString() => 'ButtonStyle($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ButtonStyle.parse(int value) : this(value); } +/// A dropdown menu in which users can select from on or more choices. class SelectMenuComponent extends MessageComponent { @override final MessageComponentType type; + /// This component's custom ID. final String customId; + /// The options in this menu. + /// + /// Will be `null` if this menu is not a [MessageComponentType.stringSelect] menu. final List? options; + /// The channel types displayed in this select menu. + /// + /// Will be `null` if this menu is not a [MessageComponentType.channelSelect] menu. final List? channelTypes; + /// The placeholder shown when the user has not yet selected a value. final String? placeholder; + /// The default selected values in this menu. + final List? defaultValues; + + /// The minimum number of values the user must select. final int? minValues; + /// The maximum number of values the user must select. final int? maxValues; + /// Whether this component is disabled. final bool? isDisabled; + /// Create a new [SelectMenuComponent]. + /// @nodoc SelectMenuComponent({ required this.type, required this.customId, required this.options, required this.channelTypes, required this.placeholder, + required this.defaultValues, required this.minValues, required this.maxValues, required this.isDisabled, }); } +/// The type of a [SelectMenuDefaultValue]. +final class SelectMenuDefaultValueType extends EnumLike { + static const user = SelectMenuDefaultValueType('user'); + static const role = SelectMenuDefaultValueType('role'); + static const channel = SelectMenuDefaultValueType('channel'); + + /// @nodoc + const SelectMenuDefaultValueType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + SelectMenuDefaultValueType.parse(String value) : this(value); +} + +/// A default value in a [SelectMenuComponent]. +class SelectMenuDefaultValue { + /// The ID of this entity. + final Snowflake id; + + /// The type of this entity. + final SelectMenuDefaultValueType type; + + /// Create a new [SelectMenuDefaultValue]. + /// @nodoc + SelectMenuDefaultValue({required this.id, required this.type}); +} + +/// An option in a [SelectMenuComponent]. class SelectMenuOption with ToStringHelper { + /// The label shown to the user. final String label; + /// The value sent to the application. final String value; + /// The description of this option. final String? description; + /// The emoji shown by this emoji. final Emoji? emoji; + /// Whether this [SelectMenuOption] is selected by default. final bool? isDefault; + /// Create a new [SelectMenuOption]. + /// @nodoc SelectMenuOption({ required this.label, required this.value, @@ -134,26 +198,37 @@ class SelectMenuOption with ToStringHelper { }); } +/// A text field in a modal. class TextInputComponent extends MessageComponent { @override MessageComponentType get type => MessageComponentType.textInput; + /// This component's custom ID. final String customId; + /// The style of this [TextInputComponent]. final TextInputStyle? style; + /// This component's label. final String? label; + /// The minimum number of characters the user must input. final int? minLength; + /// The maximum number of characters the user can input. final int? maxLength; + /// Whether this component requires input. final bool? isRequired; + /// The text contained in this component. final String? value; + /// Placeholder text shown when this component is empty. final String? placeholder; + /// Create a new [TextInputComponent]. + /// @nodoc TextInputComponent({ required this.customId, required this.style, @@ -166,19 +241,14 @@ class TextInputComponent extends MessageComponent { }); } -enum TextInputStyle { - short._(1), - paragraph._(2); - - final int value; +/// The type of a [TextInputComponent]. +final class TextInputStyle extends EnumLike { + static const short = TextInputStyle(1); + static const paragraph = TextInputStyle(2); - const TextInputStyle._(this.value); + /// @nodoc + const TextInputStyle(super.value); - factory TextInputStyle.parse(int value) => TextInputStyle.values.firstWhere( - (style) => style.value == value, - orElse: () => throw FormatException('Unknown text input style', value), - ); - - @override - String toString() => 'TextInputStyle($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + TextInputStyle.parse(int value) : this(value); } diff --git a/lib/src/models/message/embed.dart b/lib/src/models/message/embed.dart index 057aed87c..ced94e5bc 100644 --- a/lib/src/models/message/embed.dart +++ b/lib/src/models/message/embed.dart @@ -1,4 +1,5 @@ import 'package:nyxx/src/models/discord_color.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template embed} @@ -11,6 +12,9 @@ class Embed { /// The title of this embed. final String? title; + /// The type of this embed. + final EmbedType type; + /// The description of this embed. final String? description; @@ -45,8 +49,10 @@ class Embed { final List? fields; /// {@macro embed} + /// @nodoc Embed({ required this.title, + required this.type, required this.description, required this.url, required this.timestamp, @@ -61,6 +67,19 @@ class Embed { }); } +/// The type of an embed. +final class EmbedType extends EnumLike { + static const rich = EmbedType('rich'); + static const image = EmbedType('image'); + static const video = EmbedType('video'); + static const gifv = EmbedType('gifv'); + static const article = EmbedType('article'); + static const link = EmbedType('link'); + static const pollResult = EmbedType('poll_result'); +// @nodoc + const EmbedType(super.value); +} + /// {@template embed_footer} /// A footer in an [Embed]. /// @@ -78,6 +97,7 @@ class EmbedFooter with ToStringHelper { final Uri? proxiedIconUrl; /// {@macro embed_footer} + /// @nodoc EmbedFooter({ required this.text, required this.iconUrl, @@ -105,6 +125,7 @@ class EmbedImage with ToStringHelper { final int? width; /// {@macro embed_image} + /// @nodoc EmbedImage({ required this.url, required this.proxiedUrl, @@ -133,6 +154,7 @@ class EmbedThumbnail with ToStringHelper { final int? width; /// {@macro embed_thumbnail} + /// @nodoc EmbedThumbnail({ required this.url, required this.proxiedUrl, @@ -161,6 +183,7 @@ class EmbedVideo with ToStringHelper { final int? width; /// {@macro embed_video} + /// @nodoc EmbedVideo({ required this.url, required this.proxiedUrl, @@ -183,6 +206,7 @@ class EmbedProvider with ToStringHelper { final Uri? url; /// {@macro embed_provider} + /// @nodoc EmbedProvider({ required this.name, required this.url, @@ -209,6 +233,7 @@ class EmbedAuthor with ToStringHelper { final Uri? proxyIconUrl; /// {@macro embed_author} + /// @nodoc EmbedAuthor({ required this.name, required this.url, @@ -234,6 +259,7 @@ class EmbedField with ToStringHelper { final bool inline; /// {@macro embed_field} + /// @nodoc EmbedField({ required this.name, required this.value, diff --git a/lib/src/models/message/message.dart b/lib/src/models/message/message.dart index 142486f69..c4a78debc 100644 --- a/lib/src/models/message/message.dart +++ b/lib/src/models/message/message.dart @@ -11,6 +11,7 @@ import 'package:nyxx/src/models/message/channel_mention.dart'; import 'package:nyxx/src/models/message/component.dart'; import 'package:nyxx/src/models/message/embed.dart'; import 'package:nyxx/src/models/message/author.dart'; +import 'package:nyxx/src/models/message/poll.dart'; import 'package:nyxx/src/models/message/reference.dart'; import 'package:nyxx/src/models/message/reaction.dart'; import 'package:nyxx/src/models/channel/text_channel.dart'; @@ -20,6 +21,7 @@ import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/sticker/sticker.dart'; import 'package:nyxx/src/models/user/user.dart'; import 'package:nyxx/src/models/webhook.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; @@ -34,6 +36,7 @@ class PartialMessage extends WritableSnowflakeEntity { Snowflake get channelId => manager.channelId; /// {@macro partial_message} + /// @nodoc PartialMessage({required super.id, required this.manager}); /// The channel this message was sent in. @@ -73,6 +76,15 @@ class PartialMessage extends WritableSnowflakeEntity { /// Deletes reaction the current user has made on this message. Future deleteOwnReaction(ReactionBuilder emoji) => manager.deleteOwnReaction(id, emoji); + + /// Get a list of users that reacted with a given emoji on a message. + Future> fetchReactions(ReactionBuilder emoji, {Snowflake? after, int? limit}) => manager.fetchReactions(id, emoji, after: after, limit: limit); + + /// Get a list of users that voted for this specific answer. + Future> fetchAnswerVoters(int answerId, {Snowflake? after, int? limit}) => manager.fetchAnswerVoters(id, answerId, after: after, limit: limit); + + /// Immediately ends the poll. + Future endPoll() => manager.endPoll(id); } /// {@template message} @@ -84,23 +96,19 @@ class PartialMessage extends WritableSnowflakeEntity { /// External references: /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#message-object /// {@endtemplate} -class Message extends PartialMessage { +class Message extends PartialMessage implements MessageSnapshot { /// The author of this message. /// - /// This could be a [User] or a [Webhook]. + /// This could be a [User] or a [WebhookAuthor]. final MessageAuthor author; - /// The content of the message. - /// - /// {@template message_content_intent_required} - /// The message content intent is needed for this field to be non-empty. - /// {@endtemplate} + @override final String content; - /// The time when this message was sent. + @override final DateTime timestamp; - /// The time when this message was last edited, or `null` if the message was never edited. + @override final DateTime? editedTimestamp; /// Whether this was a TTS message. @@ -109,22 +117,19 @@ class Message extends PartialMessage { /// Whether this message mentions everyone. final bool mentionsEveryone; - /// A list of users specifically mentioned in this message. + @override final List mentions; + @override final List roleMentionIds; /// A list of channels specifically mentioned in this message. final List channelMentions; - /// A list of files attached to this message. - /// - /// {@macro message_content_intent_required} + @override final List attachments; - /// A list of embeds in this message. - /// - /// {@macro message_content_intent_required} + @override final List embeds; /// A list of reactions to this message. @@ -141,7 +146,7 @@ class Message extends PartialMessage { /// The ID of the webhook that sent this message if it was sent by a webhook, `null` otherwise. final Snowflake? webhookId; - /// The type of this message. + @override final MessageType type; /// Activity information if this message is related to Rich Presence, `null` otherwise. @@ -156,13 +161,21 @@ class Message extends PartialMessage { /// Data showing the source of a crosspost, channel follow add, pin, or reply message. final MessageReference? reference; - /// Any flags applied to this message. + /// The messages associated with [reference]. + final List? messageSnapshots; + + @override final MessageFlags flags; /// The message associated with [reference]. final Message? referencedMessage; /// Information about the interaction related to this message. + final MessageInteractionMetadata? interactionMetadata; + + /// Information about the interaction related to this message. + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use `interactionMetadata`') final MessageInteraction? interaction; /// The thread that was started from this message if any, `null` otherwise. @@ -185,7 +198,14 @@ class Message extends PartialMessage { /// Data about entities in this message's auto-populated select menus. final ResolvedData? resolved; + /// A poll. + final Poll? poll; + + /// Information about a call in a DM channel. + final MessageCall? call; + /// {@macro message} + /// @nodoc Message({ required super.id, required super.manager, @@ -209,8 +229,10 @@ class Message extends PartialMessage { required this.application, required this.applicationId, required this.reference, + required this.messageSnapshots, required this.flags, required this.referencedMessage, + required this.interactionMetadata, required this.interaction, required this.thread, required this.components, @@ -218,6 +240,8 @@ class Message extends PartialMessage { required this.roleSubscriptionData, required this.stickers, required this.resolved, + required this.poll, + required this.call, }); /// The webhook that sent this message if it was sent by a webhook, `null` otherwise. @@ -230,54 +254,50 @@ class Message extends PartialMessage { /// /// External references: /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#message-object-message-types -enum MessageType { - normal._(0), - recipientAdd._(1), - recipientRemove._(2), - call._(3), - channelNameChange._(4), - channelIconChange._(5), - channelPinnedMessage._(6), - userJoin._(7), - guildBoost._(8), - guildBoostTier1._(9), - guildBoostTier2._(10), - guildBoostTier3._(11), - channelFollowAdd._(12), - guildDiscoveryDisqualified._(14), - guildDiscoveryRequalified._(15), - guildDiscoveryGracePeriodInitialWarning._(16), - guildDiscoveryGracePeriodFinalWarning._(17), - threadCreated._(18), - reply._(19), - chatInputCommand._(20), - threadStarterMessage._(21), - guildInviteReminder._(22), - contextMenuCommand._(23), - autoModerationAction._(24), - roleSubscriptionPurchase._(25), - interactionPremiumUpsell._(26), - stageStart._(27), - stageEnd._(28), - stageSpeaker._(29), - stageTopic._(31), - guildApplicationPremiumSubscription._(32); - - /// The value of this [MessageType]. - final int value; - - const MessageType._(this.value); - - /// Parse a [MessageType] from an [int]. - /// - /// [value] must be a valid [MessageType]. - factory MessageType.parse(int value) => MessageType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown MessageType', value), - ); - - @override - String toString() => 'MessageType($value)'; +final class MessageType extends EnumLike { + static const normal = MessageType(0); + static const recipientAdd = MessageType(1); + static const recipientRemove = MessageType(2); + static const call = MessageType(3); + static const channelNameChange = MessageType(4); + static const channelIconChange = MessageType(5); + static const channelPinnedMessage = MessageType(6); + static const userJoin = MessageType(7); + static const guildBoost = MessageType(8); + static const guildBoostTier1 = MessageType(9); + static const guildBoostTier2 = MessageType(10); + static const guildBoostTier3 = MessageType(11); + static const channelFollowAdd = MessageType(12); + static const guildDiscoveryDisqualified = MessageType(14); + static const guildDiscoveryRequalified = MessageType(15); + static const guildDiscoveryGracePeriodInitialWarning = MessageType(16); + static const guildDiscoveryGracePeriodFinalWarning = MessageType(17); + static const threadCreated = MessageType(18); + static const reply = MessageType(19); + static const chatInputCommand = MessageType(20); + static const threadStarterMessage = MessageType(21); + static const guildInviteReminder = MessageType(22); + static const contextMenuCommand = MessageType(23); + static const autoModerationAction = MessageType(24); + static const roleSubscriptionPurchase = MessageType(25); + static const interactionPremiumUpsell = MessageType(26); + static const stageStart = MessageType(27); + static const stageEnd = MessageType(28); + static const stageSpeaker = MessageType(29); + static const stageTopic = MessageType(31); + static const guildApplicationPremiumSubscription = MessageType(32); + static const guildIncidentAlertModeEnabled = MessageType(36); + static const guildIncidentAlertModeDisabled = MessageType(37); + static const guildIncidentReportRaid = MessageType(38); + static const guildIncidentReportFalseAlarm = MessageType(39); + static const purchaseNotification = MessageType(44); + static const pollResult = MessageType(46); + + /// @nodoc + const MessageType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + MessageType.parse(int value) : this(value); } /// Flags that can be applied to a [Message]. @@ -355,6 +375,8 @@ class MessageFlags extends Flags { const MessageFlags(super.value); } +@Deprecated('Use MessageInteractionMetadata') + /// {@template message_interaction} /// Information about an interaction associated with a message. /// {@endtemplate} @@ -375,6 +397,7 @@ class MessageInteraction with ToStringHelper { final PartialMember? member; /// {@macro message_interaction} + /// @nodoc MessageInteraction({ required this.id, required this.type, @@ -383,3 +406,126 @@ class MessageInteraction with ToStringHelper { required this.member, }); } + +/// {@template message_interaction_metadata} +/// Metadata about the interaction, including the source of the interaction and relevant server and user IDs. +/// {@endtemplate} +class MessageInteractionMetadata with ToStringHelper { + /// The ID of the interaction. + final Snowflake id; + + /// The type of the interaction. + final InteractionType type; + + /// The user that triggered the interaction. + final User user; + + /// IDs for installation context(s) related to an interaction. + final Map authorizingIntegrationOwners; + + /// ID of the original response message, present only on follow-up messages. + final Snowflake? originalResponseMessageId; + + /// ID of the message that contained interactive component, present only on messages created from component interactions. + final Snowflake? interactedMessageId; + + /// Metadata for the interaction that was used to open the modal, present only on modal submit interactions + final MessageInteractionMetadata? triggeringInteractionMetadata; + + /// {@macro message_interaction_metadata} + /// @nodoc + MessageInteractionMetadata({ + required this.id, + required this.type, + required this.user, + required this.authorizingIntegrationOwners, + required this.originalResponseMessageId, + required this.interactedMessageId, + required this.triggeringInteractionMetadata, + }); + + /// ID of the user that triggered the interaction. + @Deprecated('Use user.id instead.') + Snowflake get userId => user.id; +} + +/// A limited set of fields of a [Message]. +// Technically this class should contain a single `message` field, of type +// `PartialMessage`. However, partials in nyxx require the ID of the object to +// be known, and the id field is not included in the nested partial message +// object. Since this object would then be useless as it cannot contain any +// useful data using existing nyxx types, we instead forward the field of the +// nested object into this type. +class MessageSnapshot with ToStringHelper { + /// The time when this message was sent. + final DateTime timestamp; + + /// The time when this message was last edited, or `null` if the message was never edited. + final DateTime? editedTimestamp; + + /// The type of this message. + final MessageType type; + + /// The content of the message. + /// + /// {@template message_content_intent_required} + /// The message content intent is needed for this field to be non-empty. + /// {@endtemplate} + final String content; + + /// A list of files attached to this message. + /// + /// {@macro message_content_intent_required} + final List attachments; + + /// A list of embeds in this message. + /// + /// {@macro message_content_intent_required} + final List embeds; + + /// Any flags applied to this message. + final MessageFlags flags; + + /// A list of users specifically mentioned in this message. + final List mentions; + + /// A list of roles mentioned in the message. + final List roleMentionIds; + + /// @nodoc + MessageSnapshot({ + required this.timestamp, + required this.editedTimestamp, + required this.type, + required this.content, + required this.attachments, + required this.embeds, + required this.flags, + required this.mentions, + required this.roleMentionIds, + }); +} + +/// Information about a call in a private channel. +class MessageCall with ToStringHelper { + /// The manager for this [MessageCall]. + final MessageManager manager; + + /// The IDs of the users in the call. + final List participantIds; + + /// The time at which the call ended. + final DateTime? endedAt; + + /// @nodoc + MessageCall({ + required this.manager, + required this.participantIds, + required this.endedAt, + }); + + /// The users in the call. + List get participants => [ + for (final participantId in participantIds) manager.client.users[participantId], + ]; +} diff --git a/lib/src/models/message/poll.dart b/lib/src/models/message/poll.dart new file mode 100644 index 000000000..b2cfa8653 --- /dev/null +++ b/lib/src/models/message/poll.dart @@ -0,0 +1,127 @@ +import 'package:nyxx/src/models/emoji.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; +import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; + +/// A layout type indicating how poll looks. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#layout-type +final class PollLayoutType extends EnumLike { + /// The default layout type. + static const defaultLayout = PollLayoutType(1); + + /// @nodoc + const PollLayoutType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + PollLayoutType.parse(int value) : this(value); +} + +/// {@template poll_media} +/// The poll media object is a common object that backs both the question and answers. +/// The intention is that it allows us to extensibly add new ways to display things in the future. +/// For now, [Poll.question] only supports [PollMedia.text], while answers can have an optional [PollMedia.emoji]. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#poll-media-object +/// {@endtemplate} +class PollMedia with ToStringHelper { + /// The text of the field. + String? text; + + /// The emoji of the field. + Emoji? emoji; + + /// {@macro poll_media} + /// @nodoc + PollMedia({required this.text, required this.emoji}); +} + +/// {@template poll_answer} +/// The [PollAnswer.answerId] is a number that labels each answer. +/// As an implementation detail, it currently starts at 1 for the first answer and goes up sequentially. +/// We recommend against depending on this sequence. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#poll-answer-object +/// {@endtemplate} +class PollAnswer with ToStringHelper { + /// The ID of the answer. + final int id; + + /// The data of the answer. + PollMedia pollMedia; + + /// {@macro poll_answer} + /// @nodoc + PollAnswer({required this.id, required this.pollMedia}); +} + +class PollAnswerCount with ToStringHelper { + /// The ID of the answer. + final int answerId; + + /// The number of votes for this answer. + final int count; + + /// Whether the current user voted for this answer. + final bool me; + + /// @nodoc + PollAnswerCount({required this.answerId, required this.count, required this.me}); +} + +/// {@template poll_results} +/// In a nutshell, this contains the number of votes for each answer. +/// Due to the intricacies of counting at scale, while a poll is in progress the results may not be perfectly accurate. +/// They usually are accurate, and shouldn't deviate significantly -- it's just difficult to make guarantees. +/// To compensate for this, after a poll is finished there is a background job which performs a final, accurate tally of votes. +/// This tally has concluded once [PollResults.isFinalized] is `true`. +/// If [PollResults.answerCounts] does not contain an entry for a particular answer, then there are no votes for that answer. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#poll-results-object +/// {@endtemplate} +class PollResults with ToStringHelper { + /// Whether the votes have been precisely counted. + final bool isFinalized; + + /// The counts for each answer. + final List answerCounts; + + /// {@macro poll_results} + /// @nodoc + PollResults({required this.isFinalized, required this.answerCounts}); +} + +/// {@template poll} +/// The poll object has a lot of levels and nested structures. It was also designed +/// to support future extensibility, so some fields may appear to be more complex than +/// necessary. +/// +/// External references: +/// * Discord API Reference: https://discord.com/developers/docs/resources/poll#poll-object +/// {@endtemplate} +class Poll with ToStringHelper { + /// The question of the poll. + final PollMedia question; + + /// Each of the answers available in the poll. + final List answers; + + /// The time when the poll ends. + final DateTime? endsAt; + + /// Whether a user can select multiple answers. + final bool allowsMultiselect; + + /// The layout type of the poll. + final PollLayoutType layoutType; + + /// The results of the poll. + final PollResults? results; + + /// {@macro poll} + /// @nodoc + Poll({required this.question, required this.answers, required this.endsAt, required this.allowsMultiselect, required this.layoutType, required this.results}); +} diff --git a/lib/src/models/message/reaction.dart b/lib/src/models/message/reaction.dart index 4e8aba662..8f08b7970 100644 --- a/lib/src/models/message/reaction.dart +++ b/lib/src/models/message/reaction.dart @@ -28,6 +28,7 @@ class Reaction with ToStringHelper { final List burstColors; /// {@macro reaction} + /// @nodoc Reaction({ required this.count, required this.countDetails, @@ -49,5 +50,6 @@ class ReactionCountDetails with ToStringHelper { final int normal; /// {@macro reaction_count_details} + /// @nodoc ReactionCountDetails({required this.burst, required this.normal}); } diff --git a/lib/src/models/message/reference.dart b/lib/src/models/message/reference.dart index abb969e47..bc0038eda 100644 --- a/lib/src/models/message/reference.dart +++ b/lib/src/models/message/reference.dart @@ -4,6 +4,7 @@ import 'package:nyxx/src/models/channel/text_channel.dart'; import 'package:nyxx/src/models/guild/guild.dart'; import 'package:nyxx/src/models/message/message.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template message_reference} @@ -48,6 +49,9 @@ class MessageReference with ToStringHelper { /// The manager for this [MessageReference]. final MessageManager manager; + /// The type of reference. + final MessageReferenceType type; + /// The ID of the originating [Message]. final Snowflake? messageId; @@ -58,8 +62,10 @@ class MessageReference with ToStringHelper { final Snowflake? guildId; /// {@macro message_reference} + /// @nodoc MessageReference({ required this.manager, + required this.type, required this.messageId, required this.channelId, required this.guildId, @@ -74,3 +80,10 @@ class MessageReference with ToStringHelper { /// The guild of the originating message. PartialGuild? get guild => guildId == null ? null : manager.client.guilds[guildId!]; } + +final class MessageReferenceType extends EnumLike { + static const defaultType = MessageReferenceType(0); + static const forward = MessageReferenceType(1); + + const MessageReferenceType(super.value); +} diff --git a/lib/src/models/message/role_subscription_data.dart b/lib/src/models/message/role_subscription_data.dart index 56a770e0e..bf1af1d90 100644 --- a/lib/src/models/message/role_subscription_data.dart +++ b/lib/src/models/message/role_subscription_data.dart @@ -1,4 +1,5 @@ import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template role_subscription_data} /// Information about a role subscription. @@ -6,7 +7,7 @@ import 'package:nyxx/src/models/snowflake.dart'; /// External references: /// * Discord API Reference: https://discord.com/developers/docs/resources/channel#role-subscription-data-object /// {@endtemplate} -class RoleSubscriptionData { +class RoleSubscriptionData with ToStringHelper { ///The ID of the sku and listing that the user is subscribed to. final Snowflake listingId; @@ -20,6 +21,7 @@ class RoleSubscriptionData { final bool isRenewal; /// {@macro role_subscription_data} + /// @nodoc RoleSubscriptionData({ required this.listingId, required this.tierName, diff --git a/lib/src/models/oauth2.dart b/lib/src/models/oauth2.dart new file mode 100644 index 000000000..f2496f9a1 --- /dev/null +++ b/lib/src/models/oauth2.dart @@ -0,0 +1,19 @@ +import 'package:nyxx/src/models/application.dart'; +import 'package:nyxx/src/models/user/user.dart'; + +class OAuth2Information { + /// The current application. + final PartialApplication application; + + /// The scopes the user has authorized the application for. + final List scopes; + + /// When the access token expires. + final DateTime expiresOn; + + /// The user who has authorized, if the user has authorized with the `identify` scope. + final User? user; + + /// @nodoc + OAuth2Information({required this.application, required this.scopes, required this.expiresOn, this.user}); +} diff --git a/lib/src/models/permission_overwrite.dart b/lib/src/models/permission_overwrite.dart index 11d60ce83..a77638130 100644 --- a/lib/src/models/permission_overwrite.dart +++ b/lib/src/models/permission_overwrite.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/models/permissions.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template permission_overwrite} @@ -32,6 +33,7 @@ class PermissionOverwrite with ToStringHelper { final Permissions deny; /// {@macro permission_overwrite} + /// @nodoc PermissionOverwrite({ required this.id, required this.type, @@ -41,26 +43,16 @@ class PermissionOverwrite with ToStringHelper { } /// The type of a permission overwrite. -enum PermissionOverwriteType { +final class PermissionOverwriteType extends EnumLike { /// The overwrite applies to a [Role]'s permissions. - role._(0), + static const role = PermissionOverwriteType(0); /// The overwrite applies to a [Member]'s permissions. - member._(1); + static const member = PermissionOverwriteType(1); - /// The value of this type. - final int value; + /// @nodoc + const PermissionOverwriteType(super.value); - const PermissionOverwriteType._(this.value); - - /// Parse a [PermissionOverwriteType] from a [value]. - /// - /// The [value] must be a valid [PermissionOverwriteType]. - factory PermissionOverwriteType.parse(int value) => PermissionOverwriteType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown PermissionOverwriteType', value), - ); - - @override - String toString() => 'PermissionOverwriteType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + PermissionOverwriteType.parse(int value) : this(value); } diff --git a/lib/src/models/permissions.dart b/lib/src/models/permissions.dart index b844312f7..3ed55e52b 100644 --- a/lib/src/models/permissions.dart +++ b/lib/src/models/permissions.dart @@ -95,16 +95,20 @@ class Permissions extends Flags { /// Allows management and editing of webhooks. static const manageWebhooks = Flag.fromOffset(29); - /// Allows management and editing of emojis, stickers, and soundboard sounds. + /// Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users. + @Deprecated('Use manageGuildExpressions instead') static const manageEmojisAndStickers = Flag.fromOffset(30); + /// Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users. + static const manageGuildExpressions = Flag.fromOffset(30); + /// Allows members to use application commands, including slash commands and context menu commands.. static const useApplicationCommands = Flag.fromOffset(31); /// Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.). static const requestToSpeak = Flag.fromOffset(32); - /// Allows for creating, editing, and deleting scheduled events. + /// Allows for editing and deleting scheduled events created by all users. static const manageEvents = Flag.fromOffset(33); /// Allows for deleting and archiving threads, and viewing all private threads. @@ -134,8 +138,31 @@ class Permissions extends Flags { /// Allows for using soundboard in a voice channel. static const useSoundboard = Flag.fromOffset(42); + /// Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user. + @Deprecated('Use createGuildExpressions instead') + static const createEmojiAndStickers = Flag.fromOffset(43); + + /// Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user. + static const createGuildExpressions = Flag.fromOffset(43); + + /// Allows for creating scheduled events, and editing and deleting those created by the current user. + static const createEvents = Flag.fromOffset(44); + + /// Allows the usage of custom soundboard sounds from other servers. + static const useExternalSounds = Flag.fromOffset(45); + + /// Allows sending voice messages. + static const sendVoiceMessages = Flag.fromOffset(46); + + /// Allows sending polls. + static const sendPolls = Flag.fromOffset(49); + + /// Allows user-installed apps to send public responses. When disabled, users will still be allowed to use their apps but the responses will be ephemeral. + /// This only applies to apps not also installed to the server. + static const useExternalApps = Flag.fromOffset(50); + /// A [Permissions] with all permissions enabled. - static const allPermissions = Permissions(1099511627775); + static const allPermissions = Permissions(1829587348619263); /// Whether this set of permissions has the [createInstantInvite] permission. bool get canCreateInstantInvite => has(createInstantInvite); @@ -228,8 +255,12 @@ class Permissions extends Flags { bool get canManageWebhooks => has(manageWebhooks); /// Whether this set of permissions has the [manageEmojisAndStickers] permission. + @Deprecated('Use canManageGuildExpressions instead') bool get canManageEmojisAndStickers => has(manageEmojisAndStickers); + /// Whether this set of permissions has the [manageGuildExpressions] permission. + bool get canManageGuildExpressions => has(manageGuildExpressions); + /// Whether this set of permissions has the [useApplicationCommands] permission. bool get canUseApplicationCommands => has(useApplicationCommands); @@ -266,6 +297,28 @@ class Permissions extends Flags { /// Whether this set of permissions has the [useSoundboard] permission. bool get canUseSoundboard => has(useSoundboard); + /// Whether this set of permissions has the [createEmojiAndStickers] permission. + @Deprecated('Use canCreateGuildExpressions instead') + bool get canCreateEmojiAndStickers => has(createEmojiAndStickers); + + /// Whether this set of permissions has the [createGuildExpressions] permission. + bool get canCreateGuildExpressions => has(createGuildExpressions); + + /// Whether this set of permissions has the [createEvents] permission. + bool get canCreateEvents => has(createEvents); + + /// Whether this set of permissions has the [useExternalSounds] permission. + bool get canUseExternalSounds => has(useExternalSounds); + + /// Whether this set of permissions has the [sendVoiceMessages] permission. + bool get canSendVoiceMessages => has(sendVoiceMessages); + + /// Whether this set of permissions has the [sendPolls] permission. + bool get canSendPolls => has(sendPolls); + + /// Whether this set of permissions has the [useExternalApps] permission. + bool get canUseExternalApps => has(useExternalApps); + /// Create a new [Permissions] from a permissions value. const Permissions(super.value); } diff --git a/lib/src/models/presence.dart b/lib/src/models/presence.dart index 34a9f0ce7..16027174b 100644 --- a/lib/src/models/presence.dart +++ b/lib/src/models/presence.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/models/emoji.dart'; import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; @@ -17,31 +18,22 @@ class ClientStatus with ToStringHelper { final UserStatus? web; /// {@macro client_status} + /// @nodoc ClientStatus({required this.desktop, required this.mobile, required this.web}); } /// The status of a client. -enum UserStatus { - online._('online'), - idle._('idle'), - dnd._('dnd'), - offline._('offline'); - - /// The value of this [UserStatus]. - final String value; - - const UserStatus._(this.value); - - /// Parse a [UserStatus] from a [String]. - /// - /// The [value] must be a valid user status. - factory UserStatus.parse(String value) => UserStatus.values.firstWhere( - (status) => status.value == value, - orElse: () => throw FormatException('Unknown user status', value), - ); - - @override - String toString() => 'UserStatus($value)'; +final class UserStatus extends EnumLike { + static const online = UserStatus('online'); + static const dnd = UserStatus('dnd'); + static const idle = UserStatus('idle'); + static const offline = UserStatus('offline'); + + /// @nodoc + const UserStatus(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + UserStatus.parse(String value) : this(value); } /// {@template activity} @@ -94,6 +86,7 @@ class Activity with ToStringHelper { final List? buttons; /// {@macro activity} + /// @nodoc Activity({ required this.name, required this.type, @@ -114,29 +107,18 @@ class Activity with ToStringHelper { } /// The type of an activity. -enum ActivityType { - game._(0), - streaming._(1), - listening._(2), - watching._(3), - custom._(4), - competing._(5); - - /// The value of this [ActivityType]. - final int value; - - const ActivityType._(this.value); - - /// Parse an [ActivityType] from an [int]. - /// - /// The [value] must be a valid activity type. - factory ActivityType.parse(int value) => ActivityType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown activity type', value), - ); - - @override - String toString() => 'ActivityType($value)'; +final class ActivityType extends EnumLike { + static const game = ActivityType(0); + static const streaming = ActivityType(1); + static const listening = ActivityType(2); + static const watching = ActivityType(3); + static const custom = ActivityType(4); + static const competing = ActivityType(5); + + const ActivityType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ActivityType.parse(int value) : this(value); } /// {@template activity_timestamps} @@ -150,6 +132,7 @@ class ActivityTimestamps with ToStringHelper { final DateTime? end; /// {@macro activity_timestamps} + /// @nodoc ActivityTimestamps({required this.start, required this.end}); } @@ -167,6 +150,7 @@ class ActivityParty with ToStringHelper { final int? maxSize; /// {@macro activity_party} + /// @nodoc ActivityParty({required this.id, required this.currentSize, required this.maxSize}); } @@ -189,6 +173,7 @@ class ActivityAssets with ToStringHelper { final String? smallText; /// {@macro activity_assets} + /// @nodoc ActivityAssets({ required this.largeImage, required this.largeText, @@ -211,6 +196,7 @@ class ActivitySecrets with ToStringHelper { final String? match; /// {@macro activity_secrets} + /// @nodoc ActivitySecrets({required this.join, required this.spectate, required this.match}); } @@ -275,5 +261,6 @@ class ActivityButton with ToStringHelper { final Uri url; /// {@macro activity_button} + /// @nodoc ActivityButton({required this.label, required this.url}); } diff --git a/lib/src/models/role.dart b/lib/src/models/role.dart index 70a8695dd..20ce5f0ee 100644 --- a/lib/src/models/role.dart +++ b/lib/src/models/role.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/builders/role.dart'; import 'package:nyxx/src/http/cdn/cdn_asset.dart'; import 'package:nyxx/src/http/managers/role_manager.dart'; import 'package:nyxx/src/http/route.dart'; @@ -15,7 +16,24 @@ class PartialRole extends WritableSnowflakeEntity { final RoleManager manager; /// Create a new [PartialRole]. + /// @nodoc PartialRole({required super.id, required this.manager}); + + /// Update this role, returning the updated role. + /// + /// External references: + /// * [RoleManager.update] + /// * Discord API Reference: https://discord.com/developers/docs/resources/guild#modify-guild-role + @override + Future update(RoleUpdateBuilder builder, {String? auditLogReason}) => manager.update(id, builder, auditLogReason: auditLogReason); + + /// Delete this role. + /// + /// External references: + /// * [RoleManager.delete] + /// * Discord API Reference: https://discord.com/developers/docs/resources/guild#delete-guild-role + @override + Future delete({String? auditLogReason}) => manager.delete(id, auditLogReason: auditLogReason); } /// {@template role} @@ -58,6 +76,7 @@ class Role extends PartialRole implements CommandOptionMentionable { final RoleFlags flags; /// {@macro role} + /// @nodoc Role({ required super.id, required super.manager, @@ -93,14 +112,27 @@ class RoleTags with ToStringHelper { /// The ID of the integration this role belongs to. final Snowflake? integrationId; + /// Whether this is the guild's Booster role. + final bool isPremiumSubscriber; + /// The ID of this role's subscription sku and listing. final Snowflake? subscriptionListingId; + /// Whether this role is available for purchase. + final bool isAvailableForPurchase; + + /// Whether this role is a guild's linked role + final bool isLinkedRole; + /// {@macro role_tags} + /// @nodoc RoleTags({ required this.botId, required this.integrationId, + required this.isPremiumSubscriber, required this.subscriptionListingId, + required this.isAvailableForPurchase, + required this.isLinkedRole, }); } diff --git a/lib/src/models/sku.dart b/lib/src/models/sku.dart index e8fe3cfc1..f941eee16 100644 --- a/lib/src/models/sku.dart +++ b/lib/src/models/sku.dart @@ -1,18 +1,28 @@ -import 'package:nyxx/src/http/managers/application_manager.dart'; +import 'package:nyxx/src/http/managers/sku_manager.dart'; +import 'package:nyxx/src/http/managers/subscription_manager.dart'; import 'package:nyxx/src/models/application.dart'; import 'package:nyxx/src/models/snowflake.dart'; -import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; +import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; +import 'package:nyxx/src/utils/flags.dart'; + +/// A partial [Sku]. +class PartialSku extends ManagedSnowflakeEntity { + @override + final SkuManager manager; + + /// A manager for this [Sku]'s [Subscription]s. + SubscriptionManager get subscriptions => + SubscriptionManager(manager.client.options.subscriptionConfig, manager.client, applicationId: manager.applicationId, skuId: id); + + /// @nodoc + PartialSku({required this.manager, required super.id}); +} /// {@template sku} /// A premium offering that can be made available to your application's users or guilds. /// {@endtemplate} -class Sku with ToStringHelper { - /// The [Manager] for this SKU. - final ApplicationManager manager; - - /// This SKU's ID. - final Snowflake id; - +class Sku extends PartialSku { /// This SKU's type. final SkuType type; @@ -25,37 +35,66 @@ class Sku with ToStringHelper { /// The URL slug for this SKU. final String slug; + /// This SKU's flags. + final SkuFlags flags; + /// {@macro sku} + /// @nodoc Sku({ - required this.manager, - required this.id, + required super.manager, + required super.id, required this.type, required this.applicationId, required this.name, required this.slug, + required this.flags, }); /// The application this SKU belongs to. - PartialApplication get application => PartialApplication(id: applicationId, manager: manager); + PartialApplication get application => PartialApplication(id: applicationId, manager: manager.client.applications); } /// The type of an [Sku]. -enum SkuType { - subscription._(5), - subscriptionGroup._(6); +final class SkuType extends EnumLike { + /// Durable one-time purchase. + static const durable = SkuType(2); - final int value; + /// Consumable one-time purchase. + static const consumable = SkuType(3); - const SkuType._(this.value); + /// Subscription. + static const subscription = SkuType(5); - /// Parse an [SkuType] from an [int]. - /// - /// The [value] must be a valid sku type. - factory SkuType.parse(int value) => SkuType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown SKU type', value), - ); + /// Subscription group. + static const subscriptionGroup = SkuType(6); - @override - String toString() => 'SkuType($value)'; + /// @nodoc + const SkuType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + SkuType.parse(int value) : this(value); +} + +/// Flags applied to an [Sku]. +class SkuFlags extends Flags { + /// The SKU is available for purchase. + static const available = Flag.fromOffset(2); + + /// The SKU is a guild subscription. + static const guildSubscription = Flag.fromOffset(7); + + /// The SKU is a user subscription. + static const userSubscription = Flag.fromOffset(8); + + /// Create a new [SkuFlags]. + SkuFlags(super.value); + + /// Whether this set of flags has the [available] flag set. + bool get isAvailable => has(available); + + /// Whether this set of flags has the [guildSubscription] flag set. + bool get isGuildSubscription => has(guildSubscription); + + /// Whether this set of flags has the [userSubscription] flag set. + bool get isUserSubscription => has(userSubscription); } diff --git a/lib/src/models/snowflake_entity/snowflake_entity.dart b/lib/src/models/snowflake_entity/snowflake_entity.dart index 85e5110f4..cf8493de7 100644 --- a/lib/src/models/snowflake_entity/snowflake_entity.dart +++ b/lib/src/models/snowflake_entity/snowflake_entity.dart @@ -11,6 +11,7 @@ abstract class SnowflakeEntity> with ToStringHelper final Snowflake id; /// Create a new [SnowflakeEntity]. + /// @nodoc SnowflakeEntity({required this.id}); /// If this entity exists in the manager's cache, return the cached instance. Otherwise, [fetch] @@ -36,6 +37,7 @@ abstract class ManagedSnowflakeEntity> exten ReadOnlyManager get manager; /// Create a new [ManagedSnowflakeEntity]; + /// @nodoc ManagedSnowflakeEntity({required super.id}); @override @@ -51,6 +53,7 @@ abstract class WritableSnowflakeEntity> ext Manager get manager; /// Create a new [WritableSnowflakeEntity]. + /// @nodoc WritableSnowflakeEntity({required super.id}); /// Update this entity using the provided builder and return the updated entity. diff --git a/lib/src/models/sticker/global_sticker.dart b/lib/src/models/sticker/global_sticker.dart index 80e8376d1..9748f4420 100644 --- a/lib/src/models/sticker/global_sticker.dart +++ b/lib/src/models/sticker/global_sticker.dart @@ -8,6 +8,7 @@ class PartialGlobalSticker extends ManagedSnowflakeEntity { @override final GlobalStickerManager manager; + /// @nodoc PartialGlobalSticker({required super.id, required this.manager}); } @@ -51,6 +52,7 @@ class GlobalSticker extends PartialGlobalSticker with Sticker { final Snowflake packId; /// {@macro global_sticker} + /// @nodoc GlobalSticker({ required super.id, required super.manager, diff --git a/lib/src/models/sticker/guild_sticker.dart b/lib/src/models/sticker/guild_sticker.dart index e809c653d..dcc246477 100644 --- a/lib/src/models/sticker/guild_sticker.dart +++ b/lib/src/models/sticker/guild_sticker.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/builders/sticker.dart'; import 'package:nyxx/src/http/managers/sticker_manager.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; @@ -8,7 +9,24 @@ class PartialGuildSticker extends WritableSnowflakeEntity { @override final GuildStickerManager manager; + /// @nodoc PartialGuildSticker({required super.id, required this.manager}); + + /// Update this sticker, returning the updated sticker. + /// + /// External references: + /// * [StickerManager.update] + /// * Discord API Reference: https://discord.com/developers/docs/resources/sticker#modify-guild-sticker + @override + Future update(StickerUpdateBuilder builder, {String? auditLogReason}) => manager.update(id, builder, auditLogReason: auditLogReason); + + /// Delete this sticker. + /// + /// External references: + /// * [StickerManager.delete] + /// * Discord API Reference: https://discord.com/developers/docs/resources/sticker#delete-guild-sticker + @override + Future delete({String? auditLogReason}) => manager.delete(id, auditLogReason: auditLogReason); } /// {@template guild_sticker} @@ -51,6 +69,7 @@ class GuildSticker extends PartialGuildSticker with Sticker { final int? sortValue; /// {@macro guild_sticker} + /// @nodoc GuildSticker({ required super.id, required super.manager, diff --git a/lib/src/models/sticker/sticker.dart b/lib/src/models/sticker/sticker.dart index 1173eb2a8..be4e35571 100644 --- a/lib/src/models/sticker/sticker.dart +++ b/lib/src/models/sticker/sticker.dart @@ -1,48 +1,28 @@ import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; -enum StickerType { - standard._(1), - guild._(2); +final class StickerType extends EnumLike { + static const standard = StickerType(1); + static const guild = StickerType(2); - /// The value of this [StickerType]. - final int value; + /// @nodoc + const StickerType(super.value); - const StickerType._(this.value); - - /// Parse a [StickerType] from a [value]. - /// - /// The [value] must be a valid sticker type - factory StickerType.parse(int value) => StickerType.values.firstWhere( - (state) => state.value == value, - orElse: () => throw FormatException('Unknown sticker type', value), - ); - - @override - String toString() => 'StickerType($value)'; + StickerType.parse(int value) : this(value); } -enum StickerFormatType { - png._(1), - apng._(2), - lottie._(3), - gif._(4); +final class StickerFormatType extends EnumLike { + static const png = StickerFormatType(1); + static const apng = StickerFormatType(2); + static const lottie = StickerFormatType(3); + static const gif = StickerFormatType(4); - /// The value of this [StickerFormatType]. - final int value; + /// @nodoc + const StickerFormatType(super.value); - const StickerFormatType._(this.value); - - /// Parse a [StickerFormatType] from a [value]. - /// - /// The [value] must be a valid sticker format type - factory StickerFormatType.parse(int value) => StickerFormatType.values.firstWhere( - (state) => state.value == value, - orElse: () => throw FormatException('Unknown sticker format type', value), - ); - - @override - String toString() => 'StickerFormatType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + StickerFormatType.parse(int value) : this(value); } /// Mixin with shared properties with stickers @@ -86,6 +66,7 @@ class StickerItem extends SnowflakeEntity { final StickerFormatType formatType; /// {@macro sticker_item} + /// @nodoc StickerItem({required super.id, required this.name, required this.formatType}); @override diff --git a/lib/src/models/subscription.dart b/lib/src/models/subscription.dart new file mode 100644 index 000000000..a949dfd92 --- /dev/null +++ b/lib/src/models/subscription.dart @@ -0,0 +1,81 @@ +import 'package:nyxx/src/http/managers/subscription_manager.dart'; +import 'package:nyxx/src/models/entitlement.dart'; +import 'package:nyxx/src/models/sku.dart'; +import 'package:nyxx/src/models/snowflake.dart'; +import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; + +/// A partial [Subscription]. +class PartialSubscription extends ManagedSnowflakeEntity { + @override + final SubscriptionManager manager; + + /// @nodoc + PartialSubscription({required this.manager, required super.id}); +} + +/// A subscription to an [Sku]. +class Subscription extends PartialSubscription { + /// The ID of the user this subscription is for. + final Snowflake userId; + + /// The IDs of the SKUs this subscription is for. + final List skuIds; + + /// The IDs of the entitlements this subscription grants. + final List entitlementIds; + + /// The start of the current subscription period. + final DateTime currentPeriodStart; + + /// The end of the current subscription period. + final DateTime currentPeriodEnd; + + /// The status of this subscription. + final SubscriptionStatus status; + + /// If this subscription was canceled, the time at which it was canceled. + /// + /// Otherwise, this field will be `null`. + final DateTime? canceledAt; + + /// The ISO3166-1 alpha-2 country code of the payment source used to purchase this subscription. + final String? countryCode; + + /// @nodoc + Subscription({ + required super.manager, + required super.id, + required this.userId, + required this.skuIds, + required this.entitlementIds, + required this.currentPeriodStart, + required this.currentPeriodEnd, + required this.status, + required this.canceledAt, + required this.countryCode, + }); + + /// The user this subscription is for. + PartialUser get user => manager.client.users[userId]; + + /// The SKUs this subscription is for. + List get skus => [ + for (final skuId in skuIds) manager.client.applications[manager.applicationId].skus[skuId], + ]; + + /// The entitlements this subscription grants. + List get entitlements => [ + for (final entitlementId in entitlementIds) manager.client.applications[manager.applicationId].entitlements[entitlementId], + ]; +} + +/// The status of a [Subscription]. +final class SubscriptionStatus extends EnumLike { + static const active = SubscriptionStatus(0); + static const ending = SubscriptionStatus(1); + static const inactive = SubscriptionStatus(2); + + const SubscriptionStatus(super.value); +} diff --git a/lib/src/models/team.dart b/lib/src/models/team.dart index 486e80a1d..24a176a7a 100644 --- a/lib/src/models/team.dart +++ b/lib/src/models/team.dart @@ -3,6 +3,7 @@ import 'package:nyxx/src/http/managers/application_manager.dart'; import 'package:nyxx/src/http/route.dart'; import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// {@template team} @@ -31,6 +32,7 @@ class Team with ToStringHelper { final Snowflake ownerId; /// {@macro team} + /// @nodoc Team({ required this.manager, required this.iconHash, @@ -70,6 +72,7 @@ class TeamMember with ToStringHelper { final TeamMemberRole role; /// {@macro team_member} + /// @nodoc TeamMember({ required this.membershipState, required this.teamId, @@ -79,46 +82,27 @@ class TeamMember with ToStringHelper { } /// The status of a member in a [Team]. -enum TeamMembershipState { - invited._(1), - accepted._(2); +final class TeamMembershipState extends EnumLike { + /// The user has been invited to the team. + static const invited = TeamMembershipState(1); - /// The value of this [TeamMembershipState]. - final int value; + /// The user has accepted the invitation to the team. + static const accepted = TeamMembershipState(2); - const TeamMembershipState._(this.value); + /// @nodoc + const TeamMembershipState(super.value); - /// Parse a [TeamMembershipState] from a [value]. - /// - /// The [value] must be a valid team membership state. - factory TeamMembershipState.parse(int value) => TeamMembershipState.values.firstWhere( - (state) => state.value == value, - orElse: () => throw FormatException('Unknown team membership state', value), - ); - - @override - String toString() => 'TeamMembershipState($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + TeamMembershipState.parse(int value) : this(value); } /// The role of a [TeamMember]. -enum TeamMemberRole { - admin._('admin'), - developer._('developer'), - readOnly._('read_only'); - - /// The value of this [TeamMemberRole]. - final String value; - - const TeamMemberRole._(this.value); - - /// Parse a [TeamMemberRole] from a [String]. - /// - /// The [value] must be a valid team member role. - factory TeamMemberRole.parse(String value) => TeamMemberRole.values.firstWhere( - (role) => role.value == value, - orElse: () => throw FormatException('Unknown team member role', value), - ); - - @override - String toString() => 'TeamMemberRole($value)'; +final class TeamMemberRole extends EnumLike { + static const admin = TeamMemberRole('admin'); + static const developer = TeamMemberRole('developer'); + static const readOnly = TeamMemberRole('read_only'); + + const TeamMemberRole(super.value); + + TeamMemberRole.parse(String value) : this(value); } diff --git a/lib/src/models/user/application_role_connection.dart b/lib/src/models/user/application_role_connection.dart index 4012b7934..3cddaf75e 100644 --- a/lib/src/models/user/application_role_connection.dart +++ b/lib/src/models/user/application_role_connection.dart @@ -17,6 +17,7 @@ class ApplicationRoleConnection with ToStringHelper { final Map metadata; /// {@macro application_role_connection} + /// @nodoc ApplicationRoleConnection({ required this.platformName, required this.platformUsername, diff --git a/lib/src/models/user/connection.dart b/lib/src/models/user/connection.dart index 427bff1a5..97c8e7008 100644 --- a/lib/src/models/user/connection.dart +++ b/lib/src/models/user/connection.dart @@ -1,4 +1,5 @@ import 'package:nyxx/src/models/guild/integration.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/to_string_helper/to_string_helper.dart'; /// A link to an account on a service other than Discord. @@ -37,6 +38,7 @@ class Connection with ToStringHelper { final ConnectionVisibility visibility; /// Create a new [Connection]. + /// @nodoc Connection({ required this.id, required this.name, @@ -57,6 +59,8 @@ class Connection with ToStringHelper { /// * Discord API Reference: https://discord.com/developers/docs/resources/user#connection-object-services enum ConnectionType { battleNet._('battlenet', 'Battle.net'), + bungieNet._('bungie', 'Bungie.net'), + domain._('domain', 'Domain'), ebay._('ebay', 'eBay'), epicGames._('epicgames', 'Epic Games'), facebook._('facebook', 'Facebook'), @@ -67,6 +71,7 @@ enum ConnectionType { playstation._('playstation', 'PlayStation Network'), reddit._('reddit', 'Reddit'), riotGames._('riotgames', 'Riot Games'), + roblox._('roblox', 'ROBLOX'), spotify._('spotify', 'Spotify'), skype._('skype', 'Skype'), steam._('steam', 'Steam'), @@ -87,36 +92,23 @@ enum ConnectionType { /// Parse a string to a [ConnectionType]. /// /// The [value] must be a string containing a valid [ConnectionType.value]. - factory ConnectionType.parse(String value) => ConnectionType.values.firstWhere( + factory ConnectionType.parse(String value) => values.firstWhere( (type) => type.value == value, orElse: () => throw FormatException('Unknown ConnectionType', value), ); - - @override - String toString() => 'ConnectionType($name)'; } /// The visibility level of a connection. /// /// External references: /// * Discord API Reference: https://discord.com/developers/docs/resources/user#connection-object-visibility-types -enum ConnectionVisibility { - none._(0), - everyone._(1); - - /// THe value of this connection visibility level. - final int value; - - const ConnectionVisibility._(this.value); +final class ConnectionVisibility extends EnumLike { + static const none = ConnectionVisibility(0); + static const everyone = ConnectionVisibility(1); - /// Parse an integer value to a [ConnectionVisibility]. - /// - /// The [value] must be a valid [ConnectionVisibility]. - factory ConnectionVisibility.parse(int value) => ConnectionVisibility.values.firstWhere( - (visibility) => visibility.value == value, - orElse: () => throw FormatException('Unknown ConnectionVisibility', value), - ); + /// @nodoc + const ConnectionVisibility(super.value); - @override - String toString() => 'ConnectionVisibility($name)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + ConnectionVisibility.parse(int value) : this(value); } diff --git a/lib/src/models/user/user.dart b/lib/src/models/user/user.dart index 7597c9610..3d19f2fc6 100644 --- a/lib/src/models/user/user.dart +++ b/lib/src/models/user/user.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/models/discord_color.dart'; import 'package:nyxx/src/models/locale.dart'; import 'package:nyxx/src/models/message/author.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; import 'package:nyxx/src/utils/flags.dart'; /// A partial [User] object. @@ -14,6 +15,7 @@ class PartialUser extends ManagedSnowflakeEntity { final UserManager manager; /// Create a new [PartialUser]. + /// @nodoc PartialUser({required super.id, required this.manager}); } @@ -72,6 +74,7 @@ class User extends PartialUser implements MessageAuthor, CommandOptionMentionabl final String? avatarDecorationHash; /// {@macro user} + /// @nodoc User({ required super.manager, required super.id, @@ -225,25 +228,15 @@ class UserFlags extends Flags { } /// The types of Discord Nitro subscription a user can have. -enum NitroType { - none._(0), - classic._(1), - nitro._(2), - basic._(3); - - /// The value of this [NitroType]. - final int value; - - const NitroType._(this.value); - - /// Parse an integer from the API to a [NitroType]. - /// - /// The [value] must be a valid nitro type. - factory NitroType.parse(int value) => NitroType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown NitroType', value), - ); +final class NitroType extends EnumLike { + static const none = NitroType(0); + static const classic = NitroType(1); + static const nitro = NitroType(2); + static const basic = NitroType(3); - @override - String toString() => 'NitroType($value)'; + /// @nodoc + const NitroType(super.value); + + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + NitroType.parse(int value) : this(value); } diff --git a/lib/src/models/voice/voice_region.dart b/lib/src/models/voice/voice_region.dart index e15d2c108..ab2fd6e83 100644 --- a/lib/src/models/voice/voice_region.dart +++ b/lib/src/models/voice/voice_region.dart @@ -25,6 +25,7 @@ class VoiceRegion with ToStringHelper { final bool isCustom; /// {@macro voice_region} + /// @nodoc VoiceRegion({ required this.id, required this.name, diff --git a/lib/src/models/voice/voice_state.dart b/lib/src/models/voice/voice_state.dart index 809876360..0c2536d77 100644 --- a/lib/src/models/voice/voice_state.dart +++ b/lib/src/models/voice/voice_state.dart @@ -25,6 +25,7 @@ class VoiceState with ToStringHelper { /// The ID of the user this state is for. final Snowflake userId; + /// The member this voice state is for. final Member? member; /// This state's session ID. @@ -55,6 +56,7 @@ class VoiceState with ToStringHelper { final DateTime? requestedToSpeakAt; /// {@macro voice_state} + /// @nodoc VoiceState({ required this.manager, required this.guildId, @@ -79,6 +81,7 @@ class VoiceState with ToStringHelper { bool get isMuted => isServerMuted || isSelfMuted; /// The key this voice state will have in the [NyxxRest.voice] cache. + @Deprecated('Use PartialGuild.voiceStates instead') Snowflake get cacheKey => Snowflake(Object.hash(guildId, userId)); /// The guild this voice state is in. diff --git a/lib/src/models/webhook.dart b/lib/src/models/webhook.dart index a482e76ab..81ae36e20 100644 --- a/lib/src/models/webhook.dart +++ b/lib/src/models/webhook.dart @@ -11,6 +11,7 @@ import 'package:nyxx/src/models/snowflake.dart'; import 'package:nyxx/src/models/snowflake_entity/snowflake_entity.dart'; import 'package:nyxx/src/http/managers/webhook_manager.dart'; import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/utils/enum_like.dart'; /// A partial [Webhook]. class PartialWebhook extends WritableSnowflakeEntity { @@ -18,6 +19,7 @@ class PartialWebhook extends WritableSnowflakeEntity { final WebhookManager manager; /// Create a new [PartialWebhook]. + /// @nodoc PartialWebhook({required super.id, required this.manager}); /// Update this webhook, returning the updated webhook. @@ -26,7 +28,8 @@ class PartialWebhook extends WritableSnowflakeEntity { /// * [WebhookManager.update] /// * Discord API Reference: https://discord.com/developers/docs/resources/webhook#modify-webhook @override - Future update(WebhookUpdateBuilder builder, {String? token}) => manager.update(id, builder, token: token); + Future update(WebhookUpdateBuilder builder, {String? token, String? auditLogReason}) => + manager.update(id, builder, token: token, auditLogReason: auditLogReason); /// Delete this webhook. /// @@ -45,8 +48,10 @@ class PartialWebhook extends WritableSnowflakeEntity { /// External references: /// * [WebhookManager.execute] /// * Discord API Reference: https://discord.com/developers/docs/resources/webhook#execute-webhook - Future execute(MessageBuilder builder, {required String token, bool? wait, Snowflake? threadId}) => - manager.execute(id, builder, token: token, wait: wait, threadId: threadId); + Future execute(MessageBuilder builder, + {required String token, bool? wait, Snowflake? threadId, String? threadName, List? appliedTags, String? username, String? avatarUrl}) => + manager.execute(id, builder, + token: token, wait: wait, threadId: threadId, threadName: threadName, appliedTags: appliedTags, username: username, avatarUrl: avatarUrl); /// Fetch a message sent by this webhook using its [token]. /// @@ -88,6 +93,7 @@ class WebhookAuthor extends PartialWebhook implements MessageAuthor { final String username; /// Create a new [WebhookAuthor]. + /// @nodoc WebhookAuthor({required super.id, required super.manager, required this.avatarHash, required this.username}); @override @@ -140,6 +146,7 @@ class Webhook extends PartialWebhook { final Uri? url; /// {@macro webhook} + /// @nodoc Webhook({ required super.id, required super.manager, @@ -167,29 +174,19 @@ class Webhook extends PartialWebhook { } /// The type of a [Webhook]. -enum WebhookType { +final class WebhookType extends EnumLike { /// A webhook which sends messages to a channel using a [Webhook.token]. - incoming._(1), + static const incoming = WebhookType(1); /// An internal webhook used to manage Channel Followers. - channelFollower._(2), + static const channelFollower = WebhookType(2); /// A webhook for use with interactions. - application._(3); + static const application = WebhookType(3); - /// The value of this webhook type. - final int value; + /// @nodoc + const WebhookType(super.value); - const WebhookType._(this.value); - - /// Parse a [WebhookType] from a [value]. - /// - /// The [value] must be a valid webhook type. - factory WebhookType.parse(int value) => WebhookType.values.firstWhere( - (type) => type.value == value, - orElse: () => throw FormatException('Unknown webhook type', value), - ); - - @override - String toString() => 'WebhookType($value)'; + @Deprecated('The .parse() constructor is deprecated. Use the unnamed constructor instead.') + WebhookType.parse(int value) : this(value); } diff --git a/lib/src/plugin/ignore_exceptions.dart b/lib/src/plugin/ignore_exceptions.dart index d54c41fff..e522efaf2 100644 --- a/lib/src/plugin/ignore_exceptions.dart +++ b/lib/src/plugin/ignore_exceptions.dart @@ -13,6 +13,7 @@ class IgnoreExceptions extends NyxxPlugin { String get name => 'IgnoreExceptions'; /// The logger used to report the errors. + @override Logger get logger => Logger('IgnoreExceptions'); static int _clients = 0; diff --git a/lib/src/plugin/logging.dart b/lib/src/plugin/logging.dart index a2ce512a2..2b29df65d 100644 --- a/lib/src/plugin/logging.dart +++ b/lib/src/plugin/logging.dart @@ -50,7 +50,9 @@ class Logging extends NyxxPlugin { StringSink? stdout, StringSink? stderr, }) : stdout = stdout ?? io.stdout, - stderr = stderr ?? io.stderr; + stderr = stderr ?? io.stderr { + _listenIfNeeded(); + } static int _clients = 0; @@ -108,7 +110,7 @@ class Logging extends NyxxPlugin { } } - final outSink = rec.level > stderrLevel ? stderr : stdout; + final outSink = rec.level >= stderrLevel ? stderr : stdout; outSink.write(messageString); }); } @@ -130,7 +132,6 @@ class Logging extends NyxxPlugin { } _clients++; - _listenIfNeeded(); } @override diff --git a/lib/src/plugin/plugin.dart b/lib/src/plugin/plugin.dart index 237863681..ebf882562 100644 --- a/lib/src/plugin/plugin.dart +++ b/lib/src/plugin/plugin.dart @@ -5,6 +5,11 @@ import 'package:meta/meta.dart'; import 'package:nyxx/src/api_options.dart'; import 'package:nyxx/src/client.dart'; import 'package:nyxx/src/client_options.dart'; +import 'package:nyxx/src/gateway/message.dart'; +import 'package:nyxx/src/gateway/shard.dart'; +import 'package:nyxx/src/http/handler.dart'; +import 'package:nyxx/src/http/request.dart'; +import 'package:nyxx/src/http/response.dart'; import 'package:runtime_type/runtime_type.dart'; /// Provides access to the connection and closing process for implementing plugins. @@ -38,6 +43,7 @@ abstract class NyxxPlugin { /// Perform the close operation. /// /// People overriding this method should call it to obtain the client instance. + @mustCallSuper Future doClose(ClientType client, Future Function() close) async { final state = _states[client]; await state?.beforeClose(client); @@ -54,17 +60,50 @@ abstract class NyxxPlugin { /// instances of the plugin attached to other clients. FutureOr>> createState() => NyxxPluginState(this); + /// {@template before_connect} /// Called before each client this plugin is added to connects. + /// {@endtemplate} FutureOr beforeConnect(ApiOptions apiOptions, ClientOptions clientOptions) {} + /// {@template after_connect} /// Called after each client this plugin is added to connects. + /// {@endtemplate} FutureOr afterConnect(ClientType client) {} + /// {@template before_close} /// Called before each client this plugin is added to closes. + /// {@endtemplate} FutureOr beforeClose(ClientType client) {} + /// {@template after_close} /// Called after each client this plugin is added to closes. + /// {@endtemplate} FutureOr afterClose() {} + + /// {@template intercept_request} + /// Called whenever a request is made using a client's [HttpHandler]. + /// + /// Plugins that implement this method are not required to call the [next] method. + /// {@endtemplate} + @mustCallSuper + Future interceptRequest(ClientType client, HttpRequest request, Future Function(HttpRequest) next) { + final state = _states[client]; + return state?.interceptRequest(client, request, next) ?? next(request); + } + + /// {@template intercept_shard_messages} + /// Intercept [ShardMessage]s by transforming the [messages] stream. + /// {@endtemplate} + @mustCallSuper + Stream interceptShardMessages(Shard shard, Stream messages) => + _states[shard.client]?.interceptShardMessages(shard, messages) ?? messages; + + /// {@template intercept_gateway_messages} + /// Intercept [GatewayMessage]s by transforming the [messages] stream. + /// {@endtemplate} + @mustCallSuper + Stream interceptGatewayMessages(Shard shard, Stream messages) => + _states[shard.client]?.interceptGatewayMessages(shard, messages) ?? messages; } /// Holds the state of a plugin added to a client. @@ -78,19 +117,28 @@ class NyxxPluginState beforeConnect(ApiOptions apiOptions, ClientOptions clientOptions) => plugin.beforeConnect(apiOptions, clientOptions); - /// Called after each client this plugin is added to connects. + /// {@macro after_connect} @mustCallSuper FutureOr afterConnect(ClientType client) => plugin.afterConnect(client); - /// Called before each client this plugin is added to closes. + /// {@macro before_close} @mustCallSuper FutureOr beforeClose(ClientType client) => plugin.beforeClose(client); - /// Called after each client this plugin is added to closes. + /// {@macro after_close} @mustCallSuper FutureOr afterClose() => plugin.afterClose(); + + /// {@macro intercept_request} + Future interceptRequest(ClientType client, HttpRequest request, Future Function(HttpRequest) next) => next(request); + + /// {@macro intercept_shard_messages} + Stream interceptShardMessages(Shard shard, Stream messages) => messages; + + /// {@macro intercept_gateway_messages} + Stream interceptGatewayMessages(Shard shard, Stream messages) => messages; } diff --git a/lib/src/utils/cache_helpers.dart b/lib/src/utils/cache_helpers.dart new file mode 100644 index 000000000..3b343ce61 --- /dev/null +++ b/lib/src/utils/cache_helpers.dart @@ -0,0 +1,308 @@ +import 'package:nyxx/src/client.dart'; +import 'package:nyxx/src/models/channel/channel.dart'; +import 'package:nyxx/src/models/channel/stage_instance.dart'; +import 'package:nyxx/src/models/channel/thread.dart'; +import 'package:nyxx/src/models/channel/thread_list.dart'; +import 'package:nyxx/src/models/channel/types/dm.dart'; +import 'package:nyxx/src/models/channel/types/group_dm.dart'; +import 'package:nyxx/src/models/commands/application_command.dart'; +import 'package:nyxx/src/models/commands/application_command_permissions.dart'; +import 'package:nyxx/src/models/emoji.dart'; +import 'package:nyxx/src/models/entitlement.dart'; +import 'package:nyxx/src/models/gateway/event.dart'; +import 'package:nyxx/src/models/gateway/events/application_command.dart'; +import 'package:nyxx/src/models/gateway/events/auto_moderation.dart'; +import 'package:nyxx/src/models/gateway/events/channel.dart'; +import 'package:nyxx/src/models/gateway/events/entitlement.dart'; +import 'package:nyxx/src/models/gateway/events/guild.dart'; +import 'package:nyxx/src/models/gateway/events/integration.dart'; +import 'package:nyxx/src/models/gateway/events/interaction.dart'; +import 'package:nyxx/src/models/gateway/events/invite.dart'; +import 'package:nyxx/src/models/gateway/events/message.dart'; +import 'package:nyxx/src/models/gateway/events/presence.dart'; +import 'package:nyxx/src/models/gateway/events/ready.dart'; +import 'package:nyxx/src/models/gateway/events/stage_instance.dart'; +import 'package:nyxx/src/models/gateway/events/voice.dart'; +import 'package:nyxx/src/models/gateway/events/webhook.dart'; +import 'package:nyxx/src/models/guild/audit_log.dart'; +import 'package:nyxx/src/models/guild/auto_moderation.dart'; +import 'package:nyxx/src/models/guild/ban.dart'; +import 'package:nyxx/src/models/guild/guild.dart'; +import 'package:nyxx/src/models/guild/guild_preview.dart'; +import 'package:nyxx/src/models/guild/integration.dart'; +import 'package:nyxx/src/models/guild/member.dart'; +import 'package:nyxx/src/models/guild/scheduled_event.dart'; +import 'package:nyxx/src/models/guild/template.dart'; +import 'package:nyxx/src/models/interaction.dart'; +import 'package:nyxx/src/models/invite/invite.dart'; +import 'package:nyxx/src/models/message/message.dart'; +import 'package:nyxx/src/models/presence.dart'; +import 'package:nyxx/src/models/role.dart'; +import 'package:nyxx/src/models/sticker/global_sticker.dart'; +import 'package:nyxx/src/models/sticker/guild_sticker.dart'; +import 'package:nyxx/src/models/sticker/sticker_pack.dart'; +import 'package:nyxx/src/models/sku.dart'; +import 'package:nyxx/src/models/subscription.dart'; +import 'package:nyxx/src/models/user/user.dart'; +import 'package:nyxx/src/models/voice/voice_state.dart'; +import 'package:nyxx/src/models/webhook.dart'; + +extension CacheUpdates on NyxxRest { + /// Update the caches for this client using [entity] by registering (or removing, if [entity] is a delete event) any cacheable entities reachable from [entity]. + void updateCacheWith(Object? entity) => switch (entity) { + // "Root" types - with their own cache + + VoiceState() => () { + // ignore: deprecated_member_use_from_same_package + entity.manager.cache[entity.cacheKey] = entity; + entity.guild?.voiceStates[entity.userId] = entity; + + updateCacheWith(entity.member); + }(), + StageInstance() => entity.manager.stageInstanceCache[entity.id] = entity, + CommandPermissions() => entity.manager.permissionsCache[entity.id] = entity, + AuditLogEntry() => entity.manager.cache[entity.id] = entity, + Channel() => () { + entity.manager.cache[entity.id] = entity; + + if (entity case DmChannel(:final recipient)) { + updateCacheWith(recipient); + } + if (entity case GroupDmChannel(:final recipients)) { + recipients.forEach(updateCacheWith); + } + }(), + Entitlement() => entity.manager.cache[entity.id] = entity, + Integration() => () { + entity.manager.cache[entity.id] = entity; + + updateCacheWith(entity.user); + }(), + GlobalSticker() => entity.manager.cache[entity.id] = entity, + User() => entity.manager.cache[entity.id] = entity, + ApplicationCommand() => entity.manager.cache[entity.id] = entity, + AutoModerationRule() => entity.manager.cache[entity.id] = entity, + Emoji() => () { + entity.manager.cache[entity.id] = entity; + + if (entity case GuildEmoji(:final user?)) { + updateCacheWith(user); + } + + if (entity case ApplicationEmoji(:final user)) { + updateCacheWith(user); + } + }(), + Guild() => () { + entity.manager.cache[entity.id] = entity; + + entity.roleList.forEach(updateCacheWith); + entity.emojiList.forEach(updateCacheWith); + entity.stickerList.forEach(updateCacheWith); + }(), + Member() => () { + entity.manager.cache[entity.id] = entity; + + updateCacheWith(entity.user); + }(), + Message() => () { + entity.manager.cache[entity.id] = entity; + + updateCacheWith(entity.author); + entity.mentions.forEach(updateCacheWith); + updateCacheWith(entity.referencedMessage); + updateCacheWith(entity.interaction); // ignore: deprecated_member_use_from_same_package + updateCacheWith(entity.thread); + updateCacheWith(entity.resolved); + }(), + Role() => entity.manager.cache[entity.id] = entity, + ScheduledEvent() => () { + entity.manager.cache[entity.id] = entity; + + updateCacheWith(entity.creator); + }(), + GuildSticker() => entity.manager.cache[entity.id] = entity, + Webhook() => () { + entity.manager.cache[entity.id] = entity; + + updateCacheWith(entity.user); + }(), + Sku() => entity.manager.cache[entity.id] = entity, + Subscription() => entity.manager.cache[entity.id] = entity, + + // "Aggregate" types - objects that contain other (potentially root) objects + + ThreadList(:final threads, :final members) => () { + threads.forEach(updateCacheWith); + members.forEach(updateCacheWith); + }(), + ThreadMember(:final member) => updateCacheWith(member), + // ignore: deprecated_member_use_from_same_package + MessageInteraction(:final user) => updateCacheWith(user), + ResolvedData(:final users, :final roles, :final members) => () { + users?.values.forEach(updateCacheWith); + roles?.values.forEach(updateCacheWith); + members?.values.forEach(updateCacheWith); + }(), + Activity(:final emoji) => updateCacheWith(emoji), + Interaction(:final data, :final member, :final user, :final message, :final entitlements) => () { + updateCacheWith(member); + updateCacheWith(user); + updateCacheWith(message); + entitlements.forEach(updateCacheWith); + + if (data case ApplicationCommandInteractionData(:final resolved) || MessageComponentInteractionData(:final resolved)) { + updateCacheWith(resolved); + } + }(), + Invite(:final inviter, :final targetUser, :final guildScheduledEvent) => () { + updateCacheWith(inviter); + updateCacheWith(targetUser); + updateCacheWith(guildScheduledEvent); + }(), + GuildPreview(:final emojiList, :final stickerList) => () { + emojiList.forEach(updateCacheWith); + stickerList.forEach(updateCacheWith); + }(), + Ban(:final user) => updateCacheWith(user), + // Don't update cache for serializedSourceGuild since it is populated with some fake data. + GuildTemplate(:final creator) => updateCacheWith(creator), + ScheduledEventUser(:final user, :final member) => () { + updateCacheWith(user); + updateCacheWith(member); + }(), + StickerPack(:final stickers) => stickers.forEach(updateCacheWith), + + // Events + + ReadyEvent(:final user) => updateCacheWith(user), + ResumedEvent() => null, + ApplicationCommandPermissionsUpdateEvent(:final permissions) => updateCacheWith(permissions), + AutoModerationRuleCreateEvent(:final rule) => updateCacheWith(rule), + AutoModerationRuleUpdateEvent(:final rule) => updateCacheWith(rule), + AutoModerationRuleDeleteEvent(:final rule) => rule.manager.cache.remove(rule.id), + AutoModerationActionExecutionEvent() => null, + ChannelCreateEvent(:final channel) => updateCacheWith(channel), + ChannelUpdateEvent(:final channel) => updateCacheWith(channel), + ChannelDeleteEvent(:final channel) => channel.manager.cache.remove(channel.id), + ThreadCreateEvent(:final thread) => updateCacheWith(thread), + ThreadUpdateEvent(:final thread) => updateCacheWith(thread), + ThreadDeleteEvent(:final thread) => thread.manager.cache.remove(thread.id), + ThreadListSyncEvent(:final threads, :final members) => () { + threads.forEach(updateCacheWith); + members.forEach(updateCacheWith); + }(), + ThreadMemberUpdateEvent(:final member) => updateCacheWith(member), + ThreadMembersUpdateEvent(:final addedMembers) => addedMembers?.forEach(updateCacheWith), + ChannelPinsUpdateEvent() => null, + UnavailableGuildCreateEvent() => () { + if (entity + case GuildCreateEvent( + :final guild, + :final voiceStates, + :final members, + :final channels, + :final threads, + :final presences, + :final stageInstances, + :final scheduledEvents, + )) { + updateCacheWith(guild); + voiceStates.forEach(updateCacheWith); + members.forEach(updateCacheWith); + channels.forEach(updateCacheWith); + threads.forEach(updateCacheWith); + presences.forEach(updateCacheWith); + stageInstances.forEach(updateCacheWith); + scheduledEvents.forEach(updateCacheWith); + } + }(), + GuildUpdateEvent(:final guild) => updateCacheWith(guild), + GuildDeleteEvent(:final guild) => guild.manager.cache.remove(guild.id), + GuildAuditLogCreateEvent(:final entry) => updateCacheWith(entry), + GuildBanAddEvent(:final user, :final guild) => () { + guild.members.cache.remove(user.id); + updateCacheWith(user); + }(), + GuildBanRemoveEvent(:final user) => updateCacheWith(user), + GuildEmojisUpdateEvent(:final emojis, :final guild) => () { + guild.emojis.cache.clear(); + emojis.forEach(updateCacheWith); + }(), + GuildStickersUpdateEvent(:final stickers, :final guild) => () { + guild.stickers.cache.clear(); + stickers.forEach(updateCacheWith); + }(), + GuildIntegrationsUpdateEvent() => null, + GuildMemberAddEvent(:final member) => updateCacheWith(member), + GuildMemberRemoveEvent(:final user, :final guild) => () { + guild.members.cache.remove(user.id); + updateCacheWith(user); + }(), + GuildMemberUpdateEvent(:final member) => updateCacheWith(member), + GuildMembersChunkEvent(:final members, :final presences) => () { + members.forEach(updateCacheWith); + presences?.forEach(updateCacheWith); + }(), + GuildRoleCreateEvent(:final role) => updateCacheWith(role), + GuildRoleUpdateEvent(:final role) => updateCacheWith(role), + GuildRoleDeleteEvent(:final roleId, :final guild) => guild.roles.cache.remove(roleId), + GuildScheduledEventCreateEvent(:final event) => updateCacheWith(event), + GuildScheduledEventUpdateEvent(:final event) => updateCacheWith(event), + GuildScheduledEventDeleteEvent(:final event) => event.manager.cache.remove(event.id), + GuildScheduledEventUserAddEvent() => null, + GuildScheduledEventUserRemoveEvent() => null, + IntegrationCreateEvent(:final integration) => updateCacheWith(integration), + IntegrationUpdateEvent(:final integration) => updateCacheWith(integration), + IntegrationDeleteEvent(:final id, :final guild) => guild.integrations.cache.remove(id), + InviteCreateEvent(:final invite) => updateCacheWith(invite), + InviteDeleteEvent() => null, + MessageCreateEvent(:final message, :final mentions) => () { + updateCacheWith(message); + mentions.forEach(updateCacheWith); + }(), + MessageUpdateEvent(:final message, :final mentions) => () { + // We only get a partial message, but we know it invalidates the message currently in the cache. So we remove the cached message. + message.manager.cache.remove(message.id); + mentions?.forEach(updateCacheWith); + }(), + MessageDeleteEvent(:final id, :final channel) => channel.messages.cache.remove(id), + MessageBulkDeleteEvent(:final ids, :final channel) => ids.forEach(channel.messages.cache.remove), + MessageReactionAddEvent(:final emoji, :final member) => () { + updateCacheWith(emoji); + updateCacheWith(member); + }(), + MessageReactionRemoveEvent(:final emoji) => updateCacheWith(emoji), + MessageReactionRemoveAllEvent() => null, + MessageReactionRemoveEmojiEvent() => null, + PresenceUpdateEvent(:final activities) => activities?.forEach(updateCacheWith), + TypingStartEvent(:final member) => updateCacheWith(member), + UserUpdateEvent(:final user) => updateCacheWith(user), + VoiceStateUpdateEvent(:final state) => updateCacheWith(state), + VoiceServerUpdateEvent() => null, + WebhooksUpdateEvent() => null, + InteractionCreateEvent(:final interaction) => updateCacheWith(interaction), + StageInstanceCreateEvent(:final instance) => updateCacheWith(instance), + StageInstanceUpdateEvent(:final instance) => updateCacheWith(instance), + StageInstanceDeleteEvent(:final instance) => instance.manager.cache.remove(instance.id), + EntitlementCreateEvent(:final entitlement) => updateCacheWith(entitlement), + EntitlementUpdateEvent(:final entitlement) => updateCacheWith(entitlement), + EntitlementDeleteEvent(:final entitlement) => entitlement.manager.cache.remove(entitlement.id), + MessagePollVoteAddEvent() => null, + MessagePollVoteRemoveEvent() => null, + + // null and unhandled entity types + WebhookAuthor() => null, + UnknownDispatchEvent() => null, + null => null, + _ => () { + assert(() { + logger + ..warning('Tried to update cache for ${entity.runtimeType}, but that type was not handled.') + ..info( + 'This is a bug, please report it to https://github.com/nyxx-discord/nyxx/issues or on our Discord server. Your client will still work regardless, so you can also ignore this message.'); + return true; + }()); + }(), + }; +} diff --git a/lib/src/utils/enum_like.dart b/lib/src/utils/enum_like.dart new file mode 100644 index 000000000..f16c7b45b --- /dev/null +++ b/lib/src/utils/enum_like.dart @@ -0,0 +1,16 @@ +base class EnumLike> { + /// The value this enum-like holds. + final T value; + + /// @nodoc + const EnumLike(this.value); + + @override + String toString() => '$runtimeType($value)'; + + @override + int get hashCode => value.hashCode; + + @override + bool operator ==(Object other) => identical(this, other) || (other is U && other.value == value); +} diff --git a/pubspec.yaml b/pubspec.yaml index 4b1b54cc7..1780d55bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 6.0.0 +version: 6.4.3 description: A complete, robust and efficient wrapper around Discord's API for bots & applications. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx diff --git a/test/integration/async_dispose_test.dart b/test/integration/async_dispose_test.dart new file mode 100644 index 000000000..4fc65e7a8 --- /dev/null +++ b/test/integration/async_dispose_test.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:nyxx/nyxx.dart'; +import 'package:test/test.dart'; + +void main() { + final testToken = Platform.environment['TEST_TOKEN']; + + test('client.close() disposes all async resources', skip: testToken != null ? false : 'No test token provided', () async { + final receivePort = ReceivePort(); + + Future createAndDisposeClient(SendPort sendPort) async { + // Re-declare so we don't attempt to copy the outer context to the isolate. + final testToken = Platform.environment['TEST_TOKEN']; + final testGuild = Platform.environment['TEST_GUILD']; + + final client = await Nyxx.connectGatewayWithOptions(GatewayApiOptions( + token: testToken!, + intents: GatewayIntents.allUnprivileged, + totalShards: 10, // Use many shards to ensure the client is still connecting some shards when we close it. + )); + + final user = await client.user.get(); + + await client.onReady.first; + + // Queue many shard messages to ensure the rate limiter schedules them at a later time. + for (int i = 0; i < 200; i++) { + client.gateway.updatePresence(PresenceBuilder(status: CurrentUserStatus.online, isAfk: false)); + } + + // Get a handle to all async resources exposed on the client. + + // Also handles all the onXXX streams, as they are derived from onEvent. + final clientEvents = client.onEvent.listen((_) {}); + final gatewayMessages = client.gateway.messages.listen((_) {}); + final gatewayEvents = client.gateway.events.listen((_) {}); + final membersStream = (testGuild == null ? Stream.empty() : client.gateway.listGuildMembers(Snowflake.parse(testGuild))).listen((_) {}); + final shards = [ + for (final shard in client.gateway.shards) shard.listen((_) {}), + ]; + final shardReceiveStreams = [ + for (final shard in client.gateway.shards) shard.receiveStream.listen((_) {}), + ]; + final requests = client.httpHandler.onRequest.listen((_) {}); + final responses = client.httpHandler.onResponse.listen((_) {}); + final rateLimits = client.httpHandler.onRateLimit.listen((_) {}); + final assetStream = user.avatar.fetchStreamed().listen((_) {}); + + // This single request should be representative of all methods on managers that + // create a request from a known route, execute it using httpHandler.executeSafe, + // and then parse the result (the vast majority of all manager methods). + final userRequest = client.user.fetch(); + final assetRequest = user.avatar.fetch(); + final shardsDone = [ + for (final shard in client.gateway.shards) shard.done, + ]; + final rateLimitedRequest = Future.wait([ + // GET /gateway/bot seems to have high rate limits. 10 requests should be enough to trigger it. + for (int i = 0; i < 10; i++) client.gateway.fetchGatewayBot(), + ]); + + sendPort.send('closing'); + + // Create the future before calling close so that any error handlers are installed before + // the close happens, in order to avoid any uncaught errors. + final disposedFuture = Future.wait([ + // Streams + clientEvents.asFuture(), + gatewayMessages.asFuture(), + gatewayEvents.asFuture(), + membersStream.asFuture(), + ...shards.map((s) => s.asFuture()), + ...shardReceiveStreams.map((s) => s.asFuture()), + requests.asFuture(), + responses.asFuture(), + rateLimits.asFuture(), + assetStream.asFuture(), + + // Futures + userRequest, + assetRequest, + ...shardsDone, + rateLimitedRequest, + ]); + + await client.close(); + + sendPort.send('closed'); + + // Expect all the async operations to finish in some way or another. + try { + await disposedFuture; + } catch (e) { + // Erroring is an accepted way of dealing with `client.close()` being called during an async operation. + } + + sendPort.send('done'); + } + + final isolate = await Isolate.spawn(createAndDisposeClient, receivePort.sendPort, paused: true); + isolate.addOnExitListener(receivePort.sendPort, response: 'exited'); + isolate.resume(isolate.pauseCapability!); + + var isDone = false; + Timer? failTimer; + + final subscription = receivePort.listen((message) { + if (message == 'closing') { + failTimer = Timer(Duration(seconds: 1), () { + receivePort.close(); + + // Plugins can intercept calls to close(), but we should try to make calls to close() without any plugins as + // fast as possible. + // There is some networking involved (closing the WS connection has to send a close frame), so we allow this + // to take _some_ time. Just not too much. + fail('Client took more than a second to close'); + }); + } else if (message == 'closed') { + failTimer!.cancel(); + failTimer = Timer(Duration(milliseconds: 50), () { + receivePort.close(); + + fail('Pending async operations did not complete in 50ms'); + }); + } else if (message == 'done') { + isDone = true; + + failTimer!.cancel(); + failTimer = Timer(Duration(milliseconds: 500), () { + receivePort.close(); + + // If any async operations are still pending in the isolate (which they shouldn't), they will keep it alive. + // Therefore we expect the isolate to exit immediately after client.close() completes, since that should be + // the last async operation performed. + // We allow up to 500ms of "wiggle room" to account for cross-isolate communication and isolate shutdown time. + // This delay may not be enough to prevent this test from failing on very slow devices, or if the OS schedules + // the threads execution in an unfortunate order. If this test is failing, be absolutely sure it's not failing + // for some other reason before increasing this delay. + fail('Isolate did not shut down in 500ms'); + }); + } else { + assert(message == 'exited'); + + failTimer!.cancel(); + + // Completes the test. + receivePort.close(); + + // If isDone is false, then we didn't properly dispose of all async resources and left some "hanging", so + // awaiting them caused the isolate to exit prematurely. + // This also fails the test if the isolate exits because of an error. + expect(isDone, isTrue, reason: 'isolate entrypoint should run to completion'); + } + }); + + await subscription.asFuture(); + }); +} diff --git a/test/integration/gateway_integration_test.dart b/test/integration/gateway_integration_test.dart index c14aba122..d7a47d80e 100644 --- a/test/integration/gateway_integration_test.dart +++ b/test/integration/gateway_integration_test.dart @@ -14,7 +14,7 @@ void main() { late NyxxGateway client; await expectLater(() async => client = await Nyxx.connectGatewayWithOptions(options), completes); - expect(client.gateway.messages.where((event) => event is ErrorReceived), emitsDone); + expect(client.gateway.messages, neverEmits(isA())); await expectLater(client.onEvent, emits(isA())); await expectLater(client.close(), completes); } @@ -68,6 +68,28 @@ void main() { payloadFormat: GatewayPayloadFormat.etf, )), ); + + test('Multiple shards', () async { + const shardCount = 5; + + late NyxxGateway client; + + await expectLater( + () async => client = await Nyxx.connectGatewayWithOptions( + GatewayApiOptions( + token: testToken!, + intents: GatewayIntents.none, + totalShards: shardCount, + ), + ), + completes, + ); + expect(client.gateway.messages.where((event) => event is ErrorReceived), emitsDone); + for (int i = 0; i < shardCount; i++) { + await expectLater(client.onEvent, emits(isA())); + } + await expectLater(client.close(), completes); + }); }); group('NyxxGateway', skip: testToken != null ? false : 'No test token provided', () { @@ -79,6 +101,8 @@ void main() { if (testGuild != null) { await client.onGuildCreate.firstWhere((event) => event is GuildCreateEvent && event.guild.id == Snowflake.parse(testGuild)); + } else { + await client.onReady.first; } }); @@ -90,7 +114,7 @@ void main() { final guildId = Snowflake.parse(testGuild!); // We can't list all guild members since we don't have the GUILD_MEMBERS intent, so just search for the current user - final currentUser = await client.users.fetchCurrentUser(); + final currentUser = await client.user.get(); await expectLater(client.gateway.listGuildMembers(guildId, query: currentUser.username).drain(), completes); }); @@ -112,5 +136,34 @@ void main() { expect(client.gateway.latency, greaterThan(Duration.zero)); }); }); + + test('buffers messages until shard is ready', () async { + // This test needs its own client as we need to send an event before the shard is ready + final client = await Nyxx.connectGateway(testToken!, GatewayIntents.none); + + client.gateway.updatePresence(PresenceBuilder(status: CurrentUserStatus.idle, isAfk: false)); + + expect(client.gateway.messages, neverEmits(isA())); + + await expectLater(client.close(), completes); + }); + + test('rate limits gateway events', () async { + for (int i = 0; i < 200; i++) { + client.gateway.updatePresence(PresenceBuilder(status: CurrentUserStatus.idle, isAfk: false)); + } + + ErrorReceived? receivedError; + + final errorSubscription = client.gateway.messages.listen((event) { + if (event is ErrorReceived) receivedError = event; + }); + + // Give time for the disconnection to occur, if any. + await Future.delayed(Duration(seconds: 10)); + + expect(receivedError, isNull); + errorSubscription.cancel(); + }); }); } diff --git a/test/integration/rest_integration_test.dart b/test/integration/rest_integration_test.dart index bf60540bb..cce5f9b58 100644 --- a/test/integration/rest_integration_test.dart +++ b/test/integration/rest_integration_test.dart @@ -49,6 +49,9 @@ void main() { }); tearDownAll(() async { + // Reset commands state in case we failed a test without deleting them. + await client.commands.bulkOverride([]); + await client.close(); }); @@ -64,14 +67,26 @@ void main() { late Application application; await expectLater(() async => application = await client.applications.fetchCurrentApplication(), completes); - await expectLater(application.listSkus(), completes); await expectLater(client.applications.updateCurrentApplication(ApplicationUpdateBuilder(description: application.description)), completes); }); + test('skus', () async { + await expectLater(client.application.skus.list(), completes); + }); + test('users', () async { await expectLater(client.users.fetchCurrentUser(), completes); await expectLater(client.users.listCurrentUserGuilds(), completes); await expectLater(client.users.fetchCurrentUserConnections(), completes); + + final avatar = (await client.user.get()).avatar; + + await expectLater( + client.users.updateCurrentUser(UserUpdateBuilder( + avatar: ImageBuilder(data: await avatar.fetch(), format: avatar.defaultFormat.extension), + )), + completes, + ); }); test('channels', skip: testTextChannel != null ? false : 'No test channel provided', () async { @@ -113,6 +128,21 @@ void main() { await expectLater(message.pin(), completes); await expectLater(message.unpin(), completes); + await expectLater( + () async => message = await channel.sendMessage(MessageBuilder( + referencedMessage: MessageReferenceBuilder.forward(messageId: message.id, channelId: channelId), + )), + completes, + ); + + await expectLater( + message.reference?.message?.delete(), + allOf( + isNotNull, + completes, + ), + ); + await expectLater(message.delete(), completes); await expectLater( @@ -130,7 +160,18 @@ void main() { await expectLater(message.attachments.first.fetch(), completes); await expectLater(message.attachments.first.fetchStreamed().drain(), completes); - await expectLater(message.delete(), completes); + late Message message2; + await expectLater( + () async => message2 = await channel.sendMessage(MessageBuilder( + attachments: [ + await AttachmentBuilder.fromFile(File('test/files/2.png')), + await AttachmentBuilder.fromFile(File('test/files/3.png')), + ], + )), + completes, + ); + + await expectLater(channel.messages.bulkDelete([message.id, message2.id]), completes); await expectLater( () async => message = await channel.sendMessage( @@ -177,6 +218,35 @@ void main() { ); await expectLater(message.delete(), completes); + + await expectLater( + () async => message = await channel.sendMessage( + MessageBuilder( + content: 'Polls test', + poll: PollBuilder( + question: PollMediaBuilder(text: 'Question'), + answers: [ + PollAnswerBuilder(pollMedia: PollMediaBuilder(text: 'Answer 1')), + PollAnswerBuilder( + pollMedia: + PollMediaBuilder(text: 'Answer 2', emoji: TextEmoji(id: Snowflake.zero, manager: client.guilds[Snowflake.zero].emojis, name: '👽'))), + PollAnswerBuilder.text('Answer 3'), + PollAnswerBuilder.text('Answer 4', TextEmoji(id: Snowflake.zero, manager: client.guilds[Snowflake.zero].emojis, name: '👽')) + ], + duration: Duration(hours: 5)), + ), + ), + completes, + ); + + expect(message.poll, isNotNull); + final poll = message.poll!; + + expect(poll.answers, hasLength(4)); + + await expectLater(message.fetchAnswerVoters(poll.answers[0].id), completes); + await expectLater(message.endPoll(), completes); + await expectLater(message.delete(), completes); }); test('webhooks', skip: testTextChannel != null ? false : 'No test channel provided', () async { @@ -226,7 +296,7 @@ void main() { ); await expectLater( - webhook.delete(), + webhook.delete(auditLogReason: 'Testing Unicode in audit log reason 😀'), completes, ); }); @@ -334,5 +404,33 @@ void main() { } } }); + + test('commands', () async { + late ApplicationCommand command; + + await expectLater( + () async => command = await client.commands.create(ApplicationCommandBuilder.chatInput(name: 'test', description: 'A test command', options: [])), + completes, + ); + + await expectLater(command.fetch(), completes); + await expectLater(command.update(ApplicationCommandUpdateBuilder.chatInput(name: 'new_name')), completes); + await expectLater(client.commands.list(), completion(contains(command))); + await expectLater( + () async => command = + (await client.commands.bulkOverride([ApplicationCommandBuilder.chatInput(name: 'test_2', description: 'A test command', options: [])])).single, + completes, + ); + + if (testGuild != null) { + final testGuildId = Snowflake.parse(testGuild); + final guild = client.guilds[testGuildId]; + + await expectLater(guild.commands.listPermissions(), completes); + await expectLater(guild.commands.fetchPermissions(command.id), completes); + } + + await expectLater(command.delete(), completes); + }); }); } diff --git a/test/mocks/client.dart b/test/mocks/client.dart index 7742eb43a..a36a96b4e 100644 --- a/test/mocks/client.dart +++ b/test/mocks/client.dart @@ -7,9 +7,18 @@ import 'gateway.dart'; class MockNyxx with Mock, ManagerMixin implements NyxxRest { @override PartialApplication get application => applications[Snowflake.zero]; + + @override + PartialUser get user => users[Snowflake.zero]; + + @override + late final CacheManager cache = CacheManager(this); } class MockNyxxGateway with Mock, ManagerMixin implements NyxxGateway { @override Gateway get gateway => MockGateway(); + + @override + late final CacheManager cache = CacheManager(this); } diff --git a/test/unit/builders/interaction_response_test.dart b/test/unit/builders/interaction_response_test.dart new file mode 100644 index 000000000..e5606a588 --- /dev/null +++ b/test/unit/builders/interaction_response_test.dart @@ -0,0 +1,20 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:test/test.dart'; + +void main() { + group('InteractionResponseBuilder', () { + test('autocompleteResult', () { + expect( + InteractionResponseBuilder.autocompleteResult([CommandOptionChoiceBuilder(name: 'foo', value: 'bar')]).build(), + equals({ + 'type': 8, + 'data': { + 'choices': [ + {'name': 'foo', 'value': 'bar'}, + ] + } + }), + ); + }); + }); +} diff --git a/test/unit/builders/message/message_test.dart b/test/unit/builders/message/message_test.dart index f703e1188..dec3cb446 100644 --- a/test/unit/builders/message/message_test.dart +++ b/test/unit/builders/message/message_test.dart @@ -30,7 +30,6 @@ void main() { ), ], nonce: '1234', - replyId: null, stickerIds: [Snowflake.zero], suppressEmbeds: false, suppressNotifications: true, diff --git a/test/unit/builders/permission_overwrite_test.dart b/test/unit/builders/permission_overwrite_test.dart index 67165395a..fad3f976e 100644 --- a/test/unit/builders/permission_overwrite_test.dart +++ b/test/unit/builders/permission_overwrite_test.dart @@ -7,6 +7,11 @@ void main() { expect( builder.build(), + equals({'id': '0', 'type': 1}), + ); + + expect( + builder.build(includeId: false), equals({'type': 1}), ); @@ -20,6 +25,7 @@ void main() { expect( builder2.build(), equals({ + 'id': '0', 'type': 0, 'allow': '1048640', 'deny': '8', diff --git a/test/unit/builders/user_test.dart b/test/unit/builders/user_test.dart index ae2ffa32c..26ae174f5 100644 --- a/test/unit/builders/user_test.dart +++ b/test/unit/builders/user_test.dart @@ -21,6 +21,9 @@ void main() { final builder4 = UserUpdateBuilder(); expect(builder4.build(), equals({})); + + final builder5 = UserUpdateBuilder(banner: ImageBuilder.png([])); + expect(builder5.build(), equals({'banner': 'data:image/png;base64,'})); }); }); } diff --git a/test/unit/cache/cache_test.dart b/test/unit/cache/cache_test.dart index 33285c64e..8d9ddb7be 100644 --- a/test/unit/cache/cache_test.dart +++ b/test/unit/cache/cache_test.dart @@ -11,7 +11,7 @@ class MockSnowflakeEntity extends ManagedSnowflakeEntity wi void main() { group('Cache', () { test('stores entities', () async { - final cache = Cache(MockNyxx(), 'test', CacheConfig()); + final cache = MockNyxx().cache.getCache('test', CacheConfig()); final entity = MockSnowflakeEntity(id: Snowflake.zero); @@ -26,7 +26,7 @@ void main() { }); test('respects maximum size', () async { - final cache = Cache(MockNyxx(), 'test', CacheConfig(maxSize: 3)); + final cache = MockNyxx().cache.getCache('test', CacheConfig(maxSize: 3)); final entity1 = MockSnowflakeEntity(id: Snowflake(1)); final entity2 = MockSnowflakeEntity(id: Snowflake(2)); @@ -38,13 +38,14 @@ void main() { cache[entity.id] = entity; } + // Cache filtering does not happen synchronously. await null; expect(cache, hasLength(3)); }); test('keeps most used items', () async { - final cache = Cache(MockNyxx(), 'test', CacheConfig(maxSize: 3)); + final cache = MockNyxx().cache.getCache('test', CacheConfig(maxSize: 3)); final entity1 = MockSnowflakeEntity(id: Snowflake(1)); final entity2 = MockSnowflakeEntity(id: Snowflake(2)); @@ -74,7 +75,7 @@ void main() { }); test("doesn't cache items if a filter is provided", () { - final cache = Cache(MockNyxx(), 'test', CacheConfig(shouldCache: (e) => e.id.value > 3)); + final cache = MockNyxx().cache.getCache('test', CacheConfig(shouldCache: (e) => e.id.value > 3)); final entity1 = MockSnowflakeEntity(id: Snowflake(1)); final entity2 = MockSnowflakeEntity(id: Snowflake(2)); @@ -97,8 +98,8 @@ void main() { test('shares resources with the same identifier', () { final client = MockNyxx(); - final cache1 = Cache(client, 'test', CacheConfig()); - final cache2 = Cache(client, 'test', CacheConfig()); + final cache1 = client.cache.getCache('test', CacheConfig()); + final cache2 = client.cache.getCache('test', CacheConfig()); final entity = MockSnowflakeEntity(id: Snowflake.zero); @@ -107,13 +108,30 @@ void main() { }); test("doesn't share resources across clients", () { - final cache1 = Cache(MockNyxx(), 'test', CacheConfig()); - final cache2 = Cache(MockNyxx(), 'test', CacheConfig()); + final cache1 = MockNyxx().cache.getCache('test', CacheConfig()); + final cache2 = MockNyxx().cache.getCache('test', CacheConfig()); final entity = MockSnowflakeEntity(id: Snowflake.zero); cache1[entity.id] = entity; expect(cache2.containsKey(entity.id), isFalse); }); + + test('toList', () { + final cache = MockNyxx().cache.getCache('test', CacheConfig()); + + final entity1 = MockSnowflakeEntity(id: Snowflake(1)); + final entity2 = MockSnowflakeEntity(id: Snowflake(2)); + final entity3 = MockSnowflakeEntity(id: Snowflake(3)); + final entity4 = MockSnowflakeEntity(id: Snowflake(4)); + final entity5 = MockSnowflakeEntity(id: Snowflake(5)); + + for (final entity in [entity1, entity2, entity3, entity4, entity5]) { + cache[entity.id] = entity; + } + + expect(cache.keys.toList, returnsNormally); + expect(cache.values.toList, returnsNormally); + }); }); } diff --git a/test/unit/http/handler_test.dart b/test/unit/http/handler_test.dart index 7eb346e23..f153ffeb0 100644 --- a/test/unit/http/handler_test.dart +++ b/test/unit/http/handler_test.dart @@ -27,9 +27,9 @@ void main() { group('execute', () { test('can make basic requests', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); final interceptor = nock('https://discord.com/api/v${client.apiOptions.apiVersion}').get('/test')..reply(200, jsonEncode({'message': 'success'})); @@ -46,9 +46,9 @@ void main() { test('returns the correct response type', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); final scope = nock('https://discord.com/api/v${client.apiOptions.apiVersion}'); final successInterceptor = scope.get('/succeed')..reply(200, jsonEncode({'message': 'success'})); @@ -70,9 +70,9 @@ void main() { test('executeSafe throws on request failure', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); nock('https://discord.com/api/v${client.apiOptions.apiVersion}') ..get('/succeed').reply(200, jsonEncode({'message': 'success'})) @@ -88,9 +88,9 @@ void main() { group('rate limits', () { test('creates buckets from headers', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); nock('https://discord.com/api/v${client.apiOptions.apiVersion}').get('/test').reply( 200, @@ -114,9 +114,9 @@ void main() { test('hold requests when rate limit might be exceeded', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); nock('https://discord.com/api/v${client.apiOptions.apiVersion}').get('/test').reply( 200, @@ -155,9 +155,9 @@ void main() { test('update on 429 response', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); nock('https://discord.com/api/v${client.apiOptions.apiVersion}').get('/test').reply( 429, @@ -204,9 +204,9 @@ void main() { test('handles global rate limit', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); nock('https://discord.com/api/v${client.apiOptions.apiVersion}').get('/test').reply( 429, @@ -253,9 +253,9 @@ void main() { test('handles batch request rate limits', () async { final client = MockNyxx(); - final handler = HttpHandler(client); when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'test token')); when(() => client.options).thenReturn(RestClientOptions()); + final handler = HttpHandler(client); for (final duration in [Duration.zero, Duration(seconds: 4), Duration(seconds: 9)]) { Timer(duration, () { diff --git a/test/unit/http/managers/application_command_manager_test.dart b/test/unit/http/managers/application_command_manager_test.dart index 82d150890..9b6174312 100644 --- a/test/unit/http/managers/application_command_manager_test.dart +++ b/test/unit/http/managers/application_command_manager_test.dart @@ -14,24 +14,39 @@ final sampleCommand = { "description": "Ping the bot", "description_localizations": null, "dm_permission": true, - "contexts": null, "nsfw": false, + "integration_types": [0, 1], + "contexts": [0, 1, 2], }; void checkCommand(ApplicationCommand command) { expect(command.id, equals(Snowflake(1102343284505968762))); - expect(command.type, equals(ApplicationCommandType.chatInput)); expect(command.applicationId, equals(Snowflake(1033681843708510238))); - expect(command.guildId, isNull); + expect(command.version, equals(Snowflake(1107729458535878799))); + expect(command.defaultMemberPermissions, isNull); + expect(command.type, equals(ApplicationCommandType.chatInput)); expect(command.name, equals('ping')); expect(command.nameLocalizations, isNull); expect(command.description, equals('Ping the bot')); expect(command.descriptionLocalizations, isNull); + expect(command.guildId, isNull); expect(command.options, isNull); - expect(command.defaultMemberPermissions, isNull); + // ignore: deprecated_member_use_from_same_package expect(command.hasDmPermission, isTrue); expect(command.isNsfw, isFalse); - expect(command.version, equals(Snowflake(1107729458535878799))); + expect( + command.integrationTypes, + equals([ + ApplicationIntegrationType.guildInstall, + ApplicationIntegrationType.userInstall, + ])); + expect( + command.contexts, + equals([ + InteractionContextType.guild, + InteractionContextType.botDm, + InteractionContextType.privateChannel, + ])); } final sampleCommandPermissions = { diff --git a/test/unit/http/managers/application_manager_test.dart b/test/unit/http/managers/application_manager_test.dart index c7aa6103e..ccc03b707 100644 --- a/test/unit/http/managers/application_manager_test.dart +++ b/test/unit/http/managers/application_manager_test.dart @@ -14,6 +14,20 @@ final sampleApplication = { "guild_id": "290926798626357260", "icon": null, "id": "172150183260323840", + "integration_types_config": { + "0": { + "oauth2_install_params": { + "scopes": ["applications.commands", "bot"], + "permissions": "2048" + } + }, + "1": { + "oauth2_install_params": { + "scopes": ["applications.commands"], + "permissions": "0" + } + } + }, "name": "Baba O-Riley", "owner": {"avatar": null, "discriminator": "1738", "flags": 1024, "id": "172150183260323840", "username": "i own a bot"}, "primary_sku_id": "172150183260323840", @@ -25,9 +39,10 @@ final sampleApplication = { "members": [ { "membership_state": 2, + "permissions": ["*"], "team_id": "531992624043786253", - "user": {"avatar": "d9e261cd35999608eb7e3de1fae3688b", "discriminator": "0001", "id": "511972282709709995", "username": "Mr Owner"}, "role": "admin", + "user": {"avatar": "d9e261cd35999608eb7e3de1fae3688b", "discriminator": "0001", "id": "511972282709709995", "username": "Mr Owner"} } ], @@ -60,6 +75,13 @@ void checkApplication(Application application) { expect(application.installationParameters, isNull); expect(application.customInstallUrl, isNull); expect(application.roleConnectionsVerificationUrl, isNull); + expect(application.integrationTypesConfig?[ApplicationIntegrationType.guildInstall], isNotNull); + expect(application.integrationTypesConfig![ApplicationIntegrationType.guildInstall]!, (ApplicationIntegrationTypeConfiguration config) { + expect(config.oauth2InstallParameters, isNotNull); + expect(config.oauth2InstallParameters!.scopes, equals(["applications.commands", "bot"])); + expect(config.oauth2InstallParameters!.permissions, equals(Permissions(2048))); + return true; + }); } final sampleRoleConnectionMetadata = { @@ -78,30 +100,6 @@ void checkRoleConnectionMetadata(ApplicationRoleConnectionMetadata metadata) { expect(metadata.localizedDescriptions, isNull); } -final sampleSku = { - "id": "1088510058284990888", - "type": 5, - "dependent_sku_id": null, - "application_id": "788708323867885999", - "manifest_labels": null, - "access_type": 1, - "name": "Test Premium", - "features": [], - "release_date": null, - "premium": false, - "slug": "test-premium", - "flags": 128, - "show_age_gate": false -}; - -void checkSku(Sku sku) { - expect(sku.id, equals(Snowflake(1088510058284990888))); - expect(sku.type, equals(SkuType.subscription)); - expect(sku.applicationId, equals(Snowflake(788708323867885999))); - expect(sku.name, equals('Test Premium')); - expect(sku.slug, equals('test-premium')); -} - void main() { group('ApplicationManager', () { test('parse', () { @@ -130,19 +128,6 @@ void main() { ).runWithManager(ApplicationManager(client)); }); - test('parseSku', () { - final client = MockNyxx(); - when(() => client.apiOptions).thenReturn(RestApiOptions(token: 'TEST_TOKEN')); - when(() => client.options).thenReturn(RestClientOptions()); - - ParsingTest>( - name: 'parseSku', - source: sampleSku, - parse: (manager) => manager.parseSku, - check: checkSku, - ).runWithManager(ApplicationManager(client)); - }); - testEndpoint( '/applications/0/role-connections/metadata', name: 'fetchApplicationRoleConnectionMetadata', @@ -170,11 +155,5 @@ void main() { (client) => client.applications.updateCurrentApplication(ApplicationUpdateBuilder()), response: sampleApplication, ); - - testEndpoint( - '/applications/0/skus', - (client) => client.applications.listSkus(Snowflake.zero), - response: [sampleSku], - ); }); } diff --git a/test/unit/http/managers/emoji_manager_test.dart b/test/unit/http/managers/emoji_manager_test.dart index 52e0b8c29..448be4e5f 100644 --- a/test/unit/http/managers/emoji_manager_test.dart +++ b/test/unit/http/managers/emoji_manager_test.dart @@ -1,4 +1,5 @@ import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/http/managers/emoji_manager.dart'; import 'package:test/test.dart'; import '../../../test_manager.dart'; @@ -39,7 +40,7 @@ void checkTextEmoji(Emoji emoji) { void main() { testManager( 'EmojiManager', - (config, client) => EmojiManager(config, client, guildId: Snowflake(1)), + (config, client) => GuildEmojiManager(config, client, guildId: Snowflake(1)), RegExp(r'/guilds/1/emojis/\d+'), '/guilds/1/emojis', sampleObject: sampleGuildEmoji, @@ -53,7 +54,7 @@ void main() { ), ], additionalEndpointTests: [ - EndpointTest, List>( + EndpointTest, List>( name: 'list', source: [sampleGuildEmoji], urlMatcher: '/guilds/1/emojis', diff --git a/test/unit/http/managers/guild_manager_test.dart b/test/unit/http/managers/guild_manager_test.dart index 1af7397d9..4e5b8aa0f 100644 --- a/test/unit/http/managers/guild_manager_test.dart +++ b/test/unit/http/managers/guild_manager_test.dart @@ -395,7 +395,10 @@ final sampleOnboarding = { "998678683592171602", "998678699715067986" ], - "enabled": true + "enabled": true, + + // The docs say these fields are present, but they aren't in the sample onboarding Discord provides + "mode": 0, }; void checkOnboarding(Onboarding onboarding) { @@ -640,10 +643,10 @@ void main() { execute: (manager) => manager.deleteBan(Snowflake.zero, Snowflake.zero), check: (_) {}, ), - EndpointTest( + EndpointTest>( name: 'updateMfaLevel', method: 'POST', - source: 0, + source: {'level': 0}, urlMatcher: '/guilds/0/mfa', execute: (manager) => manager.updateMfaLevel(Snowflake.zero, MfaLevel.none), check: (level) => expect(level, equals(MfaLevel.none)), @@ -730,6 +733,22 @@ void main() { execute: (manager) => manager.fetchOnboarding(Snowflake.zero), check: checkOnboarding, ), + EndpointTest>( + name: 'updateOnboarding', + source: sampleOnboarding, + urlMatcher: '/guilds/0/onboarding', + method: 'PUT', + execute: (manager) => manager.updateOnboarding( + Snowflake.zero, + OnboardingUpdateBuilder( + prompts: [], + defaultChannelIds: [], + isEnabled: true, + mode: OnboardingMode.defaultMode, + ), + ), + check: checkOnboarding, + ), EndpointTest( name: 'updateCurrentUserVoiceState', method: 'PATCH', diff --git a/test/unit/http/managers/interaction_manager_test.dart b/test/unit/http/managers/interaction_manager_test.dart index f434ebdc3..1a4668ebc 100644 --- a/test/unit/http/managers/interaction_manager_test.dart +++ b/test/unit/http/managers/interaction_manager_test.dart @@ -38,6 +38,11 @@ final sampleCommandInteraction = { }, "channel_id": "645027906669510667", "entitlements": [], + "authorizing_integration_owners": { + "0": "846136758470443069", + "1": "302359032612651009", + }, + "context": 0, // Fields not present in the example but documented "application_id": "0", @@ -63,6 +68,13 @@ void checkCommandInteraction(Interaction interaction) { expect(interaction.locale, equals(Locale.enUs)); expect(interaction.guildLocale, equals(Locale.enUs)); expect(interaction.entitlements, equals([])); + expect( + interaction.authorizingIntegrationOwners, + equals({ + ApplicationIntegrationType.guildInstall: Snowflake(846136758470443069), + ApplicationIntegrationType.userInstall: Snowflake(302359032612651009), + })); + expect(interaction.context, equals(InteractionContextType.guild)); } final sampleCommandInteraction2 = { @@ -157,6 +169,10 @@ final sampleCommandInteraction2 = { }, "application_id": "1033681843708510238", "app_permissions": "562949953421311", + "authorizing_integration_owners": { + "0": "846136758470443069", + "1": "302359032612651009", + }, }; void checkCommandInteraction2(Interaction interaction) { diff --git a/test/unit/http/managers/invite_manager_test.dart b/test/unit/http/managers/invite_manager_test.dart index 58cac573f..1ef74d071 100644 --- a/test/unit/http/managers/invite_manager_test.dart +++ b/test/unit/http/managers/invite_manager_test.dart @@ -7,6 +7,7 @@ import '../../../test_endpoint.dart'; import '../../../test_manager.dart'; final sampleInvite = { + "type": 0, "code": "0vCdhLbwjZZTWZLD", "guild": { "id": "165176875973476352", @@ -24,17 +25,17 @@ final sampleInvite = { "channel": {"id": "165176875973476352", "name": "illuminati", "type": 0}, "inviter": {"id": "115590097100865541", "username": "speed", "avatar": "deadbeef", "discriminator": "7653", "public_flags": 131328}, "target_type": 1, - "target_user": {"id": "165176875973476352", "username": "bob", "avatar": "deadbeef", "discriminator": "1234", "public_flags": 64}, - "expires_at": "2017-07-11T17:27:07.299000+00:00", + "target_user": {"id": "165176875973476352", "username": "bob", "avatar": "deadbeef", "discriminator": "1234", "public_flags": 64} }; void checkInvite(Invite invite) { + expect(invite.type, equals(InviteType.guild)); expect(invite.code, equals('0vCdhLbwjZZTWZLD')); - // expect(invite.guild.id, equals(Snowflake(165176875973476352))); + expect(invite.guild?.id, equals(Snowflake(165176875973476352))); expect(invite.channel.id, equals(Snowflake(165176875973476352))); expect(invite.inviter?.id, equals(Snowflake(115590097100865541))); expect(invite.targetType, equals(TargetType.stream)); - expect(invite.expiresAt, equals(DateTime.utc(2017, 07, 11, 17, 27, 07, 299))); + expect(invite.expiresAt, isNull); } final sampleInviteWithMetadata = { diff --git a/test/unit/http/managers/member_manager_test.dart b/test/unit/http/managers/member_manager_test.dart index 674085d93..f1ebbb11e 100644 --- a/test/unit/http/managers/member_manager_test.dart +++ b/test/unit/http/managers/member_manager_test.dart @@ -4,8 +4,8 @@ import 'package:test/test.dart'; import '../../../test_manager.dart'; import 'user_manager_test.dart'; -final sampleMember = { - "user": sampleUser, +final sampleMemberNoUser = { + "user": null, "nick": "NOT API SUPPORT", "avatar": null, "roles": [], @@ -17,9 +17,13 @@ final sampleMember = { "flags": 0, }; -void checkMember(Member member) { - expect(member.id, equals(Snowflake(80351110224678912))); - expect(member.user, isNotNull); +final sampleMember = { + ...sampleMemberNoUser, + "user": sampleUser, +}; + +void checkMemberNoUser(Member member, {Snowflake expectedUserId = const Snowflake(80351110224678912)}) { + expect(member.id, equals(expectedUserId)); expect(member.nick, equals('NOT API SUPPORT')); expect(member.avatarHash, isNull); expect(member.roleIds, equals([])); @@ -31,6 +35,10 @@ void checkMember(Member member) { expect(member.isPending, isFalse); expect(member.permissions, isNull); expect(member.communicationDisabledUntil, isNull); +} + +void checkMember(Member member, {Snowflake expectedUserId = const Snowflake(80351110224678912)}) { + checkMemberNoUser(member, expectedUserId: expectedUserId); expect(member.user, isNotNull); checkSampleUser(member.user!); diff --git a/test/unit/http/managers/message_manager_test.dart b/test/unit/http/managers/message_manager_test.dart index 9502f87ec..68050a7ba 100644 --- a/test/unit/http/managers/message_manager_test.dart +++ b/test/unit/http/managers/message_manager_test.dart @@ -37,7 +37,31 @@ final sampleMessage = { "name": "example sticker", "format_type": 1, } - ] + ], + "poll": { + "allow_multiselect": false, + "answers": [ + { + "answer_id": 1, + "poll_media": {"text": "oof"}, + } + ], + "expiry": "2024-07-12T22:00:25.095257+00:00", + "layout_type": 1, + "question": { + "text": "Why are you so dumb?", + }, + "results": { + "is_finalized": false, + "answer_counts": [ + { + "count": 1, + "id": 1, + "me_voted": true, + } + ] + } + } }; void checkMessage(Message message) { @@ -68,6 +92,21 @@ void checkMessage(Message message) { expect(message.position, isNull); expect(message.roleSubscriptionData, isNull); expect(message.stickers, hasLength(1)); + expect(message.poll, isNotNull); + expect(message.poll!.allowsMultiselect, equals(false)); + expect(message.poll!.answers, hasLength(1)); + expect(message.poll!.answers.single.id, equals(1)); + expect(message.poll!.answers.single.pollMedia.text, equals('oof')); + expect(message.poll!.endsAt, equals(DateTime.utc(2024, 07, 12, 22, 00, 25, 095, 257))); + expect(message.poll!.layoutType, equals(PollLayoutType.defaultLayout)); + expect(message.poll!.question.text, equals('Why are you so dumb?')); + expect(message.poll!.results, isNotNull); + expect(message.poll!.results!.isFinalized, isFalse); + expect(message.poll!.results!.answerCounts, hasLength(1)); + expect(message.poll!.results!.answerCounts.single.count, equals(1)); + expect(message.poll!.results!.answerCounts.single.answerId, equals(1)); + expect(message.poll!.results!.answerCounts.single.me, isTrue); + expect(message.poll!.results!.answerCounts.single.count, equals(1)); } final sampleCrosspostedMessage = { @@ -140,6 +179,205 @@ void checkCrosspostedMessage(Message message) { expect(message.roleSubscriptionData, isNull); } +final sampleForwardedMessage = { + 'type': 0, + 'tts': false, + 'timestamp': '2024-10-08T09:18:22.532000+00:00', + 'position': 0, + 'pinned': false, + 'nonce': '1293140413896458240', + 'message_snapshots': [ + { + 'message': { + 'type': 0, + 'timestamp': '2024-10-08T09:17:26.429000+00:00', + 'mentions': [], + 'flags': 0, + 'embeds': [], + 'edited_timestamp': null, + 'content': '<@&786646877335977984> I ping myself for self validation', + 'components': [], + 'attachments': [] + } + } + ], + 'message_reference': { + 'type': 1, + 'message_id': '1293140188952002611', + 'guild_id': '786638002399084594', + 'channel_id': '786638002399084597', + }, + 'mentions': [], + 'mention_roles': [], + 'mention_everyone': false, + 'member': { + 'roles': ['1034762811726901269'], + 'premium_since': null, + 'pending': false, + 'nick': null, + 'mute': false, + 'joined_at': '2022-10-23T10:03:13.019000+00:00', + 'flags': 2, + 'deaf': false, + 'communication_disabled_until': null, + 'banner': null, + 'avatar': null + }, + 'id': '1293140424264781887', + 'flags': 16384, + 'embeds': [], + 'edited_timestamp': null, + 'content': '', + 'components': [], + 'channel_id': '1038831656682930227', + 'author': { + 'username': 'abitofevrything', + 'public_flags': 128, + 'id': '506759329068613643', + 'global_name': 'Abitofevrything', + 'discriminator': '0', + 'clan': null, + 'avatar_decoration_data': null, + 'avatar': 'b591ea8a9d057669ea2a6cd3ab450301' + }, + 'attachments': [], + 'guild_id': '1033681997136146462', +}; + +void checkForwardedMessage(Message message) { + expect(message.author.id, equals(Snowflake(506759329068613643))); + expect(message.content, equals('')); + expect(message.timestamp, equals(DateTime.utc(2024, 10, 08, 09, 18, 22, 532))); + expect(message.editedTimestamp, isNull); + expect(message.isTts, isFalse); + expect(message.mentionsEveryone, isFalse); + expect(message.mentions, equals([])); + expect(message.roleMentionIds, equals([])); + expect(message.channelMentions, equals([])); + expect(message.attachments, equals([])); + expect(message.embeds, equals([])); + expect(message.reactions, equals([])); + expect(message.nonce, equals('1293140413896458240')); + expect(message.isPinned, isFalse); + expect(message.webhookId, isNull); + expect(message.type, MessageType.normal); + expect(message.activity, isNull); + expect(message.application, isNull); + expect(message.applicationId, isNull); + expect(message.reference, isNotNull); + expect(message.reference!.type, equals(MessageReferenceType.forward)); + expect(message.reference!.messageId, equals(Snowflake(1293140188952002611))); + expect(message.reference!.channelId, equals(Snowflake(786638002399084597))); + expect(message.reference!.guildId, equals(Snowflake(786638002399084594))); + expect( + message.messageSnapshots, + equals([ + wrapMatcher((MessageSnapshot snapshot) { + expect(snapshot.timestamp, equals(DateTime.utc(2024, 10, 08, 09, 17, 26, 429))); + expect(snapshot.editedTimestamp, isNull); + expect(snapshot.type, MessageType.normal); + expect(snapshot.content, equals('<@&786646877335977984> I ping myself for self validation')); + expect(snapshot.attachments, equals([])); + expect(snapshot.embeds, equals([])); + expect(snapshot.flags, equals(MessageFlags(0))); + expect(snapshot.mentions, equals([])); + expect( + snapshot.roleMentionIds, + equals([ + // TODO: Update this once Discord properly populates this field. + ]), + ); + return true; + }), + ]), + ); + expect(message.flags, equals(MessageFlags(16384))); + expect(message.referencedMessage, isNull); + expect(message.interactionMetadata, isNull); + expect(message.thread, isNull); + expect(message.components, equals([])); + expect(message.stickers, equals([])); + expect(message.position, equals(0)); + expect(message.roleSubscriptionData, isNull); + expect(message.resolved, isNull); + expect(message.poll, isNull); + expect(message.call, isNull); +} + +final sampleMessageInteractionMetadata = { + "id": "1234567891234567800", + "type": 2, + "user": { + "id": "1234567891234567801", + "username": "rizzedskibidi", + "discriminator": "0", + "global_name": "Read if cute", + "flags": 256, + "avatar": "a_abc123", + }, + "authorizing_integration_owners": { + "0": "1234567891234567802", + "1": "1234567891234567803", + }, + "original_response_message_id": "1234567891234567804", + "interacted_message_id": "1234567891234567805", + "triggering_interaction_metadata": { + "id": "1234567891234567806", + "type": 2, + "user_id": "1234567891234567807", + "user": { + "username": "nocap-fr", + "discriminator": "0", + "id": "1234567891234567807", + "avatar": "a_abc123", + "global_name": "Iloaf", + "flags": 256, + }, + "authorizing_integration_owners": { + "0": "1234567891234567808", + "1": "1234567891234567809", + }, + }, +}; + +void checkMessageInteractionMetadata(MessageInteractionMetadata metadata) { + expect(metadata.id, equals(Snowflake(1234567891234567800))); + expect(metadata.type, equals(InteractionType.applicationCommand)); + expect(metadata.user.id, equals(Snowflake(1234567891234567801))); + expect(metadata.user.username, equals('rizzedskibidi')); + expect(metadata.user.discriminator, equals('0')); + expect(metadata.user.globalName, equals('Read if cute')); + expect(metadata.user.flags, equals(UserFlags(256))); + expect(metadata.user.avatarHash, equals('a_abc123')); + expect( + metadata.authorizingIntegrationOwners, + equals({ + ApplicationIntegrationType.guildInstall: Snowflake(1234567891234567802), + ApplicationIntegrationType.userInstall: Snowflake(1234567891234567803), + })); + expect(metadata.originalResponseMessageId, Snowflake(1234567891234567804)); + expect(metadata.interactedMessageId, Snowflake(1234567891234567805)); + expect(metadata.triggeringInteractionMetadata, isNotNull); + MessageInteractionMetadata metadata2 = metadata.triggeringInteractionMetadata!; + expect(metadata2.id, equals(Snowflake(1234567891234567806))); + expect(metadata2.type, equals(InteractionType.applicationCommand)); + expect(metadata2.user.id, equals(Snowflake(1234567891234567807))); + expect(metadata2.user.username, equals('nocap-fr')); + expect(metadata2.user.discriminator, equals('0')); + expect(metadata2.user.globalName, equals('Iloaf')); + expect(metadata2.user.flags, equals(UserFlags(256))); + expect(metadata2.user.avatarHash, equals('a_abc123')); + expect( + metadata2.authorizingIntegrationOwners, + equals({ + ApplicationIntegrationType.guildInstall: Snowflake(1234567891234567808), + ApplicationIntegrationType.userInstall: Snowflake(1234567891234567809), + })); + expect(metadata2.originalResponseMessageId, isNull); + expect(metadata2.interactedMessageId, isNull); + expect(metadata2.triggeringInteractionMetadata, isNull); +} + void main() { testManager( 'MessageManager', @@ -148,11 +386,18 @@ void main() { '/channels/0/messages', sampleObject: sampleMessage, sampleMatches: checkMessage, - additionalSampleObjects: [sampleCrosspostedMessage], - additionalSampleMatchers: [checkCrosspostedMessage], + additionalSampleObjects: [sampleCrosspostedMessage, sampleForwardedMessage], + additionalSampleMatchers: [checkCrosspostedMessage, checkForwardedMessage], createBuilder: MessageBuilder(), updateBuilder: MessageUpdateBuilder(), - additionalParsingTests: [], + additionalParsingTests: [ + ParsingTest>( + name: 'parseMessageInteractionMetadata', + source: sampleMessageInteractionMetadata, + parse: (manager) => manager.parseMessageInteractionMetadata, + check: checkMessageInteractionMetadata, + ), + ], additionalEndpointTests: [ EndpointTest, List>( name: 'fetchMany', diff --git a/test/unit/http/managers/role_manager_test.dart b/test/unit/http/managers/role_manager_test.dart index 89356ed95..25c990ce1 100644 --- a/test/unit/http/managers/role_manager_test.dart +++ b/test/unit/http/managers/role_manager_test.dart @@ -38,8 +38,6 @@ void main() { RegExp(r'/guilds/0/roles(/\d+)?'), '/guilds/0/roles', sampleObject: sampleRole, - // Fetch implementation internally uses `list()`, so we return a list - fetchObjectOverride: [sampleRole], sampleMatches: checkRole, additionalParsingTests: [], additionalEndpointTests: [ diff --git a/test/unit/http/managers/scheduled_event_manager_test.dart b/test/unit/http/managers/scheduled_event_manager_test.dart index 8a79a8285..dea81d595 100644 --- a/test/unit/http/managers/scheduled_event_manager_test.dart +++ b/test/unit/http/managers/scheduled_event_manager_test.dart @@ -6,37 +6,104 @@ import 'member_manager_test.dart'; import 'user_manager_test.dart'; final sampleScheduledEvent = { - 'id': '0', - 'guild_id': '1', - 'channel_id': '2', - 'creator_id': '3', - 'name': 'test', - 'description': 'a test event', - 'scheduled_start_time': '2023-06-10T16:37:18Z', - 'scheduled_end_time': '2023-06-10T16:37:18Z', - 'privacy_level': 2, - 'status': 1, - 'entity_type': 1, - 'entity_id': '2', - 'creator': sampleUser, - 'user_count': null, - 'image': null, + "id": "1278775959230611487", + "guild_id": "1033681997136146462", + "name": "test event", + "description": null, + "channel_id": null, + "creator_id": "1033681843708510238", + "image": null, + "scheduled_start_time": "2024-08-29T19:59:07.045563+00:00", + "scheduled_end_time": "2024-08-29T19:59:17.045564+00:00", + "status": 1, + "entity_type": 3, + "entity_id": null, + "recurrence_rule": { + "start": "2024-08-29T19:59:07.045594+00:00", + "end": null, + "frequency": 2, + "interval": 1, + "by_weekday": [2], + "by_n_weekday": null, + "by_month": null, + "by_month_day": null, + "by_year_day": null, + "count": null + }, + "privacy_level": 2, + "sku_ids": [], + "guild_scheduled_event_exceptions": [], + "entity_metadata": {"location": "test location"} }; void checkScheduledEvent(ScheduledEvent event) { - expect(event.id, equals(Snowflake.zero)); - expect(event.guildId, equals(Snowflake(1))); - expect(event.channelId, equals(Snowflake(2))); - expect(event.creatorId, equals(Snowflake(3))); - expect(event.name, equals('test')); - expect(event.description, equals('a test event')); - expect(event.scheduledStartTime, equals(DateTime.utc(2023, 06, 10, 16, 37, 18))); - expect(event.scheduledEndTime, equals(DateTime.utc(2023, 06, 10, 16, 37, 18))); + expect(event.id, equals(Snowflake(1278775959230611487))); + expect(event.guildId, equals(Snowflake(1033681997136146462))); + expect(event.channelId, isNull); + expect(event.creatorId, equals(Snowflake(1033681843708510238))); + expect(event.name, equals('test event')); + expect(event.description, isNull); + expect(event.scheduledStartTime, equals(DateTime.utc(2024, 08, 29, 19, 59, 07, 45, 563))); + expect(event.scheduledEndTime, equals(DateTime.utc(2024, 08, 29, 19, 59, 17, 45, 564))); expect(event.privacyLevel, equals(PrivacyLevel.guildOnly)); expect(event.status, equals(EventStatus.scheduled)); - expect(event.type, equals(ScheduledEntityType.stageInstance)); - expect(event.entityId, equals(Snowflake(2))); - expect(event.metadata, isNull); + expect(event.type, equals(ScheduledEntityType.external)); + expect(event.entityId, isNull); + expect(event.metadata?.location, equals('test location')); + expect(event.userCount, isNull); + expect(event.coverImageHash, isNull); + expect(event.recurrenceRule, isNotNull); + expect(event.recurrenceRule, (RecurrenceRule rule) { + expect(rule.byMonth, isNull); + expect(rule.byMonthDay, isNull); + expect(rule.byNWeekday, isNull); + expect(rule.byWeekday, equals([RecurrenceRuleWeekday.wednesday])); + expect(rule.byYearDay, isNull); + expect(rule.count, isNull); + expect(rule.end, isNull); + expect(rule.frequency, equals(RecurrenceRuleFrequency.weekly)); + expect(rule.interval, equals(1)); + expect(rule.start, equals(DateTime.utc(2024, 08, 29, 19, 59, 07, 045, 594))); + return true; + }); +} + +final sampleScheduledEvent2 = { + "id": "1278778514790944793", + "guild_id": "1033681997136146462", + "name": "test event", + "description": "", + "channel_id": "1105193130237632574", + "creator_id": "506759329068613643", + "creator": sampleUser, + "image": null, + "scheduled_start_time": "2024-08-29T19:00:00.859000+00:00", + "scheduled_end_time": null, + "status": 1, + "entity_type": 2, + "entity_id": null, + "recurrence_rule": null, + "privacy_level": 2, + "sku_ids": [], + "guild_scheduled_event_exceptions": [], + "entity_metadata": {}, +}; + +void checkScheduledEvent2(ScheduledEvent event) { + expect(event.id, equals(Snowflake(1278778514790944793))); + expect(event.guildId, equals(Snowflake(1033681997136146462))); + expect(event.channelId, equals(Snowflake(1105193130237632574))); + expect(event.creatorId, equals(Snowflake(506759329068613643))); + expect(event.name, equals('test event')); + expect(event.description, equals('')); + expect(event.scheduledStartTime, equals(DateTime.utc(2024, 08, 29, 19, 00, 00, 859))); + expect(event.scheduledEndTime, isNull); + expect(event.privacyLevel, equals(PrivacyLevel.guildOnly)); + expect(event.status, equals(EventStatus.scheduled)); + expect(event.type, equals(ScheduledEntityType.voice)); + expect(event.entityId, isNull); + expect(event.metadata, isNotNull); + expect(event.metadata!.location, isNull); checkSampleUser(event.creator!); expect(event.userCount, isNull); expect(event.coverImageHash, isNull); @@ -45,13 +112,13 @@ void checkScheduledEvent(ScheduledEvent event) { final sampleScheduledEventUser = { 'guild_scheduled_event_id': '0', 'user': sampleUser, - 'member': sampleMember, + 'member': sampleMemberNoUser, }; void checkScheduledEventUser(ScheduledEventUser user) { expect(user.scheduledEventId, equals(Snowflake.zero)); checkSampleUser(user.user); - checkMember(user.member!); + checkMemberNoUser(user.member!); } void main() { @@ -62,6 +129,8 @@ void main() { '/guilds/0/scheduled-events', sampleObject: sampleScheduledEvent, sampleMatches: checkScheduledEvent, + additionalSampleObjects: [sampleScheduledEvent2], + additionalSampleMatchers: [checkScheduledEvent2], additionalParsingTests: [ ParsingTest>( name: 'parseScheduledEventUser', @@ -96,8 +165,8 @@ void main() { channelId: Snowflake.zero, name: 'test', privacyLevel: PrivacyLevel.guildOnly, - scheduledStartTime: DateTime(2023), - scheduledEndTime: DateTime(2023), + scheduledStartTime: DateTime.utc(2023), + scheduledEndTime: DateTime.utc(2023), type: ScheduledEntityType.stageInstance, ), updateBuilder: ScheduledEventUpdateBuilder(), diff --git a/test/unit/http/managers/sku_manager_test.dart b/test/unit/http/managers/sku_manager_test.dart new file mode 100644 index 000000000..54ef7f6d1 --- /dev/null +++ b/test/unit/http/managers/sku_manager_test.dart @@ -0,0 +1,54 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/http/managers/sku_manager.dart'; +import 'package:test/test.dart'; + +import '../../../test_manager.dart'; + +final sampleSku = { + "id": "1", + "type": 5, + "dependent_sku_id": null, + "application_id": "788708323867885999", + "manifest_labels": null, + "access_type": 1, + "name": "Test Premium", + "features": [], + "release_date": null, + "premium": false, + "slug": "test-premium", + "flags": 128, + "show_age_gate": false +}; + +void checkSku(Sku sku) { + expect(sku.id, equals(Snowflake(1))); + expect(sku.type, equals(SkuType.subscription)); + expect(sku.applicationId, equals(Snowflake(788708323867885999))); + expect(sku.name, equals('Test Premium')); + expect(sku.slug, equals('test-premium')); +} + +void main() { + testReadOnlyManager( + 'SkuManager', + (config, client) => SkuManager(config, client, applicationId: Snowflake.zero), + '/applications/0/skus', + sampleObject: sampleSku, + fetchObjectOverride: [sampleSku], + sampleMatches: checkSku, + additionalParsingTests: [], + additionalEndpointTests: [ + EndpointTest, List>>( + name: 'list', + source: [sampleSku], + urlMatcher: '/applications/0/skus', + execute: (manager) => manager.list(), + check: (skus) { + expect(skus, hasLength(1)); + + checkSku(skus.single); + }, + ), + ], + ); +} diff --git a/test/unit/http/managers/subscription_manager_test.dart b/test/unit/http/managers/subscription_manager_test.dart new file mode 100644 index 000000000..9a471beb9 --- /dev/null +++ b/test/unit/http/managers/subscription_manager_test.dart @@ -0,0 +1,51 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:test/test.dart'; + +import '../../../test_manager.dart'; + +final sampleSubscription = { + "id": "1278078770116427839", + "user_id": "1088605110638227537", + "sku_ids": ["1158857122189168803"], + "entitlement_ids": [], + "current_period_start": "2024-08-27T19:48:44.406602+00:00", + "current_period_end": "2024-09-27T19:48:44.406602+00:00", + "status": 0, + "canceled_at": null +}; + +void checkSubscription(Subscription subscription) { + expect(subscription.id, equals(Snowflake(1278078770116427839))); + expect(subscription.userId, equals(Snowflake(1088605110638227537))); + expect(subscription.skuIds, equals([Snowflake(1158857122189168803)])); + expect(subscription.entitlementIds, equals([])); + expect(subscription.currentPeriodStart, equals(DateTime.utc(2024, 08, 27, 19, 48, 44, 406, 602))); + expect(subscription.currentPeriodEnd, equals(DateTime.utc(2024, 09, 27, 19, 48, 44, 406, 602))); + expect(subscription.status, equals(SubscriptionStatus.active)); + expect(subscription.canceledAt, isNull); + expect(subscription.countryCode, isNull); +} + +void main() { + testReadOnlyManager( + 'SubscriptionManager', + (client, config) => SubscriptionManager(client, config, applicationId: Snowflake.zero, skuId: Snowflake(1)), + RegExp(r'/skus/1/subscriptions/\d+'), + sampleObject: sampleSubscription, + sampleMatches: checkSubscription, + additionalParsingTests: [], + additionalEndpointTests: [ + EndpointTest, List>>( + name: 'list', + source: [sampleSubscription], + urlMatcher: '/skus/1/subscriptions', + execute: (manager) => manager.list(), + check: (subscriptions) { + expect(subscriptions, hasLength(1)); + + checkSubscription(subscriptions.single); + }, + ), + ], + ); +} diff --git a/test/unit/http/managers/user_manager_test.dart b/test/unit/http/managers/user_manager_test.dart index 53b8cb7fd..72928951b 100644 --- a/test/unit/http/managers/user_manager_test.dart +++ b/test/unit/http/managers/user_manager_test.dart @@ -1,6 +1,8 @@ +import 'package:mocktail/mocktail.dart'; import 'package:nyxx/nyxx.dart'; import 'package:test/test.dart'; +import '../../../mocks/client.dart'; import '../../../test_manager.dart'; import 'channel_manager_test.dart'; import 'member_manager_test.dart'; @@ -102,24 +104,49 @@ void main() { execute: (manager) => manager.updateCurrentUser(UserUpdateBuilder()), check: checkSampleUser, ), - EndpointTest, List>( + EndpointTest, List>( name: 'listCurrentUserGuilds', source: [ - {'id': '0'} + { + 'id': '1', + 'name': 'nyxx', + 'icon': null, + 'owner': false, + 'permissions': '533130099674816', + 'features': ['COMMUNITY', 'INVITE_SPLASH', 'DISCOVERABLE', 'WELCOME_SCREEN_ENABLED', 'VERIFIED', 'VANITY_URL', 'NEWS'] + } ], urlMatcher: '/users/@me/guilds', execute: (manager) => manager.listCurrentUserGuilds(), check: (list) { expect(list, hasLength(1)); - expect(list.single.id, equals(Snowflake.zero)); + final guild = list.single; + expect(guild.id, equals(Snowflake(1))); + expect(guild.name, 'nyxx'); + expect(guild.icon, isNull); + expect(guild.isOwnedByCurrentUser, isFalse); + expect(guild.currentUserPermissions, isNotNull); + final features = guild.features; + expect(features.hasCommunity, isTrue); + expect(features.hasInviteSplash, isTrue); + expect(features.isDiscoverable, isTrue); + expect(features.hasWelcomeScreenEnabled, isTrue); + expect(features.isVerified, isTrue); + expect(features.hasVanityUrl, isTrue); + expect(features.hasNews, isTrue); }, ), EndpointTest>( name: 'fetchCurrentUserMember', - source: sampleMember, + source: sampleMemberNoUser, urlMatcher: '/users/@me/guilds/0/member', execute: (manager) => manager.fetchCurrentUserMember(Snowflake.zero), - check: checkMember, + check: (member) { + final client = MockNyxx(); + when(() => client.options).thenReturn(RestClientOptions()); + + checkMemberNoUser(member, expectedUserId: client.user.id); + }, ), EndpointTest( name: 'leaveGuild', diff --git a/test/unit/http/managers/voice_manager_test.dart b/test/unit/http/managers/voice_manager_test.dart index 8d0fbb9db..9998915c1 100644 --- a/test/unit/http/managers/voice_manager_test.dart +++ b/test/unit/http/managers/voice_manager_test.dart @@ -5,6 +5,7 @@ import 'package:test/test.dart'; import '../../../mocks/client.dart'; import '../../../test_endpoint.dart'; import '../../../test_manager.dart'; +import 'member_manager_test.dart'; final sampleVoiceState = { "channel_id": "157733188964188161", @@ -16,6 +17,7 @@ final sampleVoiceState = { "self_mute": true, "suppress": false, "request_to_speak_timestamp": "2021-03-31T18:45:31.297561+00:00", + "member": sampleMemberNoUser, // The API reference says this field is always present, but it's missing in the sample "self_video": false, @@ -34,6 +36,8 @@ void checkVoiceState(VoiceState state) { expect(state.isVideoEnabled, isFalse); expect(state.isSuppressed, isFalse); expect(state.requestedToSpeakAt, equals(DateTime.utc(2021, 3, 31, 18, 45, 31, 297, 561))); + expect(state.member, isNotNull); + checkMemberNoUser(state.member!, expectedUserId: Snowflake(80351110224678912)); } final sampleVoiceRegion = { @@ -89,5 +93,19 @@ void main() { (client) => client.voice.listRegions(), response: [sampleVoiceRegion], ); + + testEndpoint( + name: 'fetchCurrentUserVoiceState', + '/guilds/0/voice-states/@me', + (client) => client.voice.fetchCurrentUserVoiceState(Snowflake.zero), + response: sampleVoiceState, + ); + + testEndpoint( + name: 'fetchVoiceState', + '/guilds/0/voice-states/1', + (client) => client.voice.fetchVoiceState(Snowflake.zero, Snowflake(1)), + response: sampleVoiceState, + ); }); } diff --git a/test/unit/models/snowflake_entity/snowflake_entity_test.dart b/test/unit/models/snowflake_entity/snowflake_entity_test.dart index 2d2c94883..9361f7dc4 100644 --- a/test/unit/models/snowflake_entity/snowflake_entity_test.dart +++ b/test/unit/models/snowflake_entity/snowflake_entity_test.dart @@ -6,10 +6,12 @@ class PartialMockSnowflakeEntity extends WritableSnowflakeEntity