From 56830125c79666a5e2d31588a175b8043075e609 Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Thu, 31 Mar 2022 20:58:05 +0100 Subject: [PATCH 01/27] Fix target id property and add guild audit logs options (#307) * Fix target id property * Add new audit logs events * See: https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events * Add `IAuditLogsOptions` & `AuditLogsOptions` Handle options properly now * Add overwrittenType doc * Remove action types doc * Remove null check in reason * Run dart format * Fix doc of class * Remove null check Bc the switch do the job already * Fix count parameter * Run dart format * Rename deleteMemberDays & set it to duration * Add doc for properties * Rename deleteMemberDays * Fix format in doc * Fix typo * Remove constructor in interface --- lib/nyxx.dart | 1 + lib/src/core/audit_logs/audit_log_entry.dart | 30 +++++-- .../core/audit_logs/audit_log_options.dart | 90 +++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 lib/src/core/audit_logs/audit_log_options.dart diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 6450bb9e4..947ccac6e 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -18,6 +18,7 @@ export "src/core/application/oauth2_application.dart" show IOAuth2Application; export 'src/core/audit_logs/audit_log.dart' show IAuditLog; export 'src/core/audit_logs/audit_log_change.dart' show ChangeKeyType, IAuditLogChange; export 'src/core/audit_logs/audit_log_entry.dart' show IAuditLogEntry, AuditLogEntryType; +export 'src/core/audit_logs/audit_log_options.dart' show IAuditLogOptions; export 'src/core/channel/cacheable_text_channel.dart' show ICacheableTextChannel; export 'src/core/channel/channel.dart' show IChannel, ChannelType; export 'src/core/channel/dm_channel.dart' show IDMChannel; diff --git a/lib/src/core/audit_logs/audit_log_entry.dart b/lib/src/core/audit_logs/audit_log_entry.dart index 5225cc845..6b0b3c77f 100644 --- a/lib/src/core/audit_logs/audit_log_entry.dart +++ b/lib/src/core/audit_logs/audit_log_entry.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/core/user/user.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/enum.dart'; +import 'package:nyxx/src/core/audit_logs/audit_log_options.dart'; abstract class IAuditLogEntry implements SnowflakeEntity { /// Id of the affected entity (webhook, user, role, etc.) @@ -21,7 +22,7 @@ abstract class IAuditLogEntry implements SnowflakeEntity { AuditLogEntryType get type; /// Additional info for certain action types - String? get options; + IAuditLogOptions? get options; /// The reason for the change String? get reason; @@ -49,15 +50,15 @@ class AuditLogEntry extends SnowflakeEntity implements IAuditLogEntry { /// Additional info for certain action types @override - late final String? options; + late final IAuditLogOptions? options; /// The reason for the change @override late final String? reason; - /// Creates na instance of [AuditLogEntry] + /// Creates an instance of [AuditLogEntry] AuditLogEntry(RawApiMap raw, INyxx client) : super(Snowflake(raw["id"] as String)) { - targetId = raw["targetId"] as String; + targetId = raw["target_id"] as String; changes = [ if (raw["changes"] != null) @@ -67,7 +68,11 @@ class AuditLogEntry extends SnowflakeEntity implements IAuditLogEntry { user = UserCacheable(client, Snowflake(raw["user_id"])); type = AuditLogEntryType._create(raw["action_type"] as int); - options = raw["options"] as String?; + if (raw["options"] != null) { + options = AuditLogOptions(raw["options"] as RawApiMap); + } else { + options = null; + } reason = raw["reason"] as String?; } @@ -87,6 +92,9 @@ class AuditLogEntryType extends IEnum { static const AuditLogEntryType memberBanRemove = AuditLogEntryType._create(23); static const AuditLogEntryType memberUpdate = AuditLogEntryType._create(24); static const AuditLogEntryType memberRoleUpdate = AuditLogEntryType._create(25); + static const AuditLogEntryType memberMove = AuditLogEntryType._create(26); + static const AuditLogEntryType memberDisconnect = AuditLogEntryType._create(27); + static const AuditLogEntryType botAdd = AuditLogEntryType._create(28); static const AuditLogEntryType roleCreate = AuditLogEntryType._create(30); static const AuditLogEntryType roleUpdate = AuditLogEntryType._create(31); static const AuditLogEntryType roleDelete = AuditLogEntryType._create(32); @@ -106,6 +114,18 @@ class AuditLogEntryType extends IEnum { static const AuditLogEntryType integrationCreate = AuditLogEntryType._create(80); static const AuditLogEntryType integrationUpdate = AuditLogEntryType._create(81); static const AuditLogEntryType integrationDelete = AuditLogEntryType._create(82); + static const AuditLogEntryType stageInstanceCreate = AuditLogEntryType._create(83); + static const AuditLogEntryType stageInstanceUpdate = AuditLogEntryType._create(84); + static const AuditLogEntryType stageInstanceDelete = AuditLogEntryType._create(85); + static const AuditLogEntryType stickerCreate = AuditLogEntryType._create(90); + static const AuditLogEntryType stickerUpdate = AuditLogEntryType._create(91); + static const AuditLogEntryType stickerDelete = AuditLogEntryType._create(92); + static const AuditLogEntryType guildScheduledEventCreate = AuditLogEntryType._create(100); + static const AuditLogEntryType guildScheduledEventUpdate = AuditLogEntryType._create(101); + static const AuditLogEntryType guildScheduledEventDelete = AuditLogEntryType._create(102); + static const AuditLogEntryType threadCreate = AuditLogEntryType._create(110); + static const AuditLogEntryType threadUpdate = AuditLogEntryType._create(111); + static const AuditLogEntryType threadDelete = AuditLogEntryType._create(112); const AuditLogEntryType._create(int value) : super(value); diff --git a/lib/src/core/audit_logs/audit_log_options.dart b/lib/src/core/audit_logs/audit_log_options.dart new file mode 100644 index 000000000..699ef5629 --- /dev/null +++ b/lib/src/core/audit_logs/audit_log_options.dart @@ -0,0 +1,90 @@ +import 'package:nyxx/nyxx.dart'; + +/// Additionnal info for certain action types +/// +/// [Look here for more](https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events) +abstract class IAuditLogOptions { + /// The channel in which the entites were targeted. + Snowflake? get channelId; + + /// The number of entities targeted. + int? get count; + + /// The number of days after which inactive users will be kicked. + Duration? get deleteMemberDuration; + + /// Id of the overwritten entity. + Snowflake? get id; + + /// The number of the members removed by the prune. + int? get pruneCount; + + /// The id of the message that was targeted. + Snowflake? get messageId; + + /// The name of the role that was targeted. (Not present if [overwrittenType] is `member`). + String? get roleName; + + /// Type of overwritten entity. + /// One of: + /// - `role` + /// - `member` + String? get overwrittenType; +} + +class AuditLogOptions implements IAuditLogOptions { + /// The channel in which the entites were targeted. + @override + late final Snowflake? channelId; + + /// The number of entities targeted. + @override + late final int? count; + + /// The number of days after which inactive users will be kicked. + @override + late final Duration? deleteMemberDuration; + + /// Id of the overwritten entity. + @override + late final Snowflake? id; + + /// The number of the members removed by the prune. + @override + late final int? pruneCount; + + /// The id of the message that was targeted. + @override + late final Snowflake? messageId; + + /// The name of the role that was targeted. (Not present if [overwrittenType] is `member`). + @override + late final String? roleName; + + /// Type of overwritten entity. + /// One of: + /// - `role` + /// - `member` + @override + late final String? overwrittenType; + + AuditLogOptions(RawApiMap raw) { + channelId = (raw['channel_id'] as String?)?.toSnowflake(); + count = raw['count'] != null ? int.parse(raw['count'] as String) : null; + deleteMemberDuration = (raw['delete_member_days'] as String?) != null ? Duration(days: int.parse(raw['delete_member_days'] as String)) : null; + id = (raw['id'] as String?)?.toSnowflake(); + pruneCount = (raw['members_removed'] as String?) != null ? int.parse(raw['members_removed'] as String) : null; + messageId = (raw['message_id'] as String?)?.toSnowflake(); + roleName = raw['role_name'] as String?; + switch (raw['type']) { + case '0': + overwrittenType = 'role'; + break; + case '1': + overwrittenType = 'member'; + break; + default: + overwrittenType = null; + } + } +} From 2555d2315409940cc841f51210db9b77126161e9 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Mar 2022 22:00:43 +0200 Subject: [PATCH 02/27] Release 4.0.0-dev.0 --- CHANGELOG.md | 5 +++++ lib/src/internal/constants.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b2582b3..969ea8b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.0.0-dev.0 +__31.03.2022__ + +- feature: Fix target id property and add guild audit logs options (#307) + ## 3.3.1 __30.03.2022__ diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 0409b2345..610120ef4 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -33,7 +33,7 @@ class Constants { static const int apiVersion = 9; /// Version of Nyxx - static const String version = "3.3.1"; + static const String version = "4.0.0-dev.0"; /// Url to Nyxx repo static const String repoUrl = "https://github.com/nyxx-discord/nyxx"; diff --git a/pubspec.yaml b/pubspec.yaml index 9e792790b..1a6b46f89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 3.3.1 +version: 4.0.0-dev.0 description: A Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx From 6f4a7012e9c63bd5e7e603297c626934b3314091 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Mar 2022 22:27:12 +0200 Subject: [PATCH 03/27] Replace global import with specific ones --- lib/src/nyxx.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/nyxx.dart b/lib/src/nyxx.dart index 2fe108452..63780b7f7 100644 --- a/lib/src/nyxx.dart +++ b/lib/src/nyxx.dart @@ -1,10 +1,8 @@ import 'dart:async'; import 'package:logging/logging.dart'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/client_options.dart'; import 'package:nyxx/src/core/channel/invite.dart'; -import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/application/client_oauth2_application.dart'; import 'package:nyxx/src/core/channel/channel.dart'; @@ -15,9 +13,11 @@ import 'package:nyxx/src/core/guild/webhook.dart'; import 'package:nyxx/src/core/message/sticker.dart'; import 'package:nyxx/src/core/user/user.dart'; import 'package:nyxx/src/events/ready_event.dart'; +import 'package:nyxx/src/internal/cache/cache.dart'; import 'package:nyxx/src/internal/connection_manager.dart'; import 'package:nyxx/src/internal/constants.dart'; import 'package:nyxx/src/internal/event_controller.dart'; +import 'package:nyxx/src/internal/exceptions/unrecoverable_nyxx_error.dart'; import 'package:nyxx/src/internal/http/http_response.dart'; import 'package:nyxx/src/internal/http_endpoints.dart'; import 'package:nyxx/src/internal/exceptions/missing_token_error.dart'; @@ -25,6 +25,7 @@ import 'package:nyxx/src/internal/http/http_handler.dart'; import 'package:nyxx/src/internal/interfaces/disposable.dart'; import 'package:nyxx/src/internal/shard/shard_manager.dart'; import 'package:nyxx/src/plugin/plugin.dart'; +import 'package:nyxx/src/plugin/plugin_manager.dart'; import 'utils/builders/presence_builder.dart'; import 'package:nyxx/src/typedefs.dart'; From 20f3b602aa8862bfbe795e5814efff1467c80a64 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Mon, 4 Apr 2022 22:45:34 +0200 Subject: [PATCH 04/27] Correct signature of StageVoiceGuildChannel (#319) --- lib/src/core/channel/guild/voice_channel.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/core/channel/guild/voice_channel.dart b/lib/src/core/channel/guild/voice_channel.dart index 67f458059..ef4024ddd 100644 --- a/lib/src/core/channel/guild/voice_channel.dart +++ b/lib/src/core/channel/guild/voice_channel.dart @@ -207,20 +207,24 @@ abstract class IStageVoiceGuildChannel implements IVoiceGuildChannel { Future updateStageChannelInstance(String topic, {StageChannelInstancePrivacyLevel? privacyLevel}); } -class StageVoiceGuildChannel extends VoiceGuildChannel { +class StageVoiceGuildChannel extends VoiceGuildChannel implements IStageVoiceGuildChannel { StageVoiceGuildChannel(INyxx client, RawApiMap raw, [Snowflake? guildId]) : super(client, raw, guildId); /// Gets the stage instance associated with the Stage channel, if it exists. + @override Future getStageChannelInstance() => client.httpEndpoints.getStageChannelInstance(id); /// Deletes the Stage instance. + @override Future deleteStageChannelInstance() => client.httpEndpoints.deleteStageChannelInstance(id); /// Creates a new Stage instance associated to a Stage channel. + @override Future createStageChannelInstance(String topic, {StageChannelInstancePrivacyLevel? privacyLevel}) => client.httpEndpoints.createStageChannelInstance(id, topic, privacyLevel: privacyLevel); /// Updates fields of an existing Stage instance. + @override Future updateStageChannelInstance(String topic, {StageChannelInstancePrivacyLevel? privacyLevel}) => client.httpEndpoints.updateStageChannelInstance(id, topic, privacyLevel: privacyLevel); } From 452879716e07c93912e985a6ecf95a6117b49e21 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 9 Apr 2022 09:52:50 +0200 Subject: [PATCH 05/27] Handle no internet on websocket (#321) --- lib/src/internal/shard/shard.dart | 7 +++++-- lib/src/internal/shard/shard_handler.dart | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/internal/shard/shard.dart b/lib/src/internal/shard/shard.dart index 715011176..2d5cabf37 100644 --- a/lib/src/internal/shard/shard.dart +++ b/lib/src/internal/shard/shard.dart @@ -255,6 +255,9 @@ class Shard implements IShard { case 1001: _reconnect(); break; + case -1: + _connect(delay: 10); + break; default: _connect(); break; @@ -262,10 +265,10 @@ class Shard implements IShard { } // Connects to gateway - void _connect() { + void _connect({int delay = 2}) { manager.logger.info("Connecting to gateway on shard $id!"); _resume = false; - Future.delayed(const Duration(seconds: 2), () => _sendPort.send({"cmd": "CONNECT"})); + Future.delayed(Duration(seconds: delay), () => _sendPort.send({"cmd": "CONNECT"})); } // Reconnects to gateway diff --git a/lib/src/internal/shard/shard_handler.dart b/lib/src/internal/shard/shard_handler.dart index d07f20a42..060ae63a6 100644 --- a/lib/src/internal/shard/shard_handler.dart +++ b/lib/src/internal/shard/shard_handler.dart @@ -77,6 +77,7 @@ Future shardHandler(SendPort shardPort) async { Future _connect() async { try { _socket = await WebSocket.connect(gatewayUri.toString()); + _socket!.pingInterval = const Duration(seconds: 20); final zlibDecoder = RawZLibFilter.inflateFilter(); // Create zlib decoder specific to this connection. New connection should get new zlib context // ignore: unawaited_futures @@ -95,6 +96,8 @@ Future shardHandler(SendPort shardPort) async { shardPort.send({"cmd": "CONNECT_ACK"}); } on WebSocketException catch (err) { shardPort.send({"cmd": "ERROR", "error": err.toString(), "errorCode": _socket!.closeCode, "errorReason": _socket!.closeReason}); + } on SocketException catch (err) { + shardPort.send({"cmd": "ERROR", "error": err.toString(), "errorCode": -1, "errorReason": "SocketException"}); } on Exception catch (err) { print(err); } on Error catch (err) { From fc438e4468bef1363ca1f6d94fbaefdfe92f36dc Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 9 Apr 2022 09:53:06 +0200 Subject: [PATCH 06/27] bug: Remove Error form IHttpResponseError; Fixup field names on IHttpResponseError; Fixup IHttpResponseSuccess name (#324) --- lib/nyxx.dart | 2 +- lib/src/internal/connection_manager.dart | 2 +- lib/src/internal/http/http_handler.dart | 2 +- lib/src/internal/http/http_response.dart | 31 +++++++++++------------- lib/src/internal/http_endpoints.dart | 12 ++++----- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/nyxx.dart b/lib/nyxx.dart index 947ccac6e..c414a4a8d 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -138,7 +138,7 @@ export 'src/internal/exceptions/invalid_shard_exception.dart' show InvalidShardE export 'src/internal/exceptions/invalid_snowflake_exception.dart' show InvalidSnowflakeException; export 'src/internal/exceptions/missing_token_error.dart' show MissingTokenError; export 'src/internal/exceptions/unrecoverable_nyxx_error.dart' show UnrecoverableNyxxError; -export 'src/internal/http/http_response.dart' show IHttpResponse, IHttpResponseError, IHttpResponseSucess; +export 'src/internal/http/http_response.dart' show IHttpResponse, IHttpResponseError, IHttpResponseSuccess; export 'src/internal/interfaces/convertable.dart' show Convertable; export 'src/internal/interfaces/disposable.dart' show Disposable; export 'src/internal/interfaces/message_author.dart' show IMessageAuthor; diff --git a/lib/src/internal/connection_manager.dart b/lib/src/internal/connection_manager.dart index 2dadd936f..51b89be29 100644 --- a/lib/src/internal/connection_manager.dart +++ b/lib/src/internal/connection_manager.dart @@ -30,7 +30,7 @@ class ConnectionManager { final httpResponse = await (client.httpEndpoints as HttpEndpoints).getGatewayBot(); if (httpResponse is HttpResponseError) { - throw UnrecoverableNyxxError("Cannot get gateway url: [${httpResponse.errorCode}; ${httpResponse.errorMessage}]"); + throw UnrecoverableNyxxError("Cannot get gateway url: [${httpResponse.code}; ${httpResponse.message}]"); } final response = httpResponse as HttpResponseSuccess; diff --git a/lib/src/internal/http/http_handler.dart b/lib/src/internal/http/http_handler.dart index b8d468267..c17f74589 100644 --- a/lib/src/internal/http/http_handler.dart +++ b/lib/src/internal/http/http_handler.dart @@ -97,7 +97,7 @@ class HttpHandler { await responseError.finalize(); (client.eventsRest as RestEventController).onHttpErrorController.add(HttpErrorEvent(responseError)); - logger.finer("Got failure http response for endpoint: [${response.request?.url.toString()}]; Response: [${responseError.errorMessage}]"); + logger.finer("Got failure http response for endpoint: [${response.request?.url.toString()}]; Response: [${responseError.message}]"); return responseError; } diff --git a/lib/src/internal/http/http_response.dart b/lib/src/internal/http/http_response.dart index 00dbdc529..4a77021ff 100644 --- a/lib/src/internal/http/http_response.dart +++ b/lib/src/internal/http/http_response.dart @@ -39,7 +39,7 @@ abstract class HttpResponse implements IHttpResponse { } } -abstract class IHttpResponseSucess implements IHttpResponse { +abstract class IHttpResponseSuccess implements IHttpResponse { /// Body of response List get body; @@ -48,7 +48,7 @@ abstract class IHttpResponseSucess implements IHttpResponse { } /// Returned when http request is successfully executed. -class HttpResponseSuccess extends HttpResponse implements IHttpResponseSucess { +class HttpResponseSuccess extends HttpResponse implements IHttpResponseSuccess { /// Body of response @override List get body => _body; @@ -61,12 +61,12 @@ class HttpResponseSuccess extends HttpResponse implements IHttpResponseSucess { HttpResponseSuccess(http.StreamedResponse response) : super(response); } -abstract class IHttpResponseError implements IHttpResponse, Error, Exception { +abstract class IHttpResponseError implements IHttpResponse, Exception { /// Message why http request failed - String get errorMessage; + String get message; /// Error code of response - int get errorCode; + int get code; } /// Returned when client fails to execute http request. @@ -74,20 +74,20 @@ abstract class IHttpResponseError implements IHttpResponse, Error, Exception { class HttpResponseError extends HttpResponse implements IHttpResponseError { /// Message why http request failed @override - late String errorMessage; + late String message; /// Error code of response @override - late int errorCode; + late int code; /// Creates an instance of [HttpResponseError] HttpResponseError(http.StreamedResponse response) : super(response) { if (response.headers["Content-Type"] == "application/json") { - errorCode = _jsonBody["code"] as int; - errorMessage = _jsonBody["message"] as String; + code = _jsonBody["code"] as int; + message = _jsonBody["message"] as String; } else { - errorMessage = ""; - errorCode = response.statusCode; + message = ""; + code = response.statusCode; } } @@ -95,16 +95,13 @@ class HttpResponseError extends HttpResponse implements IHttpResponseError { Future finalize() async { await super.finalize(); - if (errorMessage.isEmpty) { + if (message.isEmpty) { try { - errorMessage = utf8.decode(_body); + message = utf8.decode(_body); } on Exception {} // ignore: empty_catches } } @override - String toString() => "[Code: $errorCode] [Message: $errorMessage]"; - - @override - StackTrace? get stackTrace => null; + String toString() => "[Code: $code] [Message: $message]"; } diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index 367f20b9d..eb673005b 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -1596,7 +1596,7 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(result); } - return ThreadMember(client, (result as IHttpResponseSucess).jsonBody as RawApiMap, GuildCacheable(client, guildId)); + return ThreadMember(client, (result as IHttpResponseSuccess).jsonBody as RawApiMap, GuildCacheable(client, guildId)); } @override @@ -1618,7 +1618,7 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } - return GuildEvent((response as IHttpResponseSucess).jsonBody as RawApiMap, client); + return GuildEvent((response as IHttpResponseSuccess).jsonBody as RawApiMap, client); } @override @@ -1633,7 +1633,7 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } - return GuildEvent((response as IHttpResponseSucess).jsonBody as RawApiMap, client); + return GuildEvent((response as IHttpResponseSuccess).jsonBody as RawApiMap, client); } @override @@ -1644,7 +1644,7 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } - return GuildEvent((response as IHttpResponseSucess).jsonBody as RawApiMap, client); + return GuildEvent((response as IHttpResponseSuccess).jsonBody as RawApiMap, client); } @override @@ -1661,7 +1661,7 @@ class HttpEndpoints implements IHttpEndpoints { yield* Stream.error(response); } - for (final rawGuildEventUser in (response as IHttpResponseSucess).jsonBody as RawApiList) { + for (final rawGuildEventUser in (response as IHttpResponseSuccess).jsonBody as RawApiList) { yield GuildEventUser(rawGuildEventUser as RawApiMap, client, guildId); } } @@ -1675,7 +1675,7 @@ class HttpEndpoints implements IHttpEndpoints { yield* Stream.error(response); } - for (final rawGuildEvent in (response as IHttpResponseSucess).jsonBody as RawApiList) { + for (final rawGuildEvent in (response as IHttpResponseSuccess).jsonBody as RawApiList) { yield GuildEvent(rawGuildEvent as RawApiMap, client); } } From c134c645e6de4a9164e177ae15abb2e3502eecf6 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 9 Apr 2022 16:06:40 +0200 Subject: [PATCH 07/27] feature: Move to API v10 (#325) * feature: Move to API v10; Fixes #323 * Formatting fixes * Remove deprecations; Format & analyze --- lib/src/client_options.dart | 7 ++- lib/src/core/application/app_team_user.dart | 1 - .../channel/guild/text_guild_channel.dart | 7 --- lib/src/core/guild/guild.dart | 9 ---- lib/src/core/guild/scheduled_event.dart | 6 ++- lib/src/core/message/attachment.dart | 3 +- .../message/components/message_component.dart | 2 +- lib/src/core/user/member.dart | 31 ++----------- lib/src/events/message_events.dart | 2 +- lib/src/internal/cache/cache.dart | 2 +- lib/src/internal/constants.dart | 2 +- lib/src/internal/http_endpoints.dart | 43 ++----------------- lib/src/internal/shard/shard.dart | 2 +- lib/src/internal/shard/shard_manager.dart | 2 +- lib/src/nyxx.dart | 12 +----- lib/src/utils/builders/message_builder.dart | 1 - test/integration/integration.dart | 2 +- test/unit/builders_test.dart | 13 +++++- test/unit/channel_test.dart | 1 - test/unit/nyxx_test.dart | 9 ++-- test/unit/snowflake_test.dart | 1 - 21 files changed, 48 insertions(+), 110 deletions(-) diff --git a/lib/src/client_options.dart b/lib/src/client_options.dart index 299cb3ef3..e0862364a 100644 --- a/lib/src/client_options.dart +++ b/lib/src/client_options.dart @@ -133,6 +133,11 @@ class GatewayIntents { /// Includes events: `TYPING_START` static const int directMessageTyping = 1 << 14; + /// Includes public content of messages in guilds (content, embeds, attachments, components) + /// If your bot is mentioned it will always receive full message + /// If you are not opted in for message content intent you will receive empty fields + static const int messageContent = 1 << 15; + /// Includes events: `GUILD_SCHEDULED_EVENT_CREATE`, `GUILD_SCHEDULED_EVENT_DELETE`, `GUILD_SCHEDULED_EVENT_UPDATE`, `GUILD_SCHEDULED_EVENT_USER_ADD`, `GUILD_SCHEDULED_EVENT_USER_REMOVE` static const int guildScheduledEvents = 1 << 16; @@ -153,7 +158,7 @@ class GatewayIntents { guildScheduledEvents; /// All privileged intents - static const int allPrivileged = guildMembers | guildPresences; + static const int allPrivileged = guildMembers | guildPresences | messageContent; /// All intents static const int all = allUnprivileged | allPrivileged; diff --git a/lib/src/core/application/app_team_user.dart b/lib/src/core/application/app_team_user.dart index e66ea2f74..1a93f073c 100644 --- a/lib/src/core/application/app_team_user.dart +++ b/lib/src/core/application/app_team_user.dart @@ -1,4 +1,3 @@ -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; import 'package:nyxx/src/typedefs.dart'; diff --git a/lib/src/core/channel/guild/text_guild_channel.dart b/lib/src/core/channel/guild/text_guild_channel.dart index d5714b887..a0ce85c22 100644 --- a/lib/src/core/channel/guild/text_guild_channel.dart +++ b/lib/src/core/channel/guild/text_guild_channel.dart @@ -44,9 +44,6 @@ abstract class ITextGuildChannel implements IGuildChannel, ITextChannel, Mention /// Creates a thread in a message Future createAndGetThread(ThreadBuilder builder); - /// Fetches all active threads in this channel - Future fetchActiveThreads(); - /// Fetches joined private and archived thread channels Future fetchJoinedPrivateArchivedThreads({DateTime? before, int? limit}); @@ -159,10 +156,6 @@ class TextGuildChannel extends GuildChannel implements ITextGuildChannel { @override Future sendMessage(MessageBuilder builder) => client.httpEndpoints.sendMessage(id, builder); - /// Fetches all active threads in this channel - @override - Future fetchActiveThreads() => client.httpEndpoints.fetchActiveThreads(id); - /// Fetches joined private and archived thread channels @override Future fetchJoinedPrivateArchivedThreads({DateTime? before, int? limit}) => diff --git a/lib/src/core/guild/guild.dart b/lib/src/core/guild/guild.dart index d8ae99762..0ed01535e 100644 --- a/lib/src/core/guild/guild.dart +++ b/lib/src/core/guild/guild.dart @@ -59,10 +59,6 @@ abstract class IGuild implements SnowflakeEntity { /// The guild's afk channel ID, null if not set. Cacheable? get afkChannel; - /// The guild's voice region. - @Deprecated('User IVoiceChannel.rtcRegion') - String get region; - /// The channel ID for the guild's widget if enabled. Cacheable? get embedChannel; @@ -334,10 +330,6 @@ class Guild extends SnowflakeEntity implements IGuild { @override late Cacheable? afkChannel; - /// The guild's voice region. - @override - late String region; - /// The channel ID for the guild's widget if enabled. @override late final Cacheable? embedChannel; @@ -488,7 +480,6 @@ class Guild extends SnowflakeEntity implements IGuild { /// Creates an instance of [Guild] Guild(this.client, RawApiMap raw, [bool guildCreate = false]) : super(Snowflake(raw["id"])) { name = raw["name"] as String; - region = ""; afkTimeout = raw["afk_timeout"] as int; mfaLevel = raw["mfa_level"] as int; verificationLevel = raw["verification_level"] as int; diff --git a/lib/src/core/guild/scheduled_event.dart b/lib/src/core/guild/scheduled_event.dart index bef0df0ed..093d14f52 100644 --- a/lib/src/core/guild/scheduled_event.dart +++ b/lib/src/core/guild/scheduled_event.dart @@ -1,10 +1,14 @@ -import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/core/channel/guild/voice_channel.dart'; import 'package:nyxx/src/core/guild/guild.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; import 'package:nyxx/src/core/user/member.dart'; import 'package:nyxx/src/core/user/user.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; +import 'package:nyxx/src/nyxx.dart'; +import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/src/utils/builders/guild_event_builder.dart'; +import 'package:nyxx/src/utils/enum.dart'; /// A representation of a scheduled event in a guild. abstract class IGuildEvent implements SnowflakeEntity { diff --git a/lib/src/core/message/attachment.dart b/lib/src/core/message/attachment.dart index 3b2a27894..7c557607b 100644 --- a/lib/src/core/message/attachment.dart +++ b/lib/src/core/message/attachment.dart @@ -1,7 +1,8 @@ -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; +import 'package:nyxx/src/internal/interfaces/convertable.dart'; import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/src/utils/builders/attachment_builder.dart'; abstract class IAttachment implements SnowflakeEntity, Convertable { /// The attachment's filename. diff --git a/lib/src/core/message/components/message_component.dart b/lib/src/core/message/components/message_component.dart index 30aebf501..d11ae216f 100644 --- a/lib/src/core/message/components/message_component.dart +++ b/lib/src/core/message/components/message_component.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/message/emoji.dart'; import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/message/unicode_emoji.dart'; import 'package:nyxx/src/core/message/components/component_style.dart'; +import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/enum.dart'; diff --git a/lib/src/core/user/member.dart b/lib/src/core/user/member.dart index d73e70474..43538c0fd 100644 --- a/lib/src/core/user/member.dart +++ b/lib/src/core/user/member.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; @@ -13,6 +12,7 @@ import 'package:nyxx/src/core/voice/voice_state.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; import 'package:nyxx/src/internal/interfaces/mentionable.dart'; import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/src/utils/builders/member_builder.dart'; import 'package:nyxx/src/utils/permissions.dart'; abstract class IMember implements SnowflakeEntity, Mentionable { @@ -40,9 +40,6 @@ abstract class IMember implements SnowflakeEntity, Mentionable { /// Roles of member Iterable> get roles; - /// Highest role of member - Cacheable get hoistedRole; - /// When the user starting boosting the guild DateTime? get boostingSince; @@ -86,14 +83,7 @@ abstract class IMember implements SnowflakeEntity, Mentionable { Future kick({String? auditReason}); /// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles. - Future edit( - {@Deprecated('Use "builder" parameter') String? nick = "", - @Deprecated('Use "builder" parameter') List? roles, - @Deprecated('Use "builder" parameter') bool? mute, - @Deprecated('Use "builder" parameter') bool? deaf, - @Deprecated('Use "builder" parameter') Snowflake? channel = const Snowflake.zero(), - MemberBuilder? builder, - String? auditReason}); + Future edit({required MemberBuilder builder, String? auditReason}); } class Member extends SnowflakeEntity implements IMember { @@ -129,11 +119,6 @@ class Member extends SnowflakeEntity implements IMember { @override late Iterable> roles; - /// Highest role of member - @override - @Deprecated('Use `roles` and sort by position instead. Attempting to use this field will throw an error.') - late final Cacheable hoistedRole; - /// When the user starting boosting the guild @override late DateTime? boostingSince; @@ -238,16 +223,8 @@ class Member extends SnowflakeEntity implements IMember { /// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles. @override - Future edit( - {@Deprecated('Use "builder" parameter') String? nick = "", - @Deprecated('Use "builder" parameter') List? roles, - @Deprecated('Use "builder" parameter') bool? mute, - @Deprecated('Use "builder" parameter') bool? deaf, - @Deprecated('Use "builder" parameter') Snowflake? channel = const Snowflake.zero(), - MemberBuilder? builder, - String? auditReason}) => - client.httpEndpoints - .editGuildMember(guild.id, id, nick: nick, roles: roles, mute: mute, deaf: deaf, channel: channel, builder: builder, auditReason: auditReason); + Future edit({required MemberBuilder builder, String? auditReason}) => + client.httpEndpoints.editGuildMember(guild.id, id, builder: builder, auditReason: auditReason); void updateMember(String? nickname, List roles, DateTime? boostingSince) { if (this.nickname != nickname) { diff --git a/lib/src/events/message_events.dart b/lib/src/events/message_events.dart index 76aa3bc81..86b3024c2 100644 --- a/lib/src/events/message_events.dart +++ b/lib/src/events/message_events.dart @@ -1,6 +1,6 @@ -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/message/attachment.dart'; import 'package:nyxx/src/core/message/components/message_component.dart'; +import 'package:nyxx/src/core/message/message_flags.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/channel/cacheable_text_channel.dart'; diff --git a/lib/src/internal/cache/cache.dart b/lib/src/internal/cache/cache.dart index ddb206bf5..887816356 100644 --- a/lib/src/internal/cache/cache.dart +++ b/lib/src/internal/cache/cache.dart @@ -1,7 +1,7 @@ import 'dart:collection'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/internal/interfaces/disposable.dart'; class SnowflakeCache extends InMemoryCache { final int cacheSize; diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 610120ef4..238808f94 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -30,7 +30,7 @@ class Constants { static const String baseUri = "/api/v$apiVersion"; /// Version of API - static const int apiVersion = 9; + static const int apiVersion = 10; /// Version of Nyxx static const String version = "4.0.0-dev.0"; diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index eb673005b..c37db3376 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -1,4 +1,3 @@ -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/guild/scheduled_event.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/channel/invite.dart'; @@ -39,6 +38,7 @@ import 'package:nyxx/src/utils/builders/permissions_builder.dart'; import 'package:nyxx/src/utils/builders/sticker_builder.dart'; import 'package:nyxx/src/utils/builders/thread_builder.dart'; import 'package:nyxx/src/utils/utils.dart'; +import 'package:nyxx/src/utils/builders/member_builder.dart'; /// Raw access to all http endpoints exposed by nyxx. /// Allows to execute specific action without any context. @@ -177,14 +177,7 @@ abstract class IHttpEndpoints { Future fetchUser(Snowflake userId); /// "Edits" guild member. Allows to manipulate other guild users. - Future editGuildMember(Snowflake guildId, Snowflake memberId, - {@Deprecated('Use "builder" parameter') String? nick, - @Deprecated('Use "builder" parameter') List? roles, - @Deprecated('Use "builder" parameter') bool? mute, - @Deprecated('Use "builder" parameter') bool? deaf, - @Deprecated('Use "builder" parameter') Snowflake? channel = const Snowflake.zero(), - MemberBuilder? builder, - String? auditReason}); + Future editGuildMember(Snowflake guildId, Snowflake memberId, {required MemberBuilder builder, String? auditReason}); /// Removes role from user Future removeRoleFromUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}); @@ -252,9 +245,6 @@ abstract class IHttpEndpoints { /// Removes member from thread given bot has sufficient permissions Future removeThreadMember(Snowflake channelId, Snowflake userId); - /// Returns all active threads in given channel - Future fetchActiveThreads(Snowflake channelId); - /// Returns all public archived thread in given channel Future fetchPublicArchivedThreads(Snowflake channelId, {DateTime? before, int? limit}); @@ -872,22 +862,8 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future editGuildMember(Snowflake guildId, Snowflake memberId, - {String? nick = "", - List? roles, - bool? mute, - bool? deaf, - Snowflake? channel = const Snowflake.zero(), - MemberBuilder? builder, - String? auditReason}) { - final finalBuilder = builder ?? MemberBuilder() - ..nick = nick - ..roles = roles?.map((e) => e.id).toList() - ..mute = mute - ..deaf = deaf - ..channel = channel; - - return executeSafe(BasicRequest("/guilds/$guildId/members/$memberId", method: "PATCH", auditLog: auditReason, body: finalBuilder.build())); + Future editGuildMember(Snowflake guildId, Snowflake memberId, {required MemberBuilder builder, String? auditReason}) { + return executeSafe(BasicRequest("/guilds/$guildId/members/$memberId", method: "PATCH", auditLog: auditReason, body: builder.build())); } @override @@ -1416,17 +1392,6 @@ class HttpEndpoints implements IHttpEndpoints { } } - @override - Future fetchActiveThreads(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/threads/active")); - - if (response is HttpResponseError) { - return Future.error(response); - } - - return ThreadListResultWrapper(client, (response as HttpResponseSuccess).jsonBody as RawApiMap); - } - @override Future fetchJoinedPrivateArchivedThreads(Snowflake channelId, {DateTime? before, int? limit}) async { final response = await httpHandler.execute(BasicRequest("/channels/$channelId/users/@me/threads/archived/private", diff --git a/lib/src/internal/shard/shard.dart b/lib/src/internal/shard/shard.dart index 2d5cabf37..90867a911 100644 --- a/lib/src/internal/shard/shard.dart +++ b/lib/src/internal/shard/shard.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/guild/client_user.dart'; import 'package:nyxx/src/events/channel_events.dart'; @@ -23,6 +22,7 @@ import 'package:nyxx/src/events/voice_state_update_event.dart'; import 'package:nyxx/src/internal/constants.dart'; import 'package:nyxx/src/internal/event_controller.dart'; import 'package:nyxx/src/internal/exceptions/invalid_shard_exception.dart'; +import 'package:nyxx/src/internal/exceptions/unrecoverable_nyxx_error.dart'; import 'package:nyxx/src/internal/interfaces/disposable.dart'; import 'package:nyxx/src/internal/shard/shard_manager.dart'; import 'package:nyxx/src/internal/shard/shard_handler.dart'; diff --git a/lib/src/internal/shard/shard_manager.dart b/lib/src/internal/shard/shard_manager.dart index 2d070d214..2e6ca0c56 100644 --- a/lib/src/internal/shard/shard_manager.dart +++ b/lib/src/internal/shard/shard_manager.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:collection'; import 'package:logging/logging.dart'; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/events/member_chunk_event.dart'; import 'package:nyxx/src/events/raw_event.dart'; import 'package:nyxx/src/internal/connection_manager.dart'; +import 'package:nyxx/src/internal/exceptions/unrecoverable_nyxx_error.dart'; import 'package:nyxx/src/internal/interfaces/disposable.dart'; import 'package:nyxx/src/internal/shard/shard.dart'; import 'package:nyxx/src/utils/builders/presence_builder.dart'; diff --git a/lib/src/nyxx.dart b/lib/src/nyxx.dart index 63780b7f7..46dbbf484 100644 --- a/lib/src/nyxx.dart +++ b/lib/src/nyxx.dart @@ -30,18 +30,10 @@ import 'utils/builders/presence_builder.dart'; import 'package:nyxx/src/typedefs.dart'; abstract class NyxxFactory { - static INyxx createNyxxRest(String token, int intents, Snowflake appId, - {ClientOptions? options, - CacheOptions? cacheOptions, - @Deprecated("Use IgnoreException plugin") bool ignoreExceptions = true, - @Deprecated("Use Logging plugin") bool useDefaultLogger = true}) => + static INyxx createNyxxRest(String token, int intents, Snowflake appId, {ClientOptions? options, CacheOptions? cacheOptions}) => NyxxRest(token, intents, appId, options: options, cacheOptions: cacheOptions); - static INyxxWebsocket createNyxxWebsocket(String token, int intents, - {ClientOptions? options, - CacheOptions? cacheOptions, - @Deprecated("Use IgnoreException plugin") bool ignoreExceptions = true, - @Deprecated("Use Logging plugin") bool useDefaultLogger = true}) => + static INyxxWebsocket createNyxxWebsocket(String token, int intents, {ClientOptions? options, CacheOptions? cacheOptions}) => NyxxWebsocket(token, intents, options: options, cacheOptions: cacheOptions); } diff --git a/lib/src/utils/builders/message_builder.dart b/lib/src/utils/builders/message_builder.dart index 039b99d85..21e1a238b 100644 --- a/lib/src/utils/builders/message_builder.dart +++ b/lib/src/utils/builders/message_builder.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:http/http.dart' as http; -import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/allowed_mentions.dart'; import 'package:nyxx/src/core/message/message.dart'; import 'package:nyxx/src/core/message/message_time_stamp.dart'; diff --git a/test/integration/integration.dart b/test/integration/integration.dart index 44785659c..0bf4aa2b4 100644 --- a/test/integration/integration.dart +++ b/test/integration/integration.dart @@ -13,7 +13,7 @@ final testUserBotSnowflake = Snowflake(476603965396746242); final testUserHumanSnowflake = Snowflake(302359032612651009); main() async { - final bot = NyxxFactory.createNyxxWebsocket(Platform.environment["TEST_TOKEN"]!, GatewayIntents.guildMessages, ignoreExceptions: false) + final bot = NyxxFactory.createNyxxWebsocket(Platform.environment["TEST_TOKEN"]!, GatewayIntents.guildMessages) ..registerPlugin(Logging()) ..connect(); diff --git a/test/unit/builders_test.dart b/test/unit/builders_test.dart index de91483c2..cfc7255e9 100644 --- a/test/unit/builders_test.dart +++ b/test/unit/builders_test.dart @@ -1,7 +1,18 @@ -import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/core/channel/text_channel.dart'; +import 'package:nyxx/src/core/guild/status.dart'; import 'package:nyxx/src/core/permissions/permissions.dart'; +import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/core/user/presence.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; +import 'package:nyxx/src/utils/builders/channel_builder.dart'; +import 'package:nyxx/src/utils/builders/embed_builder.dart'; +import 'package:nyxx/src/utils/builders/member_builder.dart'; +import 'package:nyxx/src/utils/builders/message_builder.dart'; +import 'package:nyxx/src/utils/builders/permissions_builder.dart'; import 'package:nyxx/src/utils/builders/presence_builder.dart'; +import 'package:nyxx/src/utils/builders/reply_builder.dart'; +import 'package:nyxx/src/utils/builders/sticker_builder.dart'; +import 'package:nyxx/src/utils/builders/thread_builder.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; diff --git a/test/unit/channel_test.dart b/test/unit/channel_test.dart index 6176dfb2d..8a80f4e98 100644 --- a/test/unit/channel_test.dart +++ b/test/unit/channel_test.dart @@ -1,5 +1,4 @@ import 'package:nyxx/nyxx.dart'; -import 'package:test/scaffolding.dart'; import 'package:test/test.dart'; import '../mocks/channel.mock.dart'; diff --git a/test/unit/nyxx_test.dart b/test/unit/nyxx_test.dart index e312c8b3a..99686c050 100644 --- a/test/unit/nyxx_test.dart +++ b/test/unit/nyxx_test.dart @@ -1,11 +1,14 @@ -import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/client_options.dart'; +import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/internal/exceptions/missing_token_error.dart'; +import 'package:nyxx/src/nyxx.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; main() { test("nyxx rest constructor", () { - expect(() => NyxxFactory.createNyxxRest("", 0, Snowflake.zero(), ignoreExceptions: false, useDefaultLogger: false, options: ClientOptions()), + expect(() => NyxxFactory.createNyxxRest("", 0, Snowflake.zero(), options: ClientOptions()), throwsA(isA())); - expect(() => NyxxFactory.createNyxxRest("test", 0, Snowflake.zero(), ignoreExceptions: true, useDefaultLogger: false, options: ClientOptions()), isNotNull); + expect(() => NyxxFactory.createNyxxRest("test", 0, Snowflake.zero(), options: ClientOptions()), isNotNull); }); } diff --git a/test/unit/snowflake_test.dart b/test/unit/snowflake_test.dart index 7e4f0fdfc..70e34b4a6 100644 --- a/test/unit/snowflake_test.dart +++ b/test/unit/snowflake_test.dart @@ -1,5 +1,4 @@ import 'package:nyxx/nyxx.dart'; -import 'package:nyxx/src/utils/extensions.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; From 0653bacae350a21dd14c27c06c9d42f07008b2ae Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 9 Apr 2022 16:09:48 +0200 Subject: [PATCH 08/27] Release 4.0.0-dev.1 --- CHANGELOG.md | 9 +++++++++ lib/src/internal/constants.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 969ea8b07..63951537f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 4.0.0-dev.1 +__09.05.2022__ + +- feature: Handle no internet on websocket (#321) +- bug: Remove Error form IHttpResponseError (#324) + - Fixup field names on IHttpResponseError + - Fixup IHttpResponseSuccess name +- feature: Move to API v10 (#325) + ## 4.0.0-dev.0 __31.03.2022__ diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index 238808f94..c30f15cbd 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -33,7 +33,7 @@ class Constants { static const int apiVersion = 10; /// Version of Nyxx - static const String version = "4.0.0-dev.0"; + static const String version = "4.0.0-dev.1"; /// Url to Nyxx repo static const String repoUrl = "https://github.com/nyxx-discord/nyxx"; diff --git a/pubspec.yaml b/pubspec.yaml index 1a6b46f89..f12b71bee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 4.0.0-dev.0 +version: 4.0.0-dev.1 description: A Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx From 9f5dc3ce86c186fda0e09dd5f5ce3de2f9c13195 Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Thu, 28 Apr 2022 23:21:50 +0200 Subject: [PATCH 09/27] feature/bugfix: `IGuild` fixes/Improvements; also add `@createGuild` on `INyxxWebsocket` (#338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `@bannerUrl` and `banner` * Add `presences` property * Fix `presences`, add `explicitContentLevel`, `maximumMembers`, `maximumPresences`,`welcomeScreen` and `large` * Null check in list * Add doc for activities * Add `description` and `vanityUrlCode` + Minor fix on `discoverySplash` * Deprecate `region` and use `AttachmentBuilder` Instead of String * Add `@createGuild` method * Add `@createGuild` to http endpoints * Remove `ExplicitContentFilterLevel` * Minor fix on widget channel * Update outdated doc * Update `@fetchWelcomeScreen` on `IGuild` * Fix `@getGuildBannerUrl()` * Add `memberCount` property * Remove `maximumPresences` * Removed `welcomeScreen` * Fix doc for `presences` * Remove useless imports * Move `memberCount` under `guildCreate` * Return null when not found * Minor fix on `@changeGuildOwner` * Add `auditReason` on `@changeOwner` * Clarify deprecation message and remove it in map * Add `SystemChannelFlags` * Run dart format * Add `name` as required parameter + fixes `roles` & `channels` passed to list Add new properties based on documentation * Add name property to `ChannelBuilder` Plus add default type to VoiceChannelBuilder & TextChannelBuilder * Add `id` field on `RoleBuilder` * Add doc * Add `id` field in `ChannelBuilder` * Fix build on id * Add doc for length of name * `IWelcomeChannel` => `IGuildWelcomeChannel` * Remove status check * Fix doc for `RoleBuilder` * Fix doc on `@createGuild` * Fix doc on `GuildBuilder#icon` * Add new properties and add `withCounts` on option on `@fetchGuild()` * Use `GuildBuilder` instead of raw options Also remove import of `nyxx` and import subdirs * Be inclusive in docs * Fix typos * Remove global import * Use `AuditLogEntryType` instead of raw integer Also, send int and not String, the docs said And fix dartdoc * Re-fix dartdoc in `@getBans` * Fix doc on `GuildBuilder` * Fix dartdoc on `IGuildWelcomeChannel` * Fix wrong name on `SystemChannelFlags` Thanks Copilot 💀 * Fix typo Damn, this is a running gag * Be consistant with `IGuildPreview` * Fixup: doc formatting * Fixup: imports * Edit doc on `@moveChannel` * Remove global import of nyxx * Remove global import of nyxx in guild_builder.dart * Stringify id in `ChannelBuilder#build()` Discord allows `int`s to be sent tho, but it's better to send a string * Also stringify auditType * Fix doc on `IPartialPresence#user` * Also fix typos * Remove (my) global import of nyxx in presence.dart --- lib/nyxx.dart | 4 +- lib/src/core/guild/guild.dart | 216 ++++++++++++++----- lib/src/core/guild/guild_welcome_screen.dart | 92 ++++++++ lib/src/core/guild/system_channel_flags.dart | 23 ++ lib/src/core/user/presence.dart | 59 ++++- lib/src/internal/http_endpoints.dart | 91 ++++++-- lib/src/nyxx.dart | 30 ++- lib/src/utils/builders/channel_builder.dart | 28 ++- lib/src/utils/builders/guild_builder.dart | 54 ++++- 9 files changed, 494 insertions(+), 103 deletions(-) create mode 100644 lib/src/core/guild/guild_welcome_screen.dart create mode 100644 lib/src/core/guild/system_channel_flags.dart diff --git a/lib/nyxx.dart b/lib/nyxx.dart index c414a4a8d..eb6b2437f 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -47,6 +47,8 @@ export 'src/core/guild/premium_tier.dart' show PremiumTier; export 'src/core/guild/role.dart' show IRole, IRoleTags; export 'src/core/guild/status.dart' show IClientStatus, UserStatus; export 'src/core/guild/webhook.dart' show IWebhook, WebhookType; +export 'src/core/guild/guild_welcome_screen.dart' show IGuildWelcomeScreen, IGuildWelcomeChannel; +export 'src/core/guild/system_channel_flags.dart' show SystemChannelFlags; export 'src/core/message/attachment.dart' show IAttachment; export 'src/core/message/emoji.dart' show IEmoji; export 'src/core/message/guild_emoji.dart' show IBaseGuildEmoji, IGuildEmoji, IGuildEmojiPartial, IResolvableGuildEmojiPartial; @@ -78,7 +80,7 @@ export 'src/core/permissions/permissions_constants.dart' show PermissionsConstan export 'src/core/user/member.dart' show IMember; export 'src/core/user/nitro_type.dart' show NitroType; export 'src/core/user/presence.dart' - show IActivity, IActivityEmoji, IActivityFlags, IActivityParty, IActivityTimestamps, IGameAssets, IGameSecrets, ActivityType; + show IActivity, IActivityEmoji, IActivityFlags, IActivityParty, IActivityTimestamps, IGameAssets, IGameSecrets, ActivityType, IPartialPresence; export 'src/core/user/user.dart' show IUser; export 'src/core/user/user_flags.dart' show IUserFlags; export 'src/core/voice/voice_region.dart' show IVoiceRegion; diff --git a/lib/src/core/guild/guild.dart b/lib/src/core/guild/guild.dart index 111258a61..02c4ee09d 100644 --- a/lib/src/core/guild/guild.dart +++ b/lib/src/core/guild/guild.dart @@ -1,32 +1,35 @@ -import 'package:nyxx/src/core/guild/scheduled_event.dart'; -import 'package:nyxx/src/internal/exceptions/invalid_shard_exception.dart'; -import 'package:nyxx/src/nyxx.dart'; +import 'package:nyxx/src/core/audit_logs/audit_log.dart'; +import 'package:nyxx/src/core/audit_logs/audit_log_entry.dart'; +import 'package:nyxx/src/core/channel/guild/guild_channel.dart'; import 'package:nyxx/src/core/channel/invite.dart'; +import 'package:nyxx/src/core/channel/text_channel.dart'; +import 'package:nyxx/src/core/guild/guild_feature.dart'; +import 'package:nyxx/src/core/guild/guild_nsfw_level.dart'; +import 'package:nyxx/src/core/guild/guild_preview.dart'; +import 'package:nyxx/src/core/guild/guild_welcome_screen.dart'; +import 'package:nyxx/src/core/guild/premium_tier.dart'; +import 'package:nyxx/src/core/guild/scheduled_event.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; -import 'package:nyxx/src/core/audit_logs/audit_log.dart'; +import 'package:nyxx/src/core/user/presence.dart'; +import 'package:nyxx/src/core/user/user.dart'; +import 'package:nyxx/src/internal/cache/cache.dart'; +import 'package:nyxx/src/internal/exceptions/invalid_shard_exception.dart'; +import 'package:nyxx/src/internal/shard/shard.dart'; +import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/channel/cacheable_text_channel.dart'; import 'package:nyxx/src/core/channel/channel.dart'; -import 'package:nyxx/src/core/channel/text_channel.dart'; -import 'package:nyxx/src/core/channel/guild/guild_channel.dart'; import 'package:nyxx/src/core/channel/guild/text_guild_channel.dart'; import 'package:nyxx/src/core/channel/guild/voice_channel.dart'; import 'package:nyxx/src/core/guild/ban.dart'; -import 'package:nyxx/src/core/guild/guild_feature.dart'; -import 'package:nyxx/src/core/guild/guild_nsfw_level.dart'; -import 'package:nyxx/src/core/guild/guild_preview.dart'; -import 'package:nyxx/src/core/guild/premium_tier.dart'; import 'package:nyxx/src/core/guild/role.dart'; import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/message/sticker.dart'; import 'package:nyxx/src/core/permissions/permissions.dart'; import 'package:nyxx/src/core/user/member.dart'; -import 'package:nyxx/src/core/user/user.dart'; import 'package:nyxx/src/core/voice/voice_region.dart'; import 'package:nyxx/src/core/voice/voice_state.dart'; -import 'package:nyxx/src/internal/cache/cache.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; -import 'package:nyxx/src/internal/shard/shard.dart'; import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/builders/attachment_builder.dart'; import 'package:nyxx/src/utils/builders/channel_builder.dart'; @@ -148,6 +151,41 @@ abstract class IGuild implements SnowflakeEntity { /// Whether the guild has the boost progress bar enabled bool get boostProgressBarEnabled; + /// The banner hash of the guild, if any. + String? get banner; + + /// List of partial presences. + /// + /// Will only include non-offline members if the size of the guild is greater than the [ClientOptions.largeThreshold] option. + List get presences; + + /// If this guild is considered large. + bool get large; + + /// The maximum amount of members that can be in this guild. + int get maximumMembers; + + /// The maximum amount of presences that can be in this guild. + int? get maximumPresences; + + /// Explicit content filter level of this guild. + int get explicitContentFilterLevel; + + /// The vanity URL code of this guild. If any. + String? get vanityUrlCode; + + /// The description of this guild. If it's a community guild. + String? get description; + + /// The total amount of members in this guild. + int? get memberCount; + + /// The approximate amount of members in this guild. + int? get approxMemberCount; + + /// The approximate amount of presences in the guild. + int? get approxPresenceCount; + /// The guild's icon, represented as URL. /// If guild doesn't have icon it returns null. String? iconURL({String format = "webp", int size = 128}); @@ -160,6 +198,10 @@ abstract class IGuild implements SnowflakeEntity { /// If guild doesn't have splash it returns null. String? discoveryURL({String format = "webp", int size = 128}); + /// URL to guild's banner. + /// If guild doesn't have banner it returns null. + String? bannerUrl({String? format, int? size}); + /// Allows to download [Guild] widget aka advert png /// Possible options for [style]: shield (default), banner1, banner2, banner3, banner4 String guildWidgetUrl([String style = "shield"]); @@ -194,7 +236,7 @@ abstract class IGuild implements SnowflakeEntity { /// Prunes the guild, returns the amount of members pruned. Future prune(int days, {Iterable? includeRoles, String? auditReason}); - /// Get"s the guild's bans. + /// Gets the guild's bans. Stream getBans({int limit = 1000, Snowflake? before, Snowflake? after}); /// Change self nickname in guild @@ -214,17 +256,14 @@ abstract class IGuild implements SnowflakeEntity { /// Returns Audit logs. /// https://discordapp.com/developers/docs/resources/audit-log - /// + /// ```dart + /// var logs = await guild.fetchAuditLogs(auditType: AuditLogEntryType.guildUpdate); /// ``` - /// var logs = await guild.fetchAuditLogs(actionType: 1); - /// ``` - Future fetchAuditLogs({Snowflake? userId, int? actionType, Snowflake? before, int? limit}); + Future fetchAuditLogs({Snowflake? userId, AuditLogEntryType? auditType, Snowflake? before, int? limit}); /// Creates new role - /// - /// ``` - /// var rb = new RoleBuilder() - /// ..name = "Dartyy" + /// ```dart + /// var rb = new RoleBuilder("Dartyy") /// ..color = DiscordColor.fromInt(0xFF04F2) /// ..hoist = true; /// @@ -239,15 +278,13 @@ abstract class IGuild implements SnowflakeEntity { Future moveChannel(IChannel channel, int position, {String? auditReason}); /// Bans a user and allows to delete messages from [deleteMessageDays] number of days. - /// ``` - /// + /// ```dart /// await guild.ban(member); /// ``` Future ban(SnowflakeEntity user, {int deleteMessageDays = 0, String? auditReason}); - /// Kicks user from guild. Member is removed from guild and he is able to rejoin - /// - /// ``` + /// Kicks user from guild. Member is removed from guild and they're able to rejoin if they have a valid invite link. + /// ```dart /// await guild.kick(member); /// ``` Future kick(SnowflakeEntity user, {String? auditReason}); @@ -256,8 +293,7 @@ abstract class IGuild implements SnowflakeEntity { Future unban(Snowflake id, Snowflake userId); /// Edits the guild. - Future edit( - {String? name, int? verificationLevel, int? notificationLevel, SnowflakeEntity? afkChannel, int? afkTimeout, String? icon, String? auditReason}); + Future edit(GuildBuilder builder, {String? auditReason}); /// Fetches member from API Future fetchMember(Snowflake memberId); @@ -295,6 +331,9 @@ abstract class IGuild implements SnowflakeEntity { /// Fetches from api list of events in guild Stream fetchGuildEvents({bool withUserCount = false}); + + /// Fetches the welcome screen of this guild if it's a community guild. + Future fetchWelcomeScreen(); } class Guild extends SnowflakeEntity implements IGuild { @@ -427,9 +466,55 @@ class Guild extends SnowflakeEntity implements IGuild { @override late final bool boostProgressBarEnabled; + /// The banner hash of the guild. If any. + @override + late final String? banner; + + /// List of partial presences. + /// + /// Will only include non-offline members if the size of the guild is greater than the [ClientOptions.largeThreshold] option. + @override + late final List presences; + + /// If this guild is considered large. + @override + late final bool large; + + /// The maximum amount of members that can be in this guild. + @override + late final int maximumMembers; + + /// The approximate amount of members in this guild. + @override + late final int? approxMemberCount; + + /// The approximate amount of presences in this guild. + @override + late final int? approxPresenceCount; + + /// The maximum amount of presences that can be in this guild. + @override + late final int? maximumPresences; + + /// Explicit content filter level of guild + @override + late final int explicitContentFilterLevel; + + /// The vanity URL code of the guild. If any. + @override + late final String? vanityUrlCode; + + /// The description of the guild. If it's a community guild. + @override + late final String? description; + + /// The total amount of members in the guild. + @override + late final int? memberCount; + /// Returns url to this guild. @override - String get url => "https://discordapp.com/channels/${id.toString()}"; + String get url => "https://discordapp.com/guilds/${id.toString()}"; /// Getter for @everyone role @override @@ -487,15 +572,25 @@ class Guild extends SnowflakeEntity implements IGuild { available = !(raw["unavailable"] as bool? ?? false); icon = raw["icon"] as String?; - discoverySplash = raw["discoverySplash"] as String?; + discoverySplash = raw["discovery_splash"] as String?; splash = raw["splash"] as String?; - embedEnabled = raw["embed_enabled"] as bool?; + embedEnabled = raw["widget_enabled"] as bool?; systemChannelFlags = raw["system_channel_flags"] as int; premiumTier = PremiumTier.from(raw["premium_tier"] as int); premiumSubscriptionCount = raw["premium_subscription_count"] as int?; preferredLocale = raw["preferred_locale"] as String; boostProgressBarEnabled = raw['premium_progress_bar_enabled'] as bool; + banner = raw['banner'] as String?; + large = raw["large"] as bool? ?? false; + maximumMembers = raw["max_members"] as int; + maximumPresences = raw["max_presences"] as int?; + explicitContentFilterLevel = raw["explicit_content_filter"] as int; + vanityUrlCode = raw["vanity_url_code"] as String?; + description = raw["description"] as String?; + memberCount = raw["member_count"] as int?; + approxMemberCount = raw["approximate_member_count"] as int?; + approxPresenceCount = raw["approximate_presence_count"] as int?; owner = UserCacheable(client, Snowflake(raw["owner_id"])); @@ -515,8 +610,8 @@ class Guild extends SnowflakeEntity implements IGuild { }); } - if (raw["embed_channel_id"] != null) { - embedChannel = ChannelCacheable(client, Snowflake(raw["embed_channel_id"])); + if (raw["widget_channel_id"] != null) { + embedChannel = ChannelCacheable(client, Snowflake(raw["widget_channel_id"])); } else { embedChannel = null; } @@ -582,6 +677,11 @@ class Guild extends SnowflakeEntity implements IGuild { publicUpdatesChannel = null; } + presences = [ + if (raw['presences'] != null) + for (final presence in raw['presences']) PartialPresence(presence as RawApiMap, client) + ]; + stageInstances = [ if (raw["stage_instances"] != null) for (final rawInstance in raw["stage_instances"]) StageChannelInstance(client, rawInstance as RawApiMap) @@ -608,6 +708,11 @@ class Guild extends SnowflakeEntity implements IGuild { @override String guildWidgetUrl([String style = "shield"]) => client.httpEndpoints.getGuildWidgetUrl(id, style); + /// Returns the URL to guild's banner. + /// If guild doesn't have banner it returns null. + @override + String? bannerUrl({String? format, int? size}) => client.httpEndpoints.getGuildBannerUrl(id, banner, format: format, size: size); + /// Fetches all stickers of current guild @override Stream fetchStickers() => client.httpEndpoints.fetchGuildStickers(id); @@ -631,7 +736,7 @@ class Guild extends SnowflakeEntity implements IGuild { /// Allows to create new guild emoji. [name] is required. You can allow to set [roles] to restrict emoji usage. /// Put your image in [emojiAttachment] field. /// - /// ``` + /// ```dart /// var emojiFile = File("weed.png"); /// var emoji = await guild.createEmoji("weed", emojiAttachment: AttachmentBuilder.file(emojiFile)); /// ``` @@ -648,7 +753,7 @@ class Guild extends SnowflakeEntity implements IGuild { Future prune(int days, {Iterable? includeRoles, String? auditReason}) => client.httpEndpoints.guildPrune(id, days, includeRoles: includeRoles, auditReason: auditReason); - /// Get"s the guild's bans. + /// Gets the guild's bans. @override Stream getBans({int limit = 1000, Snowflake? before, Snowflake? after}) => client.httpEndpoints.getGuildBans(id, limit: limit, before: before, after: after); @@ -663,7 +768,8 @@ class Guild extends SnowflakeEntity implements IGuild { /// Change guild owner. @override - Future changeOwner(SnowflakeEntity memberEntity, {String? auditReason}) => client.httpEndpoints.changeGuildOwner(id, memberEntity); + Future changeOwner(SnowflakeEntity memberEntity, {String? auditReason}) => + client.httpEndpoints.changeGuildOwner(id, memberEntity, auditReason: auditReason); /// Leaves the guild. @override @@ -676,18 +782,17 @@ class Guild extends SnowflakeEntity implements IGuild { /// Returns Audit logs. /// https://discordapp.com/developers/docs/resources/audit-log /// - /// ``` - /// var logs = await guild.fetchAuditLogs(actionType: 1); + /// ```dart + /// var logs = await guild.fetchAuditLogs(auditType: AuditLogEntryType.guildUpdate); /// ``` @override - Future fetchAuditLogs({Snowflake? userId, int? actionType, Snowflake? before, int? limit}) => - client.httpEndpoints.fetchAuditLogs(id, userId: userId, actionType: actionType, before: before, limit: limit); + Future fetchAuditLogs({Snowflake? userId, AuditLogEntryType? auditType, Snowflake? before, int? limit}) => + client.httpEndpoints.fetchAuditLogs(id, userId: userId, auditType: auditType, before: before, limit: limit); /// Creates new role /// - /// ``` - /// var rb = new RoleBuilder() - /// ..name = "Dartyy" + /// ```dart + /// var rb = RoleBuilder("Dartyy") /// ..color = DiscordColor.fromInt(0xFF04F2) /// ..hoist = true; /// @@ -700,23 +805,22 @@ class Guild extends SnowflakeEntity implements IGuild { @override Stream getVoiceRegions() => client.httpEndpoints.fetchGuildVoiceRegions(id); - /// Moves channel + /// Moves the [channel] for the given [position]. @override Future moveChannel(IChannel channel, int position, {String? auditReason}) => client.httpEndpoints.moveGuildChannel(id, channel.id, position, auditReason: auditReason); /// Bans a user and allows to delete messages from [deleteMessageDays] number of days. - /// ``` - /// + /// ```dart /// await guild.ban(member); /// ``` @override Future ban(SnowflakeEntity user, {int deleteMessageDays = 0, String? auditReason}) => client.httpEndpoints.guildBan(id, user.id, deleteMessageDays: deleteMessageDays, auditReason: auditReason); - /// Kicks user from guild. Member is removed from guild and he is able to rejoin + /// Kicks user from guild. Member is removed from guild and they're able to rejoin if they have a valid invite link. /// - /// ``` + /// ```dart /// await guild.kick(member); /// ``` @override @@ -728,16 +832,7 @@ class Guild extends SnowflakeEntity implements IGuild { /// Edits the guild. @override - Future edit( - {String? name, int? verificationLevel, int? notificationLevel, SnowflakeEntity? afkChannel, int? afkTimeout, String? icon, String? auditReason}) => - client.httpEndpoints.editGuild(id, - name: name, - verificationLevel: verificationLevel, - notificationLevel: notificationLevel, - afkChannel: afkChannel, - afkTimeout: afkTimeout, - icon: icon, - auditReason: auditReason); + Future edit(GuildBuilder builder, {String? auditReason}) => client.httpEndpoints.editGuild(id, builder, auditReason: auditReason); /// Fetches member from API @override @@ -800,4 +895,7 @@ class Guild extends SnowflakeEntity implements IGuild { @override Stream fetchGuildEvents({bool withUserCount = false}) => client.httpEndpoints.fetchGuildEvents(id); + + @override + Future fetchWelcomeScreen() => client.httpEndpoints.fetchGuildWelcomeScreen(id); } diff --git a/lib/src/core/guild/guild_welcome_screen.dart b/lib/src/core/guild/guild_welcome_screen.dart new file mode 100644 index 000000000..4f6918f8d --- /dev/null +++ b/lib/src/core/guild/guild_welcome_screen.dart @@ -0,0 +1,92 @@ +import 'package:nyxx/src/core/channel/channel.dart'; +import 'package:nyxx/src/core/message/emoji.dart'; +import 'package:nyxx/src/core/message/unicode_emoji.dart'; +import 'package:nyxx/src/core/message/guild_emoji.dart'; +import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/internal/cache/cacheable.dart'; +import 'package:nyxx/src/nyxx.dart'; +import 'package:nyxx/src/typedefs.dart'; + +abstract class IGuildWelcomeScreen { + /// The server description shown in the welcome screen. + String? get description; + + /// The channels shown in the welcome screen. + /// Up to 5 channels. + List get channels; +} + +class GuildWelcomeScreen implements IGuildWelcomeScreen { + /// The server description shown in the welcome screen. + @override + late final String? description; + + /// The channels shown in the welcome screen. + /// Up to 5 channels. + @override + late final List channels; + + /// Creates an instance of [GuildWelcomeScreen] + GuildWelcomeScreen(RawApiMap raw, INyxx client) { + description = raw["description"] as String?; + channels = [for (final rawChannel in raw["welcome_channels"]) GuildWelcomeChannel(rawChannel as RawApiMap, client)]; + } +} + +abstract class IGuildWelcomeChannel { + /// The channel of this welcome screen. + Cacheable get channel; + + /// The description shown for the channel. + String? get description; + + /// The emoji id if [emojiName] is a custom emoji. + Snowflake? get emojiId; + + /// The name of the emoji if custom, otherwise the unicode character. + /// Or `null` if no emoji is set. + String? get emojiName; + + /// The emoji in the channel. + /// This can be a [UnicodeEmoji] or a [IResolvableGuildEmojiPartial] + IEmoji? get emoji; +} + +class GuildWelcomeChannel implements IGuildWelcomeChannel { + /// The channel of this welcome screen. + @override + late final Cacheable channel; + + /// The description shown for the channel. + @override + late final String? description; + + /// The emoji id if [emojiName] is a custom emoji. + @override + late final Snowflake? emojiId; + + /// The name of the emoji if custom, otherwise the unicode character. + /// Or `null` if no emoji is set. + @override + late final String? emojiName; + + /// The emoji in the channel. + /// This can be a [UnicodeEmoji] or a [IResolvableGuildEmojiPartial] + @override + late final IEmoji? emoji; + + /// Creates an instance of [GuildWelcomeChannel] + GuildWelcomeChannel(RawApiMap raw, INyxx client) { + channel = ChannelCacheable(client, Snowflake(raw["channel_id"])); + description = raw["description"] as String?; + emojiName = raw["emoji_name"] as String?; + + if (raw['emoji_id'] != null) { + emojiId = Snowflake(raw['emoji_id']); + // Used because ResolvableEmoji takes a map and not a sole id + emoji = ResolvableGuildEmojiPartial({'id': emojiId}, client); + } else { + emoji = UnicodeEmoji(emojiName!); + } + } +} diff --git a/lib/src/core/guild/system_channel_flags.dart b/lib/src/core/guild/system_channel_flags.dart new file mode 100644 index 000000000..cbb4d76ec --- /dev/null +++ b/lib/src/core/guild/system_channel_flags.dart @@ -0,0 +1,23 @@ +import 'package:nyxx/src/utils/enum.dart'; + +class SystemChannelFlags extends IEnum { + static const suppressJoinNotifications = SystemChannelFlags._create(1 << 0); + static const suppressPremiumSubscriptions = SystemChannelFlags._create(1 << 1); + static const suppressGuildReminderNotifications = SystemChannelFlags._create(1 << 2); + static const suppressJoinNotificationReplies = SystemChannelFlags._create(1 << 3); + + const SystemChannelFlags._create(int? value) : super(value ?? 0); + SystemChannelFlags.from(int? value) : super(value ?? 0); + + @override + bool operator ==(dynamic other) { + if (other is int) { + return other == value; + } + + return super == other; + } + + @override + int get hashCode => value.hashCode; +} diff --git a/lib/src/core/user/presence.dart b/lib/src/core/user/presence.dart index c69af7912..a4856793f 100644 --- a/lib/src/core/user/presence.dart +++ b/lib/src/core/user/presence.dart @@ -1,7 +1,11 @@ +import 'package:nyxx/src/core/guild/status.dart'; import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/internal/cache/cacheable.dart'; +import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/enum.dart'; import 'package:nyxx/src/utils/permissions.dart'; +import 'package:nyxx/src/core/user/user.dart'; abstract class IActivity { /// The activity name. @@ -351,7 +355,7 @@ abstract class IGameAssets { String? get smallText; } -/// Presence"s assets +/// Presences assets class GameAssets implements IGameAssets { /// The id for a large asset of the activity, usually a snowflake. @override @@ -389,7 +393,7 @@ abstract class IGameSecrets { String get match; } -/// Represents presence"s secrets +/// Represents presences secrets class GameSecrets implements IGameSecrets { /// Join secret @override @@ -410,3 +414,54 @@ class GameSecrets implements IGameSecrets { match = raw["match"] as String; } } + +abstract class IPartialPresence { + /// Reference to [INyxx] + INyxx get client; + + /// The [IPartialPresence]'s [IUser] + Cacheable? get user; + + /// The status of the user indicating the platform they are on. + IClientStatus? get clientStatus; + + /// The status of the user eg. online, idle, dnd, invisible, offline + UserStatus? get status; + + /// The activities of the user + List get activities; +} + +class PartialPresence implements IPartialPresence { + /// Reference to [INyxx] + @override + final INyxx client; + + /// The [IPartialPresence]'s [IUser] + @override + late final Cacheable? user; + + /// The status of the user indicating the platform they are on. + @override + late final IClientStatus? clientStatus; + + /// The status of the user eg. online, idle, dnd, invisible, offline + @override + late final UserStatus? status; + + /// The activities of the user + @override + late final List activities; + + /// Creates an instance of [PartialPresence] + PartialPresence(RawApiMap raw, this.client) { + user = raw["user"] != null ? UserCacheable(client, Snowflake(raw['user']['id'])) : null; + clientStatus = raw["client_status"] != null ? ClientStatus(raw["client_status"] as RawApiMap) : null; + status = raw["status"] != null ? UserStatus.from(raw["status"] as String) : null; + + activities = [ + if (raw['activities'] != null) + for (final activity in raw["activities"]) Activity(activity as RawApiMap) + ]; + } +} diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index 4d643797a..565be06c6 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/core/audit_logs/audit_log_entry.dart'; import 'package:nyxx/src/core/guild/scheduled_event.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/channel/invite.dart'; @@ -15,6 +16,7 @@ import 'package:nyxx/src/core/guild/guild.dart'; import 'package:nyxx/src/core/guild/guild_preview.dart'; import 'package:nyxx/src/core/guild/role.dart'; import 'package:nyxx/src/core/guild/webhook.dart'; +import 'package:nyxx/src/core/guild/guild_welcome_screen.dart'; import 'package:nyxx/src/core/message/emoji.dart'; import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/message/message.dart'; @@ -33,6 +35,7 @@ import 'package:nyxx/src/utils/builders/attachment_builder.dart'; import 'package:nyxx/src/utils/builders/channel_builder.dart'; import 'package:nyxx/src/utils/builders/guild_builder.dart'; import 'package:nyxx/src/utils/builders/guild_event_builder.dart'; +import 'package:nyxx/src/utils/builders/member_builder.dart'; import 'package:nyxx/src/utils/builders/message_builder.dart'; import 'package:nyxx/src/utils/builders/permissions_builder.dart'; import 'package:nyxx/src/utils/builders/sticker_builder.dart'; @@ -65,6 +68,11 @@ abstract class IHttpEndpoints { /// Returns url to guild widget for given [guildId]. Additionally accepts [style] parameter. String getGuildWidgetUrl(Snowflake guildId, [String style = "shield"]); + /// Returns cnd url for given [guildId] and [bannerHash]. + /// Requires to specify format and size of returned image. + /// Format can be webp, png. Size should be power of 2, eg. 512, 1024 + String? getGuildBannerUrl(Snowflake guildId, String? bannerHash, {String? format, int? size}); + /// Allows to modify guild emoji. Future editGuildEmoji(Snowflake guildId, Snowflake emojiId, {String? name, List? roles, AttachmentBuilder? avatarAttachment}); @@ -81,7 +89,7 @@ abstract class IHttpEndpoints { Future addRoleToUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}); /// Fetches [Guild] object from API - Future fetchGuild(Snowflake guildId); + Future fetchGuild(Snowflake guildId, {bool? withCounts = true}); /// Fetches [IChannel] from API. Channel cas be cast to wanted type using generics Future fetchChannel(Snowflake id); @@ -89,6 +97,9 @@ abstract class IHttpEndpoints { /// Returns [BaseGuildEmoji] for given [emojiId] Future fetchGuildEmoji(Snowflake guildId, Snowflake emojiId); + /// Fetches a [IGuildWelcomeScreen] from the given [guildId] + Future fetchGuildWelcomeScreen(Snowflake guildId); + /// Creates emoji in given guild Future createEmoji(Snowflake guildId, String name, {List? roles, AttachmentBuilder? emojiAttachment}); @@ -116,6 +127,9 @@ abstract class IHttpEndpoints { /// Leaves guild with given id Future leaveGuild(Snowflake guildId); + /// Creates a new guild. + Future createGuild(GuildBuilder builder); + /// Returns list of all guild invites Stream fetchGuildInvites(Snowflake guildId); @@ -123,7 +137,7 @@ abstract class IHttpEndpoints { Future createVoiceActivityInvite(Snowflake activityId, Snowflake channelId, {int? maxAge, int? maxUses}); /// Fetches audit logs of guild - Future fetchAuditLogs(Snowflake guildId, {Snowflake? userId, int? actionType, Snowflake? before, int? limit}); + Future fetchAuditLogs(Snowflake guildId, {Snowflake? userId, AuditLogEntryType? auditType, Snowflake? before, int? limit}); /// Creates new role Future createGuildRole(Snowflake guildId, RoleBuilder roleBuilder, {String? auditReason}); @@ -144,8 +158,7 @@ abstract class IHttpEndpoints { Future guildUnban(Snowflake guildId, Snowflake userId); /// Allows to edit basic guild properties - Future editGuild(Snowflake guildId, - {String? name, int? verificationLevel, int? notificationLevel, SnowflakeEntity? afkChannel, int? afkTimeout, String? icon, String? auditReason}); + Future editGuild(Snowflake guildId, GuildBuilder builder, {String? auditReason}); /// Fetches [Member] object from guild Future fetchGuildMember(Snowflake guildId, Snowflake memberId); @@ -452,6 +465,25 @@ class HttpEndpoints implements IHttpEndpoints { return null; } + @override + String? getGuildBannerUrl(Snowflake guildId, String? bannerHash, {String? format, int? size}) { + if (bannerHash != null) { + var url = "${Constants.cdnUrl}/banners/$guildId/$bannerHash."; + if (format == null && bannerHash.startsWith('a_')) { + url += "gif"; + } else { + url += format ?? "webp"; + } + + if (size != null) { + url += "?size=$size"; + } + + return url; + } + return null; + } + @override String getGuildWidgetUrl(Snowflake guildId, [String style = "shield"]) => "https://cdn.${Constants.cdnHost}/guilds/$guildId/widget.png?style=$style"; @@ -499,8 +531,8 @@ class HttpEndpoints implements IHttpEndpoints { executeSafe(BasicRequest("/guilds/$guildId/members/$userId/roles/$roleId", method: "PUT", auditLog: auditReason)); @override - Future fetchGuild(Snowflake guildId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}")); + Future fetchGuild(Snowflake guildId, {bool? withCounts = true}) async { + final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}", queryParams: {"with_counts": (withCounts ?? true).toString()})); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); @@ -532,6 +564,17 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } + @override + Future fetchGuildWelcomeScreen(Snowflake guildId) async { + final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/welcome-screen")); + + if (response is HttpResponseSuccess) { + return GuildWelcomeScreen(response.jsonBody as RawApiMap, client); + } + + return Future.error(response); + } + @override Future createEmoji(Snowflake guildId, String name, {List? roles, AttachmentBuilder? emojiAttachment}) async { final body = { @@ -630,7 +673,8 @@ class HttpEndpoints implements IHttpEndpoints { @override Future changeGuildOwner(Snowflake guildId, SnowflakeEntity member, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id})); + final response = + await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id.toString()})); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); @@ -642,6 +686,19 @@ class HttpEndpoints implements IHttpEndpoints { @override Future leaveGuild(Snowflake guildId) async => executeSafe(BasicRequest("/users/@me/guilds/$guildId", method: "DELETE")); + @override + Future createGuild(GuildBuilder builder) async { + final response = await httpHandler.execute(BasicRequest("/guilds", method: "POST", body: builder.build())); + + if (response is HttpResponseSuccess) { + final guild = Guild(client, response.jsonBody as RawApiMap); + client.guilds[guild.id] = guild; + return guild; + } + + return Future.error(response); + } + @override Stream fetchGuildInvites(Snowflake guildId) async* { final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/invites")); @@ -673,10 +730,10 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future fetchAuditLogs(Snowflake guildId, {Snowflake? userId, int? actionType, Snowflake? before, int? limit}) async { - final queryParams = { + Future fetchAuditLogs(Snowflake guildId, {Snowflake? userId, AuditLogEntryType? auditType, Snowflake? before, int? limit}) async { + final queryParams = { if (userId != null) "user_id": userId.toString(), - if (actionType != null) "action_type": actionType.toString(), + if (auditType != null) "action_type": auditType.value.toString(), if (before != null) "before": before.toString(), if (limit != null) "limit": limit.toString() }; @@ -731,18 +788,8 @@ class HttpEndpoints implements IHttpEndpoints { Future guildUnban(Snowflake guildId, Snowflake userId) async => executeSafe(BasicRequest("/guilds/$guildId/bans/$userId", method: "DELETE")); @override - Future editGuild(Snowflake guildId, - {String? name, int? verificationLevel, int? notificationLevel, SnowflakeEntity? afkChannel, int? afkTimeout, String? icon, String? auditReason}) async { - final body = { - if (name != null) "name": name, - if (verificationLevel != null) "verification_level": verificationLevel, - if (notificationLevel != null) "default_message_notifications": notificationLevel, - if (afkChannel != null) "afk_channel_id": afkChannel, - if (afkTimeout != null) "afk_timeout": afkTimeout, - if (icon != null) "icon": icon - }; - - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: body)); + Future editGuild(Snowflake guildId, GuildBuilder builder, {String? auditReason}) async { + final response = await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: builder.build())); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); diff --git a/lib/src/nyxx.dart b/lib/src/nyxx.dart index 46dbbf484..6d3ee9f8d 100644 --- a/lib/src/nyxx.dart +++ b/lib/src/nyxx.dart @@ -26,7 +26,8 @@ import 'package:nyxx/src/internal/interfaces/disposable.dart'; import 'package:nyxx/src/internal/shard/shard_manager.dart'; import 'package:nyxx/src/plugin/plugin.dart'; import 'package:nyxx/src/plugin/plugin_manager.dart'; -import 'utils/builders/presence_builder.dart'; +import 'package:nyxx/src/utils/builders/guild_builder.dart'; +import 'package:nyxx/src/utils/builders/presence_builder.dart'; import 'package:nyxx/src/typedefs.dart'; abstract class NyxxFactory { @@ -242,7 +243,17 @@ abstract class INyxxWebsocket implements INyxxRest { Future fetchGuildPreview(Snowflake guildId); /// Returns guild with given [guildId] - Future fetchGuild(Snowflake guildId); + /// If [withCounts] is set to true, then guild will have [IGuild.approximateMemberCount] and [IGuild.approximatePresenceCount] present. + Future fetchGuild(Snowflake guildId, {bool? withCounts = true}); + + /// Creates a guild. + /// + /// **⚠️ This endpoint can only be used by bots that are in ten guilds or fewer.** + /// ```dart + /// var gb = GuildBuilder("Test Guild"); + /// var guild = await client.createGuild(gb); + /// ``` + Future createGuild(GuildBuilder builder); /// Returns channel with specified id. /// ``` @@ -365,8 +376,9 @@ class NyxxWebsocket extends NyxxRest implements INyxxWebsocket { Future fetchGuildPreview(Snowflake guildId) async => httpEndpoints.fetchGuildPreview(guildId); /// Returns guild with given [guildId] + /// If [withCounts] is set to true, then guild will have [IGuild.approximateMemberCount] and [IGuild.approximatePresenceCount] present. @override - Future fetchGuild(Snowflake guildId) => httpEndpoints.fetchGuild(guildId); + Future fetchGuild(Snowflake guildId, {bool? withCounts = true}) => httpEndpoints.fetchGuild(guildId, withCounts: withCounts); /// Returns channel with specified id. /// ``` @@ -378,10 +390,20 @@ class NyxxWebsocket extends NyxxRest implements INyxxWebsocket { /// Get user instance with specified id. /// ``` /// var user = client.getUser(Snowflake("302359032612651009")); - /// `` + /// ``` @override Future fetchUser(Snowflake userId) => httpEndpoints.fetchUser(userId); + /// Creates a guild. + /// + /// **⚠️ This endpoint can only be used by bots that are in ten guilds or fewer.** + /// ```dart + /// var gb = GuildBuilder("Test Guild"); + /// var guild = await client.createGuild(gb); + /// ``` + @override + Future createGuild(GuildBuilder builder) => httpEndpoints.createGuild(builder); + /// Gets a webhook by its id and/or token. /// If token is supplied authentication is not needed. @override diff --git a/lib/src/utils/builders/channel_builder.dart b/lib/src/utils/builders/channel_builder.dart index b06e6af67..ee26c0167 100644 --- a/lib/src/utils/builders/channel_builder.dart +++ b/lib/src/utils/builders/channel_builder.dart @@ -2,6 +2,14 @@ import 'package:nyxx/nyxx.dart'; /// Builder for creating mini channel instance abstract class ChannelBuilder implements Builder { + /// Name of the channel (1-100 characters) + String? name; + + /// Id of the channel. + /// When using the `channels` parameter on [GuildBuilder], this field within each channel object may be set to an integer placeholder, and will be replaced by the API upon consumption. + /// Its purpose is to allow you to create `GUILD_CATEGORY` channels by setting the [parentChannel.id] field on any children to the category's id field. Category channels must be listed before any children. + Snowflake? id; + /// Type of channel ChannelType? type; @@ -16,6 +24,8 @@ abstract class ChannelBuilder implements Builder { @override RawApiMap build() => { + if (name != null) "name": name, + if (id != null) "id": id!.toString(), if (type != null) "type": type!.value, if (position != null) "position": position, if (parentChannel != null) "parent_id": parentChannel!.id.toString(), @@ -24,6 +34,11 @@ abstract class ChannelBuilder implements Builder { } class VoiceChannelBuilder extends ChannelBuilder { + /// Type of channel + @override + // ignore: overridden_fields + ChannelType? type = ChannelType.voice; + /// The bitrate (in bits) of the voice channel (voice only) int? bitrate; @@ -48,8 +63,10 @@ class VoiceChannelBuilder extends ChannelBuilder { } class TextChannelBuilder extends ChannelBuilder { - /// Name of channel - String? name; + /// Type of channel + @override + // ignore: overridden_fields + ChannelType? type = ChannelType.text; /// Channel topic (0-1024 characters) String? topic; @@ -58,12 +75,15 @@ class TextChannelBuilder extends ChannelBuilder { bool? nsfw; TextChannelBuilder(); - TextChannelBuilder.create(this.name); + factory TextChannelBuilder.create(String name) { + final builder = TextChannelBuilder(); + builder.name = name; + return builder; + } @override RawApiMap build() => { ...super.build(), - if (name != null) "name": name, if (topic != null) "topic": topic, if (nsfw != null) "nsfw": nsfw, }; diff --git a/lib/src/utils/builders/guild_builder.dart b/lib/src/utils/builders/guild_builder.dart index 46b7be633..6353fc3ab 100644 --- a/lib/src/utils/builders/guild_builder.dart +++ b/lib/src/utils/builders/guild_builder.dart @@ -1,4 +1,6 @@ import 'package:nyxx/src/core/discord_color.dart'; +import 'package:nyxx/src/core/guild/system_channel_flags.dart'; +import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/builders/attachment_builder.dart'; import 'package:nyxx/src/utils/builders/builder.dart'; @@ -8,13 +10,14 @@ import 'package:nyxx/src/utils/builders/permissions_builder.dart'; /// Allows to build guild object for creating new one or modifying existing class GuildBuilder extends Builder { /// Name of Guild - String? name; + final String name; /// Voice region id + @Deprecated('IGuild.region is deprecated, consider using IVoiceChannel.rtcRegion instead') String? region; - /// Base64 encoded 128x128 image - String? icon; + /// The 128x128 icon for the guild + AttachmentBuilder? icon; /// Verification level int? verificationLevel; @@ -25,22 +28,45 @@ class GuildBuilder extends Builder { /// Explicit content filter level int? explicitContentFilter; - /// List of roles to create at guild creation + /// List of roles to create at guild creation. + /// When using this parameter, the first member of the list is the `@everyone` role - So all the permissions that you give to this role will be applied to all the members of the guild. List? roles; /// List of channel to create at guild creation + /// When using this field, the `position` field of the channel is ignored. + /// And none of the default channels are created. List? channels; + /// The channel id to use for the afk channel + /// The id provided sould be the same of a given id in [channels]. + Snowflake? afkChannelId; + + /// The afk timeout in seconds + int? afkTimeout; + + /// The id of the system channel + /// The id provided sould be the same of a given id in [channels]. + Snowflake? systemChannelId; + + /// The [SystemChannelFlags] to apply + SystemChannelFlags? systemChannelFlags; + + /// Create new instance of [GuildBuilder] + GuildBuilder(this.name); + @override RawApiMap build() => { - if (name != null) "name": name, - if (region != null) "region": region, - if (icon != null) "icon": icon, + "name": name, + if (icon != null) "icon": icon!.getBase64(), if (verificationLevel != null) "verification_level": verificationLevel, if (defaultMessageNotifications != null) "default_message_notifications": defaultMessageNotifications, if (explicitContentFilter != null) "explicit_content_filter": explicitContentFilter, - if (roles != null) "roles": _genIterable(roles!), - if (channels != null) "channels": _genIterable(channels!) + if (roles != null) "roles": _genIterable(roles!).toList(), + if (channels != null) "channels": _genIterable(channels!).toList(), + if (afkChannelId != null) "afk_channel_id": afkChannelId, + if (afkTimeout != null) "afk_timeout": afkTimeout, + if (systemChannelId != null) "system_channel_id": systemChannelId, + if (systemChannelFlags != null) "system_channel_flags": systemChannelFlags!.value, }; Iterable _genIterable(List list) sync* { @@ -55,6 +81,11 @@ class RoleBuilder extends Builder { /// Name of role String name; + /// When using the `roles` parameter in [GuildBuilder], this field is required. It is a [Snowflake] placeholder for the role and will be replaced by the API consumption. + /// + /// Its purpose is to allow overwrite a role's permission in a channel when also passing the `channels` list. + Snowflake? id; + /// Integer representation of hexadecimal color code DiscordColor? color; @@ -70,7 +101,7 @@ class RoleBuilder extends Builder { /// Whether role is mentionable bool? mentionable; - /// ole icon attachment + /// Role icon attachment AttachmentBuilder? roleIcon; /// Role icon emoji @@ -88,6 +119,7 @@ class RoleBuilder extends Builder { if (permission != null) "permissions": permission!.calculatePermissionValue().toString(), if (mentionable != null) "mentionable": mentionable, if (roleIcon != null) "icon": roleIcon!.getBase64(), - if (roleIconEmoji != null) "unicode_emoji": roleIconEmoji + if (roleIconEmoji != null) "unicode_emoji": roleIconEmoji, + if (id != null) "id": id!.id, }; } From f2e70742800f7e97062c20a1146aef77c6790bb3 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 30 Apr 2022 11:42:08 +0200 Subject: [PATCH 10/27] fix: Fixup build --- test/unit/builders_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/builders_test.dart b/test/unit/builders_test.dart index cfc7255e9..7f5a620e2 100644 --- a/test/unit/builders_test.dart +++ b/test/unit/builders_test.dart @@ -61,6 +61,7 @@ main() { 'permission_overwrites': [ {'allow': "0", 'deny': "122406567679", 'id': '0', 'type': 0} ], + 'type': 0, 'name': 'test' }; expect(builder.build(), expectedResult); From 9d9937e0cca0056f657eacafc19fb363a449a4bb Mon Sep 17 00:00:00 2001 From: Gergely Date: Thu, 5 May 2022 19:28:48 +0200 Subject: [PATCH 11/27] Remove redundant argument (#340) --- lib/src/core/guild/guild.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/core/guild/guild.dart b/lib/src/core/guild/guild.dart index 02c4ee09d..dc19e0f49 100644 --- a/lib/src/core/guild/guild.dart +++ b/lib/src/core/guild/guild.dart @@ -290,7 +290,7 @@ abstract class IGuild implements SnowflakeEntity { Future kick(SnowflakeEntity user, {String? auditReason}); /// Unbans a user by ID. - Future unban(Snowflake id, Snowflake userId); + Future unban(Snowflake userId); /// Edits the guild. Future edit(GuildBuilder builder, {String? auditReason}); @@ -828,7 +828,7 @@ class Guild extends SnowflakeEntity implements IGuild { /// Unbans a user by ID. @override - Future unban(Snowflake id, Snowflake userId) => client.httpEndpoints.guildUnban(this.id, userId); + Future unban(Snowflake userId) => client.httpEndpoints.guildUnban(id, userId); /// Edits the guild. @override From 7a019844083d52479f23f4e0fda8412054529eee Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Sun, 8 May 2022 10:07:32 +0100 Subject: [PATCH 12/27] Remove 3 and 7 days archive duration (#341) As they're no longer boost locked --- lib/src/core/guild/guild_feature.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/core/guild/guild_feature.dart b/lib/src/core/guild/guild_feature.dart index dc2e41c0d..5162e3768 100644 --- a/lib/src/core/guild/guild_feature.dart +++ b/lib/src/core/guild/guild_feature.dart @@ -47,12 +47,6 @@ class GuildFeature extends IEnum { /// Guild has increased custom sticker slots static const GuildFeature moreStickers = GuildFeature._create("MORE_STICKERS"); - /// Guild has access to the three day archive time for threads - static const GuildFeature threeDayThreadArchive = GuildFeature._create("THREE_DAY_THREAD_ARCHIVE"); - - /// Guild has access to the seven day archive time for threads - static const GuildFeature sevenDayThreadArchive = GuildFeature._create("SEVEN_DAY_THREAD_ARCHIVE"); - /// Guild has access to create private threads static const GuildFeature privateThreadsEnabled = GuildFeature._create("PRIVATE_THREADS"); From 866fa57f18e0a6ac2bf00ec2ed928236c10b759c Mon Sep 17 00:00:00 2001 From: Nicholas Shrefler <16249086+NDSo@users.noreply.github.com> Date: Thu, 19 May 2022 05:06:04 -0400 Subject: [PATCH 13/27] Implement Dynamic Bucket Rate Limits (#316) * Implement Dynamic Bucket Rate Limits * Implement Dynamic Bucket Rate Limits - address comments * Implement Dynamic Bucket Rate Limits - run dart format * Address feedback * Address feedback * Address feedback * Don't export internal classes Co-authored-by: Abitofevrything --- lib/src/internal/http/http_bucket.dart | 114 ++-- lib/src/internal/http/http_handler.dart | 125 ++-- lib/src/internal/http/http_request.dart | 27 +- lib/src/internal/http/http_route.dart | 281 ++++++++ lib/src/internal/http/http_route_param.dart | 6 + lib/src/internal/http/http_route_part.dart | 8 + lib/src/internal/http_endpoints.dart | 670 +++++++++++++++----- 7 files changed, 948 insertions(+), 283 deletions(-) create mode 100644 lib/src/internal/http/http_route.dart create mode 100644 lib/src/internal/http/http_route_param.dart create mode 100644 lib/src/internal/http/http_route_part.dart diff --git a/lib/src/internal/http/http_bucket.dart b/lib/src/internal/http/http_bucket.dart index 5841227e1..17ac7fe79 100644 --- a/lib/src/internal/http/http_bucket.dart +++ b/lib/src/internal/http/http_bucket.dart @@ -1,94 +1,74 @@ -import 'dart:convert'; - import 'package:http/http.dart' as http; -import 'package:nyxx/src/events/ratelimit_event.dart'; -import 'package:nyxx/src/internal/event_controller.dart'; -import 'package:nyxx/src/internal/exceptions/http_client_exception.dart'; -import 'package:nyxx/src/internal/http/http_handler.dart'; import 'package:nyxx/src/internal/http/http_request.dart'; class HttpBucket { - // Rate limits - int _remaining = 10; - DateTime? _resetAt; - double? _resetAfter; - RestEventController get _events => _httpHandler.client.eventsRest as RestEventController; + static const String xRateLimitBucket = "x-ratelimit-bucket"; + static const String xRateLimitLimit = "x-ratelimit-limit"; + static const String xRateLimitRemaining = "x-ratelimit-remaining"; + static const String xRateLimitReset = "x-ratelimit-reset"; + static const String xRateLimitResetAfter = "x-ratelimit-reset-after"; + + int _limit; + int _remaining; + DateTime _reset; + Duration _resetAfter; + final String _bucketId; + + final List _inFlightRequests = []; - // Bucket ID - late final String id; + int get remaining => _remaining - _inFlightRequests.length; - // Reference to http handler - final HttpHandler _httpHandler; + DateTime get reset => _reset; - /// Creates an instance of [HttpBucket] - HttpBucket(this.id, this._httpHandler); + Duration get resetAfter => _resetAfter; - Future execute(HttpRequest request) async { - _httpHandler.logger.fine( - "Executing request: [${request.uri.toString()}]; Bucket ID: [$id]; Reset at: [$_resetAt]; Remaining: [$_remaining]; Reset after: [$_resetAfter]; Body: [${request is BasicRequest && request.body != null ? request.body : 'EMPTY'}]"); + String get id => _bucketId; - // Get actual time and check if request can be executed based on data that bucket already have - // and wait if rate limit could be possibly hit - final now = DateTime.now(); - if ((_resetAt != null && _resetAt!.isAfter(now)) && _remaining < 2) { - final waitTime = _resetAt!.millisecondsSinceEpoch - now.millisecondsSinceEpoch; + HttpBucket(this._limit, this._remaining, this._reset, this._resetAfter, this._bucketId); - if (waitTime > 0) { - _events.onRateLimitedController.add(RatelimitEvent(request, true)); - _httpHandler.logger.warning("Rate limited internally on endpoint: ${request.uri}. Trying to send request again in $waitTime ms..."); + static HttpBucket? fromResponseSafe(http.StreamedResponse response) { + final limit = getLimitFromHeaders(response.headers); + final remaining = getRemainingFromHeaders(response.headers); + final reset = getResetFromHeaders(response.headers); + final resetAfter = getResetAfterFromHeaders(response.headers); + final bucketId = getBucketIdFromHeaders(response.headers); - return Future.delayed(Duration(milliseconds: waitTime), () => execute(request)); - } + if (limit == null || remaining == null || reset == null || resetAfter == null || bucketId == null) { + return null; } - // Execute request - try { - final response = await _httpHandler.httpClient.send(await request.prepareRequest()); + return HttpBucket(limit, remaining, reset, resetAfter, bucketId); + } - _setBucketValues(response.headers); - return response; - } on HttpClientException catch (e) { - if (e.response == null) { - _httpHandler.logger.warning("Http Error on endpoint: ${request.uri}. Error: [${e.message.toString()}]."); - return Future.error(e); - } + static String? getBucketIdFromHeaders(Map headers) => headers[xRateLimitBucket]; - final response = e.response as http.StreamedResponse; + static int? getLimitFromHeaders(Map headers) => headers[xRateLimitLimit] == null ? null : int.parse(headers[xRateLimitLimit]!); - // Check for 429, emmit events and wait given in response body time - if (response.statusCode == 429) { - final responseBody = jsonDecode(await response.stream.bytesToString()); - final retryAfter = ((responseBody["retry_after"] as double) * 1000).round(); + static int? getRemainingFromHeaders(Map headers) => headers[xRateLimitRemaining] == null ? null : int.parse(headers[xRateLimitRemaining]!); - _events.onRateLimitedController.add(RatelimitEvent(request, false, response)); - _httpHandler.logger.warning("Rate limited via 429 on endpoint: ${request.uri}. Trying to send request again in $retryAfter ms..."); + // Server-Client clock drift makes headers.reset useless, build reset from headers.resetAfter and DateTime.now() + static DateTime? getResetFromHeaders(Map headers) => + headers[xRateLimitResetAfter] == null ? null : DateTime.now().add(getResetAfterFromHeaders(headers)!); - return Future.delayed(Duration(milliseconds: retryAfter), () => execute(request)); - } + static Duration? getResetAfterFromHeaders(Map headers) => + headers[xRateLimitResetAfter] == null ? null : Duration(milliseconds: (double.parse(headers[xRateLimitResetAfter]!) * 1000).ceil()); - // Return http error - _setBucketValues(response.headers); - return response; - } + void addInFlightRequest(HttpRequest httpRequest) => _inFlightRequests.add(httpRequest); + + void removeInFlightRequest(HttpRequest httpRequest) => _inFlightRequests.remove(httpRequest); + + bool isInBucket(http.StreamedResponse response) { + return getBucketIdFromHeaders(response.headers) == _bucketId; } - void _setBucketValues(Map headers) { - if (headers["x-ratelimit-remaining"] != null) { - _remaining = int.parse(headers["x-ratelimit-remaining"]!); - } + void updateRateLimit(http.StreamedResponse response) { + if (isInBucket(response)) { + _remaining = getRemainingFromHeaders(response.headers) ?? _remaining; - // seconds since epoch - if (headers["x-ratelimit-reset"] != null) { - final secondsSinceEpoch = (double.parse(headers["x-ratelimit-reset"]!) * 1000000).toInt(); - _resetAt = DateTime.fromMicrosecondsSinceEpoch(secondsSinceEpoch); - } + _reset = getResetFromHeaders(response.headers) ?? _reset; - if (headers["x-ratelimit-reset-after"] != null) { - _resetAfter = double.parse(headers["x-ratelimit-reset-after"]!); + _resetAfter = getResetAfterFromHeaders(response.headers) ?? _resetAfter; } - - _httpHandler.logger.finer( - "Added http header values: HTTP Bucket ID: [${headers['x-ratelimit-bucket']}]; Reset at: [$_resetAt]; Remaining: [$_remaining]; Reset after: [$_resetAfter]"); } } diff --git a/lib/src/internal/http/http_handler.dart b/lib/src/internal/http/http_handler.dart index c17f74589..bf5cce638 100644 --- a/lib/src/internal/http/http_handler.dart +++ b/lib/src/internal/http/http_handler.dart @@ -1,84 +1,117 @@ +import 'dart:convert'; + import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:nyxx/src/events/http_events.dart'; +import 'package:nyxx/src/events/ratelimit_event.dart'; import 'package:nyxx/src/internal/event_controller.dart'; +import 'package:nyxx/src/internal/exceptions/http_client_exception.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/internal/http/http_bucket.dart'; import 'package:nyxx/src/internal/http/http_request.dart'; import 'package:nyxx/src/internal/http/http_response.dart'; +import 'package:nyxx/src/utils/utils.dart'; class HttpHandler { - final RegExp _bucketRegexp = RegExp(r"\/(channels|guilds)\/(\d+)"); - final RegExp _bucketReactionsRegexp = RegExp(r"\/channels/(\d+)\/messages\/(\d+)\/reactions"); - final RegExp _bucketCommandPermissions = RegExp(r"\/applications/(\d+)\/guilds\/(\d+)\/commands/permissions"); - - final List _buckets = []; - late final HttpBucket _noRateBucket; - late final http.Client httpClient; final Logger logger = Logger("Http"); final INyxxRest client; + RestEventController get _events => client.eventsRest as RestEventController; + + final Map _bucketByRequestRateLimitId = {}; + DateTime globalRateLimitReset = DateTime.fromMillisecondsSinceEpoch(0); + /// Creates an instance of [HttpHandler] HttpHandler(this.client) { - _noRateBucket = HttpBucket("", this); httpClient = http.Client(); } + HttpBucket? _upsertBucket(HttpRequest request, http.StreamedResponse response) { + //Get or Create Bucket + final bucket = _bucketByRequestRateLimitId.values.toList().firstWhereSafe((bucket) => bucket.isInBucket(response)) ?? HttpBucket.fromResponseSafe(response); + //Update Bucket + bucket?.updateRateLimit(response); + + //Update request -> bucket mapping + if (bucket != null) { + _bucketByRequestRateLimitId.update( + request.rateLimitId, + (b) => bucket, + ifAbsent: () => bucket, + ); + } + + return bucket; + } + Future execute(HttpRequest request) async { if (request.auth) { request.headers.addAll({"Authorization": "Bot ${client.token}"}); } - if (!request.rateLimit) { - return _handle(await _noRateBucket.execute(request)); - } - - final bucket = _getBucketForRequest(request); - return _handle(await bucket.execute(request)); - } + HttpBucket? currentBucket = _bucketByRequestRateLimitId[request.rateLimitId]; + logger.fine( + "Executing request: [${request.uri.toString()}]; Bucket ID: [${currentBucket?.id}]; Reset at: [${currentBucket?.reset}]; Remaining: [${currentBucket?.remaining}]; Reset after: [${currentBucket?.resetAfter}]; Body: [${request is BasicRequest && request.body != null ? request.body : 'EMPTY'}]"); - HttpBucket _getBucketForRequest(HttpRequest request) { - final reactionsRegexMatch = _bucketReactionsRegexp.firstMatch(request.uri.toString()); - if (reactionsRegexMatch != null) { - final bucketMajorId = reactionsRegexMatch.group(1); - final bucketMessageId = reactionsRegexMatch.group(2); + // Get actual time and check if request can be executed based on data that bucket already have + // and wait if rate limit could be possibly hit + final now = DateTime.now(); + final globalWaitTime = request.globalRateLimit ? globalRateLimitReset.difference(now) : Duration.zero; + final bucketWaitTime = (currentBucket?.remaining ?? 1) > 0 ? Duration.zero : currentBucket!.reset.difference(now); + final waitTime = globalWaitTime.compareTo(bucketWaitTime) > 0 ? globalWaitTime : bucketWaitTime; - return _findBucketById("reactions/$bucketMajorId/$bucketMessageId"); + if (globalWaitTime > Duration.zero) { + logger.warning("Global rate limit reached on endpoint: ${request.uri}"); } - final commandPermissionRegexMatch = _bucketCommandPermissions.firstMatch(request.uri.toString()); - if (commandPermissionRegexMatch != null) { - final bucketMajorId = commandPermissionRegexMatch.group(1); - final bucketMessageId = commandPermissionRegexMatch.group(2); - - return _findBucketById("commands/permissions/$bucketMajorId/$bucketMessageId"); + if (bucketWaitTime > Duration.zero) { + logger.warning("Bucket rate limit reached on endpoint: ${request.uri}"); } - final bucketRegexMatch = _bucketRegexp.firstMatch(request.uri.toString()); - late String bucketId; - - if (bucketRegexMatch == null) { - bucketId = request.uri.toString(); - } else { - final bucketName = bucketRegexMatch.group(1); - final bucketMajorId = bucketRegexMatch.group(2); - bucketId = "${request.method}/$bucketName/$bucketMajorId"; + if (waitTime > Duration.zero) { + logger.warning("Trying to send request again in $waitTime"); + _events.onRateLimitedController.add(RatelimitEvent(request, true)); + return await Future.delayed(waitTime, () async => await execute(request)); } - return _findBucketById(bucketId); - } - - HttpBucket _findBucketById(String bucketId) { + // Execute request try { - return _buckets.firstWhere((element) => element.id == bucketId); - } on StateError { - final newBucket = HttpBucket(bucketId, this); - _buckets.add(newBucket); - - return newBucket; + currentBucket?.addInFlightRequest(request); + final response = await httpClient.send(await request.prepareRequest()); + currentBucket?.removeInFlightRequest(request); + currentBucket = _upsertBucket(request, response); + return _handle(response); + } on HttpClientException catch (e) { + currentBucket?.removeInFlightRequest(request); + if (e.response == null) { + logger.warning("Http Error on endpoint: ${request.uri}. Error: [${e.message.toString()}]."); + return Future.error(e); + } + + final response = e.response as http.StreamedResponse; + _upsertBucket(request, response); + + // Check for 429, emmit events and wait given in response body time + if (response.statusCode == 429) { + final responseBody = jsonDecode(await response.stream.bytesToString()); + final retryAfter = Duration(milliseconds: ((responseBody["retry_after"] as double) * 1000).ceil()); + final isGlobal = responseBody["global"] as bool; + + if (isGlobal) { + globalRateLimitReset = DateTime.now().add(retryAfter); + } + + _events.onRateLimitedController.add(RatelimitEvent(request, false, response)); + logger.warning("${isGlobal ? "Global" : ""} Rate limited via 429 on endpoint: ${request.uri}. Trying to send request again in $retryAfter"); + + return Future.delayed(retryAfter, () => execute(request)); + } + + // Return http error + return _handle(response); } } diff --git a/lib/src/internal/http/http_request.dart b/lib/src/internal/http/http_request.dart index a9b040e9c..6e3303670 100644 --- a/lib/src/internal/http/http_request.dart +++ b/lib/src/internal/http/http_request.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:nyxx/src/internal/constants.dart'; +import 'package:nyxx/src/internal/http/http_route.dart'; import 'package:nyxx/src/typedefs.dart'; abstract class HttpRequest { @@ -14,11 +15,13 @@ abstract class HttpRequest { final String? auditLog; final bool auth; - final bool rateLimit; + final bool globalRateLimit; + final HttpRoute route; + String get rateLimitId => method + route.routeId; /// Creates and instance of [HttpRequest] - HttpRequest(String path, {this.method = "GET", this.queryParams, Map? headers, this.auditLog, this.rateLimit = true, this.auth = true}) { - uri = Uri.https(Constants.host, Constants.baseUri + path); + HttpRequest(this.route, {this.method = "GET", this.queryParams, Map? headers, this.auditLog, this.globalRateLimit = true, this.auth = true}) { + uri = Uri.https(Constants.host, Constants.baseUri + route.path); this.headers = headers ?? {}; } @@ -33,9 +36,9 @@ class BasicRequest extends HttpRequest { /// Body of request final dynamic body; - BasicRequest(String path, - {String method = "GET", this.body, RawApiMap? queryParams, String? auditLog, Map? headers, bool rateLimit = true, bool auth = true}) - : super(path, method: method, queryParams: queryParams, auditLog: auditLog, headers: headers, rateLimit: rateLimit, auth: auth); + BasicRequest(HttpRoute route, + {String method = "GET", this.body, RawApiMap? queryParams, String? auditLog, Map? headers, bool globalRateLimit = true, bool auth = true}) + : super(route, method: method, queryParams: queryParams, auditLog: auditLog, headers: headers, globalRateLimit: globalRateLimit, auth: auth); @override Future prepareRequest() async { @@ -65,9 +68,15 @@ class MultipartRequest extends HttpRequest { final dynamic fields; /// Creates an instance of [MultipartRequest] - MultipartRequest(String path, this.files, - {this.fields, String method = "GET", RawApiMap? queryParams, Map? headers, String? auditLog, bool auth = true, bool rateLimit = true}) - : super(path, method: method, queryParams: queryParams, headers: headers, auditLog: auditLog, rateLimit: rateLimit, auth: auth); + MultipartRequest(HttpRoute route, this.files, + {this.fields, + String method = "GET", + RawApiMap? queryParams, + Map? headers, + String? auditLog, + bool auth = true, + bool globalRateLimit = true}) + : super(route, method: method, queryParams: queryParams, headers: headers, auditLog: auditLog, globalRateLimit: globalRateLimit, auth: auth); @override Future prepareRequest() async { diff --git a/lib/src/internal/http/http_route.dart b/lib/src/internal/http/http_route.dart new file mode 100644 index 000000000..131f8a1ed --- /dev/null +++ b/lib/src/internal/http/http_route.dart @@ -0,0 +1,281 @@ +import 'http_route_param.dart'; +import 'http_route_part.dart'; + +/// Builds routes according to Discord's dynamic bucket rate limiting scheme. +/// +/// Use builder syntax such as: +/// ```dart +/// var route = HttpRoute()..guilds(id: id)..members()..search(); +/// ``` +/// to keep route definitions brief while reusing route rate limiting definitions. +/// If creating custom routes with [add], remember to comply with Discord's +/// rate limiting scheme by toggling the appropriate [HttpRouteParam.isMajor]. +abstract class IHttpRoute { + /// Creates a new empty [IHttpRoute]. + factory IHttpRoute() = HttpRoute; + + /// Adds a [HttpRoutePart] to this [IHttpRoute]. + void add(HttpRoutePart httpRoutePart); + + /// Adds the [`guilds`](https://discord.com/developers/docs/resources/guild#get-guild) part to this [IHttpRoute]. + void guilds({String? id}); + + /// Adds the [`channels`](https://discord.com/developers/docs/resources/channel#get-channel) part to this [IHttpRoute]. + void channels({String? id}); + + /// Adds the [`webhooks`](https://discord.com/developers/docs/resources/webhook#get-webhook) part to this [IHttpRoute]. + void webhooks({String? id, String? token}); + + /// Adds the [`reactions`](https://discord.com/developers/docs/resources/channel#get-reactions) part to this [IHttpRoute]. + void reactions({String? emoji, String? userId}); + + /// Adds the [`emojis`](https://discord.com/developers/docs/resources/emoji#get-guild-emoji) part to this [IHttpRoute]. + void emojis({String? id}); + + /// Adds the [`roles`](https://discord.com/developers/docs/resources/guild#get-guild-roles) part to this [IHttpRoute]. + void roles({String? id}); + + /// Adds the [`members`](https://discord.com/developers/docs/resources/guild#get-guild-member) part to this [IHttpRoute]. + void members({String? id}); + + /// Adds the [`bans`](https://discord.com/developers/docs/resources/guild#get-guild-bans) part to this [IHttpRoute]. + void bans({String? id}); + + /// Adds the [`users`](https://discord.com/developers/docs/resources/user#get-user) part to this [IHttpRoute]. + void users({String? id}); + + /// Adds the [`permissions`](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions) part to this [IHttpRoute]. + void permissions({String? id}); + + /// Adds the [`messages`](https://discord.com/developers/docs/resources/channel#get-channel-messages) part to this [IHttpRoute]. + void messages({String? id}); + + /// Adds the [`pins`](https://discord.com/developers/docs/resources/channel#get-pinned-messages) part to this [IHttpRoute]. + void pins({String? id}); + + /// Adds the [`invites`](https://discord.com/developers/docs/resources/guild#get-guild-invites) part to this [IHttpRoute]. + void invites({String? id}); + + /// Adds the [`applications`](https://discord.com/developers/docs/topics/oauth2#get-current-bot-application-information) part to this [IHttpRoute]. + void applications({String? id}); + + /// Adds the [`stage-instances`](https://discord.com/developers/docs/resources/stage-instance#get-stage-instance) part to this [IHttpRoute]. + void stageinstances({String? id}); + + /// Adds the [`thread-members`](https://discord.com/developers/docs/resources/channel#get-thread-member) part to this [IHttpRoute]. + void threadMembers({String? id}); + + /// Adds the [`stickers`](https://discord.com/developers/docs/resources/sticker#get-sticker) part to this [IHttpRoute]. + void stickers({String? id}); + + /// Adds the `avatars` part to this [IHttpRoute]. + /// + /// Note: this part only exists for the Discord CDN. + void avatars(); + + /// Adds the [`scheduled-events`](https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event) part to this [IHttpRoute]. + void scheduledEvents({String? id}); + + /// Adds the [`prune`](https://discord.com/developers/docs/resources/guild#get-guild-prune-count) part to this [IHttpRoute]. + void prune(); + + /// Adds the [`nick`](https://discord.com/developers/docs/resources/guild#modify-current-user-nick) part to this [IHttpRoute]. + void nick(); + + /// Adds the [`audit-logs`](https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log) part to this [IHttpRoute]. + void auditlogs(); + + /// Adds the [`regions`](https://discord.com/developers/docs/resources/voice#list-voice-regions) part to this [IHttpRoute]. + void regions(); + + /// Adds the [`search`](https://discord.com/developers/docs/resources/guild#search-guild-members) part to this [IHttpRoute]. + void search(); + + /// Adds the [`bulk-delete`](https://discord.com/developers/docs/resources/channel#bulk-delete-messages) part to this [IHttpRoute]. + void bulkdelete(); + + /// Adds the [`typing`](https://discord.com/developers/docs/resources/channel#trigger-typing-indicator) part to this [IHttpRoute]. + void typing(); + + /// Adds the [`crosspost`](https://discord.com/developers/docs/resources/channel#crosspost-message) part to this [IHttpRoute]. + void crosspost(); + + /// Adds the [`threads`](https://discord.com/developers/docs/resources/channel#start-thread-from-message) part to this [IHttpRoute]. + void threads(); + + /// Adds the [`gateway`](https://discord.com/developers/docs/topics/gateway#get-gateway) part to this [IHttpRoute]. + void gateway(); + + /// Adds the [`bot`](https://discord.com/developers/docs/topics/gateway#get-gateway-bot) part to this [IHttpRoute]. + void bot(); + + /// Adds the [`oauth2`](https://discord.com/developers/docs/topics/oauth2#get-current-authorization-information) part to this [IHttpRoute]. + void oauth2(); + + /// Adds the [`preview`](https://discord.com/developers/docs/resources/guild#get-guild-preview) part to this [IHttpRoute]. + void preview(); + + /// Adds the [`active`](https://discord.com/developers/docs/resources/guild#list-active-guild-threads) part to this [IHttpRoute]. + void active(); + + /// Adds the [`archived`](https://discord.com/developers/docs/resources/channel#list-public-archived-threads) part to this [IHttpRoute]. + void archived(); + + /// Adds the [`private`](https://discord.com/developers/docs/resources/channel#list-private-archived-threads) part to this [IHttpRoute]. + void private(); + + /// Adds the [`public`](https://discord.com/developers/docs/resources/channel#list-public-archived-threads) part to this [IHttpRoute]. + void public(); + + /// Adds the [`sticker-packs`](https://discord.com/developers/docs/resources/sticker#list-nitro-sticker-packs) part to this [IHttpRoute]. + void stickerpacks(); + + /// Adds the [`welcome-screen`](https://discord.com/developers/docs/resources/guild#get-guild-welcome-screen) part to this [IHttpRoute]. + void welcomeScreen(); +} + +class HttpRoute implements IHttpRoute { + final List _httpRouteParts = []; + + List get pathSegments => _httpRouteParts + .expand((part) => [ + part.path, + ...part.params.map((param) => param.param), + ]) + .toList(); + + String get path => "/" + pathSegments.join("/"); + + String get routeId => _httpRouteParts + .expand((part) => [ + part.path, + ...List.generate( + part.params.length, + (index) => part.params[index].isMajor ? part.params[index].param : r"$param", + ), + ]) + .join("/"); + + @override + void add(HttpRoutePart httpRoutePart) => _httpRouteParts.add(httpRoutePart); + + @override + void guilds({String? id}) => add(HttpRoutePart("guilds", [if (id != null) HttpRouteParam(id, isMajor: true)])); + + @override + void channels({String? id}) => add(HttpRoutePart("channels", [if (id != null) HttpRouteParam(id, isMajor: true)])); + + @override + void webhooks({String? id, String? token}) => _httpRouteParts.add(HttpRoutePart("webhooks", [ + if (id != null) HttpRouteParam(id, isMajor: token != null), + if (token != null) HttpRouteParam(token, isMajor: id != null), + ])); + + @override + void reactions({String? emoji, String? userId}) => add(HttpRoutePart("reactions", [ + if (emoji != null) HttpRouteParam(emoji), + if (userId != null) HttpRouteParam(userId), + ])); + + @override + void emojis({String? id}) => add(HttpRoutePart("emojis", [if (id != null) HttpRouteParam(id)])); + + @override + void roles({String? id}) => add(HttpRoutePart("roles", [if (id != null) HttpRouteParam(id)])); + + @override + void members({String? id}) => add(HttpRoutePart("members", [if (id != null) HttpRouteParam(id)])); + + @override + void bans({String? id}) => add(HttpRoutePart("bans", [if (id != null) HttpRouteParam(id)])); + + @override + void users({String? id}) => add(HttpRoutePart("users", [if (id != null) HttpRouteParam(id)])); + + @override + void permissions({String? id}) => add(HttpRoutePart("permissions", [if (id != null) HttpRouteParam(id)])); + + @override + void messages({String? id}) => add(HttpRoutePart("messages", [if (id != null) HttpRouteParam(id)])); + + @override + void pins({String? id}) => add(HttpRoutePart("pins", [if (id != null) HttpRouteParam(id)])); + + @override + void invites({String? id}) => add(HttpRoutePart("invites", [if (id != null) HttpRouteParam(id)])); + + @override + void applications({String? id}) => add(HttpRoutePart("applications", [if (id != null) HttpRouteParam(id)])); + + @override + void stageinstances({String? id}) => add(HttpRoutePart("stage-instances", [if (id != null) HttpRouteParam(id)])); + + @override + void threadMembers({String? id}) => add(HttpRoutePart("thread-members", [if (id != null) HttpRouteParam(id)])); + + @override + void stickers({String? id}) => add(HttpRoutePart("stickers", [if (id != null) HttpRouteParam(id)])); + + @override + void avatars() => add(HttpRoutePart("avatars")); + + @override + void scheduledEvents({String? id}) => add(HttpRoutePart("scheduled-events", [if (id != null) HttpRouteParam(id)])); + + @override + void prune() => add(HttpRoutePart("prune")); + + @override + void nick() => add(HttpRoutePart("nick")); + + @override + void auditlogs() => add(HttpRoutePart("audit-logs")); + + @override + void regions() => add(HttpRoutePart("regions")); + + @override + void search() => add(HttpRoutePart("search")); + + @override + void bulkdelete() => add(HttpRoutePart("bulk-delete")); + + @override + void typing() => add(HttpRoutePart("typing")); + + @override + void crosspost() => add(HttpRoutePart("crosspost")); + + @override + void threads() => add(HttpRoutePart("threads")); + + @override + void gateway() => add(HttpRoutePart("gateway")); + + @override + void bot() => add(HttpRoutePart("bot")); + + @override + void oauth2() => add(HttpRoutePart("oauth2")); + + @override + void preview() => add(HttpRoutePart("preview")); + + @override + void active() => add(HttpRoutePart("active")); + + @override + void archived() => add(HttpRoutePart("archived")); + + @override + void private() => add(HttpRoutePart("private")); + + @override + void public() => add(HttpRoutePart("public")); + + @override + void stickerpacks() => add(HttpRoutePart("sticker-packs")); + + @override + void welcomeScreen() => add(HttpRoutePart('welcome-screen')); +} diff --git a/lib/src/internal/http/http_route_param.dart b/lib/src/internal/http/http_route_param.dart new file mode 100644 index 000000000..2a05100ff --- /dev/null +++ b/lib/src/internal/http/http_route_param.dart @@ -0,0 +1,6 @@ +class HttpRouteParam { + final String param; + final bool isMajor; + + HttpRouteParam(this.param, {this.isMajor = false}); +} diff --git a/lib/src/internal/http/http_route_part.dart b/lib/src/internal/http/http_route_part.dart new file mode 100644 index 000000000..71980daf9 --- /dev/null +++ b/lib/src/internal/http/http_route_part.dart @@ -0,0 +1,8 @@ +import 'http_route_param.dart'; + +class HttpRoutePart { + final String path; + final List params; + + HttpRoutePart(this.path, [this.params = const []]); +} diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index 565be06c6..5694602ec 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -1,5 +1,6 @@ import 'package:nyxx/src/core/audit_logs/audit_log_entry.dart'; import 'package:nyxx/src/core/guild/scheduled_event.dart'; +import 'package:nyxx/src/internal/http/http_route.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/channel/invite.dart'; import 'package:nyxx/src/core/snowflake.dart'; @@ -41,7 +42,6 @@ import 'package:nyxx/src/utils/builders/permissions_builder.dart'; import 'package:nyxx/src/utils/builders/sticker_builder.dart'; import 'package:nyxx/src/utils/builders/thread_builder.dart'; import 'package:nyxx/src/utils/utils.dart'; -import 'package:nyxx/src/utils/builders/member_builder.dart'; /// Raw access to all http endpoints exposed by nyxx. /// Allows to execute specific action without any context. @@ -340,7 +340,7 @@ abstract class IHttpEndpoints { Future createDMChannel(Snowflake userId); /// Used to send a request including standard bot authentication. - Future sendRawRequest(String url, String method, + Future sendRawRequest(IHttpRoute route, String method, {dynamic body, Map? headers, List files = const [], @@ -400,10 +400,15 @@ abstract class IHttpEndpoints { String? userBannerURL(Snowflake userId, String? hash, {String? format, int? size}); Stream fetchGuildEvents(Snowflake guildId, {bool withUserCount = false}); + Future createGuildEvent(Snowflake guildId, GuildEventBuilder builder); + Future fetchGuildEvent(Snowflake guildId, Snowflake guildEventId); + Future editGuildEvent(Snowflake guildId, Snowflake guildEventId, GuildEventBuilder builder); + Future deleteGuildEvent(Snowflake guildId, Snowflake guildEventId); + Stream fetchGuildEventUsers(Snowflake guildId, Snowflake guildEventId, {int limit = 100, bool withMember = false, Snowflake? before, Snowflake? after}); } @@ -499,7 +504,12 @@ class HttpEndpoints implements IHttpEndpoints { if (avatarAttachment != null) "avatar": avatarAttachment.getBase64() }; - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/emojis/$emojiId", method: "PATCH", body: body)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..emojis(id: emojiId.toString()), + method: "PATCH", + body: body)); if (response is HttpResponseSuccess) { return GuildEmoji(client, response.jsonBody as RawApiMap, guildId); @@ -509,11 +519,21 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future deleteGuildEmoji(Snowflake guildId, Snowflake emojiId) async => executeSafe(BasicRequest("/guilds/$guildId/emojis/$emojiId", method: "DELETE")); + Future deleteGuildEmoji(Snowflake guildId, Snowflake emojiId) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..emojis(id: emojiId.toString()), + method: "DELETE")); @override Future editRole(Snowflake guildId, Snowflake roleId, RoleBuilder role, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/roles/$roleId", method: "PATCH", body: role.build(), auditLog: auditReason)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..roles(id: roleId.toString()), + method: "PATCH", + body: role.build(), + auditLog: auditReason)); if (response is HttpResponseSuccess) { return Role(client, response.jsonBody as RawApiMap, guildId); @@ -523,16 +543,26 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future deleteRole(Snowflake guildId, Snowflake roleId, {String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/roles/$roleId", method: "DELETE", auditLog: auditReason)); + Future deleteRole(Snowflake guildId, Snowflake roleId, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..roles(id: roleId.toString()), + method: "DELETE", + auditLog: auditReason)); @override - Future addRoleToUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/members/$userId/roles/$roleId", method: "PUT", auditLog: auditReason)); + Future addRoleToUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: userId.toString()) + ..roles(id: roleId.toString()), + method: "PUT", + auditLog: auditReason)); @override Future fetchGuild(Snowflake guildId, {bool? withCounts = true}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}", queryParams: {"with_counts": (withCounts ?? true).toString()})); + final response = + await httpHandler.execute(BasicRequest(HttpRoute()..guilds(id: guildId.toString()), queryParams: {"with_counts": (withCounts ?? true).toString()})); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); @@ -543,7 +573,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchChannel(Snowflake id) async { - final response = await httpHandler.execute(BasicRequest("/channels/$id")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..channels(id: id.toString()))); if (response is HttpResponseError) { return Future.error(response); @@ -555,7 +585,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchGuildEmoji(Snowflake guildId, Snowflake emojiId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/emojis/$emojiId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..emojis(id: emojiId.toString()))); if (response is HttpResponseSuccess) { return GuildEmoji(client, response.jsonBody as RawApiMap, guildId); @@ -566,7 +598,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchGuildWelcomeScreen(Snowflake guildId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/welcome-screen")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..welcomeScreen(), + )); if (response is HttpResponseSuccess) { return GuildWelcomeScreen(response.jsonBody as RawApiMap, client); @@ -583,7 +619,12 @@ class HttpEndpoints implements IHttpEndpoints { if (emojiAttachment != null) "image": emojiAttachment.getBase64() }; - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/emojis", method: "POST", body: body)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..emojis(), + method: "POST", + body: body)); if (response is HttpResponseSuccess) { return GuildEmoji(client, response.jsonBody as RawApiMap, guildId); @@ -594,7 +635,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchEmojiCreator(Snowflake guildId, Snowflake emojiId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/emojis/$emojiId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..emojis(id: emojiId.toString()))); if (response is HttpResponseSuccess) { if (response.jsonBody["managed"] as bool) { @@ -613,7 +656,10 @@ class HttpEndpoints implements IHttpEndpoints { @override Future guildPruneCount(Snowflake guildId, int days, {Iterable? includeRoles}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/prune", + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..prune(), queryParams: {"days": days.toString(), if (includeRoles != null) "include_roles": includeRoles.map((e) => e.id.toString())})); if (response is HttpResponseSuccess) { @@ -625,7 +671,10 @@ class HttpEndpoints implements IHttpEndpoints { @override Future guildPrune(Snowflake guildId, int days, {Iterable? includeRoles, String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/prune", + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..prune(), method: "POST", auditLog: auditReason, queryParams: {"days": days.toString()}, @@ -640,11 +689,15 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream getGuildBans(Snowflake guildId, {int limit = 1000, Snowflake? before, Snowflake? after}) async* { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/bans", queryParams: { - "limit": limit, - if (before != null) "before": before, - if (after != null) "after": after, - })); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..bans(), + queryParams: { + "limit": limit, + if (before != null) "before": before, + if (after != null) "after": after, + })); if (response is HttpResponseError) { yield* Stream.error(response); @@ -657,12 +710,19 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future modifyCurrentMember(Snowflake guildId, {String? nick}) async => - executeSafe(BasicRequest("/guilds/$guildId/members/@me/nick", method: "PATCH", body: {if (nick != null) "nick": nick})); + Future modifyCurrentMember(Snowflake guildId, {String? nick}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: "@me") + ..nick(), + method: "PATCH", + body: {if (nick != null) "nick": nick})); @override Future getGuildBan(Snowflake guildId, Snowflake bannedUserId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/bans/$bannedUserId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..bans(id: bannedUserId.toString()))); if (response is HttpResponseSuccess) { return Ban(response.jsonBody as RawApiMap, client); @@ -673,8 +733,8 @@ class HttpEndpoints implements IHttpEndpoints { @override Future changeGuildOwner(Snowflake guildId, SnowflakeEntity member, {String? auditReason}) async { - final response = - await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id.toString()})); + final response = await httpHandler + .execute(BasicRequest(HttpRoute()..guilds(id: guildId.toString()), method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id.toString()})); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); @@ -684,11 +744,15 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future leaveGuild(Snowflake guildId) async => executeSafe(BasicRequest("/users/@me/guilds/$guildId", method: "DELETE")); + Future leaveGuild(Snowflake guildId) async => executeSafe(BasicRequest( + HttpRoute() + ..users(id: "@me") + ..guilds(id: guildId.toString()), + method: "DELETE")); @override Future createGuild(GuildBuilder builder) async { - final response = await httpHandler.execute(BasicRequest("/guilds", method: "POST", body: builder.build())); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..guilds(), method: "POST", body: builder.build())); if (response is HttpResponseSuccess) { final guild = Guild(client, response.jsonBody as RawApiMap); @@ -701,7 +765,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildInvites(Snowflake guildId) async* { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/invites")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..invites())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -715,12 +781,17 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createVoiceActivityInvite(Snowflake activityId, Snowflake channelId, {int? maxAge, int? maxUses}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/invites", method: "POST", body: { - "max_age": maxAge ?? 0, - "max_uses": maxUses ?? 0, - "target_application_id": "$activityId", - "target_type": 2, - })); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..invites(), + method: "POST", + body: { + "max_age": maxAge ?? 0, + "max_uses": maxUses ?? 0, + "target_application_id": activityId.toString(), + "target_type": 2, + })); if (response is HttpResponseSuccess) { return Invite(response.jsonBody as RawApiMap, client); @@ -738,7 +809,11 @@ class HttpEndpoints implements IHttpEndpoints { if (limit != null) "limit": limit.toString() }; - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/audit-logs", queryParams: queryParams)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..auditlogs(), + queryParams: queryParams)); if (response is HttpResponseSuccess) { return AuditLog(response.jsonBody as RawApiMap, client); @@ -749,7 +824,13 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createGuildRole(Snowflake guildId, RoleBuilder roleBuilder, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/roles", method: "POST", auditLog: auditReason, body: roleBuilder.build())); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..roles(), + method: "POST", + auditLog: auditReason, + body: roleBuilder.build())); if (response is HttpResponseSuccess) { return Role(client, response.jsonBody as RawApiMap, guildId); @@ -760,7 +841,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildVoiceRegions(Snowflake guildId) async* { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/regions")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..regions())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -773,23 +856,42 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future moveGuildChannel(Snowflake guildId, Snowflake channelId, int position, {String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/channels", method: "PATCH", auditLog: auditReason, body: {"id": channelId.toString(), "position": position})); + Future moveGuildChannel(Snowflake guildId, Snowflake channelId, int position, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..channels(), + method: "PATCH", + auditLog: auditReason, + body: {"id": channelId.toString(), "position": position})); @override - Future guildBan(Snowflake guildId, Snowflake userId, {int deleteMessageDays = 0, String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/bans/$userId", method: "PUT", auditLog: auditReason, body: {"delete-message-days": deleteMessageDays})); + Future guildBan(Snowflake guildId, Snowflake userId, {int deleteMessageDays = 0, String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..bans(id: userId.toString()), + method: "PUT", + auditLog: auditReason, + body: {"delete-message-days": deleteMessageDays})); @override - Future guildKick(Snowflake guildId, Snowflake userId, {String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/members/$userId", method: "DELETE", auditLog: auditReason)); + Future guildKick(Snowflake guildId, Snowflake userId, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: userId.toString()), + method: "DELETE", + auditLog: auditReason)); @override - Future guildUnban(Snowflake guildId, Snowflake userId) async => executeSafe(BasicRequest("/guilds/$guildId/bans/$userId", method: "DELETE")); + Future guildUnban(Snowflake guildId, Snowflake userId) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..bans(id: userId.toString()), + method: "DELETE")); @override Future editGuild(Snowflake guildId, GuildBuilder builder, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId", method: "PATCH", auditLog: auditReason, body: builder.build())); + final response = + await httpHandler.execute(BasicRequest(HttpRoute()..guilds(id: guildId.toString()), method: "PATCH", auditLog: auditReason, body: builder.build())); if (response is HttpResponseSuccess) { return Guild(client, response.jsonBody as RawApiMap); @@ -800,7 +902,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchGuildMember(Snowflake guildId, Snowflake memberId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/members/$memberId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: memberId.toString()))); if (response is HttpResponseSuccess) { final member = Member(client, response.jsonBody as RawApiMap, guildId); @@ -817,8 +921,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildMembers(Snowflake guildId, {int limit = 1, Snowflake? after}) async* { - final request = await httpHandler - .execute(BasicRequest("/guilds/$guildId/members", queryParams: {"limit": limit.toString(), if (after != null) "after": after.toString()})); + final request = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(), + queryParams: {"limit": limit.toString(), if (after != null) "after": after.toString()})); if (request is HttpResponseError) { yield* Stream.error(request); @@ -843,7 +950,12 @@ class HttpEndpoints implements IHttpEndpoints { return; } - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/members/search", queryParams: {"query": query, "limit": limit.toString()})); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members() + ..search(), + queryParams: {"query": query, "limit": limit.toString()})); if (response is HttpResponseError) { yield* Stream.error(response); @@ -863,7 +975,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchChannelWebhooks(Snowflake channelId) async* { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/webhooks")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..webhooks())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -876,11 +990,13 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future deleteGuild(Snowflake guildId) async => executeSafe(BasicRequest("/guilds/$guildId", method: "DELETE")); + Future deleteGuild(Snowflake guildId) async => executeSafe(BasicRequest(HttpRoute()..guilds(id: guildId.toString()), method: "DELETE")); @override Stream fetchGuildRoles(Snowflake guildId) async* { - final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}/roles")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..roles())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -903,7 +1019,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchUser(Snowflake userId) async { - final response = await httpHandler.execute(BasicRequest("/users/$userId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..users(id: userId.toString()))); if (response is HttpResponseError) { return Future.error(response); @@ -914,16 +1030,29 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editGuildMember(Snowflake guildId, Snowflake memberId, {required MemberBuilder builder, String? auditReason}) { - return executeSafe(BasicRequest("/guilds/$guildId/members/$memberId", method: "PATCH", auditLog: auditReason, body: builder.build())); + return executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: memberId.toString()), + method: "PATCH", + auditLog: auditReason, + body: builder.build())); } @override - Future removeRoleFromUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}) async => - executeSafe(BasicRequest("/guilds/$guildId/members/$userId/roles/$roleId", method: "DELETE", auditLog: auditReason)); + Future removeRoleFromUser(Snowflake guildId, Snowflake roleId, Snowflake userId, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..members(id: userId.toString()) + ..roles(id: roleId.toString()), + method: "DELETE", + auditLog: auditReason)); @override Stream fetchChannelInvites(Snowflake channelId) async* { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/invites")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..invites())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -939,19 +1068,33 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editChannelPermissions(Snowflake channelId, PermissionsBuilder perms, SnowflakeEntity entity, {String? auditReason}) async { - await executeSafe(BasicRequest("/channels/$channelId/permissions/${entity.id.toString()}", - method: "PUT", body: {"type": entity is IRole ? 0 : 1, ...perms.build()}, auditLog: auditReason)); + await executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..permissions(id: entity.id.toString()), + method: "PUT", + body: {"type": entity is IRole ? 0 : 1, ...perms.build()}, + auditLog: auditReason)); } @override Future editChannelPermissionOverrides(Snowflake channelId, PermissionOverrideBuilder permissionBuilder, {String? auditReason}) async { - await executeSafe(BasicRequest("/channels/$channelId/permissions/${permissionBuilder.id.toString()}", - method: "PUT", body: permissionBuilder.build(), auditLog: auditReason)); + await executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..permissions(id: permissionBuilder.id.toString()), + method: "PUT", + body: permissionBuilder.build(), + auditLog: auditReason)); } @override - Future deleteChannelPermission(Snowflake channelId, SnowflakeEntity id, {String? auditReason}) async => - executeSafe(BasicRequest("/channels/$channelId/permissions/$id", method: "PUT", auditLog: auditReason)); + Future deleteChannelPermission(Snowflake channelId, SnowflakeEntity id, {String? auditReason}) async => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..permissions(id: id.toString()), + method: "PUT", + auditLog: auditReason)); @override Future createInvite(Snowflake channelId, {int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async { @@ -962,7 +1105,13 @@ class HttpEndpoints implements IHttpEndpoints { if (unique != null) "unique": unique, }; - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/invites", method: "POST", body: body, auditLog: auditReason)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..invites(), + method: "POST", + body: body, + auditLog: auditReason)); if (response is HttpResponseError) { return Future.error(response); @@ -979,10 +1128,20 @@ class HttpEndpoints implements IHttpEndpoints { HttpResponse response; if (builder.hasFiles()) { - response = await httpHandler.execute(MultipartRequest("/channels/$channelId/messages", builder.getMappedFiles().toList(), - method: "POST", fields: builder.build(client.options.allowedMentions))); + response = await httpHandler.execute(MultipartRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(), + builder.getMappedFiles().toList(), + method: "POST", + fields: builder.build(client.options.allowedMentions))); } else { - response = await httpHandler.execute(BasicRequest("/channels/$channelId/messages", body: builder.build(client.options.allowedMentions), method: "POST")); + response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(), + body: builder.build(client.options.allowedMentions), + method: "POST")); } if (response is HttpResponseSuccess) { @@ -994,7 +1153,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchMessage(Snowflake channelId, Snowflake messageId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/messages/$messageId")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()))); if (response is HttpResponseError) { return Future.error(response); @@ -1006,8 +1167,13 @@ class HttpEndpoints implements IHttpEndpoints { @override Future bulkRemoveMessages(Snowflake channelId, Iterable messagesIds) async { await for (final chunk in messagesIds.toList().chunk(90)) { - final response = await httpHandler - .execute(BasicRequest("/channels/$channelId/messages/bulk-delete", method: "POST", body: {"messages": chunk.map((f) => f.id.toString()).toList()})); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages() + ..bulkdelete(), + method: "POST", + body: {"messages": chunk.map((f) => f.id.toString()).toList()})); if (response is HttpResponseError) { return Future.error(response); @@ -1024,7 +1190,11 @@ class HttpEndpoints implements IHttpEndpoints { if (around != null) "around": around.toString() }; - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/messages", queryParams: queryParams)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(), + queryParams: queryParams)); if (response is HttpResponseError) { yield* Stream.error(response); @@ -1038,7 +1208,8 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editGuildChannel(Snowflake channelId, ChannelBuilder builder, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId", method: "PATCH", body: builder.build(), auditLog: auditReason)); + final response = + await httpHandler.execute(BasicRequest(HttpRoute()..channels(id: channelId.toString()), method: "PATCH", body: builder.build(), auditLog: auditReason)); if (response is HttpResponseSuccess) { return Channel.deserialize(client, response.jsonBody as RawApiMap) as T; @@ -1058,7 +1229,13 @@ class HttpEndpoints implements IHttpEndpoints { if (avatarAttachment != null) "avatar": avatarAttachment.getBase64(), }; - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/webhooks", method: "POST", body: body, auditLog: auditReason)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..webhooks(), + method: "POST", + body: body, + auditLog: auditReason)); if (response is HttpResponseSuccess) { return Webhook(response.jsonBody as RawApiMap, client); @@ -1069,7 +1246,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchPinnedMessages(Snowflake channelId) async* { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/pins")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..pins())); if (response is HttpResponseError) { yield* Stream.error(response); @@ -1082,17 +1261,28 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future triggerTyping(Snowflake channelId) => executeSafe(BasicRequest("/channels/$channelId/typing", method: "POST")); + Future triggerTyping(Snowflake channelId) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..typing(), + method: "POST")); @override - Future crossPostGuildMessage(Snowflake channelId, Snowflake messageId) async => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId/crosspost", method: "POST")); + Future crossPostGuildMessage(Snowflake channelId, Snowflake messageId) async => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..crosspost(), + method: "POST")); @override Future createThreadWithMessage(Snowflake channelId, Snowflake messageId, ThreadBuilder builder) async { final response = await httpHandler.execute( BasicRequest( - "/channels/$channelId/messages/$messageId/threads", + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..threads(), method: "POST", body: builder.build(), ), @@ -1109,7 +1299,9 @@ class HttpEndpoints implements IHttpEndpoints { Future createThread(Snowflake channelId, ThreadBuilder builder) async { final response = await httpHandler.execute( BasicRequest( - "/channels/$channelId/threads", + HttpRoute() + ..channels(id: channelId.toString()) + ..threads(), method: "POST", body: builder.build(), ), @@ -1124,7 +1316,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchThreadMembers(Snowflake channelId, Snowflake guildId) async* { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/thread-members")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers())); if (response is HttpResponseSuccess) { final guild = GuildCacheable(client, guildId); @@ -1141,7 +1335,12 @@ class HttpEndpoints implements IHttpEndpoints { Future suppressMessageEmbeds(Snowflake channelId, Snowflake messageId) async { final body = {"flags": 1 << 2}; - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/messages/$messageId", method: "PATCH", body: body)); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()), + method: "PATCH", + body: body)); if (response is HttpResponseSuccess) { return Message(client, response.jsonBody as RawApiMap); @@ -1158,11 +1357,20 @@ class HttpEndpoints implements IHttpEndpoints { HttpResponse response; if (builder.hasFiles()) { - response = await httpHandler.execute(MultipartRequest("/channels/$channelId/messages/$messageId", builder.getMappedFiles().toList(), - method: "PATCH", fields: builder.build(client.options.allowedMentions))); + response = await httpHandler.execute(MultipartRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()), + builder.getMappedFiles().toList(), + method: "PATCH", + fields: builder.build(client.options.allowedMentions))); } else { - response = await httpHandler - .execute(BasicRequest("/channels/$channelId/messages/$messageId", body: builder.build(client.options.allowedMentions), method: "PATCH")); + response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()), + body: builder.build(client.options.allowedMentions), + method: "PATCH")); } if (response is HttpResponseSuccess) { @@ -1177,11 +1385,21 @@ class HttpEndpoints implements IHttpEndpoints { HttpResponse response; if (builder.hasFiles()) { response = await httpHandler.execute(MultipartRequest( - "/webhooks/$webhookId/${token != null ? '$token/' : ''}messages/$messageId", builder.getMappedFiles().toList(), - method: "PATCH", fields: builder.build(client.options.allowedMentions), queryParams: {if (threadId != null) 'thread_id': threadId})); + HttpRoute() + ..webhooks(id: webhookId.toString(), token: token?.toString()) + ..messages(id: messageId.toString()), + builder.getMappedFiles().toList(), + method: "PATCH", + fields: builder.build(client.options.allowedMentions), + queryParams: {if (threadId != null) 'thread_id': threadId})); } else { - response = await httpHandler.execute(BasicRequest("/webhooks/$webhookId/${token != null ? '$token/' : ''}messages/$messageId", - body: builder.build(client.options.allowedMentions), method: "PATCH", queryParams: {if (threadId != null) 'thread_id': threadId})); + response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..webhooks(id: webhookId.toString(), token: token?.toString()) + ..messages(id: messageId.toString()), + body: builder.build(client.options.allowedMentions), + method: "PATCH", + queryParams: {if (threadId != null) 'thread_id': threadId})); } if (response is HttpResponseSuccess) { @@ -1192,35 +1410,68 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future createMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji) => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId/reactions/${emoji.encodeForAPI()}/@me", method: "PUT")); + Future createMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(emoji: emoji.encodeForAPI(), userId: "@me"), + method: "PUT")); @override - Future deleteMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji) => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId/reactions/${emoji.encodeForAPI()}/@me", method: "DELETE")); + Future deleteMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(emoji: emoji.encodeForAPI(), userId: "@me"), + method: "DELETE")); @override - Future deleteMessageUserReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji, Snowflake userId) => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId/reactions/${emoji.encodeForAPI()}/$userId", method: "DELETE")); + Future deleteMessageUserReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji, Snowflake userId) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(emoji: emoji.encodeForAPI(), userId: userId.toString()), + method: "DELETE")); @override - Future deleteMessageAllReactions(Snowflake channelId, Snowflake messageId) => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId/reactions", method: "DELETE")); + Future deleteMessageAllReactions(Snowflake channelId, Snowflake messageId) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(), + method: "DELETE")); @override - Future deleteMessage(Snowflake channelId, Snowflake messageId, {String? auditReason}) => - executeSafe(BasicRequest("/channels/$channelId/messages/$messageId", method: "DELETE", auditLog: auditReason)); + Future deleteMessage(Snowflake channelId, Snowflake messageId, {String? auditReason}) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()), + method: "DELETE", + auditLog: auditReason)); @override Future deleteWebhookMessage(Snowflake webhookId, Snowflake messageId, {String? auditReason, String? token, Snowflake? threadId}) => - executeSafe(BasicRequest("/webhooks/$webhookId/${token != null ? '$token/' : ''}messages/$messageId", - method: "DELETE", auditLog: auditReason, queryParams: {if (threadId != null) 'thread_id': threadId})); + executeSafe(BasicRequest( + HttpRoute() + ..webhooks(id: webhookId.toString(), token: token?.toString()) + ..messages(id: messageId.toString()), + method: "DELETE", + auditLog: auditReason, + queryParams: {if (threadId != null) 'thread_id': threadId})); @override - Future pinMessage(Snowflake channelId, Snowflake messageId) => executeSafe(BasicRequest("/channels/$channelId/pins/$messageId", method: "PUT")); + Future pinMessage(Snowflake channelId, Snowflake messageId) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..pins(id: messageId.toString()), + method: "PUT")); @override - Future unpinMessage(Snowflake channelId, Snowflake messageId) => executeSafe(BasicRequest("/channels/$channelId/pins/$messageId", method: "DELETE")); + Future unpinMessage(Snowflake channelId, Snowflake messageId) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..pins(id: messageId.toString()), + method: "DELETE")); @override Future editSelfUser({String? username, AttachmentBuilder? avatarAttachment}) async { @@ -1229,7 +1480,7 @@ class HttpEndpoints implements IHttpEndpoints { if (avatarAttachment != null) "avatar": avatarAttachment.getBase64(), }; - final response = await httpHandler.execute(BasicRequest("/users/@me", method: "PATCH", body: body)); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..users(id: "@me"), method: "PATCH", body: body)); if (response is HttpResponseSuccess) { return User(client, response.jsonBody as RawApiMap); @@ -1239,11 +1490,12 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future deleteInvite(String code, {String? auditReason}) async => executeSafe(BasicRequest("/invites/$code", method: "DELETE", auditLog: auditReason)); + Future deleteInvite(String code, {String? auditReason}) async => + executeSafe(BasicRequest(HttpRoute()..invites(id: code.toString()), method: "DELETE", auditLog: auditReason)); @override - Future deleteWebhook(Snowflake id, {String token = "", String? auditReason}) => - executeSafe(BasicRequest("/webhooks/$id/$token", method: "DELETE", auditLog: auditReason, auth: token.isEmpty)); + Future deleteWebhook(Snowflake id, {String token = "", String? auditReason}) => executeSafe( + BasicRequest(HttpRoute()..webhooks(id: id.toString(), token: token.toString()), method: "DELETE", auditLog: auditReason, auth: token.isEmpty)); @override Future editWebhook(Snowflake webhookId, @@ -1254,8 +1506,8 @@ class HttpEndpoints implements IHttpEndpoints { if (avatarAttachment != null) "avatar": avatarAttachment.getBase64(), }; - final response = - await httpHandler.execute(BasicRequest("/webhooks/$webhookId/$token", method: "PATCH", auditLog: auditReason, body: body, auth: token.isEmpty)); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..webhooks(id: webhookId.toString(), token: token.toString()), + method: "PATCH", auditLog: auditReason, body: body, auth: token.isEmpty)); return Future.error(response); } @@ -1273,11 +1525,12 @@ class HttpEndpoints implements IHttpEndpoints { HttpResponse response; if (builder.files != null && builder.files!.isNotEmpty) { - response = await httpHandler - .execute(MultipartRequest("/webhooks/$webhookId/$token", builder.getMappedFiles().toList(), method: "POST", fields: body, queryParams: queryParams)); + response = await httpHandler.execute(MultipartRequest( + HttpRoute()..webhooks(id: webhookId.toString(), token: token.toString()), builder.getMappedFiles().toList(), + method: "POST", fields: body, queryParams: queryParams)); } else { - response = - await httpHandler.execute(BasicRequest("/webhooks/$webhookId/$token", body: body, method: "POST", queryParams: queryParams, auth: token.isEmpty)); + response = await httpHandler.execute(BasicRequest(HttpRoute()..webhooks(id: webhookId.toString(), token: token.toString()), + body: body, method: "POST", queryParams: queryParams, auth: token.isEmpty)); } if (response is HttpResponseSuccess) { @@ -1293,7 +1546,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchWebhook(Snowflake id, {String token = ""}) async { - final response = await httpHandler.execute(BasicRequest("/webhooks/$id/$token", auth: token.isEmpty)); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..webhooks(id: id.toString(), token: token.toString()), auth: token.isEmpty)); if (response is HttpResponseSuccess) { return Webhook(response.jsonBody as RawApiMap, client); @@ -1304,7 +1557,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchInvite(String code) async { - final response = await httpHandler.execute(BasicRequest("/invites/$code")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..invites(id: code))); if (response is HttpResponseSuccess) { return Invite(response.jsonBody as RawApiMap, client); @@ -1321,7 +1574,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createDMChannel(Snowflake userId) async { - final response = await httpHandler.execute(BasicRequest("/users/@me/channels", method: "POST", body: {"recipient_id": userId.toString()})); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..users(id: "@me") + ..channels(), + method: "POST", + body: {"recipient_id": userId.toString()})); if (response is HttpResponseError) { return Future.error(response); @@ -1331,7 +1589,7 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future sendRawRequest(String url, String method, + Future sendRawRequest(covariant HttpRoute route, String method, {dynamic body, Map? headers, List files = const [], @@ -1340,10 +1598,10 @@ class HttpEndpoints implements IHttpEndpoints { bool rateLimit = true}) async { HttpResponse response; if (files.isNotEmpty) { - response = await httpHandler.execute(MultipartRequest(url, mapMessageBuilderAttachments(files).toList(), - method: method, fields: body, queryParams: queryParams, rateLimit: rateLimit, auth: auth)); + response = await httpHandler.execute(MultipartRequest(route, mapMessageBuilderAttachments(files).toList(), + method: method, fields: body, queryParams: queryParams, globalRateLimit: rateLimit, auth: auth)); } else { - response = await httpHandler.execute(BasicRequest(url, body: body, method: method, queryParams: queryParams, rateLimit: rateLimit, auth: auth)); + response = await httpHandler.execute(BasicRequest(route, body: body, method: method, queryParams: queryParams, globalRateLimit: rateLimit, auth: auth)); } if (response is HttpResponseError) { @@ -1353,13 +1611,19 @@ class HttpEndpoints implements IHttpEndpoints { return response; } - Future getGatewayBot() => executeSafe(BasicRequest("/gateway/bot")); + Future getGatewayBot() => executeSafe(BasicRequest(HttpRoute() + ..gateway() + ..bot())); - Future getMeApplication() => executeSafe(BasicRequest("/oauth2/applications/@me")); + Future getMeApplication() => executeSafe(BasicRequest(HttpRoute() + ..oauth2() + ..applications(id: "@me"))); @override Future fetchGuildPreview(Snowflake guildId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/preview")); + final response = await httpHandler.execute(BasicRequest(HttpRoute() + ..guilds(id: guildId.toString()) + ..preview())); if (response is HttpResponseSuccess) { return GuildPreview(client, response.jsonBody as RawApiMap); @@ -1370,7 +1634,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createGuildChannel(Snowflake guildId, ChannelBuilder channelBuilder) async { - final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}/channels", method: "POST", body: channelBuilder.build())); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..channels(), + method: "POST", + body: channelBuilder.build())); if (response is HttpResponseSuccess) { return Channel.deserialize(client, response.jsonBody as RawApiMap); @@ -1381,7 +1650,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future deleteChannel(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/channels/${channelId.toString()}", method: "DELETE")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..channels(id: channelId.toString()), method: "DELETE")); if (response is HttpResponseError) { return Future.error(response); @@ -1392,7 +1661,7 @@ class HttpEndpoints implements IHttpEndpoints { Future createStageChannelInstance(Snowflake channelId, String topic, {StageChannelInstancePrivacyLevel? privacyLevel}) async { final body = {"topic": topic, "channel_id": channelId.toString(), if (privacyLevel != null) "privacy_level": privacyLevel.value}; - final response = await httpHandler.execute(BasicRequest("/stage-instances", method: "POST", body: body)); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..stageinstances(), method: "POST", body: body)); if (response is HttpResponseError) { return Future.error(response); @@ -1403,7 +1672,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future deleteStageChannelInstance(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/stage-instances/${channelId.toString()}", method: "DELETE")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..stageinstances(id: channelId.toString()), method: "DELETE")); if (response is HttpResponseError) { return Future.error(response); @@ -1412,7 +1681,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future getStageChannelInstance(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/stage-instances/${channelId.toString()}")); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..stageinstances(id: channelId.toString()))); if (response is HttpResponseError) { return Future.error(response); @@ -1425,7 +1694,7 @@ class HttpEndpoints implements IHttpEndpoints { Future updateStageChannelInstance(Snowflake channelId, String topic, {StageChannelInstancePrivacyLevel? privacyLevel}) async { final body = {"topic": topic, if (privacyLevel != null) "privacy_level": privacyLevel.value}; - final response = await httpHandler.execute(BasicRequest("/stage-instances/${channelId.toString()}", method: "POST", body: body)); + final response = await httpHandler.execute(BasicRequest(HttpRoute()..stageinstances(id: channelId.toString()), method: "POST", body: body)); if (response is HttpResponseError) { return Future.error(response); @@ -1436,7 +1705,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future addThreadMember(Snowflake channelId, Snowflake userId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/thread-members/$userId", method: "PUT")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers(id: userId.toString()), + method: "PUT")); if (response is HttpResponseError) { return Future.error(response); @@ -1445,7 +1718,13 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchJoinedPrivateArchivedThreads(Snowflake channelId, {DateTime? before, int? limit}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/users/@me/threads/archived/private", + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..users(id: "@me") + ..threads() + ..archived() + ..private(), queryParams: {if (before != null) "before": before.toIso8601String(), if (limit != null) "limit": limit})); if (response is HttpResponseError) { @@ -1457,7 +1736,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchPrivateArchivedThreads(Snowflake channelId, {DateTime? before, int? limit}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/threads/archived/private", + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threads() + ..archived() + ..private(), queryParams: {if (before != null) "before": before.toIso8601String(), if (limit != null) "limit": limit})); if (response is HttpResponseError) { @@ -1469,7 +1753,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchPublicArchivedThreads(Snowflake channelId, {DateTime? before, int? limit}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/threads/archived/public", + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threads() + ..archived() + ..public(), queryParams: {if (before != null) "before": before.toIso8601String(), if (limit != null) "limit": limit})); if (response is HttpResponseError) { @@ -1481,7 +1770,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future joinThread(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/thread-members/@me", method: "PUT")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers(id: "@me"), + method: "PUT")); if (response is HttpResponseError) { return Future.error(response); @@ -1490,7 +1783,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future leaveThread(Snowflake channelId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/thread-members/@me", method: "DELETE")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers(id: "@me"), + method: "DELETE")); if (response is HttpResponseError) { return Future.error(response); @@ -1499,7 +1796,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future removeThreadMember(Snowflake channelId, Snowflake userId) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId/thread-members/$userId", method: "DELETE")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers(id: userId.toString()), + method: "DELETE")); if (response is HttpResponseError) { return Future.error(response); @@ -1508,8 +1809,13 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createGuildSticker(Snowflake guildId, StickerBuilder builder) async { - final response = - await httpHandler.execute(MultipartRequest("/guilds/$guildId/stickers", [builder.file.getMultipartFile()], fields: builder.build(), method: "POST")); + final response = await httpHandler.execute(MultipartRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..stickers(), + [builder.file.getMultipartFile()], + fields: builder.build(), + method: "POST")); if (response is HttpResponseError) { return Future.error(response); @@ -1520,7 +1826,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editGuildSticker(Snowflake guildId, Snowflake stickerId, StickerBuilder builder) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/stickers/$stickerId", method: "PATCH")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..stickers(id: stickerId.toString()), + method: "PATCH")); if (response is HttpResponseError) { return Future.error(response); @@ -1531,7 +1841,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future deleteGuildSticker(Snowflake guildId, Snowflake stickerId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/stickers/$stickerId", method: "DELETE")); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..stickers(id: stickerId.toString()), + method: "DELETE")); if (response is HttpResponseError) { return Future.error(response); @@ -1541,7 +1855,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchGuildSticker(Snowflake guildId, Snowflake stickerId) async { final response = await httpHandler.execute(BasicRequest( - "/guilds/$guildId/stickers/$stickerId", + HttpRoute() + ..guilds(id: guildId.toString()) + ..stickers(id: stickerId.toString()), )); if (response is HttpResponseError) { @@ -1554,7 +1870,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildStickers(Snowflake guildId) async* { final response = await httpHandler.execute(BasicRequest( - "/guilds/$guildId/stickers", + HttpRoute() + ..guilds(id: guildId.toString()) + ..stickers(), )); if (response is HttpResponseError) { @@ -1569,7 +1887,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Future getSticker(Snowflake id) async { final response = await httpHandler.execute(BasicRequest( - "/stickers/$id", + HttpRoute()..stickers(id: id.toString()), )); if (response is HttpResponseError) { @@ -1582,7 +1900,7 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream listNitroStickerPacks() async* { final response = await httpHandler.execute(BasicRequest( - "/sticker-packs", + HttpRoute()..stickerpacks(), )); if (response is HttpResponseError) { @@ -1628,7 +1946,9 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchThreadMember(Snowflake channelId, Snowflake guildId, Snowflake memberId) async { - final result = await httpHandler.execute(BasicRequest('/channels/$channelId/thread-members/$memberId')); + final result = await httpHandler.execute(BasicRequest(HttpRoute() + ..channels(id: channelId.toString()) + ..threadMembers(id: memberId.toString()))); if (result is IHttpResponseError) { return Future.error(result); @@ -1639,7 +1959,8 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editThreadChannel(Snowflake channelId, ThreadBuilder builder, {String? auditReason}) async { - final response = await httpHandler.execute(BasicRequest("/channels/$channelId", method: "PATCH", body: builder.build(), auditLog: auditReason)); + final response = + await httpHandler.execute(BasicRequest(HttpRoute()..channels(id: channelId.toString()), method: "PATCH", body: builder.build(), auditLog: auditReason)); if (response is HttpResponseSuccess) { return ThreadChannel(client, response.jsonBody as RawApiMap); @@ -1650,7 +1971,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Future createGuildEvent(Snowflake guildId, GuildEventBuilder builder) async { - final response = await httpHandler.execute(BasicRequest("/guilds/${guildId.toString()}/scheduled-events", method: 'POST', body: builder.build())); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(), + method: 'POST', + body: builder.build())); if (response is IHttpResponseError) { return Future.error(response); @@ -1660,12 +1986,20 @@ class HttpEndpoints implements IHttpEndpoints { } @override - Future deleteGuildEvent(Snowflake guildId, Snowflake guildEventId) => - executeSafe(BasicRequest("/guilds/$guildId/scheduled-events/$guildEventId", method: 'DELETE')); + Future deleteGuildEvent(Snowflake guildId, Snowflake guildEventId) => executeSafe(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(id: guildEventId.toString()), + method: 'DELETE')); @override Future editGuildEvent(Snowflake guildId, Snowflake guildEventId, GuildEventBuilder builder) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/scheduled-events/$guildEventId", method: 'PATCH', body: builder.build())); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(id: guildEventId.toString()), + method: 'PATCH', + body: builder.build())); if (response is IHttpResponseError) { return Future.error(response); @@ -1676,7 +2010,11 @@ class HttpEndpoints implements IHttpEndpoints { @override Future fetchGuildEvent(Snowflake guildId, Snowflake guildEventId) async { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/scheduled-events/$guildEventId", method: 'GET')); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(id: guildEventId.toString()), + method: 'GET')); if (response is IHttpResponseError) { return Future.error(response); @@ -1688,12 +2026,18 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildEventUsers(Snowflake guildId, Snowflake guildEventId, {int limit = 100, bool withMember = false, Snowflake? before, Snowflake? after}) async* { - final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/scheduled-events/$guildEventId/users", method: 'GET', queryParams: { - 'limit': limit, - 'with_member': withMember, - if (before != null) 'before': before.toString(), - if (after != null) 'after': after.toString(), - })); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(id: guildEventId.toString()) + ..users(), + method: 'GET', + queryParams: { + 'limit': limit, + 'with_member': withMember, + if (before != null) 'before': before.toString(), + if (after != null) 'after': after.toString(), + })); if (response is IHttpResponseError) { yield* Stream.error(response); @@ -1706,8 +2050,12 @@ class HttpEndpoints implements IHttpEndpoints { @override Stream fetchGuildEvents(Snowflake guildId, {bool withUserCount = false}) async* { - final response = - await httpHandler.execute(BasicRequest("/guilds/$guildId/scheduled-events", method: 'GET', queryParams: {'with_user_count': withUserCount.toString()})); + final response = await httpHandler.execute(BasicRequest( + HttpRoute() + ..guilds(id: guildId.toString()) + ..scheduledEvents(), + method: 'GET', + queryParams: {'with_user_count': withUserCount.toString()})); if (response is IHttpResponseError) { yield* Stream.error(response); From 4f58d70032d33144556489580136afabbb18bfb0 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Tue, 24 May 2022 22:16:31 +0200 Subject: [PATCH 14/27] feature: Implement forum channels (#332) * feature: Implement forum channels * feature: Initial implementation of forum tags * fixup: formatting * feature: Redesign create forum thread api * fix: Fixup formatting * fix: Merge next branch fixes * fixup: Export ThreadMessageBuilder --- lib/nyxx.dart | 3 + lib/src/core/channel/channel.dart | 5 ++ .../channel/guild/forum/forum_channel.dart | 60 +++++++++++++++++++ .../core/channel/guild/forum/forum_tag.dart | 37 ++++++++++++ lib/src/core/channel/guild/guild_channel.dart | 2 +- .../channel/guild/text_guild_channel.dart | 2 +- lib/src/internal/http_endpoints.dart | 22 +++++++ .../utils/builders/forum_thread_builder.dart | 31 ++++++++++ lib/src/utils/builders/thread_builder.dart | 2 +- test/unit/builders_test.dart | 15 +++++ 10 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 lib/src/core/channel/guild/forum/forum_channel.dart create mode 100644 lib/src/core/channel/guild/forum/forum_tag.dart create mode 100644 lib/src/utils/builders/forum_thread_builder.dart diff --git a/lib/nyxx.dart b/lib/nyxx.dart index eb6b2437f..dde8bf1e3 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -23,6 +23,8 @@ export 'src/core/channel/cacheable_text_channel.dart' show ICacheableTextChannel export 'src/core/channel/channel.dart' show IChannel, ChannelType; export 'src/core/channel/dm_channel.dart' show IDMChannel; export 'src/core/channel/text_channel.dart' show ITextChannel; +export 'src/core/channel/guild/forum/forum_channel.dart' show IForumChannel; +export 'src/core/channel/guild/forum/forum_tag.dart' show IForumTag; export 'src/core/channel/thread_channel.dart' show IThreadMember, IThreadChannel; export 'src/core/channel/thread_preview_channel.dart' show IThreadPreviewChannel; export 'src/core/channel/guild/activity_types.dart' show VoiceActivityType; @@ -167,6 +169,7 @@ export 'src/utils/builders/reply_builder.dart' show ReplyBuilder; export 'src/utils/builders/sticker_builder.dart' show StickerBuilder; export 'src/utils/builders/thread_builder.dart' show ThreadArchiveTime, ThreadBuilder; export 'src/utils/builders/guild_event_builder.dart' show GuildEventBuilder; +export 'src/utils/builders/forum_thread_builder.dart' show ForumThreadBuilder; export 'src/utils/extensions.dart' show IntExtensions, SnowflakeEntityListExtensions, StringExtensions; export 'src/utils/permissions.dart' show PermissionsUtils; export 'src/utils/utils.dart' show ListSafeFirstWhere; diff --git a/lib/src/core/channel/channel.dart b/lib/src/core/channel/channel.dart index f1bd591fd..f681eceab 100644 --- a/lib/src/core/channel/channel.dart +++ b/lib/src/core/channel/channel.dart @@ -1,3 +1,4 @@ +import 'package:nyxx/src/core/channel/guild/forum/forum_channel.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; @@ -63,6 +64,8 @@ abstract class Channel extends SnowflakeEntity implements IChannel { return ThreadChannel(client, raw); case 13: return StageVoiceGuildChannel(client, raw, guildId); + case 15: + return ForumChannel(client, raw, guildId); default: return _InternalChannel._new(client, raw, guildId); } @@ -100,6 +103,8 @@ class ChannelType extends IEnum { /// Channel in a Student Hub containing the listed servers static const ChannelType guildDirectory = ChannelType._create(14); + static const ChannelType forumChannel = ChannelType._create(15); + /// Type of channel is unknown static const ChannelType unknown = ChannelType._create(1337); diff --git a/lib/src/core/channel/guild/forum/forum_channel.dart b/lib/src/core/channel/guild/forum/forum_channel.dart new file mode 100644 index 000000000..3c41ded4e --- /dev/null +++ b/lib/src/core/channel/guild/forum/forum_channel.dart @@ -0,0 +1,60 @@ +import 'package:nyxx/src/core/channel/guild/forum/forum_tag.dart'; +import 'package:nyxx/src/core/channel/guild/guild_channel.dart'; +import 'package:nyxx/src/core/channel/thread_channel.dart'; +import 'package:nyxx/src/core/channel/thread_preview_channel.dart'; +import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/internal/interfaces/mentionable.dart'; +import 'package:nyxx/src/internal/response_wrapper/thread_list_result_wrapper.dart'; +import 'package:nyxx/src/nyxx.dart'; +import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/src/utils/builders/forum_thread_builder.dart'; + +abstract class IForumChannel implements IGuildChannel, Mentionable { + /// Tags available to assign to forum posts + List get availableTags; + + /// Creates a thread in a channel, that only retrieves a [ThreadPreviewChannel] + Future createThread(ForumThreadBuilder builder); + + /// Fetches joined private and archived thread channels + Future fetchJoinedPrivateArchivedThreads({DateTime? before, int? limit}); + + /// Fetches private, archived thread channels + Future fetchPrivateArchivedThreads({DateTime? before, int? limit}); + + /// Fetches public, archives thread channels + Future fetchPublicArchivedThreads({DateTime? before, int? limit}); +} + +class ForumChannel extends GuildChannel implements IForumChannel { + @override + late final List availableTags; + + /// Creates an instance of [TextGuildChannel] + ForumChannel(INyxx client, RawApiMap raw, [Snowflake? guildId]) : super(client, raw, guildId) { + availableTags = (raw['available_tags'] as List).cast().map((e) => ForumTag(e)).toList(); + } + + /// The channel's mention string. + @override + String get mention => "<#$id>"; + + /// Creates a thread in a channel, that only retrieves a [ThreadPreviewChannel] + @override + Future createThread(ForumThreadBuilder builder) => client.httpEndpoints.startForumThread(id, builder); + + /// Fetches joined private and archived thread channels + @override + Future fetchJoinedPrivateArchivedThreads({DateTime? before, int? limit}) => + client.httpEndpoints.fetchJoinedPrivateArchivedThreads(id, before: before, limit: limit); + + /// Fetches private, archived thread channels + @override + Future fetchPrivateArchivedThreads({DateTime? before, int? limit}) => + client.httpEndpoints.fetchPrivateArchivedThreads(id, before: before, limit: limit); + + /// Fetches public, archives thread channels + @override + Future fetchPublicArchivedThreads({DateTime? before, int? limit}) => + client.httpEndpoints.fetchPublicArchivedThreads(id, before: before, limit: limit); +} diff --git a/lib/src/core/channel/guild/forum/forum_tag.dart b/lib/src/core/channel/guild/forum/forum_tag.dart new file mode 100644 index 000000000..a22d08db2 --- /dev/null +++ b/lib/src/core/channel/guild/forum/forum_tag.dart @@ -0,0 +1,37 @@ +import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/src/typedefs.dart'; + +abstract class IForumTag { + /// Id of forum tag + Snowflake get id; + + /// Name of forum tag + String? get name; + + /// Id of corresponding emoji if guild emoji + Snowflake? get emojiId; + + /// Unicode emoji of emoji if non guild emoji + String? get emojiName; +} + +class ForumTag implements IForumTag { + @override + late final Snowflake id; + + @override + late final String? name; + + @override + late final Snowflake? emojiId; + + @override + late final String? emojiName; + + ForumTag(RawApiMap raw) { + id = Snowflake(raw['id']); + name = raw['string'] as String?; + emojiId = raw['emoji_id'] != 0 ? Snowflake(raw['emoji_id']) : null; + emojiName = raw['emoji_name'] as String?; + } +} diff --git a/lib/src/core/channel/guild/guild_channel.dart b/lib/src/core/channel/guild/guild_channel.dart index b18813ca8..dff2c78be 100644 --- a/lib/src/core/channel/guild/guild_channel.dart +++ b/lib/src/core/channel/guild/guild_channel.dart @@ -208,7 +208,7 @@ abstract class MinimalGuildChannel extends Channel implements IMinimalGuildChann guild = GuildCacheable(client, guildId); } else { throw Exception( - "Cannot initialize instance of GuildChannelNex due missing `guild_id` in json payload and/or missing optional guildId parameter. Report this issue to developer"); + "Cannot initialize instance of GuildChannel due missing `guild_id` in json payload and/or missing optional guildId parameter. Report this issue to developer"); } if (raw["parent_id"] != null) { diff --git a/lib/src/core/channel/guild/text_guild_channel.dart b/lib/src/core/channel/guild/text_guild_channel.dart index a0ce85c22..75e5dcfbb 100644 --- a/lib/src/core/channel/guild/text_guild_channel.dart +++ b/lib/src/core/channel/guild/text_guild_channel.dart @@ -34,7 +34,7 @@ abstract class ITextGuildChannel implements IGuildChannel, ITextChannel, Mention /// Valid file types for [avatarFile] are jpeg, gif and png. /// /// ``` - /// final webhook = await channnel.createWebhook("!a Send nudes kek6407"); + /// final webhook = await channel.createWebhook("!a Send nudes kek6407"); /// ``` Future createWebhook(String name, {AttachmentBuilder? avatarAttachment, String? auditReason}); diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index 5694602ec..079a7133c 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -34,6 +34,7 @@ import 'package:nyxx/src/internal/response_wrapper/thread_list_result_wrapper.da import 'package:nyxx/src/typedefs.dart'; import 'package:nyxx/src/utils/builders/attachment_builder.dart'; import 'package:nyxx/src/utils/builders/channel_builder.dart'; +import 'package:nyxx/src/utils/builders/forum_thread_builder.dart'; import 'package:nyxx/src/utils/builders/guild_builder.dart'; import 'package:nyxx/src/utils/builders/guild_event_builder.dart'; import 'package:nyxx/src/utils/builders/member_builder.dart'; @@ -411,6 +412,8 @@ abstract class IHttpEndpoints { Stream fetchGuildEventUsers(Snowflake guildId, Snowflake guildEventId, {int limit = 100, bool withMember = false, Snowflake? before, Snowflake? after}); + + Future startForumThread(Snowflake channelId, ForumThreadBuilder builder); } class HttpEndpoints implements IHttpEndpoints { @@ -542,6 +545,25 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } + @override + Future startForumThread(Snowflake channelId, ForumThreadBuilder builder) async { + final response = await httpHandler.execute( + BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..threads(), + method: "POST", + body: builder.build(), + ), + ); + + if (response is HttpResponseSuccess) { + return ThreadChannel(client, response.jsonBody as RawApiMap); + } + + return Future.error(response); + } + @override Future deleteRole(Snowflake guildId, Snowflake roleId, {String? auditReason}) async => executeSafe(BasicRequest( HttpRoute() diff --git a/lib/src/utils/builders/forum_thread_builder.dart b/lib/src/utils/builders/forum_thread_builder.dart new file mode 100644 index 000000000..922b2509d --- /dev/null +++ b/lib/src/utils/builders/forum_thread_builder.dart @@ -0,0 +1,31 @@ +import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/src/utils/builders/builder.dart'; +import 'package:nyxx/src/utils/builders/message_builder.dart'; +import 'package:nyxx/src/utils/builders/thread_builder.dart'; + +class ForumThreadBuilder implements Builder { + /// The name fo the thread + String name; + + /// First message in thread + MessageBuilder message; + + /// Amount of seconds a user has to wait before sending another message (0-21600); + /// bots, as well as users with the permission manage_messages, manage_thread, or manage_channel, are unaffected + int? rateLimitPerUser; + + /// The time after which the thread is automatically archived. + ThreadArchiveTime? archiveAfter; + + ForumThreadBuilder(this.name, this.message); + + @override + RawApiMap build() { + return { + "name": name, + "message": message.build(), + if (archiveAfter != null) "auto_archive_duration": archiveAfter!.value, + if (rateLimitPerUser != null) 'rate_limit_per_user': rateLimitPerUser! + }; + } +} diff --git a/lib/src/utils/builders/thread_builder.dart b/lib/src/utils/builders/thread_builder.dart index b3a0ccb3d..23c1c0ef0 100644 --- a/lib/src/utils/builders/thread_builder.dart +++ b/lib/src/utils/builders/thread_builder.dart @@ -3,7 +3,7 @@ import 'package:nyxx/src/utils/enum.dart'; import 'package:nyxx/src/utils/builders/builder.dart'; class ThreadBuilder extends Builder { - /// The name fo the thread + /// The name for the thread String? name; /// Whether or not the thread is private diff --git a/test/unit/builders_test.dart b/test/unit/builders_test.dart index 7f5a620e2..32c90fce3 100644 --- a/test/unit/builders_test.dart +++ b/test/unit/builders_test.dart @@ -6,6 +6,7 @@ import 'package:nyxx/src/core/user/presence.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; import 'package:nyxx/src/utils/builders/channel_builder.dart'; import 'package:nyxx/src/utils/builders/embed_builder.dart'; +import 'package:nyxx/src/utils/builders/forum_thread_builder.dart'; import 'package:nyxx/src/utils/builders/member_builder.dart'; import 'package:nyxx/src/utils/builders/message_builder.dart'; import 'package:nyxx/src/utils/builders/permissions_builder.dart'; @@ -210,5 +211,19 @@ main() { expect(MessageDecoration.bold.format('test'), equals('**test**')); }); + + test("ForumThreadBuilder", () { + final builder = ForumThreadBuilder("test", MessageBuilder.content("test")); + + expect( + builder.build(), + equals({ + 'name': 'test', + 'message': { + 'content': 'test' + } + }) + ); + }); }); } From 6444aaeb13f47b9ad9578b2545db3635581f5d4d Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 29 May 2022 00:00:21 +0200 Subject: [PATCH 15/27] Implement graceful shutdown (#347) --- lib/src/internal/shard/shard.dart | 4 ++- lib/src/internal/shard/shard_handler.dart | 14 +---------- lib/src/internal/shard/shard_manager.dart | 4 +-- lib/src/nyxx.dart | 13 +++++++--- lib/src/plugin/plugins/cli_integration.dart | 19 ++++++++------ lib/src/plugin/plugins/ignore_exception.dart | 26 +++++++++++++++++--- lib/src/plugin/plugins/logging.dart | 4 +-- 7 files changed, 50 insertions(+), 34 deletions(-) diff --git a/lib/src/internal/shard/shard.dart b/lib/src/internal/shard/shard.dart index 90867a911..91317d988 100644 --- a/lib/src/internal/shard/shard.dart +++ b/lib/src/internal/shard/shard.dart @@ -550,9 +550,11 @@ class Shard implements IShard { _sendPort.send({"cmd": "KILL"}); final killFuture = _receiveStream.firstWhere((element) => (element as RawApiMap)["cmd"] == "TERMINATE_OK"); - _shardIsolate.kill(priority: Isolate.immediate); await killFuture; + _receivePort.close(); + _heartbeatTimer.cancel(); + manager.logger.info("Shard $id disposed."); } } diff --git a/lib/src/internal/shard/shard_handler.dart b/lib/src/internal/shard/shard_handler.dart index 060ae63a6..a49eedf74 100644 --- a/lib/src/internal/shard/shard_handler.dart +++ b/lib/src/internal/shard/shard_handler.dart @@ -58,21 +58,10 @@ Future shardHandler(SendPort shardPort) async { Future terminate() async { await _socketSubscription?.cancel(); await _socket?.close(1000); + receivePort.close(); shardPort.send({"cmd": "TERMINATE_OK"}); } - if (!Platform.isWindows) { - // ignore: unawaited_futures - ProcessSignal.sigterm.watch().forEach((event) async { - await terminate(); - }); - } - - // ignore: unawaited_futures - ProcessSignal.sigint.watch().forEach((event) async { - await terminate(); - }); - // Attempts to connect to ws Future _connect() async { try { @@ -80,7 +69,6 @@ Future shardHandler(SendPort shardPort) async { _socket!.pingInterval = const Duration(seconds: 20); final zlibDecoder = RawZLibFilter.inflateFilter(); // Create zlib decoder specific to this connection. New connection should get new zlib context - // ignore: unawaited_futures _socket!.done.then((value) { shardPort.send({"cmd": "DISCONNECTED", "errorCode": _socket!.closeCode, "errorReason": _socket!.closeReason}); }); diff --git a/lib/src/internal/shard/shard_manager.dart b/lib/src/internal/shard/shard_manager.dart index 2e6ca0c56..c3cb0e7a8 100644 --- a/lib/src/internal/shard/shard_manager.dart +++ b/lib/src/internal/shard/shard_manager.dart @@ -186,9 +186,9 @@ class ShardManager implements IShardManager { for (final shard in _shards.values) { if (connectionManager.client.options.shutdownShardHook != null) { - connectionManager.client.options.shutdownShardHook!(connectionManager.client, shard); // ignore: unawaited_futures + await connectionManager.client.options.shutdownShardHook!(connectionManager.client, shard); } - shard.dispose(); // ignore: unawaited_futures + await shard.dispose(); } await onConnectController.close(); diff --git a/lib/src/nyxx.dart b/lib/src/nyxx.dart index 6d3ee9f8d..041f8b399 100644 --- a/lib/src/nyxx.dart +++ b/lib/src/nyxx.dart @@ -220,6 +220,10 @@ class NyxxRest extends INyxxRest { await plugin.onBotStop(this, _logger); } + await eventsRest.dispose(); + + onReadyController.close(); + await guilds.dispose(); await users.dispose(); await channels.dispose(); @@ -333,7 +337,7 @@ abstract class INyxxWebsocket implements INyxxRest { /// ``` /// or setup `CommandsFramework` and `Voice`. class NyxxWebsocket extends NyxxRest implements INyxxWebsocket { - late final ConnectionManager ws; // ignore: unused_field + late final ConnectionManager ws; /// Current client"s shard @override @@ -457,14 +461,15 @@ class NyxxWebsocket extends NyxxRest implements INyxxWebsocket { Future dispose() async { _logger.info("Disposing and closing bot..."); + await super.dispose(); + if (options.shutdownHook != null) { await options.shutdownHook!(this); } await shardManager.dispose(); - await eventsRest.dispose(); + await eventsWs.dispose(); - _logger.info("Exiting..."); - throw UnrecoverableNyxxError('Exiting nyxx...'); + _logger.info("Bot disposed."); } } diff --git a/lib/src/plugin/plugins/cli_integration.dart b/lib/src/plugin/plugins/cli_integration.dart index 0fbe06135..da3df9fe3 100644 --- a/lib/src/plugin/plugins/cli_integration.dart +++ b/lib/src/plugin/plugins/cli_integration.dart @@ -6,18 +6,23 @@ import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/plugin/plugin.dart'; class CliIntegration extends BasePlugin { + StreamSubscription? _sigtermSubscription; + StreamSubscription? _sigintSubscription; + @override - FutureOr onRegister(INyxx nyxx, Logger logger) { + void onRegister(INyxx nyxx, Logger logger) { if (!Platform.isWindows) { - ProcessSignal.sigterm.watch().forEach((event) async { - await nyxx.dispose(); - }); + _sigtermSubscription = ProcessSignal.sigterm.watch().listen((event) => nyxx.dispose()); } - ProcessSignal.sigint.watch().forEach((event) async { - await nyxx.dispose(); - }); + _sigintSubscription = ProcessSignal.sigint.watch().listen((event) => nyxx.dispose()); logger.info("Starting bot with pid: $pid. To stop the bot gracefully send SIGTERM or SIGKILL"); } + + @override + void onBotStop(INyxx nyxx, Logger logger) { + _sigintSubscription?.cancel(); + _sigtermSubscription?.cancel(); + } } diff --git a/lib/src/plugin/plugins/ignore_exception.dart b/lib/src/plugin/plugins/ignore_exception.dart index f060da24c..7559a199e 100644 --- a/lib/src/plugin/plugins/ignore_exception.dart +++ b/lib/src/plugin/plugins/ignore_exception.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:isolate'; import 'package:logging/logging.dart'; @@ -6,10 +5,20 @@ import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/plugin/plugin.dart'; class IgnoreExceptions extends BasePlugin { + late final ReceivePort _errorsPort; + @override - FutureOr onRegister(INyxx nyxx, Logger logger) { + void onRegister(INyxx nyxx, Logger logger) { + _errorsPort = _getErrorPort(logger); + Isolate.current.setErrorsFatal(false); + Isolate.current.addErrorListener(_errorsPort.sendPort); + } + + @override + void onBotStop(INyxx nyxx, Logger logger) => _stop(); + ReceivePort _getErrorPort(Logger logger) { final errorsPort = ReceivePort(); errorsPort.listen((err) { final stackTrace = err[1] != null ? ". Stacktrace: \n${err[1]}" : ""; @@ -17,10 +26,19 @@ class IgnoreExceptions extends BasePlugin { logger.shout("Got Error: Message: [${err[0]}]$stackTrace"); if (err[0].startsWith('UnrecoverableNyxxError') as bool) { - Isolate.current.kill(); + _stop(); + + throw err[0] as String; } }); - Isolate.current.addErrorListener(errorsPort.sendPort); + return errorsPort; + } + + void _stop() { + Isolate.current.removeErrorListener(_errorsPort.sendPort); + Isolate.current.setErrorsFatal(true); + + _errorsPort.close(); } } diff --git a/lib/src/plugin/plugins/logging.dart b/lib/src/plugin/plugins/logging.dart index dce414dd4..919bdf537 100644 --- a/lib/src/plugin/plugins/logging.dart +++ b/lib/src/plugin/plugins/logging.dart @@ -1,12 +1,10 @@ -import 'dart:async'; - import 'package:logging/logging.dart'; import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/plugin/plugin.dart'; class Logging extends BasePlugin { @override - FutureOr onRegister(INyxx nyxx, Logger logger) { + void onRegister(INyxx nyxx, Logger logger) { Logger.root.onRecord.listen((LogRecord rec) { print("[${rec.time}] [${rec.level.name}] [${rec.loggerName}] ${rec.message}"); }); From b9afdeb03a92137199b451b46550420ab8fa7a7a Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 29 May 2022 00:00:36 +0200 Subject: [PATCH 16/27] Add missing emoji endpoints (#346) * Implement missing emoji endpoints * Add methods to IMessage for using reaction endpoints * Address feedback * Remove unused parameter --- lib/src/core/message/message.dart | 12 +++++++ lib/src/internal/http_endpoints.dart | 51 +++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/src/core/message/message.dart b/lib/src/core/message/message.dart index 04eac5df9..488122c91 100644 --- a/lib/src/core/message/message.dart +++ b/lib/src/core/message/message.dart @@ -134,6 +134,12 @@ abstract class IMessage implements SnowflakeEntity, Disposable, Convertable deleteAllReactions(); + /// Fetches the users that reacted to this message with a given emoji. + Stream fetchReactionUsers(IEmoji emoji); + + /// Deletes reactions to this message with a given emoji + Future deleteReactions(IEmoji emoji); + /// Deletes the message. Future delete({String? auditReason}); @@ -418,6 +424,12 @@ class Message extends SnowflakeEntity implements IMessage { @override Future deleteAllReactions() => client.httpEndpoints.deleteMessageAllReactions(channel.id, id); + @override + Stream fetchReactionUsers(IEmoji emoji) => client.httpEndpoints.fetchMessageReactionUsers(channel.id, id, emoji); + + @override + Future deleteReactions(IEmoji emoji) => client.httpEndpoints.deleteMessageReactions(channel.id, id, emoji); + /// Deletes the message. @override Future delete({String? auditReason}) => client.httpEndpoints.deleteMessage(channel.id, id); diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index 079a7133c..e8a108140 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -280,7 +280,7 @@ abstract class IHttpEndpoints { /// Creates reaction with given [emoji] on given message Future createMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji); - /// Deletes all reactions for given [emoji] from message + /// Deletes the bot's reaction with a given [emoji] from message Future deleteMessageReaction(Snowflake channelId, Snowflake messageId, IEmoji emoji); /// Deletes all reactions of given user from message. @@ -289,6 +289,18 @@ abstract class IHttpEndpoints { /// Deletes all reactions on given message Future deleteMessageAllReactions(Snowflake channelId, Snowflake messageId); + /// Fetches all reactions with a given emoji on a message + Stream fetchMessageReactionUsers( + Snowflake channelId, + Snowflake messageId, + IEmoji emoji, { + Snowflake? after, + int? limit, + }); + + /// Deletes all reactions with a given emoji on a message + Future deleteMessageReactions(Snowflake channelId, Snowflake messageId, IEmoji emoji); + /// Deletes message from given channel Future deleteMessage(Snowflake channelId, Snowflake messageId, {String? auditReason}); @@ -1463,6 +1475,43 @@ class HttpEndpoints implements IHttpEndpoints { ..reactions(), method: "DELETE")); + @override + Stream fetchMessageReactionUsers( + Snowflake channelId, + Snowflake messageId, + IEmoji emoji, { + Snowflake? after, + int? limit, + }) async* { + final response = await executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(emoji: emoji.encodeForAPI()), + queryParams: { + if (after != null) "after": after.toString(), + if (limit != null) "limit": limit, + }, + )); + + if (response is HttpResponseSuccess) { + for (final rawUser in (response.jsonBody as RawApiList).cast()) { + yield User(client, rawUser); + } + } + + yield* Stream.error(response); + } + + @override + Future deleteMessageReactions(Snowflake channelId, Snowflake messageId, IEmoji emoji) => executeSafe(BasicRequest( + HttpRoute() + ..channels(id: channelId.toString()) + ..messages(id: messageId.toString()) + ..reactions(emoji: emoji.encodeForAPI()), + method: "DELETE", + )); + @override Future deleteMessage(Snowflake channelId, Snowflake messageId, {String? auditReason}) => executeSafe(BasicRequest( HttpRoute() From cfc78201954c639378645dd7dacc4de151996edf Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Mon, 6 Jun 2022 11:49:42 +0200 Subject: [PATCH 17/27] feature: Add `threadName` on `IWebhook#execute()` (#348) * Add `threadName` param * Ran format --- lib/src/core/guild/webhook.dart | 9 ++++++--- lib/src/internal/http_endpoints.dart | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/core/guild/webhook.dart b/lib/src/core/guild/webhook.dart index d0cb5c7a7..e1eed8d3c 100644 --- a/lib/src/core/guild/webhook.dart +++ b/lib/src/core/guild/webhook.dart @@ -74,7 +74,9 @@ abstract class IWebhook implements SnowflakeEntity, IMessageAuthor { /// /// [wait] - waits for server confirmation of message send before response, /// and returns the created message body (defaults to false; when false a message that is not save does not return an error) - Future execute(MessageBuilder builder, {bool wait = true, Snowflake? threadId, String? avatarUrl, String? username}); + /// [threadId] is the id of thread in the channel to send to. + /// If [threadName] is specified, this will create a thread in the forum channel with the given name - **this is only available for forum channels.** + Future execute(MessageBuilder builder, {bool wait = true, Snowflake? threadId, String? threadName, String? avatarUrl, String? username}); @override String avatarURL({String format = "webp", int size = 128}); @@ -173,8 +175,9 @@ class Webhook extends SnowflakeEntity implements IWebhook { /// [wait] - waits for server confirmation of message send before response, /// and returns the created message body (defaults to false; when false a message that is not save does not return an error) @override - Future execute(MessageBuilder builder, {bool wait = true, Snowflake? threadId, String? avatarUrl, String? username}) => - client.httpEndpoints.executeWebhook(id, builder, token: token, threadId: threadId, username: username, wait: wait, avatarUrl: avatarUrl); + Future execute(MessageBuilder builder, {bool wait = true, Snowflake? threadId, String? threadName, String? avatarUrl, String? username}) => + client.httpEndpoints + .executeWebhook(id, builder, token: token, threadId: threadId, username: username, wait: wait, avatarUrl: avatarUrl, threadName: threadName); @override String avatarURL({String format = "webp", int size = 128}) => client.httpEndpoints.userAvatarURL(id, avatarHash, 0, format: format, size: size); diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index e8a108140..b1ae88761 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -332,7 +332,7 @@ abstract class IHttpEndpoints { /// /// If [wait] is set to true -- request will return resulting message. Future executeWebhook(Snowflake webhookId, MessageBuilder builder, - {String token = "", bool wait = true, String? avatarUrl, String? username, Snowflake? threadId}); + {String token = "", bool wait = true, String? avatarUrl, String? username, Snowflake? threadId, String? threadName}); /// Fetches webhook using its [id] and optionally [token]. /// If [token] is specified it will be used to fetch webhook data. @@ -1585,13 +1585,14 @@ class HttpEndpoints implements IHttpEndpoints { @override Future executeWebhook(Snowflake webhookId, MessageBuilder builder, - {String token = "", bool wait = true, String? avatarUrl, String? username, Snowflake? threadId}) async { + {String token = "", bool wait = true, String? avatarUrl, String? username, Snowflake? threadId, String? threadName}) async { final queryParams = {"wait": wait, if (threadId != null) "thread_id": threadId}; final body = { ...builder.build(client.options.allowedMentions), if (avatarUrl != null) "avatar_url": avatarUrl, if (username != null) "username": username, + if (threadName != null) 'thread_name': threadName, }; HttpResponse response; From ed867d665778010faa6d3705333ecc52367ec14a Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 12 Jun 2022 10:01:27 +0200 Subject: [PATCH 18/27] (bug) Invalid serialization of query params (#352) * bug: Fixup invalid serialization of query params * style: Fixup style --- lib/src/internal/http/http_request.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/internal/http/http_request.dart b/lib/src/internal/http/http_request.dart index 6e3303670..9280c4a2c 100644 --- a/lib/src/internal/http/http_request.dart +++ b/lib/src/internal/http/http_request.dart @@ -42,7 +42,8 @@ class BasicRequest extends HttpRequest { @override Future prepareRequest() async { - final request = http.Request(method, uri.replace(queryParameters: queryParams))..headers.addAll(genHeaders()); + final request = http.Request(method, uri.replace(queryParameters: queryParams?.map((key, value) => MapEntry(key, value.toString())))) + ..headers.addAll(genHeaders()); if (body != null && method != "GET") { request.headers.addAll(_getJsonContentTypeHeader()); From 405cffb313a1fcb54c372e6f53158a09e442af66 Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Sun, 12 Jun 2022 10:01:50 +0200 Subject: [PATCH 19/27] Fix some serialization bugs (#351) * Fix some serialization bugs * Fix extension var throw if extension is an empty string Also add defaultFormat named param Co-Authored-By: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> * Remove nulling defaultFormat Co-Authored-By: Szymon Uglis <23033957+l7ssha@users.noreply.github.com> Co-Authored-By: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Co-authored-by: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Co-authored-by: Szymon Uglis <23033957+l7ssha@users.noreply.github.com> --- lib/src/utils/builders/attachment_builder.dart | 5 +++-- lib/src/utils/builders/guild_builder.dart | 4 ++-- lib/src/utils/builders/guild_event_builder.dart | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/utils/builders/attachment_builder.dart b/lib/src/utils/builders/attachment_builder.dart index 7ebb4893b..c933ba5ec 100644 --- a/lib/src/utils/builders/attachment_builder.dart +++ b/lib/src/utils/builders/attachment_builder.dart @@ -60,9 +60,10 @@ class AttachmentBuilder { /// Returns attachment encoded in Data URI scheme format /// See: https://discord.com/developers/docs/reference#image-data - String getBase64() { + String getBase64({String defaultFormat = 'png'}) { final encodedData = base64Encode(_bytes); - final extension = path_utils.extension(_name); + final fileExtension = path_utils.extension(_name); + final extension = fileExtension.isNotEmpty ? fileExtension.substring(1) : defaultFormat; return "data:image/$extension;base64,$encodedData"; } } diff --git a/lib/src/utils/builders/guild_builder.dart b/lib/src/utils/builders/guild_builder.dart index 6353fc3ab..c01f7881c 100644 --- a/lib/src/utils/builders/guild_builder.dart +++ b/lib/src/utils/builders/guild_builder.dart @@ -63,9 +63,9 @@ class GuildBuilder extends Builder { if (explicitContentFilter != null) "explicit_content_filter": explicitContentFilter, if (roles != null) "roles": _genIterable(roles!).toList(), if (channels != null) "channels": _genIterable(channels!).toList(), - if (afkChannelId != null) "afk_channel_id": afkChannelId, + if (afkChannelId != null) "afk_channel_id": afkChannelId!.toString(), if (afkTimeout != null) "afk_timeout": afkTimeout, - if (systemChannelId != null) "system_channel_id": systemChannelId, + if (systemChannelId != null) "system_channel_id": systemChannelId.toString(), if (systemChannelFlags != null) "system_channel_flags": systemChannelFlags!.value, }; diff --git a/lib/src/utils/builders/guild_event_builder.dart b/lib/src/utils/builders/guild_event_builder.dart index 93dbd2e06..69b3d2281 100644 --- a/lib/src/utils/builders/guild_event_builder.dart +++ b/lib/src/utils/builders/guild_event_builder.dart @@ -38,7 +38,7 @@ class GuildEventBuilder implements Builder { if (startDate != null) 'scheduled_start_time': startDate!.toIso8601String(), if (endDate != null) 'scheduled_end_time': endDate!.toIso8601String(), if (description != null) 'description': description, - if (type != null) 'entity_type': type, + if (type != null) 'entity_type': type!.value, if (status != null) 'status': status!.value }; } From 9d2ba120dcd5600c0baed96988387ce82fc222e6 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 12 Jun 2022 10:13:18 +0200 Subject: [PATCH 20/27] Release 4.0.0-dev.2 --- CHANGELOG.md | 15 +++++++++++++++ lib/src/internal/constants.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0059ccc4e..4d61ce6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 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: 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) + ## 4.0.0-dev.1 __09.05.2022__ diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index c30f15cbd..cb6ef954a 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -33,7 +33,7 @@ class Constants { static const int apiVersion = 10; /// Version of Nyxx - static const String version = "4.0.0-dev.1"; + static const String version = "4.0.0-dev.2"; /// Url to Nyxx repo static const String repoUrl = "https://github.com/nyxx-discord/nyxx"; diff --git a/pubspec.yaml b/pubspec.yaml index f12b71bee..c6d362113 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 4.0.0-dev.1 +version: 4.0.0-dev.2 description: A Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx From 36386350bc19249cc57f03304f06ae5595e92f4b Mon Sep 17 00:00:00 2001 From: Nicholas Shrefler <16249086+NDSo@users.noreply.github.com> Date: Sat, 25 Jun 2022 19:10:33 -0400 Subject: [PATCH 21/27] Fix Global Rate Limit Handling (#354) --- lib/src/internal/http/http_handler.dart | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/src/internal/http/http_handler.dart b/lib/src/internal/http/http_handler.dart index bf5cce638..07f76b6e6 100644 --- a/lib/src/internal/http/http_handler.dart +++ b/lib/src/internal/http/http_handler.dart @@ -83,7 +83,7 @@ class HttpHandler { final response = await httpClient.send(await request.prepareRequest()); currentBucket?.removeInFlightRequest(request); currentBucket = _upsertBucket(request, response); - return _handle(response); + return _handle(request, response); } on HttpClientException catch (e) { currentBucket?.removeInFlightRequest(request); if (e.response == null) { @@ -94,28 +94,12 @@ class HttpHandler { final response = e.response as http.StreamedResponse; _upsertBucket(request, response); - // Check for 429, emmit events and wait given in response body time - if (response.statusCode == 429) { - final responseBody = jsonDecode(await response.stream.bytesToString()); - final retryAfter = Duration(milliseconds: ((responseBody["retry_after"] as double) * 1000).ceil()); - final isGlobal = responseBody["global"] as bool; - - if (isGlobal) { - globalRateLimitReset = DateTime.now().add(retryAfter); - } - - _events.onRateLimitedController.add(RatelimitEvent(request, false, response)); - logger.warning("${isGlobal ? "Global" : ""} Rate limited via 429 on endpoint: ${request.uri}. Trying to send request again in $retryAfter"); - - return Future.delayed(retryAfter, () => execute(request)); - } - // Return http error - return _handle(response); + return _handle(request, response); } } - Future _handle(http.StreamedResponse response) async { + Future _handle(HttpRequest request, http.StreamedResponse response) async { if (response.statusCode >= 200 && response.statusCode < 300) { final responseSuccess = HttpResponseSuccess(response); await responseSuccess.finalize(); @@ -126,6 +110,22 @@ class HttpHandler { return responseSuccess; } + // Check for 429, emmit events and wait given in response body time + if (response.statusCode == 429) { + final responseBody = jsonDecode(await response.stream.bytesToString()); + final retryAfter = Duration(milliseconds: ((responseBody["retry_after"] as double) * 1000).ceil()); + final isGlobal = responseBody["global"] as bool; + + if (isGlobal) { + globalRateLimitReset = DateTime.now().add(retryAfter); + } + + _events.onRateLimitedController.add(RatelimitEvent(request, false, response)); + logger.warning("${isGlobal ? "Global " : ""}Rate limited via 429 on endpoint: ${request.uri}. Trying to send request again in $retryAfter"); + + return Future.delayed(retryAfter, () => execute(request)); + } + final responseError = HttpResponseError(response); await responseError.finalize(); From 763b054a284984fa5f84e794f4d093be04d8154f Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Wed, 20 Jul 2022 13:45:37 +0200 Subject: [PATCH 22/27] Add MessageBuilder.limitLength (#356) * Add MessageBuilder.limitLength * Document limitLength --- lib/src/utils/builders/message_builder.dart | 15 +++++++++++++++ test/unit/builders_test.dart | 17 ++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/src/utils/builders/message_builder.dart b/lib/src/utils/builders/message_builder.dart index 21e1a238b..b526ef77b 100644 --- a/lib/src/utils/builders/message_builder.dart +++ b/lib/src/utils/builders/message_builder.dart @@ -125,6 +125,21 @@ class MessageBuilder { /// Appends timestamp to message from [dateTime] void appendTimestamp(DateTime dateTime, {TimeStampStyle style = TimeStampStyle.def}) => append(style.format(dateTime)); + /// Limits the length of the content of the builder to [length]. + /// + /// If [content] is shorter than [length], this method does nothing. Else, it truncates content and appends [ellipsis] (if non-null) in a way that the new + /// content length equals [length]. + void limitLength({int length = 2000, String? ellipsis = '...'}) { + if (_content.length < length) { + return; + } + + ellipsis ??= ''; + + final cutContent = content.substring(0, length - ellipsis.length); + content = cutContent + ellipsis; + } + /// Add attachment void addAttachment(AttachmentBuilder attachment) { files ??= []; diff --git a/test/unit/builders_test.dart b/test/unit/builders_test.dart index 32c90fce3..81dffc911 100644 --- a/test/unit/builders_test.dart +++ b/test/unit/builders_test.dart @@ -212,6 +212,16 @@ main() { expect(MessageDecoration.bold.format('test'), equals('**test**')); }); + test('limitLength', () { + final builder = MessageBuilder.content('abc' * 1000)..limitLength(ellipsis: null); + + expect(builder.content, equals(('abc' * 1000).substring(0, 2000))); + + builder.limitLength(length: 10, ellipsis: '...'); + + expect(builder.content, equals('abcabca...')); + }); + test("ForumThreadBuilder", () { final builder = ForumThreadBuilder("test", MessageBuilder.content("test")); @@ -219,11 +229,8 @@ main() { builder.build(), equals({ 'name': 'test', - 'message': { - 'content': 'test' - } - }) - ); + 'message': {'content': 'test'} + })); }); }); } From 1a7eed340a7659ada4ad44bba85e3b1dc74399a6 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:24:07 +0200 Subject: [PATCH 23/27] Correct uninitialised fields in events (#358) --- lib/src/events/guild_events.dart | 2 ++ lib/src/events/message_events.dart | 24 +++++++++++++------ lib/src/events/presence_update_event.dart | 12 +++++----- .../events/thread_members_update_event.dart | 1 + 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/src/events/guild_events.dart b/lib/src/events/guild_events.dart index 9f781398e..35703104a 100644 --- a/lib/src/events/guild_events.dart +++ b/lib/src/events/guild_events.dart @@ -405,6 +405,8 @@ class RoleUpdateEvent implements IRoleUpdateEvent { if (guildInstance != null) { oldRole = guildInstance.roles[role.id]; guildInstance.roles[role.id] = role; + } else { + oldRole = null; } } } diff --git a/lib/src/events/message_events.dart b/lib/src/events/message_events.dart index d4d179f32..7878dc34f 100644 --- a/lib/src/events/message_events.dart +++ b/lib/src/events/message_events.dart @@ -139,19 +139,19 @@ abstract class IMessageReactionEvent { CacheableTextChannel get channel; /// Reference to guild if event happened in guild - Cacheable get guild; + Cacheable? get guild; /// Message reference - late final IMessage? message; + IMessage? get message; /// Id of message - late final Snowflake messageId; + Snowflake get messageId; /// The member who reacted if this happened in a guild - late final IMember member; + IMember? get member; /// Emoji object. - late final IEmoji emoji; + IEmoji get emoji; } /// Emitted when reaction is added or removed from message @@ -163,7 +163,7 @@ abstract class MessageReactionEvent { late final CacheableTextChannel channel; /// Reference to guild if event happened in guild - late final Cacheable guild; + late final Cacheable? guild; /// Message reference late final IMessage? message; @@ -172,7 +172,7 @@ abstract class MessageReactionEvent { late final Snowflake messageId; /// The member who reacted if this happened in a guild - late final IMember member; + late final IMember? member; /// Emoji object. late final IEmoji emoji; @@ -181,6 +181,13 @@ abstract class MessageReactionEvent { MessageReactionEvent(RawApiMap json, INyxx client) { user = UserCacheable(client, Snowflake(json["d"]["user_id"])); channel = CacheableTextChannel(client, Snowflake(json["d"]["channel_id"])); + guild = GuildCacheable(client, Snowflake(json["d"]["guild_id"])); + + if (json["d"]["member"] != null) { + member = Member(client, json["d"]["member"] as RawApiMap, guild!.id); + } else { + member = null; + } messageId = Snowflake(json["d"]["message_id"]); @@ -368,12 +375,15 @@ class MessageUpdateEvent implements IMessageUpdateEvent { final channelInstance = channel.getFromCache(); if (channelInstance == null) { + updatedMessage = null; + oldMessage = null; return; } oldMessage = channelInstance.messageCache[messageId]; if (oldMessage == null) { + updatedMessage = null; return; } diff --git a/lib/src/events/presence_update_event.dart b/lib/src/events/presence_update_event.dart index 63f4ff35e..71a248aac 100644 --- a/lib/src/events/presence_update_event.dart +++ b/lib/src/events/presence_update_event.dart @@ -35,15 +35,15 @@ class PresenceUpdateEvent implements IPresenceUpdateEvent { PresenceUpdateEvent(RawApiMap raw, INyxx client) { presences = [for (final rawActivity in raw["d"]["activities"]) Activity(rawActivity as RawApiMap)]; clientStatus = ClientStatus(raw["d"]["client_status"] as RawApiMap); - this.user = UserCacheable(client, Snowflake(raw["d"]["user"]["id"])); + user = UserCacheable(client, Snowflake(raw["d"]["user"]["id"])); - final user = this.user.getFromCache(); - if (user != null) { - if (clientStatus != user.status) { - (user as User).status = clientStatus; + final cachedUser = user.getFromCache(); + if (cachedUser != null) { + if (clientStatus != cachedUser.status) { + (cachedUser as User).status = clientStatus; } - (user as User).presence = presences.isNotEmpty ? presences.first : null; + (cachedUser as User).presence = presences.isNotEmpty ? presences.first : null; } } } diff --git a/lib/src/events/thread_members_update_event.dart b/lib/src/events/thread_members_update_event.dart index 96a8c126d..28264bb64 100644 --- a/lib/src/events/thread_members_update_event.dart +++ b/lib/src/events/thread_members_update_event.dart @@ -53,6 +53,7 @@ class ThreadMembersUpdateEvent implements IThreadMembersUpdateEvent { thread = CacheableTextChannel(client, Snowflake(data["id"])); guild = GuildCacheable(client, Snowflake(data["guild_id"])); + approxMemberCount = data["member_count"] as int; addedMembers = [ if (data["added_members"] != null) From ba9e282a02f0de271843dd9e1d28f8067d2e5ab6 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:30:23 +0200 Subject: [PATCH 24/27] Correctly export HTTP route builders (#359) * Export IHttpRoute, HttpRoutePart and HttpRouteParam * Document HttpRoutePart and HttpRouteParam --- lib/nyxx.dart | 3 +++ lib/src/internal/http/http_route_param.dart | 11 +++++++++++ lib/src/internal/http/http_route_part.dart | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/lib/nyxx.dart b/lib/nyxx.dart index dde8bf1e3..4b3fe5493 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -142,6 +142,9 @@ export 'src/internal/exceptions/invalid_shard_exception.dart' show InvalidShardE export 'src/internal/exceptions/invalid_snowflake_exception.dart' show InvalidSnowflakeException; export 'src/internal/exceptions/missing_token_error.dart' show MissingTokenError; export 'src/internal/exceptions/unrecoverable_nyxx_error.dart' show UnrecoverableNyxxError; +export 'src/internal/http/http_route_param.dart' show HttpRouteParam; +export 'src/internal/http/http_route_part.dart' show HttpRoutePart; +export 'src/internal/http/http_route.dart' show IHttpRoute; export 'src/internal/http/http_response.dart' show IHttpResponse, IHttpResponseError, IHttpResponseSuccess; export 'src/internal/interfaces/convertable.dart' show Convertable; export 'src/internal/interfaces/disposable.dart' show Disposable; diff --git a/lib/src/internal/http/http_route_param.dart b/lib/src/internal/http/http_route_param.dart index 2a05100ff..bd0c593f9 100644 --- a/lib/src/internal/http/http_route_param.dart +++ b/lib/src/internal/http/http_route_param.dart @@ -1,5 +1,16 @@ +/// Represents a HTTP route parameter. +/// +/// A HTTP route parameter is a URL fragment that contains data specific to an invocation of a route (i.e a guild or message id). +/// +/// In the Discord documentation, these are the parts of URLs {enclosed in curly braces}. class HttpRouteParam { + /// The value of this parameter. final String param; + + /// Whether this parameter is a major parameter. + /// + /// Major parameters influence Discord's rate limiting. Requests with different major parameters will go into separate buckets for rate limiting, whereas + /// routes with different minor parameters will use the same bucket. final bool isMajor; HttpRouteParam(this.param, {this.isMajor = false}); diff --git a/lib/src/internal/http/http_route_part.dart b/lib/src/internal/http/http_route_part.dart index 71980daf9..8834a2045 100644 --- a/lib/src/internal/http/http_route_part.dart +++ b/lib/src/internal/http/http_route_part.dart @@ -1,7 +1,14 @@ import 'http_route_param.dart'; +/// Represents a HTTP route part. +/// +/// A HTTP route part is a route fragment (i.e /foo or /bar) followed by 0 or more parameters (i.e a guild or message id) that can change across invocations of +/// the route.. class HttpRoutePart { + /// The unchanging part of this route part. final String path; + + /// The parameters of this route. May change across invocations of this route. final List params; HttpRoutePart(this.path, [this.params = const []]); From 362adcb14b08a310d2ed43bca8178ebe74a77f1d Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:06:25 +0200 Subject: [PATCH 25/27] Remove dollar prefix for identify payload (#361) https://discord.com/developers/docs/change-log#jun-17-2022 --- lib/src/internal/shard/shard.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/internal/shard/shard.dart b/lib/src/internal/shard/shard.dart index 91317d988..1eb90bc34 100644 --- a/lib/src/internal/shard/shard.dart +++ b/lib/src/internal/shard/shard.dart @@ -317,9 +317,9 @@ class Shard implements IShard { final identifyMsg = { "token": manager.connectionManager.client.token, "properties": { - "\$os": Platform.operatingSystem, - "\$browser": "nyxx", - "\$device": "nyxx", + "os": Platform.operatingSystem, + "browser": "nyxx", + "device": "nyxx", }, "large_threshold": manager.connectionManager.client.options.largeThreshold, "guild_subscriptions": manager.connectionManager.client.options.guildSubscriptions, From f5b1b1b67971d2062d31fcc5b09456f0a65cbede Mon Sep 17 00:00:00 2001 From: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:07:30 +0200 Subject: [PATCH 26/27] Fix mention string, and use a better approach to retrieve everyone role (#360) --- lib/src/core/guild/guild.dart | 2 +- lib/src/core/guild/role.dart | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/src/core/guild/guild.dart b/lib/src/core/guild/guild.dart index dc19e0f49..b2b6e5dac 100644 --- a/lib/src/core/guild/guild.dart +++ b/lib/src/core/guild/guild.dart @@ -518,7 +518,7 @@ class Guild extends SnowflakeEntity implements IGuild { /// Getter for @everyone role @override - IRole get everyoneRole => roles.values.firstWhere((r) => r.name == "@everyone"); + IRole get everyoneRole => roles[id]!; /// Returns member object for bot user @override diff --git a/lib/src/core/guild/role.dart b/lib/src/core/guild/role.dart index 421800573..4a77e74d9 100644 --- a/lib/src/core/guild/role.dart +++ b/lib/src/core/guild/role.dart @@ -49,7 +49,7 @@ abstract class IRole implements SnowflakeEntity, Mentionable { /// Mention of role. If role cannot be mentioned it returns name of role (@name) @override - String get mention => mentionable ? "<@&$id>" : "@$name"; + String get mention; /// Returns url to role icon String? iconURL({String format = "webp", int size = 128}); @@ -113,7 +113,25 @@ class Role extends SnowflakeEntity implements IRole { /// Mention of role. If role cannot be mentioned it returns name of role (@name) @override - String get mention => mentionable ? "<@&$id>" : "@$name"; + String get mention { + String mentionString; + + if (mentionable) { + if (id == guild.id) { + mentionString = name; + } else { + mentionString = '<@&$id>'; + } + } else { + if (id == guild.id) { + mentionString = name; + } else { + mentionString = '@$name'; + } + } + + return mentionString; + } /// Creates an instance of [Role] Role(this.client, RawApiMap raw, Snowflake guildId) : super(Snowflake(raw["id"])) { From 903d114d0110a73900247d13d54eb197ae993eda Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Fri, 29 Jul 2022 23:11:15 +0200 Subject: [PATCH 27/27] Release 4.0.0 (#357) * Release 4.0.0 * Update changelog * Add 2 missing changelog fixtures Co-authored-by: Szymon Uglis --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ lib/src/internal/constants.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d61ce6a0..1637ecc4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +## 4.0.0 +___29.07.2022__ + +- breaking: Fix typo in `IHttpResponseSucess` +- breaking: Remove `threeDayThreadArchive` and `sevenDayThreadArchive` guild features +- 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 + - Added the Message Content privileged intent +- feature: Add guild Audit Log options +- feature: Implement forum channels +- feature: Implement guild Welcome Screen & Channel +- feature: Add missing Audit log types +- feature: Implement guild Banners +- feature: Implement partial presences +- feature: Add missing guild properties +- 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) +- bug: Fix incorrect guild URLs +- bug: Fix incorrect file encoding +- bug: Fix member editing +- bug: Fix serialization issues +- bug: Fix uninitialized fields + ## 4.0.0-dev.2 __12.06.2022__ diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index cb6ef954a..48efff17e 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -33,7 +33,7 @@ class Constants { static const int apiVersion = 10; /// Version of Nyxx - static const String version = "4.0.0-dev.2"; + static const String version = "4.0.0"; /// Url to Nyxx repo static const String repoUrl = "https://github.com/nyxx-discord/nyxx"; diff --git a/pubspec.yaml b/pubspec.yaml index c6d362113..33132aa9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 4.0.0-dev.2 +version: 4.0.0 description: A Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx