diff --git a/.gitignore b/.gitignore index 3c8a157..2cf44b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ # Conventional directory for build output. build/ + +# IntelliJ Files +.idea + +# Executables +*.exe \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e0..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index f71feb6..0000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 064833e..0000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 639900d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 19450bb..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 797acea..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bin/exec_error.dart b/bin/exec_error.dart new file mode 100644 index 0000000..9c08b3b --- /dev/null +++ b/bin/exec_error.dart @@ -0,0 +1,11 @@ +class ExecError { + final ErrorType type; + final String message; + + ExecError({required this.type, this.message = ''}); +} + +enum ErrorType { + authentication, + missingEndpoint, +} diff --git a/bin/friendica_archiver.dart b/bin/friendica_archiver.dart index d721de6..85b227c 100644 --- a/bin/friendica_archiver.dart +++ b/bin/friendica_archiver.dart @@ -1,3 +1,85 @@ -void main(List arguments) { - print('Hello world!'); +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'friendica_client.dart'; +import 'json_printer.dart'; + +const defaultRequestDelayMilliseconds = 5000; +const defaultMaxPosts = 1; +const defaultReadComments = false; +const defaultReadImages = false; + +void main(List arguments) async { + final argParser = ArgParser() + ..addOption('archive-folder', + abbr: 'a', + help: + 'Specifies the local folder all data files pulled from the server will be stored', + mandatory: true) + ..addOption('username', + abbr: 'u', help: 'Username on your Friendica instance', mandatory: true) + ..addOption('server-name', + abbr: 's', + help: + 'The server name for your instance. (e.g. if the URL in your browser is "https://friendica.com/" then this would be "friendica.com', + mandatory: true) + ..addOption('delay', + abbr: 'd', + help: + 'Delay in milliseconds between requests to try not to stress the server (thousands of API calls can be made)', + defaultsTo: '$defaultRequestDelayMilliseconds') + ..addOption('max-post-requests', + abbr: 'm', + help: 'The maximum number of times to query for posts', + defaultsTo: '$defaultMaxPosts') + ..addFlag('read-comments', + abbr: 'c', + help: + 'Whether to read comments on posts (defaults to $defaultReadComments)', + defaultsTo: defaultReadComments) + ..addFlag('download-images', + abbr: 'i', + help: + 'Whether to download images from posts when those images are stored on the server (not links to other sites) (defaults to $defaultReadImages)', + defaultsTo: defaultReadComments); + + late ArgResults settings; + try { + settings = argParser.parse(arguments); + } on ArgParserException catch (e) { + print("Error with arguments: ${e.message}"); + print(argParser.usage); + return; + } + + stdout.write('Enter Password: '); + _setEcho(false); + final password = stdin.readLineSync() ?? ''; + _setEcho(true); + print(''); + + final username = settings['username']; + final client = FriendicaClient( + username: username, + password: password, + serverName: settings['server-name']); + final timelineResult = await client.getTimeline(username, 1, 3); + timelineResult.match( + onSuccess: (posts) => File('/tmp/test.json') + .writeAsStringSync(PrettyJsonEncoder().convert(posts)), + onError: (error) => print('Error getting posts: $error')); + print("Done processing API requests"); + return; +} + +// Seems in IntelliJ and release build mode setting echo fails +void _setEcho(bool value) { + try { + stdin.echoMode = value; + // ignore: empty_catches + } catch (e) { + print(''); + print('Error toggling echo to $value, so will stay current value...'); + } } diff --git a/bin/friendica_client.dart b/bin/friendica_client.dart new file mode 100644 index 0000000..fc229b2 --- /dev/null +++ b/bin/friendica_client.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:result_monad/result_monad.dart'; + +class FriendicaClient { + final String username; + final String password; + final String serverName; + final _client = HttpClient(); + late final String _authHeader; + + FriendicaClient( + {required this.username, + required this.password, + required this.serverName}) { + final authenticationString = "$username:$password"; + final encodedAuthString = base64Encode(utf8.encode(authenticationString)); + _authHeader = "Basic $encodedAuthString"; + } + + FutureResult, String> getTimeline( + String userId, int page, int count) async { + final request = Uri.parse( + 'https://$serverName/api/statuses/user_timelineuser_id=$userId&count=$count&page=$page'); + return await _getApiRequest(request); + } + + FutureResult, String> getPostComments(int postId) async { + return Result.error("Not Implemented"); + } + + FutureResult, String> _getApiRequest(Uri url) async { + // TODO Error mode against: bad server URL, bad auth, bad path, empty response + final request = await _client.getUrl(url); + request.headers.add('authorization', _authHeader); + request.headers.contentType = + ContentType('application', 'json', charset: 'utf-8'); + final response = await request.close(); + final body = await response.transform(utf8.decoder).join(''); + final bodyJson = jsonDecode(body) as List; + return Result.ok(bodyJson); + } +} diff --git a/bin/json_printer.dart b/bin/json_printer.dart new file mode 100644 index 0000000..c5a4661 --- /dev/null +++ b/bin/json_printer.dart @@ -0,0 +1,11 @@ +import 'dart:convert'; + +class PrettyJsonEncoder { + late JsonEncoder encoder; + + PrettyJsonEncoder() { + encoder = JsonEncoder.withIndent('\t'); + } + + String convert(Object json) => encoder.convert(json); +} diff --git a/pubspec.lock b/pubspec.lock index 50a12ba..0404e4b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: "direct main" + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" lints: dependency: "direct dev" description: @@ -8,5 +15,12 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + result_monad: + dependency: "direct main" + description: + name: result_monad + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" sdks: dart: ">=2.15.1 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f8b0d0d..cc70bf4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,9 +6,9 @@ version: 1.0.0 environment: sdk: '>=2.15.1 <3.0.0' - dependencies: args: ^2.3.0 + result_monad: ^1.0.2 dev_dependencies: lints: ^1.0.0