From e051a3ea5274ad85bb0ce47456b9b29cbffb1a42 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 1 Mar 2022 17:21:42 -0500 Subject: [PATCH] Implement Diaspora Contact Importing --- .../diaspora_contact_serializer.dart | 41 ++++++ .../models/friendica_timeline_entry.dart | 24 ++-- .../models/friendica_contact.dart | 13 +- .../services/diaspora_archive_service.dart | 127 ++++++++++++++++++ .../diaspora_profile_json_reader.dart | 25 ++++ .../src/services/friendica_connections.dart | 12 +- .../src/utils/top_interactors_generator.dart | 8 +- .../disapora_profile_json_reader_test.dart | 14 ++ 8 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart rename friendica_archive_browser/lib/src/{friendica => }/models/friendica_contact.dart (82%) create mode 100644 friendica_archive_browser/lib/src/services/diaspora_archive_service.dart create mode 100644 friendica_archive_browser/lib/src/services/diaspora_profile_json_reader.dart create mode 100644 friendica_archive_browser/test/disapora_profile_json_reader_test.dart diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart new file mode 100644 index 0000000..d60d00b --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart @@ -0,0 +1,41 @@ +import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; + +Contact friendicaContactFromDiasporaJson(Map json) { + const network = "Diaspora"; + final accountId = json['account_id'] ?? ''; + final profileUrl = _profileUrlFromAccountId(accountId); + final name = json['person_name'] ?? ''; + final id = json['person_guid'] ?? ''; + final following = json['following'] ?? false; + final followed = json['followed'] ?? false; + var status = ConnectionStatus.none; + if (following && followed) { + status = ConnectionStatus.mutual; + } else if (following) { + status = ConnectionStatus.youFollowThem; + } else if (followed) { + status = ConnectionStatus.theyFollowYou; + } + + return Contact( + status: status, + name: name, + id: id, + profileUrl: profileUrl, + network: network); +} + +Uri _profileUrlFromAccountId(String accountId) { + if (accountId.isEmpty) { + return Uri(); + } + final accountIdPieces = accountId.split('@'); + if (accountIdPieces.length != 2) { + return Uri(); + } + + final userName = accountIdPieces[0]; + final server = accountIdPieces[1]; + + return Uri.parse('https://$server/u/$userName'); +} diff --git a/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart b/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart index 32cc935..676079d 100644 --- a/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart +++ b/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart @@ -1,5 +1,5 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart'; import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; import 'package:friendica_archive_browser/src/services/friendica_connections.dart'; import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart'; import 'package:intl/intl.dart'; @@ -44,9 +44,9 @@ class FriendicaTimelineEntry { final List links; - final List likes; + final List likes; - final List dislikes; + final List dislikes; FriendicaTimelineEntry( {this.id = '', @@ -63,8 +63,8 @@ class FriendicaTimelineEntry { this.parentAuthorId = '', this.externalLink = '', this.locationData = const LocationData(), - this.likes = const [], - this.dislikes = const [], + this.likes = const [], + this.dislikes = const [], List? mediaAttachments, List? links}) : mediaAttachments = mediaAttachments ?? [], @@ -85,8 +85,8 @@ class FriendicaTimelineEntry { parentAuthor = 'Random parent author ${randomId()}', parentAuthorId = 'Random parent author id ${randomId()}', locationData = LocationData.randomBuilt(), - likes = const [], - dislikes = const [], + likes = const [], + dislikes = const [], links = [ Uri.parse('http://localhost/${randomId()}'), Uri.parse('http://localhost/${randomId()}') @@ -112,8 +112,8 @@ class FriendicaTimelineEntry { String? parentAuthorId, LocationData? locationData, List? mediaAttachments, - List? likes, - List? dislikes, + List? likes, + List? dislikes, List? links}) { return FriendicaTimelineEntry( creationTimestamp: creationTimestamp ?? this.creationTimestamp, @@ -207,15 +207,15 @@ class FriendicaTimelineEntry { .toList(); final likes = (json['friendica_activities']?['like'] as List? ?? []) - .map((json) => FriendicaContact.fromJson(json)) + .map((json) => Contact.fromJson(json)) .toList(); final dislikes = (json['friendica_activities']?['dislike'] as List? ?? []) - .map((json) => FriendicaContact.fromJson(json)) + .map((json) => Contact.fromJson(json)) .toList(); final announce = (json['friendica_activities']?['announce'] as List? ?? []) - .map((json) => FriendicaContact.fromJson(json)) + .map((json) => Contact.fromJson(json)) .toList(); for (final contact in [...likes, ...dislikes, ...announce]) { diff --git a/friendica_archive_browser/lib/src/friendica/models/friendica_contact.dart b/friendica_archive_browser/lib/src/models/friendica_contact.dart similarity index 82% rename from friendica_archive_browser/lib/src/friendica/models/friendica_contact.dart rename to friendica_archive_browser/lib/src/models/friendica_contact.dart index 2374460..62aef8f 100644 --- a/friendica_archive_browser/lib/src/friendica/models/friendica_contact.dart +++ b/friendica_archive_browser/lib/src/models/friendica_contact.dart @@ -1,4 +1,4 @@ -class FriendicaContact { +class Contact { final ConnectionStatus status; final String name; @@ -9,14 +9,14 @@ class FriendicaContact { final String network; - FriendicaContact( + Contact( {required this.status, required this.name, required this.id, required this.profileUrl, required this.network}); - static FriendicaContact fromJson(Map json) { + static Contact fromJson(Map json) { final status = (json['following'] ?? '') == 'true' ? ConnectionStatus.youFollowThem : ConnectionStatus.none; @@ -25,13 +25,18 @@ class FriendicaContact { final profileUrl = Uri.parse(json['url'] ?? ''); final network = json['network'] ?? 'unkn'; - return FriendicaContact( + return Contact( status: status, name: name, id: id, profileUrl: profileUrl, network: network); } + + @override + String toString() { + return 'FriendicaContact{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}'; + } } enum ConnectionStatus { diff --git a/friendica_archive_browser/lib/src/services/diaspora_archive_service.dart b/friendica_archive_browser/lib/src/services/diaspora_archive_service.dart new file mode 100644 index 0000000..ba1a841 --- /dev/null +++ b/friendica_archive_browser/lib/src/services/diaspora_archive_service.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:result_monad/result_monad.dart'; + +import '../friendica/models/friendica_entry_tree_item.dart'; +import '../friendica/models/friendica_timeline_entry.dart'; +import '../friendica/services/path_mapping_service.dart'; +import '../models/local_image_archive_entry.dart'; +import '../utils/exec_error.dart'; +import 'friendica_connections.dart'; + +class DiasporaArchiveService { + final PathMappingService pathMappingService; + final Map _imagesByRequestUrl = {}; + final List _postEntries = []; + final List _orphanedCommentEntries = []; + final List _allComments = []; + final FriendicaConnections connections = FriendicaConnections(); + String _ownersName = ''; + + DiasporaArchiveService({required this.pathMappingService}); + + String get ownersName => _ownersName; + + void clearCaches() { + connections.clearCaches(); + _imagesByRequestUrl.clear(); + _orphanedCommentEntries.clear(); + _allComments.clear(); + _postEntries.clear(); + } + + FutureResult, ExecError> getPosts() async { + if (_postEntries.isEmpty && _allComments.isEmpty) { + _loadEntries(); + } + + return Result.ok(_postEntries); + } + + FutureResult, ExecError> getAllComments() async { + if (_postEntries.isEmpty && _allComments.isEmpty) { + _loadEntries(); + } + + return Result.ok(_allComments); + } + + FutureResult, ExecError> + getOrphanedComments() async { + if (_postEntries.isEmpty && _allComments.isEmpty) { + _loadEntries(); + } + + return Result.ok(_orphanedCommentEntries); + } + + Result getImageByUrl(String url) { + if (_imagesByRequestUrl.isEmpty) { + _loadImages(); + } + + final result = _imagesByRequestUrl[url]; + return result == null + ? Result.error(ExecError(errorMessage: '$url not found')) + : Result.ok(result); + } + + String get _baseArchiveFolder => pathMappingService.rootFolder; + + void _loadEntries() { + final entriesJsonPath = p.join(_baseArchiveFolder, 'postsAndComments.json'); + final jsonFile = File(entriesJsonPath); + if (jsonFile.existsSync()) { + final json = jsonDecode(jsonFile.readAsStringSync()) as List; + final entries = + json.map((j) => FriendicaTimelineEntry.fromJson(j, connections)); + final topLevelEntries = + entries.where((element) => element.parentId.isEmpty); + final commentEntries = + entries.where((element) => element.parentId.isNotEmpty).toList(); + final entryTrees = {}; + + final postTreeEntries = []; + for (final entry in topLevelEntries) { + final treeEntry = FriendicaEntryTreeItem(entry, false); + entryTrees[entry.id] = treeEntry; + postTreeEntries.add(treeEntry); + } + + final commentTreeEntries = []; + commentEntries.sort( + (c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp)); + for (final entry in commentEntries) { + final parent = entryTrees[entry.parentId]; + final treeEntry = FriendicaEntryTreeItem(entry, parent == null); + parent?.addChild(treeEntry); + entryTrees[entry.id] = treeEntry; + commentTreeEntries.add(treeEntry); + } + + _postEntries.clear(); + _postEntries.addAll(postTreeEntries); + + _allComments.clear(); + _allComments.addAll(commentTreeEntries); + + _orphanedCommentEntries.clear(); + _orphanedCommentEntries + .addAll(entryTrees.values.where((element) => element.isOrphaned)); + } + } + + void _loadImages() { + final imageJsonPath = p.join(_baseArchiveFolder, 'images.json'); + final jsonFile = File(imageJsonPath); + if (jsonFile.existsSync()) { + final json = jsonDecode(jsonFile.readAsStringSync()) as List; + final imageEntries = json.map((j) => ImageEntry.fromJson(j)); + for (final entry in imageEntries) { + _imagesByRequestUrl[entry.url] = entry; + } + } + } +} diff --git a/friendica_archive_browser/lib/src/services/diaspora_profile_json_reader.dart b/friendica_archive_browser/lib/src/services/diaspora_profile_json_reader.dart new file mode 100644 index 0000000..f4dab26 --- /dev/null +++ b/friendica_archive_browser/lib/src/services/diaspora_profile_json_reader.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart'; +import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; + +class DiasporaProfileJsonReader { + final String jsonFilePath; + + DiasporaProfileJsonReader(this.jsonFilePath); + + List readContacts() { + final jsonFile = File(jsonFilePath); + if (jsonFile.existsSync()) { + final json = + jsonDecode(jsonFile.readAsStringSync()) as Map; + final contactsJson = json['user']?['contacts'] as List; + final contacts = + contactsJson.map((j) => friendicaContactFromDiasporaJson(j)).toList(); + return contacts; + } + + return []; + } +} diff --git a/friendica_archive_browser/lib/src/services/friendica_connections.dart b/friendica_archive_browser/lib/src/services/friendica_connections.dart index bd665e0..e3a36df 100644 --- a/friendica_archive_browser/lib/src/services/friendica_connections.dart +++ b/friendica_archive_browser/lib/src/services/friendica_connections.dart @@ -1,16 +1,16 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart'; +import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; import 'package:result_monad/result_monad.dart'; class FriendicaConnections { - final _connectionsById = {}; - final _connectionsByName = {}; + final _connectionsById = {}; + final _connectionsByName = {}; void clearCaches() { _connectionsById.clear(); _connectionsByName.clear(); } - bool addConnection(FriendicaContact contact) { + bool addConnection(Contact contact) { if (_connectionsById.containsKey(contact.id)) { return false; } @@ -20,13 +20,13 @@ class FriendicaConnections { return true; } - Result getById(String id) { + Result getById(String id) { final result = _connectionsById[id]; return result != null ? Result.ok(result) : Result.error('$id not found'); } - Result getByName(String name) { + Result getByName(String name) { final result = _connectionsByName[name]; return result != null ? Result.ok(result) : Result.error('$name not found'); diff --git a/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart b/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart index 47d7798..be2eccc 100644 --- a/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart +++ b/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart @@ -1,5 +1,5 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart'; import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; +import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; import 'package:friendica_archive_browser/src/services/friendica_connections.dart'; class TopInteractorsGenerator { @@ -66,7 +66,7 @@ class TopInteractorsGenerator { final contact = contacts.getById(id).fold( onSuccess: (contact) => contact, - onError: (error) => FriendicaContact( + onError: (error) => Contact( status: ConnectionStatus.none, name: '', id: id, @@ -77,7 +77,7 @@ class TopInteractorsGenerator { } class InteractorItem { - final FriendicaContact contact; + final Contact contact; final int resharedOrCommentedOn; final int likeCount; final int dislikeCount; @@ -94,7 +94,7 @@ class InteractorItem { } InteractorItem copy( - {FriendicaContact? contact, + {Contact? contact, int? resharedOrCommentedOn, int? likeCount, int? dislikeCount}) { diff --git a/friendica_archive_browser/test/disapora_profile_json_reader_test.dart b/friendica_archive_browser/test/disapora_profile_json_reader_test.dart new file mode 100644 index 0000000..7877b9b --- /dev/null +++ b/friendica_archive_browser/test/disapora_profile_json_reader_test.dart @@ -0,0 +1,14 @@ +// ignore_for_file: avoid_print + +import 'package:flutter_test/flutter_test.dart'; +import 'package:friendica_archive_browser/src/services/diaspora_profile_json_reader.dart'; + +void main() { + test('Diaspora Connections Test', () { + final reader = DiasporaProfileJsonReader( + '/Users/hankdev/Desktop/diaspora_pretty.json'); + final contacts = reader.readContacts(); + print(contacts.length); + print(contacts.first); + }); +}