From 83c1907e31460d7fc5f4679249e52f6dfe1ed1e4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 24 Jan 2023 20:24:46 +0200 Subject: [PATCH] add twitter api --- lib/core/address_validator.dart | 18 +++++++++ lib/entities/parse_address_from_domain.dart | 30 ++++++++++++--- lib/entities/parsed_address.dart | 11 +++++- .../widgets/extract_address_from_parsed.dart | 5 +++ lib/twitter/twitter_api.dart | 37 +++++++++++++++++++ lib/twitter/twitter_user.dart | 16 ++++++++ 6 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 lib/twitter/twitter_api.dart create mode 100644 lib/twitter/twitter_user.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 519cd92a3..20cb66a4d 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -198,4 +198,22 @@ class AddressValidator extends TextValidator { return []; } } + + static String? getAddressFromStringPattern(CryptoCurrency type) { + switch (type) { + case CryptoCurrency.xmr: + return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.btc: + return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)'; + default: + return null; + } + } } diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 775f6e229..0affa1d7e 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -1,18 +1,19 @@ +import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; +import 'package:cake_wallet/twitter/twitter_api.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; class AddressResolver { - AddressResolver({required this.yatService, required this.walletType}); - final YatService yatService; final WalletType walletType; - + static const unstoppableDomains = [ 'crypto', 'zil', @@ -26,9 +27,28 @@ class AddressResolver { 'blockchain' ]; + static String? extractAddressByType({required String raw, required CryptoCurrency type}) { + final addressPattern = AddressValidator.getAddressFromStringPattern(type); + + if (addressPattern == null) { + throw 'Unexpected token: $type for getAddressFromStringPattern'; + } + + final match = RegExp(addressPattern).firstMatch(raw); + return match?.group(0)?.replaceAll(RegExp('[^0-9a-zA-Z]'), ''); + } + Future resolve(String text, String ticker) async { try { - if (text.contains('@') && !text.contains('.')) { + if (text.startsWith('@') && !text.substring(1).contains('@')) { + final formattedName = text.substring(1); + final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); + final address = extractAddressByType(raw: twitterUser.description ?? '', type: CryptoCurrency.fromString(ticker)); + if (address != null) { + return ParsedAddress.fetchTwitterAddress(address: address, name: text); + } + } + if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { final bool isFioRegistered = await FioAddressProvider.checkAvail(text); if (isFioRegistered) { final address = await FioAddressProvider.getPubAddress(text, ticker); @@ -58,7 +78,7 @@ class AddressResolver { final record = await OpenaliasRecord.fetchAddressAndName( formattedName: formattedName, ticker: ticker); return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); - + } catch (e) { print(e.toString()); } diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 09cecbf04..afadb7b98 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -1,8 +1,7 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -import 'package:flutter/material.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed } +enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter } class ParsedAddress { ParsedAddress({ @@ -62,6 +61,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchTwitterAddress({required String address, required String name}){ + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.twitter, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 4ddd6ada0..1cd7bf0b9 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -28,6 +28,11 @@ Future extractAddressFromParsed( content = S.of(context).openalias_alert_content(parsedAddress.name); address = parsedAddress.addresses.first; break; + case ParseFrom.twitter: + title = S.of(context).address_detected; + content = S.of(context).openalias_alert_content(parsedAddress.name); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart new file mode 100644 index 000000000..bd03e6e00 --- /dev/null +++ b/lib/twitter/twitter_api.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'package:cake_wallet/twitter/twitter_user.dart'; +import 'package:http/http.dart' as http; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +class TwitterApi { + static const twitterBearerToken = secrets.twitterBearerToken; + static const httpsScheme = 'https'; + static const apiHost = 'api.twitter.com'; + static const userPath = '/2/users/by/username/'; + + static Future lookupUserByName({required String userName}) async { + final queryParams = {'user.fields': 'description'}; + + final headers = {'authorization': 'Bearer ${secrets.twitterBearerToken}'}; + + final uri = Uri( + scheme: httpsScheme, + host: apiHost, + path: userPath + userName, + queryParameters: queryParams, + ); + + var response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + final responseJSON = json.decode(response.body) as Map; + + if (responseJSON['errors'] != null) { + throw Exception(responseJSON['errors'][0]['detail']); + } + + return TwitterUser.fromJson(responseJSON['data'] as Map); + } +} diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart new file mode 100644 index 000000000..c4bda7859 --- /dev/null +++ b/lib/twitter/twitter_user.dart @@ -0,0 +1,16 @@ +class TwitterUser { + TwitterUser({required this.id, required this.username, required this.name, this.description}); + + final String id; + final String username; + final String name; + final String? description; + + factory TwitterUser.fromJson(Map json) { + return TwitterUser( + id: json['id'] as String, + username: json['username'] as String, + name: json['name'] as String, + description: json['description'] as String?); + } +}