diff --git a/lib/services/fediverse_server_validator.dart b/lib/services/fediverse_server_validator.dart new file mode 100644 index 0000000..e1b7b8b --- /dev/null +++ b/lib/services/fediverse_server_validator.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +import 'package:result_monad/result_monad.dart'; + +import '../models/exec_error.dart'; +import '../models/server_data.dart'; +import '../utils/network_utils.dart'; + +class FediverseServiceValidator { + final knownServers = {}; + + FutureResult checkIfFediverseLink(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) { + return buildErrorResult( + type: ErrorType.parsingError, + message: 'Invalid URL: $url', + ); + } + + final domain = uri.host; + ServerData? data = knownServers[domain]; + if (data != null) { + return Result.ok(data.isFediverse); + } + + final updateResult = await refreshServerData(domain); + return updateResult.andThenSuccess((sd) { + knownServers[sd.domainName] = sd; + return sd.isFediverse; + }).execErrorCast(); + } + + static FutureResult refreshServerData( + String domainName) async { + final uri = Uri.https( + domainName, + '/.well-known/nodeinfo', + ); + final result = await getUrl(uri) + .andThenSuccessAsync((page) async { + return jsonDecode(page.data); + }) + .andThenAsync( + (json) async => json is Map + ? Result.ok(json) + : Result.error('Unknown response type for well-know/nodeinfo'), + ) + .andThenSuccessAsync((json) async => json['links'] ?? []) + .andThenAsync( + (nodeInfos) async => nodeInfos.isNotEmpty + ? Result.ok(nodeInfos.last) + : Result.error('Unknown response type for well-know/nodeinfo'), + ) + .andThenAsync((nodeInfo) async { + final rel = nodeInfo['rel']?.toString() ?? ''; + if (!rel.startsWith('http://nodeinfo.diaspora.software/ns/schema/')) { + return Result.error('Unknown response type for well-know/nodeinfo'); + } + + final nodeInfoUrl = Uri.tryParse(nodeInfo['href'] ?? ''); + if (nodeInfoUrl == null) { + return Result.error('Unknown response type for well-know/nodeinfo'); + } + return await getUrl(nodeInfoUrl); + }) + .andThenSuccessAsync( + (nodeInfoData) async => jsonDecode(nodeInfoData.data)) + .andThenSuccessAsync((nodeInfoJson) async { + final softwareName = + nodeInfoJson['software']?['name']?.toString() ?? ''; + final softwareVersion = + nodeInfoJson['software']?['version']?.toString() ?? ''; + final isFediverse = + softwareName.isNotEmpty && softwareVersion.isNotEmpty; + return ServerData( + domainName: domainName, + isFediverse: isFediverse, + softwareName: softwareName, + softwareVersion: softwareVersion, + ); + }); + + return result.execErrorCast(); + } +} diff --git a/test/fediverse_server_validator_test.dart b/test/fediverse_server_validator_test.dart new file mode 100644 index 0000000..1ea4902 --- /dev/null +++ b/test/fediverse_server_validator_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:relatica/services/fediverse_server_validator.dart'; + +void main() { + test('Test against Diaspora Server', () async { + await testDomain('diasp.org', 'diaspora'); + }); + + test('Test against Friendica Server', () async { + await testDomain('friendica.myportal.social', 'friendica'); + }); + + test('Test against Mastodon Server', () async { + await testDomain('mastodon.social', 'mastodon'); + }); + + test('Test against GNUSocial Server', () async { + await testDomain('gnusocial.net', 'gnusocial'); + }); + + test('Test against MissKey Server', () async { + await testDomain('misskey.io', 'misskey'); + }); + + test('Test against HubZilla Server', () async { + await testDomain('hub.hubzilla.de', 'redmatrix'); + }); + + test('Test against PeerTube Server', () async { + await testDomain('tilvids.com', 'peertube'); + }); + + test('Test against PixelFed Server', () async { + await testDomain('pixels.gsi.li', 'pixelfed'); + }); + + test('Test against Funkwhale Server', () async { + await testDomain('open.audio', 'funkwhale'); + }); + + test('Test against Akkoma Server', () async { + await testDomain('social.kernel.org', 'akkoma'); + }); + + test('Test against Pleroma Server', () async { + await testDomain('stereophonic.space', 'pleroma'); + }); +} + +Future testDomain(String domain, String softwareName) async { + final result = await FediverseServiceValidator.refreshServerData(domain); + expect(result.isSuccess, equals(true)); + expect(result.value.isFediverse, equals(true)); + expect(result.value.domainName, equals(domain)); + expect(result.value.softwareName, equals(softwareName)); +}