From 46ba15f3f01b3f7e8f801a6abe3ba5eb188364a3 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Fri, 21 Nov 2025 18:08:21 +0100 Subject: [PATCH] feat: parsing addresses+unstoppable domains from twitter profiles (#2663) * feat: parsing addresses+unstoppable domains from twitter profiles * fix: address resolution --- lib/entities/parse_address_from_domain.dart | 77 ++++++++++++++++++++- lib/twitter/twitter_api.dart | 2 +- lib/twitter/twitter_user.dart | 3 + 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 442810875..818a78184 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -193,6 +193,17 @@ class AddressResolver { }); } + static String extractUnstoppableDomain(String raw) { + // sort by length to avoid matching shorter tld instead of longer + final domains = List.from(unstoppableDomains)..sort((a, b) => b.length.compareTo(a.length)); + for (final tld in domains) { + final pattern = RegExp(r'([A-Za-z0-9-]+)\.' + RegExp.escape(tld), caseSensitive: false); + final match = pattern.firstMatch(raw); + if (match != null) return match.group(0)!; + } + return ''; + } + bool isEmailFormat(String address) { final RegExp emailRegex = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', @@ -228,19 +239,78 @@ class AddressResolver { name: text, profileImageUrl: twitterUser.profileImageUrl, profileName: twitterUser.name); + } else { + final domain = extractUnstoppableDomain(twitterUser.description); + if (domain.isNotEmpty) { + final parsedAddressFromDomain = await resolve(context, domain, currency); + if (parsedAddressFromDomain.addresses.isNotEmpty) { + return ParsedAddress( + addresses: parsedAddressFromDomain.addresses, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name, + parseFrom: ParseFrom.twitter, + ); + } + + } + + } + + final addressFromLocation = extractAddressByType( + raw: twitterUser.location, + type: CryptoCurrency.fromString(ticker, + walletCurrency: wallet.currency), requireSurroundingWhitespaces: false); + if (addressFromLocation != null && addressFromLocation.isNotEmpty) { + return ParsedAddress.fetchTwitterAddress( + address: addressFromLocation, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name); + } else { + final domain = extractUnstoppableDomain(twitterUser.location); + if (domain.isNotEmpty) { + final parsedAddressFromDomain = await resolve(context, domain, currency); + if (parsedAddressFromDomain.addresses.isNotEmpty) { + return ParsedAddress( + addresses: parsedAddressFromDomain.addresses, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name, + parseFrom: ParseFrom.twitter, + ); + } + + } + } final pinnedTweet = twitterUser.pinnedTweet?.text; if (pinnedTweet != null) { final addressFromPinnedTweet = extractAddressByType( raw: pinnedTweet, - type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency)); + type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency), + requireSurroundingWhitespaces: false); if (addressFromPinnedTweet != null) { return ParsedAddress.fetchTwitterAddress( address: addressFromPinnedTweet, name: text, profileImageUrl: twitterUser.profileImageUrl, profileName: twitterUser.name); + } else { + final domain = extractUnstoppableDomain(pinnedTweet); + if (domain.isNotEmpty) { + final parsedAddressFromDomain = await resolve(context, domain, currency); + if (parsedAddressFromDomain.addresses.isNotEmpty) { + return ParsedAddress( + addresses: parsedAddressFromDomain.addresses, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name, + parseFrom: ParseFrom.twitter, + ); + } + } } } } @@ -258,7 +328,7 @@ class AddressResolver { await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); if (mastodonUser != null) { - String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency); + String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency, requireSurroundingWhitespaces: false); if (addressFromBio != null && addressFromBio.isNotEmpty) { return ParsedAddress.fetchMastodonAddress( @@ -273,7 +343,8 @@ class AddressResolver { if (pinnedPosts.isNotEmpty) { final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n'); String? addressFromPinnedPost = - extractAddressByType(raw: userPinnedPostsText, type: currency); + extractAddressByType(raw: userPinnedPostsText, type: currency, + requireSurroundingWhitespaces: false); if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) { return ParsedAddress.fetchMastodonAddress( diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart index cccf769ad..a508b3edf 100644 --- a/lib/twitter/twitter_api.dart +++ b/lib/twitter/twitter_api.dart @@ -12,7 +12,7 @@ class TwitterApi { static Future lookupUserByName({required String userName}) async { final queryParams = { - 'user.fields': 'description,profile_image_url', + 'user.fields': 'description,profile_image_url,location', 'expansions': 'pinned_tweet_id', 'tweet.fields': 'note_tweet' }; diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart index 01db25684..f124de405 100644 --- a/lib/twitter/twitter_user.dart +++ b/lib/twitter/twitter_user.dart @@ -4,6 +4,7 @@ class TwitterUser { required this.username, required this.name, required this.description, + required this.location, required this.profileImageUrl, this.pinnedTweet}); @@ -11,6 +12,7 @@ class TwitterUser { final String username; final String name; final String description; + final String location; final String profileImageUrl; final Tweet? pinnedTweet; @@ -22,6 +24,7 @@ class TwitterUser { username: json['data']['username'] as String? ?? '', name: json['data']['name'] as String, description: json['data']['description'] as String? ?? '', + location: json['data']['location'] as String? ?? '', profileImageUrl: scaledProfileImageUrl, pinnedTweet: pinnedTweet, );