import 'dart:async'; import 'dart:typed_data'; import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart'; import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart'; /// A [ProfileGateway] in memory, on an instance whose account domain is /// `halkyon.eu`: one profile, refusing the handles others have (whatever /// their case) and, once its handle is fixed, any other handle. class FakeProfileGateway({ /// The person's profile; null while they have none. var PublicProfile? profile, /// Handles others have. final Set taken = const {}, /// Others' profiles, by their handles. final Map others = const {}, }) implements ProfileGateway { @override Future byHandle(String handle) async { if (fail case final error?) { throw error; } return others[handle.toLowerCase()]; } /// When set, every request fails with it. Exception? fail; @override Future mine() async { if (fail case final error?) { throw error; } return profile; } /// The handles checked, in order. final checked = []; @override Future checkHandle(String handle) async { if (fail case final error?) { throw error; } checked.add(handle); return _problemOf(handle); } ProfileProblem? _problemOf(String handle) { final current = profile; if (!RegExp(r'^[A-Za-z0-9_]{1,30}$').hasMatch(handle)) { return ProfileProblem.handleInvalid; } if (handle.toLowerCase() == 'admin') { return ProfileProblem.handleReserved; } if (taken.any((other) => other.toLowerCase() == handle.toLowerCase())) { return ProfileProblem.handleTaken; } if (current != null && current.handleFixed && current.handle.toLowerCase() != handle.toLowerCase()) { return ProfileProblem.published; } return null; } /// Every profile saved, in order. final saves = []; @override Future save({ required String handle, required String displayName, required String bio, required List links, }) async { if (fail case final error?) { throw error; } final current = profile; if (_problemOf(handle) case final problem?) { throw ProfileRefused(problem); } final saved = profile = PublicProfile( handle: handle, address: '$handle@halkyon.eu', displayName: displayName, bio: bio, links: links, // As the instance keeps them: those of links left as they were, under // the same handle. verifiedLinks: [ if (current?.handle.toLowerCase() == handle.toLowerCase()) for (final link in current?.verifiedLinks ?? const []) if (links.contains(link)) link, ], pageUrl: 'https://halkyon.eu/@$handle', avatarUrl: current?.avatarUrl, handleFixed: current?.handleFixed ?? false, ); saves.add(saved); return saved; } /// The avatars set, in order. final avatars = []; @override Future setAvatar(Uint8List png) async { if (fail case final error?) { throw error; } avatars.add(png); final current = profile!; return profile = PublicProfile( handle: current.handle, address: current.address, displayName: current.displayName, bio: current.bio, links: current.links, avatarUrl: 'https://learn.halkyon.eu/avatars/${avatars.length}.png', handleFixed: current.handleFixed, ); } @override Future removeAvatar() async { final current = profile!; return profile = PublicProfile( handle: current.handle, address: current.address, displayName: current.displayName, bio: current.bio, links: current.links, handleFixed: current.handleFixed, ); } @override Future remove() async { if (fail case final error?) { throw error; } if (profile?.handleFixed ?? false) { throw ProfileRefused(ProfileProblem.published); } profile = null; } /// The links whose pages link back to the profile page. final linkingBack = {}; /// How often the links were checked. int linkChecks = 0; /// While set, checking the links is refused as asked too often. bool refuseLinkChecks = false; /// While set, a check waits for it before it answers. Completer? checkWaits; @override Future checkLinks() async { if (fail case final error?) { throw error; } if (refuseLinkChecks) { throw ProfileRefused(ProfileProblem.tooManyLinkChecks); } linkChecks++; // The profile as the check began, as an answer may come after a save. final began = profile!; await checkWaits?.future; final verified = [ for (final link in began.links) if (linkingBack.contains(link)) link, ]; profile = profile!.withVerifiedLinks(verified); return began.withVerifiedLinks(verified); } }