Skip to content

Commit

Permalink
Implement feature #234 Ability to import specific subscriptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
j-fbriere committed Feb 28, 2024
1 parent 0ae2a4f commit 1be16d9
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 88 deletions.
10 changes: 10 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/40.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
* Fix issue #226 App does not open, showing a black screen. Some db records have unexpected null fields.
* Implement feature #218 Share tweet as image.
* Implement feature #232 Allow unauthenticated access like with a browser.
What is possible to do with unauthenticated access:
- view specific tweets (but not their threads);
- view a profile;
- view what is trending;
- import subscriptions.
What is not possible to do with unauthenticated access:
- view search results;
- view feed/groups timelines;
- view tweet threads.
* Implement feature #203 View profile picture and banner.
* Implement feature #234 Ability to import specific subscriptions.
10 changes: 10 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/default.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
* Fix issue #226 App does not open, showing a black screen. Some db records have unexpected null fields.
* Implement feature #218 Share tweet as image.
* Implement feature #232 Allow unauthenticated access like with a browser.
What is possible to do with unauthenticated access:
- view specific tweets (but not their threads);
- view a profile;
- view what is trending;
- import subscriptions.
What is not possible to do with unauthenticated access:
- view search results;
- view feed/groups timelines;
- view tweet threads.
* Implement feature #203 View profile picture and banner.
* Implement feature #234 Ability to import specific subscriptions.
75 changes: 42 additions & 33 deletions lib/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import 'package:quiver/iterables.dart';

const Duration _defaultTimeout = Duration(seconds: 30);

class _SquawkerTwitterClientAllowUnauthenticated extends _SquawkerTwitterClient {
@override
Future<http.Response> get(Uri uri, {Map<String, String>? headers, Duration? timeout}) async {
return getWithRateFetchCtx(uri, headers: headers, timeout: timeout, allowUnauthenticated: true);
}
}

class _SquawkerTwitterClient extends TwitterClient {
static final log = Logger('_SquawkerTwitterClient');

Expand All @@ -26,10 +33,6 @@ class _SquawkerTwitterClient extends TwitterClient {
return getWithRateFetchCtx(uri, headers: headers, timeout: timeout);
}

Future<http.Response> getAllowUnauthenticated(Uri uri, {Map<String, String>? headers, Duration? timeout}) async {
return getWithRateFetchCtx(uri, headers: headers, timeout: timeout, allowUnauthenticated: true);
}

Future<http.Response> getWithRateFetchCtx(Uri uri, {Map<String, String>? headers, Duration? timeout, RateFetchContext? fetchContext, bool allowUnauthenticated = false}) async {
try {
if (allowUnauthenticated && !TwitterAccount.hasAccountAvailable()) {
Expand Down Expand Up @@ -83,6 +86,7 @@ class UnknownProfileUnavailableReason implements Exception {

class Twitter {
static final TwitterApi _twitterApi = TwitterApi(client: _SquawkerTwitterClient());
static final TwitterApi _twitterApiAllowUnauthenticated = TwitterApi(client: _SquawkerTwitterClientAllowUnauthenticated());

static final FFCache _cache = FFCache();

Expand Down Expand Up @@ -217,7 +221,7 @@ class Twitter {
}

static Future<Profile> _getProfile(Uri uri, {bool allowAuthenticated = false}) async {
var response = await (allowAuthenticated ? (_twitterApi.client as _SquawkerTwitterClient).getAllowUnauthenticated(uri) : _twitterApi.client.get(uri));
var response = await (allowAuthenticated ? _twitterApiAllowUnauthenticated.client.get(uri) : _twitterApi.client.get(uri));
if (response.body.isEmpty) {
throw TwitterError(code: 0, message: 'Response is empty', uri: uri.toString());
}
Expand Down Expand Up @@ -266,9 +270,9 @@ class Twitter {

static Future<Follows> getProfileFollows(String screenName, String type, {int? cursor, int? count = 200}) async {
var response = type == 'following'
? await _twitterApi.userService
? await _twitterApiAllowUnauthenticated.userService
.friendsList(screenName: screenName, cursor: cursor, count: count, skipStatus: true)
: await _twitterApi.userService
: await _twitterApiAllowUnauthenticated.userService
.followersList(screenName: screenName, cursor: cursor, count: count, skipStatus: true);

return Follows(
Expand Down Expand Up @@ -341,7 +345,7 @@ class Twitter {
'includePromotedContent': false,
'withVoice': false
};
var response = await (_twitterApi.client as _SquawkerTwitterClient).getAllowUnauthenticated(Uri.https('api.twitter.com', '/graphql/pq4JqttrkAz73WE6s2yUqg/TweetResultByRestId', {
var response = await _twitterApiAllowUnauthenticated.client.get(Uri.https('api.twitter.com', '/graphql/pq4JqttrkAz73WE6s2yUqg/TweetResultByRestId', {
'variables': jsonEncode(variables),
'features': jsonEncode(defaultFeaturesUnauthenticated),
}));
Expand Down Expand Up @@ -632,31 +636,9 @@ class Twitter {
return List.from(jsonDecode(result)).map((e) => TrendLocation.fromJson(e)).toList(growable: false);
}

static void _addParameter(Map<String, String> map, String param, dynamic value) {
if (value is List) {
map[param] = value.join(',');
} else if (value != null) {
map[param] = '$value';
}
}

// reference: dart_twitter_api::TrendsService.place()
static Future<List<Trends>> _placeAllowUnauthenticated({
required int id,
String? exclude,
TransformResponse<List<Trends>> transform = defaultTrendsListTransform
}) async {
final params = <String, String>{};
_addParameter(params, 'id', id);
_addParameter(params, 'exclude', exclude);

return (_twitterApi.client as _SquawkerTwitterClient)
.getAllowUnauthenticated(Uri.https('api.twitter.com', '1.1/trends/place.json', params)).then(transform);
}

static Future<List<Trends>> getTrends(int location) async {
var result = await _cache.getOrCreateAsJSON('trends.$location', const Duration(minutes: 2), () async {
var trends = await _placeAllowUnauthenticated(id: location);
var trends = await _twitterApiAllowUnauthenticated.trendsService.place(id: location);

return jsonEncode(trends.map((e) => e.toJson()).toList());
});
Expand All @@ -675,7 +657,7 @@ class Twitter {
'withVoice': true,
'withV2Timeline': true
};
var response = await (_twitterApi.client as _SquawkerTwitterClient).getAllowUnauthenticated(Uri.https('api.twitter.com', '/graphql/WmvfySbQ0FeY1zk4HU_5ow/UserTweets', {
var response = await _twitterApiAllowUnauthenticated.client.get(Uri.https('api.twitter.com', '/graphql/WmvfySbQ0FeY1zk4HU_5ow/UserTweets', {
'variables': jsonEncode(variables),
'features': jsonEncode(defaultFeaturesUnauthenticated)
}));
Expand Down Expand Up @@ -1009,12 +991,39 @@ class Twitter {
return (await Future.wait(futures)).expand((element) => element).toList();
}

static Future<List<UserWithExtra>> getUsersByScreenName(Iterable<String> screenNames) async {
// Split into groups of 100, as the API only supports that many at a time
List<Future<List<UserWithExtra>>> futures = [];

var groups = partition(screenNames, 100);
for (var group in groups) {
futures.add(_getUsersPageByScreenName(group));
}

return (await Future.wait(futures)).expand((element) => element).toList();
}

static Future<List<UserWithExtra>> _getUsersPage(Iterable<String> ids) async {
var response = await _twitterApi.client.get(Uri.https('api.twitter.com', '/1.1/users/lookup.json', {
var response = await _twitterApiAllowUnauthenticated.client.get(Uri.https('api.twitter.com', '/1.1/users/lookup.json', {
...defaultParams,
'user_id': ids.join(','),
}));

if (response.body.isEmpty) {
return [];
}

var result = json.decode(response.body);

return List.from(result).map((e) => UserWithExtra.fromJson(e)).toList(growable: false);
}

static Future<List<UserWithExtra>> _getUsersPageByScreenName(Iterable<String> screenNames) async {
var response = await _twitterApiAllowUnauthenticated.client.get(Uri.https('api.twitter.com', '/1.1/users/lookup.json', {
...defaultParams,
'screen_name': screenNames.join(','),
}));

var result = json.decode(response.body);

return List.from(result).map((e) => UserWithExtra.fromJson(e)).toList(growable: false);
Expand Down
7 changes: 7 additions & 0 deletions lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Enhanced requests for searches (but with lower rate limits)"),
"enhanced_searches_label":
MessageLookupByLibrary.simpleMessage("Enhanced searches"),
"enter_comma_separated_twitter_usernames":
MessageLookupByLibrary.simpleMessage(
"Enter your comma separated Twitter/X usernames"),
"enter_your_twitter_username": MessageLookupByLibrary.simpleMessage(
"Enter your Twitter/X username"),
"error_from_twitter":
Expand Down Expand Up @@ -455,6 +458,9 @@ class MessageLookup extends MessageLookupByLibrary {
"thumbnail_not_available":
MessageLookupByLibrary.simpleMessage("Thumbnail not available"),
"timed_out": MessageLookupByLibrary.simpleMessage("Timed out"),
"to_import_specific_subscriptions_enter_your_comma_separated_usernames_below":
MessageLookupByLibrary.simpleMessage(
"To import specific subscriptions, enter your comma separated usernames below."),
"to_import_subscriptions_from_an_existing_twitter_account_enter_your_username_below":
MessageLookupByLibrary.simpleMessage(
"To import subscriptions from an existing Twitter/X account, enter your username below."),
Expand Down Expand Up @@ -550,6 +556,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("User not found"),
"username": MessageLookupByLibrary.simpleMessage("Username"),
"username_label": MessageLookupByLibrary.simpleMessage("Username:"),
"usernames": MessageLookupByLibrary.simpleMessage("Usernames"),
"version": MessageLookupByLibrary.simpleMessage("Version"),
"warning_regular_account_unauthenticated_access_description":
MessageLookupByLibrary.simpleMessage(
Expand Down
7 changes: 7 additions & 0 deletions lib/generated/intl/messages_fr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Requêtes améliorés pour les recherches (mais avec des limites plus basses de fréquence)"),
"enhanced_searches_label":
MessageLookupByLibrary.simpleMessage("Recherches améliorés"),
"enter_comma_separated_twitter_usernames":
MessageLookupByLibrary.simpleMessage(
"Saisissez la liste d\'utilisateurs séparés par des virgules"),
"enter_your_twitter_username": MessageLookupByLibrary.simpleMessage(
"Entrer votre nom d\'utilisateur Twitter/X"),
"error_from_twitter":
Expand Down Expand Up @@ -471,6 +474,9 @@ class MessageLookup extends MessageLookupByLibrary {
"thumbnail_not_available":
MessageLookupByLibrary.simpleMessage("Miniature non disponible"),
"timed_out": MessageLookupByLibrary.simpleMessage("Délai expiré"),
"to_import_specific_subscriptions_enter_your_comma_separated_usernames_below":
MessageLookupByLibrary.simpleMessage(
"Pour importer des abonnements spécifiques, saisissez une liste d\'utilisateurs séparés par des virgules ci-dessous."),
"to_import_subscriptions_from_an_existing_twitter_account_enter_your_username_below":
MessageLookupByLibrary.simpleMessage(
"Pour importer des abonnements depuis un compte Twitter/X existant, saisissez votre nom d\'utilisateur ci-dessous."),
Expand Down Expand Up @@ -566,6 +572,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Utilisateur non trouvé"),
"username": MessageLookupByLibrary.simpleMessage("Nom d’utilisateur"),
"username_label": MessageLookupByLibrary.simpleMessage("Identifiant :"),
"usernames": MessageLookupByLibrary.simpleMessage("Utilisateurs"),
"version": MessageLookupByLibrary.simpleMessage("Version"),
"warning_regular_account_unauthenticated_access_description":
MessageLookupByLibrary.simpleMessage(
Expand Down
32 changes: 32 additions & 0 deletions lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -578,5 +578,11 @@
"proxy_error": "Proxy Error",
"@proxy_error": {},
"share_tweet_as_image": "Share tweet as image",
"@share_tweet_as_image": {}
"@share_tweet_as_image": {},
"to_import_specific_subscriptions_enter_your_comma_separated_usernames_below": "To import specific subscriptions, enter your comma separated usernames below.",
"@to_import_specific_subscriptions_enter_your_comma_separated_usernames_below": {},
"enter_comma_separated_twitter_usernames": "Enter your comma separated Twitter/X usernames",
"@enter_comma_separated_twitter_usernames": {},
"usernames": "Usernames",
"@usernames": {}
}
8 changes: 7 additions & 1 deletion lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -608,5 +608,11 @@
"proxy_error": "Erreur Proxy",
"@proxy_error": {},
"share_tweet_as_image": "Partager le tweet en image",
"@share_tweet_as_image": {}
"@share_tweet_as_image": {},
"to_import_specific_subscriptions_enter_your_comma_separated_usernames_below": "Pour importer des abonnements spécifiques, saisissez une liste d'utilisateurs séparés par des virgules ci-dessous.",
"@to_import_specific_subscriptions_enter_your_comma_separated_usernames_below": {},
"enter_comma_separated_twitter_usernames": "Saisissez la liste d'utilisateurs séparés par des virgules",
"@enter_comma_separated_twitter_usernames": {},
"usernames": "Utilisateurs",
"@usernames": {}
}
10 changes: 5 additions & 5 deletions lib/settings/_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ class _AddAccountDialogState extends State<AddAccountDialog> {
controller: _usernameController,
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (text) {
_username = text;
_username = text.trim();
_checkEnabledSave();
},
),
Expand Down Expand Up @@ -287,7 +287,7 @@ class _AddAccountDialogState extends State<AddAccountDialog> {
),
keyboardType: TextInputType.visiblePassword,
onChanged: (text) {
_password = text;
_password = text.trim();
_checkEnabledSave();
},
),
Expand All @@ -309,7 +309,7 @@ class _AddAccountDialogState extends State<AddAccountDialog> {
controller: _nameController,
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (text) {
_name = text;
_name = text.trim();
},
),
),
Expand All @@ -328,7 +328,7 @@ class _AddAccountDialogState extends State<AddAccountDialog> {
controller: _emailController,
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (text) {
_email = text;
_email = text.trim();
},
),
),
Expand All @@ -347,7 +347,7 @@ class _AddAccountDialogState extends State<AddAccountDialog> {
controller: _phoneController,
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (text) {
_phone = text;
_phone = text.trim();
},
),
),
Expand Down
Loading

0 comments on commit 1be16d9

Please sign in to comment.