mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 137 lines 4.4 kB view raw
1import 'package:flutter/foundation.dart'; 2 3enum AtProtoIdentifierValidationErrorCode { 4 empty, 5 unsupportedDid, 6 invalidDid, 7 invalidHandle; 8 9 String get message => switch (this) { 10 AtProtoIdentifierValidationErrorCode.empty => 'Enter a Bluesky handle or DID', 11 AtProtoIdentifierValidationErrorCode.unsupportedDid => 'Use a did:plc:... or did:web:... identifier', 12 AtProtoIdentifierValidationErrorCode.invalidDid => 'Enter a complete DID like did:plc:... or did:web:...', 13 AtProtoIdentifierValidationErrorCode.invalidHandle => 'Enter a full handle like username.bsky.social', 14 }; 15} 16 17class AtProtoIdentifierValidationError { 18 const AtProtoIdentifierValidationError(this.code); 19 20 final AtProtoIdentifierValidationErrorCode code; 21} 22 23final RegExp _atprotoHandlePattern = RegExp( 24 r'^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$', 25); 26final RegExp _didPlcSuffixPattern = RegExp(r'^[a-z2-7]{24}$'); 27final RegExp _didWebLocalhostPortPattern = RegExp(r'^localhost%3A[0-9]+$', caseSensitive: false); 28final RegExp _didWebHostLabelPattern = RegExp(r'^[A-Za-z0-9-]+$'); 29const Set<String> _disallowedDidWebTopLevelDomains = { 30 'alt', 31 'arpa', 32 'example', 33 'internal', 34 'invalid', 35 'local', 36 'localhost', 37 'onion', 38}; 39 40String normalizeAtProtoIdentifierForAuth(String identifier) { 41 final trimmed = identifier.trim(); 42 final lower = trimmed.toLowerCase(); 43 if (lower.startsWith('did:plc:')) { 44 return 'did:plc:${trimmed.substring('did:plc:'.length).toLowerCase()}'; 45 } 46 if (lower.startsWith('did:web:')) { 47 return 'did:web:${trimmed.substring('did:web:'.length).toLowerCase()}'; 48 } 49 if (lower.startsWith('did:')) { 50 return 'did:${trimmed.substring('did:'.length)}'; 51 } 52 53 final withoutAt = trimmed.replaceFirst(RegExp(r'^@+'), ''); 54 return withoutAt.toLowerCase(); 55} 56 57AtProtoIdentifierValidationError? validateAtProtoIdentifierForAuth( 58 String identifier, { 59 bool allowDevelopmentDidWebHosts = !kReleaseMode, 60}) { 61 if (identifier.isEmpty) { 62 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.empty); 63 } 64 65 final normalizedLower = identifier.toLowerCase(); 66 if (normalizedLower.startsWith('did:')) { 67 if (normalizedLower.startsWith('did:plc:')) { 68 final suffix = identifier.substring('did:plc:'.length).trim(); 69 if (!_didPlcSuffixPattern.hasMatch(suffix)) { 70 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.invalidDid); 71 } 72 return null; 73 } 74 75 if (normalizedLower.startsWith('did:web:')) { 76 final suffix = identifier.substring('did:web:'.length).trim(); 77 if (suffix.isEmpty || 78 suffix.contains(RegExp(r'\s')) || 79 suffix.contains(':') || 80 suffix.contains('/') || 81 suffix.contains('?') || 82 suffix.contains('#')) { 83 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.invalidDid); 84 } 85 86 if (!_isValidDidWebHostSuffix(suffix, allowDevelopmentDidWebHosts: allowDevelopmentDidWebHosts)) { 87 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.invalidDid); 88 } 89 return null; 90 } 91 92 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.unsupportedDid); 93 } 94 95 if (!_atprotoHandlePattern.hasMatch(identifier)) { 96 return const AtProtoIdentifierValidationError(AtProtoIdentifierValidationErrorCode.invalidHandle); 97 } 98 99 return null; 100} 101 102bool _isValidDidWebHostSuffix(String suffix, {required bool allowDevelopmentDidWebHosts}) { 103 if (allowDevelopmentDidWebHosts && _didWebLocalhostPortPattern.hasMatch(suffix)) { 104 return true; 105 } 106 107 final lower = suffix.toLowerCase(); 108 if (allowDevelopmentDidWebHosts && lower == 'localhost') { 109 return true; 110 } 111 112 if (suffix.length > 253 || suffix.startsWith('.') || suffix.endsWith('.') || suffix.contains('..')) { 113 return false; 114 } 115 116 final labels = suffix.split('.'); 117 if (labels.length < 2) { 118 return false; 119 } 120 121 for (final label in labels) { 122 if (label.isEmpty || 123 label.length > 63 || 124 !_didWebHostLabelPattern.hasMatch(label) || 125 label.startsWith('-') || 126 label.endsWith('-')) { 127 return false; 128 } 129 } 130 131 final tld = labels.last.toLowerCase(); 132 if (_disallowedDidWebTopLevelDomains.contains(tld)) { 133 return false; 134 } 135 136 return true; 137}