Refactored auth system to support multiple logins (hypothetically)

codemagic-setup
Hank Grabowski 2023-02-26 22:12:40 -05:00
rodzic d2f5c347bc
commit a6bf00aea4
23 zmienionych plików z 490 dodań i 305 usunięć

Wyświetl plik

@ -1,5 +1,4 @@
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'data/interfaces/connections_repo_intf.dart';
import 'data/interfaces/groups_repo.intf.dart';
@ -9,7 +8,6 @@ import 'data/objectbox/objectbox_cache.dart';
import 'data/objectbox/objectbox_connections_repo.dart';
import 'data/objectbox/objectbox_hashtag_repo.dart';
import 'globals.dart';
import 'models/TimelineIdentifiers.dart';
import 'services/auth_service.dart';
import 'services/connections_manager.dart';
import 'services/direct_message_service.dart';
@ -27,15 +25,24 @@ import 'services/timeline_manager.dart';
final _logger = Logger('DI_Init');
Future<void> dependencyInjectionInitialization() async {
final authService = AccountsService();
final secretsService = SecretsService();
final entryManagerService = EntryManagerService();
final timelineManager = TimelineManager();
final galleryService = GalleryService();
final service = SettingsService();
await service.initialize();
getIt.registerSingleton<SettingsService>(service);
final settingsService = SettingsService();
await settingsService.initialize();
getIt.registerSingleton<SettingsService>(settingsService);
getIt.registerLazySingleton<NetworkStatusService>(
() => NetworkStatusService());
final secretsService = SecretsService();
final serviceInit = await secretsService.initialize();
final authService = AccountsService(secretsService);
if (serviceInit.isFailure) {
_logger.severe('Error initializing credentials');
} else {
await authService.initialize();
}
final objectBoxCache = await ObjectBoxCache.create();
getIt.registerSingleton<ObjectBoxCache>(objectBoxCache);
@ -46,7 +53,6 @@ Future<void> dependencyInjectionInitialization() async {
getIt.registerLazySingleton<HashtagService>(() => HashtagService());
getIt.registerSingleton(galleryService);
getIt.registerSingleton<EntryManagerService>(entryManagerService);
getIt.registerSingleton<SecretsService>(secretsService);
getIt.registerSingleton<AccountsService>(authService);
getIt.registerSingleton<TimelineManager>(timelineManager);
getIt.registerLazySingleton<MediaUploadAttachmentHelper>(
@ -55,24 +61,5 @@ Future<void> dependencyInjectionInitialization() async {
() => NotificationsManager());
getIt.registerLazySingleton<DirectMessageService>(
() => DirectMessageService());
getIt.registerLazySingleton<NetworkStatusService>(
() => NetworkStatusService());
getIt.registerLazySingleton<InteractionsManager>(() => InteractionsManager());
await secretsService.initialize().andThenSuccessAsync((credentials) async {
if (credentials.isEmpty) {
return;
}
final wasLoggedIn = await authService.getStoredLoginState();
if (wasLoggedIn) {
final result = await authService.signIn(credentials);
if (result.isSuccess) {
timelineManager.updateTimeline(
TimelineIdentifiers.home(), TimelineRefreshType.loadOlder);
}
} else {
_logger.severe('Was not logged in');
}
});
}

Wyświetl plik

@ -9,7 +9,7 @@ import 'package:result_monad/result_monad.dart';
import '../friendica_client/paged_response.dart';
import '../globals.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/auth/credentials.dart';
import '../models/auth/profile.dart';
import '../models/connection.dart';
import '../models/direct_message.dart';
import '../models/exec_error.dart';
@ -309,7 +309,7 @@ class RelationshipsClient extends FriendicaClient {
PagingData page) async {
_logger.finest(() => 'Getting following with paging data $page');
_networkStatusService.startConnectionUpdateStatus();
final myId = credentials.userId;
final myId = profile.userId;
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
final result = await _getApiListRequest(
Uri.parse('$baseUrl/following?${page.toQueryParameters()}'),
@ -326,7 +326,7 @@ class RelationshipsClient extends FriendicaClient {
PagingData page) async {
_logger.finest(() => 'Getting followers data with page data $page');
_networkStatusService.startConnectionUpdateStatus();
final myId = credentials.userId;
final myId = profile.userId;
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
final result1 = await _getApiListRequest(
Uri.parse('$baseUrl/followers&${page.toQueryParameters()}'),
@ -344,7 +344,7 @@ class RelationshipsClient extends FriendicaClient {
Connection connection) async {
_logger.finest(() => 'Getting group (Mastodon List) data');
_networkStatusService.startConnectionUpdateStatus();
final myId = credentials.userId;
final myId = profile.userId;
final id = int.parse(connection.id);
final paging = '?min_id=${id - 1}&max_id=${id + 1}';
final baseUrl = 'https://$serverName/api/v1/accounts/$myId';
@ -491,7 +491,7 @@ class RemoteFileClient extends FriendicaClient {
final response = await http.get(
url,
headers: {
'Authorization': _credentials.authHeaderValue,
'Authorization': _profile.credentials.authHeaderValue,
},
);
@ -515,7 +515,7 @@ class RemoteFileClient extends FriendicaClient {
}) async {
final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
final request = http.MultipartRequest('POST', postUri);
request.headers['Authorization'] = _credentials.authHeaderValue;
request.headers['Authorization'] = _profile.credentials.authHeaderValue;
request.fields['desc'] = description;
request.fields['album'] = album;
request.files.add(await http.MultipartFile.fromBytes(
@ -721,7 +721,7 @@ class TimelineClient extends FriendicaClient {
case TimelineType.profile:
return '/accounts/${type.auxData}/statuses';
case TimelineType.self:
final myId = credentials.userId;
final myId = profile.userId;
return '/accounts/$myId/statuses';
}
}
@ -743,15 +743,15 @@ class TimelineClient extends FriendicaClient {
abstract class FriendicaClient {
static final _logger = Logger('$FriendicaClient');
final Credentials _credentials;
final Profile _profile;
late final NetworkStatusService _networkStatusService;
String get serverName => _credentials.serverName;
String get serverName => _profile.serverName;
Credentials get credentials => _credentials;
Profile get profile => _profile;
FriendicaClient(Credentials credentials) : _credentials = credentials {
FriendicaClient(Profile credentials) : _profile = credentials {
_networkStatusService = getIt<NetworkStatusService>();
}
@ -761,7 +761,7 @@ abstract class FriendicaClient {
final response = await http.get(
url,
headers: {
'Authorization': _credentials.authHeaderValue,
'Authorization': _profile.credentials.authHeaderValue,
'Content-Type': 'application/json; charset=UTF-8'
},
);
@ -788,7 +788,7 @@ abstract class FriendicaClient {
final response = await http.post(
url,
headers: {
'Authorization': _credentials.authHeaderValue,
'Authorization': _profile.credentials.authHeaderValue,
'Content-Type': 'application/json; charset=UTF-8'
},
body: jsonEncode(body),
@ -813,7 +813,7 @@ abstract class FriendicaClient {
final response = await http.delete(
url,
headers: {
'Authorization': _credentials.authHeaderValue,
'Authorization': _profile.credentials.authHeaderValue,
'Content-Type': 'application/json; charset=UTF-8'
},
body: jsonEncode(body),

Wyświetl plik

@ -0,0 +1,75 @@
import 'dart:convert';
import 'package:relatica/models/auth/credentials_intf.dart';
import 'package:relatica/models/exec_error.dart';
import 'package:result_monad/src/result_monad_base.dart';
import 'package:uuid/uuid.dart';
class BasicCredentials extends ICredentials {
late final String id;
final String username;
final String password;
final String serverName;
late final String _authHeaderValue;
@override
String get authHeaderValue => _authHeaderValue;
BasicCredentials({
String? id,
required this.username,
required this.password,
required this.serverName,
}) {
this.id = id ?? const Uuid().v4();
final authenticationString = "$username:$password";
final encodedAuthString = base64Encode(utf8.encode(authenticationString));
_authHeaderValue = "Basic $encodedAuthString";
}
factory BasicCredentials.fromJson(Map<String, dynamic> json) =>
BasicCredentials(
username: json['username'],
password: json['password'],
serverName: json['serverName'],
);
factory BasicCredentials.empty() => BasicCredentials(
username: '',
password: '',
serverName: '',
);
bool get isEmpty =>
username.isEmpty && password.isEmpty && serverName.isEmpty;
BasicCredentials copy({
String? username,
String? password,
String? serverName,
bool? loggedIn,
}) {
return BasicCredentials(
username: username ?? this.username,
password: password ?? this.password,
serverName: serverName ?? this.serverName,
);
}
@override
String toString() {
return 'Credentials{username: $username, password?: ${password.isNotEmpty}, serverName: $serverName}';
}
@override
FutureResult<ICredentials, ExecError> signIn() async {
return Result.ok(this);
}
@override
Map<String, dynamic> toJson() => {
'username': username,
'password': password,
'serverName': serverName,
};
}

Wyświetl plik

@ -1,62 +0,0 @@
import 'dart:convert';
class Credentials {
final String username;
final String password;
final String serverName;
final String userId;
final String avatar;
late final String _authHeaderValue;
String get authHeaderValue => _authHeaderValue;
Credentials({
required this.username,
required this.password,
required this.serverName,
required this.userId,
required this.avatar,
}) {
final authenticationString = "$username:$password";
final encodedAuthString = base64Encode(utf8.encode(authenticationString));
_authHeaderValue = "Basic $encodedAuthString";
}
factory Credentials.empty() => Credentials(
username: '',
password: '',
serverName: '',
userId: '',
avatar: '',
);
bool get isEmpty =>
username.isEmpty &&
password.isEmpty &&
serverName.isEmpty &&
userId.isEmpty &&
avatar.isEmpty;
String get handle => '$username@$serverName';
Credentials copy({
String? username,
String? password,
String? serverName,
String? userId,
String? avatar,
}) {
return Credentials(
username: username ?? this.username,
password: password ?? this.password,
serverName: serverName ?? this.serverName,
userId: userId ?? this.userId,
avatar: avatar ?? this.avatar,
);
}
@override
String toString() {
return 'Credentials{username: $username, password?: ${password.isNotEmpty}, serverName: $serverName, userId: $userId}';
}
}

Wyświetl plik

@ -0,0 +1,15 @@
import 'package:result_monad/result_monad.dart';
import '../exec_error.dart';
abstract class ICredentials {
String get authHeaderValue;
String get serverName;
String get id;
FutureResult<ICredentials, ExecError> signIn();
Map<String, dynamic> toJson();
}

Wyświetl plik

@ -0,0 +1,78 @@
import 'package:uuid/uuid.dart';
import 'credentials_intf.dart';
class Profile {
final ICredentials credentials;
final String username;
final String userId;
final String avatar;
final String serverName;
final String id;
final bool loggedIn;
String get handle => '$username@$serverName';
Profile({
String? id,
required this.credentials,
required this.username,
required this.userId,
required this.avatar,
required this.serverName,
required this.loggedIn,
}) : id = id ?? const Uuid().v4();
factory Profile.credentialsOnly(ICredentials credentials) => Profile(
credentials: credentials,
username: '',
userId: '',
avatar: '',
serverName: credentials.serverName,
loggedIn: false,
);
factory Profile.fromJson(
Map<String, dynamic> json,
ICredentials Function(Map<String, dynamic> json) credentialsFromJson,
) {
final credentials = credentialsFromJson(json['credentials']);
return Profile(
credentials: credentials,
username: json['username'],
userId: json['userId'],
avatar: json['avatar'],
serverName: json['serverName'],
loggedIn: json['loggedIn'],
id: json['id'],
);
}
Profile copyWithLoginUpdate(bool status) => Profile(
credentials: credentials,
username: username,
userId: userId,
avatar: avatar,
serverName: serverName,
id: id,
loggedIn: status,
);
Map<String, dynamic> toJson() => {
'credentials': credentials.toJson(),
'username': username,
'userId': userId,
'avatar': avatar,
'serverName': serverName,
'loggedIn': loggedIn,
'id': id,
};
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Profile && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}

Wyświetl plik

@ -46,7 +46,7 @@ class _ImageViewerScreenState extends State<ImageViewerScreen> {
final appsDir = await getApplicationDocumentsDirectory();
final filename = p.basename(attachment.fullFileUri.path);
final bytesResult =
await RemoteFileClient(getIt<AccountsService>().currentCredentials)
await RemoteFileClient(getIt<AccountsService>().currentProfile)
.getFileBytes(attachment.uri);
if (bytesResult.isFailure && mounted) {
buildSnackbar(context,

Wyświetl plik

@ -5,7 +5,6 @@ import '../controls/app_bottom_nav_bar.dart';
import '../data/interfaces/connections_repo_intf.dart';
import '../globals.dart';
import '../routes.dart';
import '../services/auth_service.dart';
import '../services/notifications_manager.dart';
import '../services/timeline_manager.dart';
@ -31,12 +30,6 @@ class MenusScreen extends StatelessWidget {
getIt<NotificationsManager>().clear();
}
}),
buildMenuButton('Logout', () async {
final confirm = await showYesNoDialog(context, 'Log out account?');
if (confirm == true) {
await getIt<AccountsService>().signOut();
}
}),
];
return Scaffold(
body: Center(

Wyświetl plik

@ -47,9 +47,8 @@ class _MessageThreadScreenState extends State<MessageThreadScreen> {
) {
return result.fold(
onSuccess: (thread) {
final yourId = getIt<AccountsService>().currentCredentials.userId;
final yourAvatarUrl =
getIt<AccountsService>().currentCredentials.avatar;
final yourId = getIt<AccountsService>().currentProfile.userId;
final yourAvatarUrl = getIt<AccountsService>().currentProfile.avatar;
final participants =
Map.fromEntries(thread.participants.map((p) => MapEntry(p.id, p)));
return Center(

Wyświetl plik

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:relatica/controls/padding.dart';
import '../controls/standard_appbar.dart';
import '../globals.dart';
import '../services/auth_service.dart';
class ProfileScreen extends StatefulWidget {
@ -15,13 +17,24 @@ class _ProfileScreenState extends State<ProfileScreen> {
@override
Widget build(BuildContext context) {
final authService = context.watch<AccountsService>();
final profile = authService.currentProfile;
return Scaffold(
appBar: StandardAppBar.build(context, 'Profile'),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Profile: ${authService.currentCredentials.handle}'),
Text('Profile: ${profile.handle}'),
const VerticalPadding(),
ElevatedButton(
onPressed: () async {
final confirm =
await showYesNoDialog(context, 'Log out account?');
if (confirm == true) {
await getIt<AccountsService>().signOut(profile);
}
},
child: const Text('Logout'))
],
),
),

Wyświetl plik

@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:relatica/models/auth/basic_credentials.dart';
import 'package:string_validator/string_validator.dart';
import '../controls/padding.dart';
import '../globals.dart';
import '../models/auth/credentials.dart';
import '../services/auth_service.dart';
import '../services/secrets_service.dart';
import '../utils/snackbar_builder.dart';
class SignInScreen extends StatefulWidget {
@ -23,15 +22,14 @@ class _SignInScreenState extends State<SignInScreen> {
@override
void initState() {
super.initState();
getIt<SecretsService>().credentials.andThenSuccess((credentials) {
if (credentials.isEmpty) {
return;
}
usernameController.text = credentials.username;
passwordController.text = credentials.password;
serverNameController.text = credentials.serverName;
});
final profiles = getIt<AccountsService>().loggedOutProfiles;
if (profiles.isEmpty) {
return;
}
final credentials = profiles.first.credentials as BasicCredentials;
usernameController.text = credentials.username;
passwordController.text = credentials.password;
serverNameController.text = credentials.serverName;
}
@override
@ -56,14 +54,14 @@ class _SignInScreenState extends State<SignInScreen> {
isFQDN(value ?? '') ? null : 'Not a valid server name',
decoration: InputDecoration(
prefixIcon: const Icon(Icons.alternate_email),
hintText: 'Username (user@example.com)',
hintText: 'Server Name (friendica.example.com)',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).backgroundColor,
),
borderRadius: BorderRadius.circular(5.0),
),
labelText: 'Username',
labelText: 'Server Name',
),
),
const VerticalPadding(),
@ -139,12 +137,10 @@ class _SignInScreenState extends State<SignInScreen> {
void _signIn(BuildContext context) async {
if (formKey.currentState?.validate() ?? false) {
final creds = Credentials(
final creds = BasicCredentials(
username: usernameController.text,
password: passwordController.text,
serverName: serverNameController.text,
userId: '',
avatar: '');
serverName: serverNameController.text);
final result = await getIt<AccountsService>().signIn(creds);
if (result.isFailure) {

Wyświetl plik

@ -43,7 +43,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
final manager = context.watch<ConnectionsManager>();
final body = manager.getById(widget.userId).fold(onSuccess: (profile) {
final notMyProfile =
getIt<AccountsService>().currentCredentials.userId != profile.id;
getIt<AccountsService>().currentProfile.userId != profile.id;
return RefreshIndicator(
onRefresh: () async {

Wyświetl plik

@ -41,14 +41,14 @@ extension DirectMessageFriendicaExtension on DirectMessage {
final String parentUri = json['friendica_parent_uri'];
final cm = getIt<ConnectionsManager>();
if (getIt<AccountsService>().currentCredentials.userId != senderId) {
if (getIt<AccountsService>().currentProfile.userId != senderId) {
final s = ConnectionFriendicaExtensions.fromJson(json['sender']);
if (cm.getById(s.id).isFailure) {
cm.addConnection(s);
}
}
if (getIt<AccountsService>().currentCredentials.userId != recipientId) {
if (getIt<AccountsService>().currentProfile.userId != recipientId) {
final r = ConnectionFriendicaExtensions.fromJson(json['recipient']);
if (cm.getById(r.id).isFailure) {
cm.addConnection(r);

Wyświetl plik

@ -20,7 +20,7 @@ extension ConnectionMastodonExtensions on Connection {
if (handleFromJson.contains('@')) {
handle = handleFromJson;
} else {
final server = getIt<AccountsService>().currentCredentials.serverName;
final server = getIt<AccountsService>().currentProfile.serverName;
handle = '$handleFromJson@$server';
}

Wyświetl plik

@ -1,64 +1,145 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:relatica/services/secrets_service.dart';
import 'package:result_monad/result_monad.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../friendica_client/friendica_client.dart';
import '../globals.dart';
import '../models/auth/credentials.dart';
import '../models/auth/credentials_intf.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
import 'secrets_service.dart';
import 'timeline_manager.dart';
class AccountsService extends ChangeNotifier {
Credentials? _currentCredentials;
bool _loggedIn = false;
static final _logger = Logger('$AccountsService');
Profile? _currentProfile;
final _loggedInProfiles = <Profile>{};
final _loggedOutProfiles = <Profile>{};
bool get loggedIn => _loggedIn && _currentCredentials != null;
final SecretsService secretsService;
Credentials get currentCredentials => _currentCredentials!;
AccountsService(this.secretsService);
Future<bool> getStoredLoginState() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('logged-in') ?? false;
bool get loggedIn => _currentProfile != null;
List<Profile> get loggedInProfiles => UnmodifiableListView(_loggedInProfiles);
List<Profile> get loggedOutProfiles =>
UnmodifiableListView(_loggedOutProfiles);
Profile get currentProfile => _currentProfile!;
FutureResult<bool, ExecError> initialize() async {
final lastActiveProfile = await _getStoredLoginState();
final result = await runCatchingAsync(() async {
for (final p in secretsService.profiles) {
if (!p.loggedIn) {
_loggedOutProfiles.add(p);
}
final pr = await signIn(p.credentials, withNotification: false);
if (pr.isSuccess) {
final profile = pr.value;
if (profile.id.isNotEmpty && profile.id == lastActiveProfile) {
_currentProfile = profile;
}
}
}
if (_currentProfile == null && _loggedInProfiles.isNotEmpty) {
setActiveProfile(_loggedInProfiles.first);
}
return Result.ok(loggedIn);
});
return result.execErrorCast();
}
FutureResult<FriendicaClient, ExecError> signIn(
Credentials credentials) async {
final client = ProfileClient(credentials);
final result = await client.getMyProfile();
FutureResult<Profile, ExecError> signIn(ICredentials credentials,
{bool withNotification = true}) async {
ICredentials? credentialsCache;
final result =
await credentials.signIn().andThenAsync((signedInCredentials) async {
final client =
ProfileClient(Profile.credentialsOnly(signedInCredentials));
credentialsCache = signedInCredentials;
return await client.getMyProfile();
}).andThenAsync((profileData) async {
final loginProfile = Profile(
credentials: credentialsCache!,
username: profileData.name,
serverName: credentialsCache!.serverName,
avatar: profileData.avatarUrl,
userId: profileData.id,
id: credentials.id,
loggedIn: true,
);
if (_loggedInProfiles.isEmpty) {
setActiveProfile(loginProfile, withNotification: withNotification);
}
_loggedInProfiles.add(loginProfile);
_loggedOutProfiles.remove(loginProfile);
secretsService.addOrUpdateProfile(loginProfile);
if (withNotification) {
notifyListeners();
}
return Result.ok(loginProfile);
});
if (result.isFailure) {
await clearCredentials();
return result.errorCast();
_logger.severe('Error signing in: ${result.error}');
}
getIt<SecretsService>().storeCredentials(
client.credentials.copy(
userId: result.value.id,
avatar: result.value.avatarUrl,
),
);
await _setLoginState(true);
_currentCredentials = credentials;
notifyListeners();
return Result.ok(client);
return result.execErrorCast();
}
Future signOut() async {
await _setLoginState(false);
getIt<TimelineManager>().clear();
_currentCredentials = null;
notifyListeners();
Future signOut(Profile profile, {bool withNotification = true}) async {
if (_currentProfile == profile) {
clearActiveProfile(withNotification: withNotification);
}
_loggedInProfiles.remove(profile);
secretsService.addOrUpdateProfile(profile);
if (_loggedInProfiles.isNotEmpty) {
setActiveProfile(
_loggedInProfiles.first,
withNotification: withNotification,
);
}
if (withNotification) {
notifyListeners();
}
}
Future clearCredentials() async {
_currentCredentials = null;
await _setLoginState(false);
notifyListeners();
Future clearActiveProfile({bool withNotification = true}) async {
_currentProfile = null;
if (withNotification) {
notifyListeners();
}
await _saveStoredLoginState();
}
Future<void> _setLoginState(bool state) async {
Future<void> setActiveProfile(Profile profile,
{bool withNotification = true}) async {
_currentProfile = profile;
if (withNotification) {
notifyListeners();
}
await _saveStoredLoginState();
}
Future<void> _saveStoredLoginState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('logged-in', state);
_loggedIn = state;
await prefs.setString('active_profile_id', _currentProfile?.id ?? '');
}
Future<String> _getStoredLoginState() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('active_profile_id') ?? '';
}
}

Wyświetl plik

@ -44,7 +44,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> acceptFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.acceptFollow(connection)
.match(
onSuccess: (update) {
@ -62,7 +62,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> rejectFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.rejectFollow(connection)
.match(
onSuccess: (update) {
@ -80,7 +80,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> ignoreFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.ignoreFollow(connection)
.match(
onSuccess: (update) {
@ -98,7 +98,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> follow(Connection connection) async {
_logger.finest(
'Attempting to follow ${connection.name}: ${connection.status}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.followConnection(connection)
.match(
onSuccess: (update) {
@ -116,7 +116,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> unfollow(Connection connection) async {
_logger.finest(
'Attempting to unfollow ${connection.name}: ${connection.status}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.unFollowConnection(connection)
.match(
onSuccess: (update) {
@ -137,8 +137,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> updateAllContacts() async {
_logger.fine('Updating all contacts');
final client =
RelationshipsClient(getIt<AccountsService>().currentCredentials);
final client = RelationshipsClient(getIt<AccountsService>().currentProfile);
final results = <String, Connection>{};
var moreResults = true;
var maxId = -1;
@ -215,9 +214,8 @@ class ConnectionsManager extends ChangeNotifier {
FutureResult<bool, ExecError> addUserToGroup(
GroupData group, Connection connection) async {
_logger.finest('Adding ${connection.name} to group: ${group.name}');
final result =
await GroupsClient(getIt<AccountsService>().currentCredentials)
.addConnectionToGroup(group, connection);
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
.addConnectionToGroup(group, connection);
result.match(
onSuccess: (_) => _refreshGroupListData(connection.id, true),
onError: (error) {
@ -232,9 +230,8 @@ class ConnectionsManager extends ChangeNotifier {
FutureResult<bool, ExecError> removeUserFromGroup(
GroupData group, Connection connection) async {
_logger.finest('Removing ${connection.name} from group: ${group.name}');
final result =
await GroupsClient(getIt<AccountsService>().currentCredentials)
.removeConnectionFromGroup(group, connection);
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
.removeConnectionFromGroup(group, connection);
result.match(
onSuccess: (_) => _refreshGroupListData(connection.id, true),
onError: (error) {
@ -282,7 +279,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> _refreshGroupListData(String id, bool withNotification) async {
_logger.finest('Refreshing member list data for Connection $id');
await GroupsClient(getIt<AccountsService>().currentCredentials)
await GroupsClient(getIt<AccountsService>().currentProfile)
.getMemberGroupsForConnection(id)
.match(
onSuccess: (groups) {
@ -300,7 +297,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> _refreshConnection(
Connection connection, bool withNotification) async {
_logger.finest('Refreshing connection data for ${connection.name}');
await RelationshipsClient(getIt<AccountsService>().currentCredentials)
await RelationshipsClient(getIt<AccountsService>().currentProfile)
.getConnectionWithStatus(connection)
.match(
onSuccess: (update) {
@ -317,7 +314,7 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> _updateMyGroups(bool withNotification) async {
_logger.finest('Refreshing my groups list');
await GroupsClient(getIt<AccountsService>().currentCredentials)
await GroupsClient(getIt<AccountsService>().currentProfile)
.getGroups()
.match(
onSuccess: (groups) {

Wyświetl plik

@ -37,7 +37,7 @@ class DirectMessageService extends ChangeNotifier {
}
Future<void> updateThreads() async {
await DirectMessagingClient(getIt<AccountsService>().currentCredentials)
await DirectMessagingClient(getIt<AccountsService>().currentProfile)
.getDirectMessages(PagingData())
.match(
onSuccess: (update) {
@ -62,7 +62,7 @@ class DirectMessageService extends ChangeNotifier {
FutureResult<DirectMessage, ExecError> newThread(
Connection receiver, String text) async {
final result =
await DirectMessagingClient(getIt<AccountsService>().currentCredentials)
await DirectMessagingClient(getIt<AccountsService>().currentProfile)
.postDirectMessage(
null,
receiver.id,
@ -102,7 +102,7 @@ class DirectMessageService extends ChangeNotifier {
}
final result =
await DirectMessagingClient(getIt<AccountsService>().currentCredentials)
await DirectMessagingClient(getIt<AccountsService>().currentProfile)
.postDirectMessage(
original.id,
original.senderId,
@ -130,7 +130,7 @@ class DirectMessageService extends ChangeNotifier {
return;
}
await DirectMessagingClient(getIt<AccountsService>().currentCredentials)
await DirectMessagingClient(getIt<AccountsService>().currentProfile)
.markDirectMessageRead(m)
.match(
onSuccess: (update) {

Wyświetl plik

@ -43,7 +43,7 @@ class EntryManagerService extends ChangeNotifier {
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
_logger.finest('Getting post: $id');
final currentId = getIt<AccountsService>().currentCredentials.userId;
final currentId = getIt<AccountsService>().currentProfile.userId;
final postNode = _getPostRootNode(id);
if (postNode == null) {
return Result.error(ExecError(
@ -69,8 +69,8 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<bool, ExecError> deleteEntryById(String id) async {
_logger.finest('Delete entry: $id');
final result =
await StatusesClient(getIt<AccountsService>().currentCredentials)
.deleteEntryById(id);
await StatusesClient(getIt<AccountsService>().currentProfile)
.deleteEntryById(id);
if (result.isFailure) {
return result.errorCast();
}
@ -80,8 +80,7 @@ class EntryManagerService extends ChangeNotifier {
return Result.ok(true);
}
FutureResult<bool, ExecError> createNewStatus(
String text, {
FutureResult<bool, ExecError> createNewStatus(String text, {
String spoilerText = '',
String inReplyToId = '',
required NewEntryMediaItems mediaItems,
@ -109,12 +108,12 @@ class EntryManagerService extends ChangeNotifier {
}
final uploadResult =
await MediaUploadAttachmentHelper.getUploadableImageBytes(
await MediaUploadAttachmentHelper.getUploadableImageBytes(
item.localFilePath,
).andThenAsync(
(imageBytes) async =>
await RemoteFileClient(getIt<AccountsService>().currentCredentials)
.uploadFileAsAttachment(
(imageBytes) async =>
await RemoteFileClient(getIt<AccountsService>().currentProfile)
.uploadFileAsAttachment(
bytes: imageBytes,
album: mediaItems.albumName,
description: item.description,
@ -131,15 +130,15 @@ class EntryManagerService extends ChangeNotifier {
}
final result =
await StatusesClient(getIt<AccountsService>().currentCredentials)
.createNewStatus(
text: text,
spoilerText: spoilerText,
inReplyToId: inReplyToId,
mediaIds: mediaIds)
.andThenSuccessAsync((item) async {
await StatusesClient(getIt<AccountsService>().currentProfile)
.createNewStatus(
text: text,
spoilerText: spoilerText,
inReplyToId: inReplyToId,
mediaIds: mediaIds)
.andThenSuccessAsync((item) async {
await processNewItems(
[item], getIt<AccountsService>().currentCredentials.username, null);
[item], getIt<AccountsService>().currentProfile.username, null);
return item;
}).andThenSuccessAsync((item) async {
if (inReplyToId.isNotEmpty) {
@ -158,7 +157,7 @@ class EntryManagerService extends ChangeNotifier {
_logger.finest('${status.id} status created');
return true;
}).mapError(
(error) {
(error) {
_logger.finest('Error creating post: $error');
return ExecError(
type: ErrorType.localError,
@ -171,7 +170,7 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<List<EntryTreeItem>, ExecError> updateTimeline(
TimelineIdentifiers type, int maxId, int sinceId) async {
_logger.fine(() => 'Updating timeline');
final client = TimelineClient(getIt<AccountsService>().currentCredentials);
final client = TimelineClient(getIt<AccountsService>().currentProfile);
final itemsResult = await client.getTimeline(
type: type,
page: PagingData(
@ -186,9 +185,11 @@ class EntryManagerService extends ChangeNotifier {
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
final updatedPosts = await processNewItems(
itemsResult.value, client.credentials.userId, client);
itemsResult.value, client.profile.userId, client);
_logger.finest(() {
final postCount = _entries.values.where((e) => e.parentId.isEmpty).length;
final postCount = _entries.values
.where((e) => e.parentId.isEmpty)
.length;
final commentCount = _entries.length - postCount;
final orphanCount = _entries.values
.where(
@ -199,11 +200,9 @@ class EntryManagerService extends ChangeNotifier {
return Result.ok(updatedPosts);
}
Future<List<EntryTreeItem>> processNewItems(
List<TimelineEntry> items,
String currentId,
FriendicaClient? client,
) async {
Future<List<EntryTreeItem>> processNewItems(List<TimelineEntry> items,
String currentId,
FriendicaClient? client,) async {
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
final allSeenItems = [...items];
for (final item in items) {
@ -227,10 +226,12 @@ class EntryManagerService extends ChangeNotifier {
}
for (final o in orphans) {
await StatusesClient(getIt<AccountsService>().currentCredentials)
await StatusesClient(getIt<AccountsService>().currentProfile)
.getPostOrComment(o.id, fullContext: true)
.andThenSuccessAsync((items) async {
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
final parentPostId = items
.firstWhere((e) => e.parentId.isEmpty)
.id;
_parentPostIds[o.id] = parentPostId;
allSeenItems.addAll(items);
for (final item in items) {
@ -258,7 +259,7 @@ class EntryManagerService extends ChangeNotifier {
for (final item in seenItemsCopy) {
if (item.parentId.isEmpty) {
final postNode =
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
postNodesToReturn.add(postNode);
allSeenItems.remove(item);
} else {
@ -300,28 +301,31 @@ class EntryManagerService extends ChangeNotifier {
.toList();
_logger.finest(
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
'Completed processing new items ${client == null
? 'sub level'
: 'top level'}');
return updatedPosts;
}
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
_logger.finest('Refreshing post: $id');
final client = StatusesClient(getIt<AccountsService>().currentCredentials);
final client = StatusesClient(getIt<AccountsService>().currentProfile);
final result = await client
.getPostOrComment(id, fullContext: false)
.andThenAsync((rootItems) async => await client
.getPostOrComment(id, fullContext: true)
.andThenSuccessAsync(
(contextItems) async => [...rootItems, ...contextItems]))
.andThenAsync((rootItems) async =>
await client
.getPostOrComment(id, fullContext: true)
.andThenSuccessAsync(
(contextItems) async => [...rootItems, ...contextItems]))
.andThenSuccessAsync((items) async {
await processNewItems(items, client.credentials.username, null);
await processNewItems(items, client.profile.username, null);
});
return result.mapValue((_) {
_logger.finest('$id post updated');
return _nodeToTreeItem(_getPostRootNode(id)!, client.credentials.userId);
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
}).mapError(
(error) {
(error) {
_logger.finest('$id error updating: $error');
return ExecError(
type: ErrorType.localError,
@ -333,17 +337,17 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<EntryTreeItem, ExecError> resharePost(String id) async {
_logger.finest('Resharing post: $id');
final client = StatusesClient(getIt<AccountsService>().currentCredentials);
final client = StatusesClient(getIt<AccountsService>().currentProfile);
final result =
await client.resharePost(id).andThenSuccessAsync((item) async {
await processNewItems([item], client.credentials.username, null);
await client.resharePost(id).andThenSuccessAsync((item) async {
await processNewItems([item], client.profile.username, null);
});
return result.mapValue((_) {
_logger.finest('$id post updated after reshare');
return _nodeToTreeItem(_postNodes[id]!, client.credentials.userId);
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
}).mapError(
(error) {
(error) {
_logger.finest('$id error updating: $error');
return ExecError(
type: ErrorType.localError,
@ -355,10 +359,10 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<bool, ExecError> unResharePost(String id) async {
_logger.finest('Unresharing post: $id');
final client = StatusesClient(getIt<AccountsService>().currentCredentials);
final client = StatusesClient(getIt<AccountsService>().currentProfile);
final result =
await client.unResharePost(id).andThenSuccessAsync((item) async {
await processNewItems([item], client.credentials.username, null);
await client.unResharePost(id).andThenSuccessAsync((item) async {
await processNewItems([item], client.profile.username, null);
});
if (result.isFailure) {
@ -371,10 +375,10 @@ class EntryManagerService extends ChangeNotifier {
return Result.ok(true);
}
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
String id, bool newStatus) async {
FutureResult<EntryTreeItem, ExecError> toggleFavorited(String id,
bool newStatus) async {
final client =
InteractionsClient(getIt<AccountsService>().currentCredentials);
InteractionsClient(getIt<AccountsService>().currentProfile);
final result = await client.changeFavoriteStatus(id, newStatus);
if (result.isFailure) {
return result.errorCast();
@ -387,7 +391,7 @@ class EntryManagerService extends ChangeNotifier {
: _postNodes[update.parentId]!.getChildById(update.id)!;
notifyListeners();
return Result.ok(_nodeToTreeItem(node, client.credentials.userId));
return Result.ok(_nodeToTreeItem(node, client.profile.userId));
}
EntryTreeItem _nodeToTreeItem(_Node node, String currentId) {
@ -464,7 +468,7 @@ class _Node {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _Node && runtimeType == other.runtimeType && id == other.id;
other is _Node && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;

Wyświetl plik

@ -40,9 +40,8 @@ class GalleryService extends ChangeNotifier {
}
FutureResult<List<GalleryData>, ExecError> updateGalleries() async {
final result =
await GalleryClient(getIt<AccountsService>().currentCredentials)
.getGalleryData();
final result = await GalleryClient(getIt<AccountsService>().currentProfile)
.getGalleryData();
if (result.isFailure) {
return result.errorCast();
}
@ -92,7 +91,7 @@ class GalleryService extends ChangeNotifier {
final pagesToUse = nextPageOnly ? [pages.last] : pages;
for (final page in pagesToUse) {
final result =
await GalleryClient(getIt<AccountsService>().currentCredentials)
await GalleryClient(getIt<AccountsService>().currentProfile)
.getGalleryImages(galleryName, page);
if (result.isFailure) {
return result.errorCast();

Wyświetl plik

@ -32,7 +32,7 @@ class InteractionsManager extends ChangeNotifier {
FutureResult<List<Connection>, ExecError> updateLikesForStatus(
String statusId) async {
final likesResult =
await InteractionsClient(getIt<AccountsService>().currentCredentials)
await InteractionsClient(getIt<AccountsService>().currentProfile)
.getLikes(statusId);
if (likesResult.isSuccess) {
_likesByStatusId[statusId] = likesResult.value;
@ -44,7 +44,7 @@ class InteractionsManager extends ChangeNotifier {
FutureResult<List<Connection>, ExecError> updateResharesForStatus(
String statusId) async {
final resharesResult =
await InteractionsClient(getIt<AccountsService>().currentCredentials)
await InteractionsClient(getIt<AccountsService>().currentProfile)
.getReshares(statusId);
if (resharesResult.isSuccess) {
_resharesByStatusId[statusId] = resharesResult.value;

Wyświetl plik

@ -129,7 +129,7 @@ class NotificationsManager extends ChangeNotifier {
FutureResult<bool, ExecError> markSeen(UserNotification notification) async {
final result =
await NotificationsClient(getIt<AccountsService>().currentCredentials)
await NotificationsClient(getIt<AccountsService>().currentProfile)
.clearNotification(notification);
if (result.isSuccess) {
notifyListeners();
@ -141,7 +141,7 @@ class NotificationsManager extends ChangeNotifier {
FutureResult<List<UserNotification>, ExecError> markAllAsRead() async {
final result =
await NotificationsClient(getIt<AccountsService>().currentCredentials)
await NotificationsClient(getIt<AccountsService>().currentProfile)
.clearNotifications();
if (result.isFailure) {
return result.errorCast();
@ -153,7 +153,7 @@ class NotificationsManager extends ChangeNotifier {
}
List<UserNotification> buildUnreadMessageNotifications() {
final myId = getIt<AccountsService>().currentCredentials.userId;
final myId = getIt<AccountsService>().currentProfile.userId;
final result =
getIt<DirectMessageService>().getThreads(unreadyOnly: true).map((t) {
final fromAccount = t.participants.firstWhere((p) => p.id != myId);
@ -178,7 +178,7 @@ class NotificationsManager extends ChangeNotifier {
static FutureResult<PagedResponse<List<UserNotification>>, ExecError>
_clientGetNotificationsRequest(PagingData page) async {
final result =
await NotificationsClient(getIt<AccountsService>().currentCredentials)
await NotificationsClient(getIt<AccountsService>().currentProfile)
.getNotifications(page);
return result;
}

Wyświetl plik

@ -1,27 +1,21 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:result_monad/result_monad.dart';
import '../models/auth/credentials.dart';
import '../models/auth/basic_credentials.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
class SecretsService {
static const _usernameKey = 'username';
static const _passwordKey = 'password';
static const _serverNameKey = 'server-name';
static const _userIdKey = 'user-id';
static const _avatarKey = 'avatar';
static const _basicProfilesKey = 'basic_profiles';
static const _oauthProfilesKey = 'oauth_profiles';
Credentials? _cachedCredentials;
final _cachedProfiles = <Profile>{};
Result<Credentials, ExecError> get credentials => _cachedCredentials != null
? Result.ok(_cachedCredentials!)
: Result.error(
ExecError(
type: ErrorType.localError,
message: 'Credentials not initialized',
),
);
List<Profile> get profiles => UnmodifiableListView(_cachedProfiles);
final _secureStorage = const FlutterSecureStorage(
iOptions: IOSOptions(
@ -29,18 +23,15 @@ class SecretsService {
),
);
FutureResult<Credentials, ExecError> initialize() async {
return await getCredentials();
FutureResult<List<Profile>, ExecError> initialize() async {
return await loadProfiles();
}
FutureResult<Credentials, ExecError> clearCredentials() async {
FutureResult<List<Profile>, ExecError> clearCredentials() async {
try {
await _secureStorage.delete(key: _usernameKey);
await _secureStorage.delete(key: _passwordKey);
await _secureStorage.delete(key: _serverNameKey);
await _secureStorage.delete(key: _userIdKey);
await _secureStorage.delete(key: _avatarKey);
return Result.ok(Credentials.empty());
await _secureStorage.delete(key: _basicProfilesKey);
await _secureStorage.delete(key: _oauthProfilesKey);
return Result.ok(profiles);
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
@ -49,15 +40,11 @@ class SecretsService {
}
}
Result<Credentials, ExecError> storeCredentials(Credentials credentials) {
FutureResult<List<Profile>, ExecError> addOrUpdateProfile(
Profile profile) async {
try {
_secureStorage.write(key: _usernameKey, value: credentials.username);
_secureStorage.write(key: _passwordKey, value: credentials.password);
_secureStorage.write(key: _serverNameKey, value: credentials.serverName);
_secureStorage.write(key: _userIdKey, value: credentials.userId);
_secureStorage.write(key: _avatarKey, value: credentials.avatar);
_cachedCredentials = credentials;
return Result.ok(credentials);
_cachedProfiles.add(profile);
return await saveCredentials();
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
@ -66,24 +53,47 @@ class SecretsService {
}
}
FutureResult<Credentials, ExecError> getCredentials() async {
FutureResult<List<Profile>, ExecError> removeProfile(Profile profile) async {
try {
final username = await _secureStorage.read(key: _usernameKey);
final password = await _secureStorage.read(key: _passwordKey);
final serverName = await _secureStorage.read(key: _serverNameKey);
final userId = await _secureStorage.read(key: _userIdKey);
final avatar = await _secureStorage.read(key: _avatarKey);
if (username == null || password == null || serverName == null) {
return Result.ok(Credentials.empty());
_cachedProfiles.remove(profile);
return await saveCredentials();
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
message: e.message ?? '',
));
}
}
FutureResult<List<Profile>, ExecError> loadProfiles() async {
try {
final basicJson = await _secureStorage.read(key: _basicProfilesKey);
if (basicJson == null) {
return Result.ok(profiles);
}
_cachedCredentials = Credentials(
username: username,
password: password,
serverName: serverName,
userId: userId ?? '',
avatar: avatar ?? '',
);
return Result.ok(_cachedCredentials!);
final basicCreds = (jsonDecode(basicJson) as List<dynamic>)
.map((json) => Profile.fromJson(json, BasicCredentials.fromJson))
.toList();
_cachedProfiles.addAll(basicCreds);
return Result.ok(profiles);
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,
message: e.message ?? '',
));
}
}
FutureResult<List<Profile>, ExecError> saveCredentials() async {
try {
final basicCredsJson = _cachedProfiles
.where((p) => p.credentials is BasicCredentials)
.map((p) => p.toJson())
.toList();
final basicCredsString = jsonEncode(basicCredsJson);
await _secureStorage.write(
key: _basicProfilesKey, value: basicCredsString);
return Result.ok(profiles);
} on PlatformException catch (e) {
return Result.error(ExecError(
type: ErrorType.localError,

Wyświetl plik

@ -46,7 +46,7 @@ class TimelineManager extends ChangeNotifier {
Future<void> _refreshGroupData() async {
_logger.finest('Refreshing member group data ');
await GroupsClient(getIt<AccountsService>().currentCredentials)
await GroupsClient(getIt<AccountsService>().currentProfile)
.getGroups()
.match(
onSuccess: (groups) {