From 5f1d57920794c66551d0ca47d5489c540b3333c4 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 20 May 2025 10:19:44 -0400 Subject: [PATCH] Add rules prompt when initially connecting to server --- lib/models/instance_info.dart | 8 + .../account_services.dart | 32 ++-- .../account_services.g.dart | 2 +- .../fediverse_instance_client_services.dart | 19 +++ .../fediverse_instance_client_services.g.dart | 140 ++++++++++++++++++ lib/screens/sign_in.dart | 38 ++++- 6 files changed, 228 insertions(+), 11 deletions(-) diff --git a/lib/models/instance_info.dart b/lib/models/instance_info.dart index 0a3b6de..e2a8403 100644 --- a/lib/models/instance_info.dart +++ b/lib/models/instance_info.dart @@ -48,6 +48,14 @@ class InstanceRules { return 'InstanceRules{rules: $rules}'; } + String toListString() { + if (rules.isEmpty) { + return ''; + } + + return rules.entries.map((e) => '${e.key}: ${e.value}').join('\n'); + } + @override bool operator ==(Object other) => identical(this, other) || diff --git a/lib/riverpod_controllers/account_services.dart b/lib/riverpod_controllers/account_services.dart index 6d0d6c3..0465bad 100644 --- a/lib/riverpod_controllers/account_services.dart +++ b/lib/riverpod_controllers/account_services.dart @@ -106,7 +106,7 @@ class CredentialSignin extends _$CredentialSignin { FutureResult signIn(bool activateProfileOnSuccess) async { final result = - await credentials.signIn(ref).andThenAsync((signedInCredentials) async { + await credentials.signIn(ref).andThenAsync((signedInCredentials) async { ref.read(statusServiceProvider.notifier).setStatus( 'Getting user profile from ${signedInCredentials.serverName}'); return await ref.read( @@ -134,6 +134,8 @@ class CredentialSignin extends _$CredentialSignin { ref.read(loggedOutProfilesProvider.notifier).remove(loginProfile); await ref.read(secretsServiceProvider).addOrUpdateProfile(loginProfile); await ref.read(connectionRepoInitProvider(loginProfile).future); + ref.read( + instanceRulesManagerProvider(loginProfile)); await ref .read(instanceInfoManagerProvider(loginProfile).notifier) .update(); @@ -176,10 +178,14 @@ class ProfileManager extends _$ProfileManager { .read(secretsServiceProvider) .addOrUpdateProfile(profile.copyWithLoginUpdate(false)); - if (ref.read(loggedInProfilesProvider).isNotEmpty) { + if (ref + .read(loggedInProfilesProvider) + .isNotEmpty) { ref.read(activeProfileProvider.notifier).setActiveProfile( - ref.read(loggedInProfilesProvider).first, - ); + ref + .read(loggedInProfilesProvider) + .first, + ); } if (withNotification) { @@ -221,7 +227,7 @@ class AccountServicesInitializer extends _$AccountServicesInitializer { } final pr = - await ref.read(profileManagerProvider(p).notifier).signIn(false); + await ref.read(profileManagerProvider(p).notifier).signIn(false); if (pr.isSuccess) { final profile = pr.value; if (profile.id.isNotEmpty && profile.id == lastActiveProfile) { @@ -232,14 +238,22 @@ class AccountServicesInitializer extends _$AccountServicesInitializer { } } - if (!ref.read(activeProfileProvider.notifier).hasActiveProfile && - ref.read(loggedInProfilesProvider).isNotEmpty) { - final firstProfile = ref.read(loggedOutProfilesProvider).first; + if (!ref + .read(activeProfileProvider.notifier) + .hasActiveProfile && + ref + .read(loggedInProfilesProvider) + .isNotEmpty) { + final firstProfile = ref + .read(loggedOutProfilesProvider) + .first; ref.read(activeProfileProvider.notifier).setActiveProfile(firstProfile); } return Result.ok( - ref.read(activeProfileProvider.notifier).hasActiveProfile); + ref + .read(activeProfileProvider.notifier) + .hasActiveProfile); }); initialized = true; diff --git a/lib/riverpod_controllers/account_services.g.dart b/lib/riverpod_controllers/account_services.g.dart index 8d5f805..0b906df 100644 --- a/lib/riverpod_controllers/account_services.g.dart +++ b/lib/riverpod_controllers/account_services.g.dart @@ -69,7 +69,7 @@ final activeProfileProvider = NotifierProvider.internal( ); typedef _$ActiveProfile = Notifier; -String _$credentialSigninHash() => r'e9731ad5b916838122a2a86961af4cfe4bef57b5'; +String _$credentialSigninHash() => r'9e82b7bcc5c1bc79fd18a2f65916669ed44760dd'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/riverpod_controllers/networking/fediverse_instance_client_services.dart b/lib/riverpod_controllers/networking/fediverse_instance_client_services.dart index f131565..46b1d9e 100644 --- a/lib/riverpod_controllers/networking/fediverse_instance_client_services.dart +++ b/lib/riverpod_controllers/networking/fediverse_instance_client_services.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; @@ -9,6 +11,7 @@ import '../../models/instance_info.dart'; import '../../serializers/mastodon/instance_info_mastodon_extensions.dart'; import '../rp_provider_extension.dart'; import 'friendica_client_services.dart'; +import 'network_services.dart'; part 'fediverse_instance_client_services.g.dart'; @@ -69,3 +72,19 @@ Future> instanceRules( .andThen((page) => instanceRulesfromJson(page.data)) .execErrorCastAsync(); } + +@riverpod +Future> instanceRulesByServerName( + Ref ref, + String serverName, +) async { + _logger.finest(() => 'Getting $serverName rules info'); + final url = Uri.parse('https://$serverName/api/v1/instance/rules'); + final headers = ref.read(userAgentHeaderProvider); + final request = NetworkRequest(url, headers: headers); + return await ref + .read(httpGetProvider(request).future) + .transform((response) => response.map((data) => jsonDecode(data))) + .andThen((page) => instanceRulesfromJson(page.data)) + .execErrorCastAsync(); +} diff --git a/lib/riverpod_controllers/networking/fediverse_instance_client_services.g.dart b/lib/riverpod_controllers/networking/fediverse_instance_client_services.g.dart index 6e6ec94..808d6a7 100644 --- a/lib/riverpod_controllers/networking/fediverse_instance_client_services.g.dart +++ b/lib/riverpod_controllers/networking/fediverse_instance_client_services.g.dart @@ -573,5 +573,145 @@ class _InstanceRulesProviderElement @override Profile get profile => (origin as InstanceRulesProvider).profile; } + +String _$instanceRulesByServerNameHash() => + r'54fadb3a1ee5527bde3ae8e9767b3fffe1fe0d77'; + +/// See also [instanceRulesByServerName]. +@ProviderFor(instanceRulesByServerName) +const instanceRulesByServerNameProvider = InstanceRulesByServerNameFamily(); + +/// See also [instanceRulesByServerName]. +class InstanceRulesByServerNameFamily + extends Family>> { + /// See also [instanceRulesByServerName]. + const InstanceRulesByServerNameFamily(); + + /// See also [instanceRulesByServerName]. + InstanceRulesByServerNameProvider call( + String serverName, + ) { + return InstanceRulesByServerNameProvider( + serverName, + ); + } + + @override + InstanceRulesByServerNameProvider getProviderOverride( + covariant InstanceRulesByServerNameProvider provider, + ) { + return call( + provider.serverName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'instanceRulesByServerNameProvider'; +} + +/// See also [instanceRulesByServerName]. +class InstanceRulesByServerNameProvider + extends AutoDisposeFutureProvider> { + /// See also [instanceRulesByServerName]. + InstanceRulesByServerNameProvider( + String serverName, + ) : this._internal( + (ref) => instanceRulesByServerName( + ref as InstanceRulesByServerNameRef, + serverName, + ), + from: instanceRulesByServerNameProvider, + name: r'instanceRulesByServerNameProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$instanceRulesByServerNameHash, + dependencies: InstanceRulesByServerNameFamily._dependencies, + allTransitiveDependencies: + InstanceRulesByServerNameFamily._allTransitiveDependencies, + serverName: serverName, + ); + + InstanceRulesByServerNameProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.serverName, + }) : super.internal(); + + final String serverName; + + @override + Override overrideWith( + FutureOr> Function( + InstanceRulesByServerNameRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: InstanceRulesByServerNameProvider._internal( + (ref) => create(ref as InstanceRulesByServerNameRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + serverName: serverName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> + createElement() { + return _InstanceRulesByServerNameProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is InstanceRulesByServerNameProvider && + other.serverName == serverName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, serverName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin InstanceRulesByServerNameRef + on AutoDisposeFutureProviderRef> { + /// The parameter `serverName` of this provider. + String get serverName; +} + +class _InstanceRulesByServerNameProviderElement + extends AutoDisposeFutureProviderElement> + with InstanceRulesByServerNameRef { + _InstanceRulesByServerNameProviderElement(super.provider); + + @override + String get serverName => + (origin as InstanceRulesByServerNameProvider).serverName; +} // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/sign_in.dart b/lib/screens/sign_in.dart index f9dc0a1..b515f79 100644 --- a/lib/screens/sign_in.dart +++ b/lib/screens/sign_in.dart @@ -11,7 +11,10 @@ import '../models/auth/basic_credentials.dart'; import '../models/auth/credentials_intf.dart'; import '../models/auth/oauth_credentials.dart'; import '../models/auth/profile.dart'; +import '../models/instance_info.dart'; import '../riverpod_controllers/account_services.dart'; +import '../riverpod_controllers/instance_info_services.dart'; +import '../riverpod_controllers/networking/fediverse_instance_client_services.dart'; import '../routes.dart'; import '../utils/snackbar_builder.dart'; @@ -276,6 +279,21 @@ class _SignInScreenState extends ConsumerState { child: const Text('Signin'), ) : const SizedBox(), + if (!signInButtonEnabled && existingProfile != null) + ElevatedButton( + onPressed: () async { + final rules = ref.read( + instanceRulesManagerProvider(existingProfile!)); + if (context.mounted) { + await showConfirmDialog( + context, + rules.rules.isEmpty + ? "Your server doesn't publish a list of rules we have access to programmatically." + : rules.toListString()); + } + }, + child: const Text('Show Rules'), + ), const VerticalPadding(), Text( 'Logged out:', @@ -443,7 +461,24 @@ class _SignInScreenState extends ConsumerState { return; } - buildSnackbar(context, 'Attempting to sign in account...'); + final rulesResult = await ref.read( + instanceRulesByServerNameProvider(serverNameController.text).future); + final rules = rulesResult.getValueOrElse(() => const InstanceRules()); + final rulesPrompt = + 'Do you agree to abide by the rules of the server you are connecting to as you did when you created the account?\n ${rules.toListString()}'; + final confirmRules = await showYesNoDialog(context, rulesPrompt); + if (confirmRules != true) { + if (context.mounted) { + buildSnackbar(context, + 'Cannot connect to a server unless you reaffirm you will follow the listed rules'); + } + return; + } + + if (context.mounted) { + buildSnackbar(context, 'Attempting to sign in account...'); + } + final result = await ref.read(credentialSigninProvider(creds).notifier).signIn(true); @@ -456,6 +491,7 @@ class _SignInScreenState extends ConsumerState { buildSnackbar(context, 'Account signed in...'); } ref.read(activeProfileProvider.notifier).setActiveProfile(result.value); + ref.read(instanceRulesManagerProvider(result.value)); if (context.mounted) { context.goNamed(ScreenPaths.timelines); }