import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../models/exec_error.dart'; import '../models/server_data.dart'; import 'networking/network_services.dart'; part 'fediverse_server_validator_services.g.dart'; final _blueSkyData = ServerData( domainName: 'bsky.app', isFediverse: true, protocols: [ 'ATProto', ], ); final _threadsData = ServerData( domainName: 'threads.net', isFediverse: true, protocols: [ 'activitypub', ], ); final _threadsWwwData = ServerData( domainName: 'www.threads.net', isFediverse: true, protocols: [ 'activitypub', ], ); final _knownServers = { threadsDomain: _threadsData, threadsWwwDomain: _threadsData, blueskyDomain: _blueSkyData, }; const blueskyDomain = 'bsky.app'; const threadsDomain = 'threads.net'; const threadsWwwDomain = 'www.threads.net'; const softwareTypeDiaspora = 'diaspora'; @riverpod Future> checkIfFromFediverse( Ref ref, String url, ) async { final dataResult = await ref.watch(serverDataProvider(url).future); return dataResult.mapValue((d) => d.isFediverse); } @riverpod Future> serverData(Ref ref, String url) async { final uri = Uri.tryParse(url); if (uri == null || uri.scheme != 'https') { return buildErrorResult( type: ErrorType.parsingError, message: 'Invalid URL: $url', ); } final domain = uri.host; ServerData? data = _knownServers[domain]; if (data != null) { return Result.ok(data); } final serverData = await ref.read(serverDataFromServerProvider(domain).future); return Result.ok(serverData); } @riverpod Future serverDataFromServer(Ref ref, String domainName) async { if (domainName == threadsDomain) { return _threadsData; } if (domainName == threadsWwwDomain) { return _threadsWwwData; } final uri = Uri.https( domainName, '/.well-known/nodeinfo', ); final result = await ref .read(httpGetProvider(NetworkRequest(uri)).future) .transform((page) => jsonDecode(page.data)) .andThen( (json) => json is Map ? Result.ok(json) : Result.error(''), ) .transform((json) => json['links'] ?? []) .andThen( (nodeInfos) => nodeInfos.isNotEmpty ? Result.ok(nodeInfos.last) : Result.error(''), ) .andThenAsync((nodeInfo) async { final rel = nodeInfo['rel']?.toString() ?? ''; if (!rel.startsWith('http://nodeinfo.diaspora.software/ns/schema/')) { return Result.error(''); } final nodeInfoUrl = Uri.tryParse(nodeInfo['href'] ?? ''); if (nodeInfoUrl == null) { return Result.error(''); } return await ref .read(httpGetProvider(NetworkRequest(nodeInfoUrl)).future); }) .transform((nodeInfoData) => jsonDecode(nodeInfoData.data)) .transform((nodeInfoJson) { 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.getValueOrElse( () => ServerData(domainName: domainName, isFediverse: false)); }