Skip to content

Commit

Permalink
Fix OAuth2 requiring identify scope, and add ability to quickly lis…
Browse files Browse the repository at this point in the history
…t guilds with `Nyxx.connectRest` (#634)

* fix `Nyxx.connectOAuth2` throwing "Unauthorized" exception if `identify` scope missing and replace `listGuilds`/`listCurrentUserGuilds` return types with new `UserGuild` class

* add missing `lib/src/models/oauth2.dart` file

* remove `print` call (left when debugging)

* fix `UserManager.listCurrentUserGuilds` tests

* apply abito changes
  • Loading branch information
MCausc78 authored Mar 8, 2024
1 parent a1d89ca commit 408fe03
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 63 deletions.
4 changes: 3 additions & 1 deletion lib/nyxx.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ export 'src/models/guild/guild.dart'
MfaLevel,
NsfwLevel,
PremiumTier,
VerificationLevel;
VerificationLevel,
UserGuild;
export 'src/models/guild/integration.dart' show PartialIntegration, Integration, IntegrationAccount, IntegrationApplication, IntegrationExpireBehavior;
export 'src/models/guild/member.dart' show Member, MemberFlags, PartialMember;
export 'src/models/guild/onboarding.dart' show Onboarding, OnboardingPrompt, OnboardingPromptOption, OnboardingPromptType;
Expand Down Expand Up @@ -300,6 +301,7 @@ export 'src/models/interaction.dart'
PingInteraction;
export 'src/models/entitlement.dart' show Entitlement, PartialEntitlement, EntitlementType;
export 'src/models/sku.dart' show Sku, SkuType, SkuFlags;
export 'src/models/oauth2.dart' show OAuth2Information;

export 'src/utils/flags.dart' show Flag, Flags;
export 'src/intents.dart' show GatewayIntents;
Expand Down
15 changes: 10 additions & 5 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,14 @@ abstract class Nyxx {

/// Create an instance of [NyxxOAuth2] that can perform requests to the HTTP API and is
/// authenticated with OAuth2 [Credentials].
///
/// Note that `client.user.id` will contain [Snowflake.zero] if there no `identify` scope.
static Future<NyxxOAuth2> connectOAuth2(Credentials credentials, {RestClientOptions options = const RestClientOptions()}) =>
connectOAuth2WithOptions(OAuth2ApiOptions(credentials: credentials), options);

/// Create an instance of [NyxxOAuth2] using the provided options.
///
/// Note that `client.user.id` will contain [Snowflake.zero] if there no `identify` scope.
static Future<NyxxOAuth2> connectOAuth2WithOptions(OAuth2ApiOptions apiOptions, [RestClientOptions clientOptions = const RestClientOptions()]) async {
clientOptions.logger
..info('Connecting to the REST API via OAuth2')
Expand All @@ -110,10 +114,11 @@ abstract class Nyxx {

return _doConnect(apiOptions, clientOptions, () async {
final client = NyxxOAuth2._(apiOptions, clientOptions);
final information = await client.users.fetchCurrentOAuth2Information();

return client
.._application = await client.applications.fetchCurrentApplication()
.._user = await client.users.fetchCurrentUser();
.._application = information.application
.._user = information.user ?? PartialUser(id: Snowflake.zero, manager: client.users);
}, clientOptions.plugins);
}

Expand Down Expand Up @@ -199,7 +204,7 @@ class NyxxRest with ManagerMixin implements Nyxx {
Future<void> leaveThread(Snowflake id) => channels.leaveThread(id);

/// List the guilds the current user is a member of.
Future<List<PartialGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
Future<List<UserGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
users.listCurrentUserGuilds(before: before, after: after, limit: limit);

@override
Expand Down Expand Up @@ -246,7 +251,7 @@ class NyxxOAuth2 with ManagerMixin implements NyxxRest {
Future<void> leaveThread(Snowflake id) => channels.leaveThread(id);

@override
Future<List<PartialGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
Future<List<UserGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
users.listCurrentUserGuilds(before: before, after: after, limit: limit);

@override
Expand Down Expand Up @@ -299,7 +304,7 @@ class NyxxGateway with ManagerMixin, EventMixin implements NyxxRest {
Future<void> leaveThread(Snowflake id) => channels.leaveThread(id);

@override
Future<List<PartialGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
Future<List<UserGuild>> listGuilds({Snowflake? before, Snowflake? after, int? limit}) =>
users.listCurrentUserGuilds(before: before, after: after, limit: limit);

/// Update the client's voice state in the guild with the ID [guildId].
Expand Down
11 changes: 11 additions & 0 deletions lib/src/http/managers/application_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ class ApplicationManager {
return parse(response.jsonBody as Map<String, Object?>);
}

/// Fetch the current OAuth2 application.
Future<Application> fetchOAuth2CurrentApplication() async {
final route = HttpRoute()
..oauth2()
..applications(id: '@me');
final request = BasicRequest(route);

final response = await client.httpHandler.executeSafe(request);
return parse(response.jsonBody as Map<String, Object?>);
}

/// Update the current application.
Future<Application> updateCurrentApplication(ApplicationUpdateBuilder builder) async {
final route = HttpRoute()..applications(id: '@me');
Expand Down
16 changes: 16 additions & 0 deletions lib/src/http/managers/guild_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ class GuildManager extends Manager<Guild> {
);
}

/// Parse [UserGuild] from [raw].
UserGuild parseUserGuild(Map<String, Object?> raw) {
final id = Snowflake.parse(raw['id']!);
return UserGuild(
id: id,
manager: this,
name: raw['name'] as String,
iconHash: raw['icon'] as String?,
isOwnedByCurrentUser: raw['owner'] as bool,
currentUserPermissions: Permissions(int.parse(raw['permissions'] as String)),
features: parseGuildFeatures(raw['features'] as List),
approximateMemberCount: raw['approximate_member_count'] as int?,
approximatePresenceCount: raw['approximate_presence_count'] as int?,
);
}

static final Map<String, Flag<GuildFeatures>> _nameToGuildFeature = {
'ANIMATED_BANNER': GuildFeatures.animatedBanner,
'ANIMATED_ICON': GuildFeatures.animatedIcon,
Expand Down
20 changes: 18 additions & 2 deletions lib/src/http/managers/user_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import 'package:nyxx/src/builders/user.dart';
import 'package:nyxx/src/http/managers/manager.dart';
import 'package:nyxx/src/http/request.dart';
import 'package:nyxx/src/http/route.dart';
import 'package:nyxx/src/models/application.dart';
import 'package:nyxx/src/models/channel/types/dm.dart';
import 'package:nyxx/src/models/channel/types/group_dm.dart';
import 'package:nyxx/src/models/discord_color.dart';
import 'package:nyxx/src/models/guild/guild.dart';
import 'package:nyxx/src/models/guild/integration.dart';
import 'package:nyxx/src/models/guild/member.dart';
import 'package:nyxx/src/models/locale.dart';
import 'package:nyxx/src/models/oauth2.dart';
import 'package:nyxx/src/models/snowflake.dart';
import 'package:nyxx/src/models/user/application_role_connection.dart';
import 'package:nyxx/src/models/user/connection.dart';
Expand Down Expand Up @@ -129,7 +131,7 @@ class UserManager extends ReadOnlyManager<User> {
}

/// List the guilds the current user is a member of.
Future<List<PartialGuild>> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit, bool? withCounts}) async {
Future<List<UserGuild>> listCurrentUserGuilds({Snowflake? after, Snowflake? before, int? limit, bool? withCounts}) async {
final route = HttpRoute()
..users(id: '@me')
..guilds();
Expand All @@ -143,7 +145,7 @@ class UserManager extends ReadOnlyManager<User> {
final response = await client.httpHandler.executeSafe(request);
return parseMany(
response.jsonBody as List,
(Map<String, Object?> raw) => PartialGuild(id: Snowflake.parse(raw['id']!), manager: client.guilds),
(Map<String, Object?> raw) => client.guilds.parseUserGuild(raw),
);
}

Expand Down Expand Up @@ -248,4 +250,18 @@ class UserManager extends ReadOnlyManager<User> {
final response = await client.httpHandler.executeSafe(request);
return parseApplicationRoleConnection(response.jsonBody as Map<String, Object?>);
}

Future<OAuth2Information> fetchCurrentOAuth2Information() async {
final route = HttpRoute()
..oauth2()
..add(HttpRoutePart('@me'));
final request = BasicRequest(route);
final response = await client.httpHandler.executeSafe(request);
final body = response.jsonBody as Map<String, Object?>;
return OAuth2Information(
application: PartialApplication(manager: client.applications, id: Snowflake.parse((body['application'] as Map<String, Object?>)['id']!)),
scopes: (body['scopes'] as List).cast(),
expiresOn: DateTime.parse(body['expires'] as String),
user: maybeParse(body['user'], client.users.parse));
}
}
111 changes: 61 additions & 50 deletions lib/src/models/guild/guild.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class PartialGuild extends WritableSnowflakeEntity<Guild> {
Future<ThreadList> listActiveThreads() => manager.listActiveThreads(id);

/// List the bans in this guild.
Future<List<Ban>> listBans() => manager.listBans(id);
Future<List<Ban>> listBans({int? limit, Snowflake? after, Snowflake? before}) => manager.listBans(id, limit: limit, after: after, before: before);

/// Ban a member in this guild.
Future<void> createBan(Snowflake userId, {Duration? deleteMessages, String? auditLogReason}) =>
Expand Down Expand Up @@ -197,39 +197,74 @@ class PartialGuild extends WritableSnowflakeEntity<Guild> {
Future<String?> fetchVanityCode() => manager.fetchVanityCode(id);
}

/// {@template guild}
/// A collection of channels & users.
///
/// Guilds are often referred to as servers.
/// {@endtemplate}
class Guild extends PartialGuild {
/// {@macro guild}
class UserGuild extends PartialGuild {
/// This guild's name.
final String name;

/// The hash of this guild's icon.
final String? iconHash;

/// Whether this guild is owned by the current user.
final bool? isOwnedByCurrentUser;

/// The current user's permissions.
final Permissions? currentUserPermissions;

/// A set of features enabled in this guild.
final GuildFeatures features;

/// An approximate number of members in this guild.
///
/// {@template fetch_with_counts_only}
/// This is only returned when fetching this guild with `withCounts` set to `true`.
/// {@endtemplate}
final int? approximateMemberCount;

/// An approximate number of presences in this guild.
///
/// {@macro fetch_with_counts_only}
final int? approximatePresenceCount;

/// {@macro guild}
/// @nodoc
UserGuild({
required super.id,
required super.manager,
required this.name,
required this.iconHash,
required this.isOwnedByCurrentUser,
required this.currentUserPermissions,
required this.features,
required this.approximateMemberCount,
required this.approximatePresenceCount,
});

/// This guild's icon.
CdnAsset? get icon => iconHash == null
? null
: CdnAsset(
client: manager.client,
base: HttpRoute()..icons(id: id.toString()),
hash: iconHash!,
);
}

/// {@template guild}
/// A collection of channels & users.
///
/// Guilds are often referred to as servers.
/// {@endtemplate}
class Guild extends UserGuild {
/// The hash of this guild's splash image.
final String? splashHash;

/// The hash of this guild's discovery splash image.
final String? discoverySplashHash;

/// Whether this guild is owned by the current user.
///
/// {@template get_current_user_guilds_only}
/// This field is only present when fetching the current user's guilds.
/// {@endtemplate}
final bool? isOwnedByCurrentUser;

/// The ID of this guild's owner.
final Snowflake ownerId;

/// The current user's permissions.
///
/// {@macro get_current_user_guilds_only}
final Permissions? currentUserPermissions;

/// The ID of this guild's AFK channel.
final Snowflake? afkChannelId;

Expand Down Expand Up @@ -259,9 +294,6 @@ class Guild extends PartialGuild {
// Renamed to avoid conflict with the emojis manager.
final List<Emoji> emojiList;

/// A set of features enabled in this guild.
final GuildFeatures features;

/// This guild's MFA level.
final MfaLevel mfaLevel;

Expand Down Expand Up @@ -310,18 +342,6 @@ class Guild extends PartialGuild {
/// The maximum number of users in a stage video channel.
final int? maxStageChannelUsers;

/// An approximate number of members in this guild.
///
/// {@template fetch_with_counts_only}
/// This is only returned when fetching this guild with `withCounts` set to `true`.
/// {@endtemplate}
final int? approximateMemberCount;

/// An approximate number of presences in this guild.
///
/// {@macro fetch_with_counts_only}
final int? approximatePresenceCount;

/// This guild's welcome screen.
final WelcomeScreen? welcomeScreen;

Expand All @@ -343,13 +363,13 @@ class Guild extends PartialGuild {
Guild({
required super.id,
required super.manager,
required this.name,
required this.iconHash,
required super.name,
required super.iconHash,
required this.splashHash,
required this.discoverySplashHash,
required this.isOwnedByCurrentUser,
required super.isOwnedByCurrentUser,
required this.ownerId,
required this.currentUserPermissions,
required super.currentUserPermissions,
required this.afkChannelId,
required this.afkTimeout,
required this.isWidgetEnabled,
Expand All @@ -358,7 +378,7 @@ class Guild extends PartialGuild {
required this.defaultMessageNotificationLevel,
required this.explicitContentFilterLevel,
required this.roleList,
required this.features,
required super.features,
required this.mfaLevel,
required this.applicationId,
required this.systemChannelId,
Expand All @@ -375,8 +395,8 @@ class Guild extends PartialGuild {
required this.publicUpdatesChannelId,
required this.maxVideoChannelUsers,
required this.maxStageChannelUsers,
required this.approximateMemberCount,
required this.approximatePresenceCount,
required super.approximateMemberCount,
required super.approximatePresenceCount,
required this.welcomeScreen,
required this.nsfwLevel,
required this.hasPremiumProgressBarEnabled,
Expand Down Expand Up @@ -413,15 +433,6 @@ class Guild extends PartialGuild {
/// The channel safety alerts are sent to.
PartialTextChannel? get safetyAlertsChannel => safetyAlertsChannelId == null ? null : manager.client.channels[safetyAlertsChannelId!] as PartialTextChannel;

/// This guild's icon.
CdnAsset? get icon => iconHash == null
? null
: CdnAsset(
client: manager.client,
base: HttpRoute()..icons(id: id.toString()),
hash: iconHash!,
);

/// This guild's splash image.
CdnAsset? get splash => splashHash == null
? null
Expand Down
18 changes: 18 additions & 0 deletions lib/src/models/oauth2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:nyxx/src/models/application.dart';
import 'package:nyxx/src/models/user/user.dart';

class OAuth2Information {
/// The current application.
final PartialApplication application;

/// The scopes the user has authorized the application for.
final List<String> scopes;

/// When the access token expires.
final DateTime expiresOn;

/// The user who has authorized, if the user has authorized with the `identify` scope.
final User? user;

OAuth2Information({required this.application, required this.scopes, required this.expiresOn, this.user});
}
3 changes: 1 addition & 2 deletions test/unit/builders/permission_overwrite_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import 'package:test/test.dart';

void main() {
test('PermissionOverwriteBuilder', () {
final builder = PermissionOverwriteBuilder(
id: Snowflake.zero, type: PermissionOverwriteType.member);
final builder = PermissionOverwriteBuilder(id: Snowflake.zero, type: PermissionOverwriteType.member);

expect(
builder.build(),
Expand Down
Loading

0 comments on commit 408fe03

Please sign in to comment.