Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/client/accounts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:quacker/database/repository.dart';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved from client_regular_account.dart

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before you start the review, let me precise this is the first time I work with Dart and a Flutter application. I'm a full time C++ developer. I don't know the best practices for Dart. I tried to mimicate the rest of the code but don't hesitate to ask me to do some corrections if needed :)


Future<List<Map<String, Object?>>> getAccounts() async {
var database = await Repository.readOnly();
return database.query(tableAccounts);
}
6 changes: 3 additions & 3 deletions lib/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:ffcache/ffcache.dart';
import 'package:quacker/catcher/exceptions.dart';
import 'package:quacker/client/client_regular_account.dart';
import 'package:quacker/client/client_unauthenticated.dart';
import 'package:quacker/client/headers.dart';
import 'package:quacker/generated/l10n.dart';
import 'package:quacker/profile/profile_model.dart';
import 'package:quacker/user.dart';
Expand Down Expand Up @@ -36,12 +37,11 @@ class _QuackerTwitterClient extends TwitterClient {
}

static Future<http.Response?> fetch(Uri uri, {Map<String, String>? headers}) async {
var prefs = await PrefServiceShared.init(prefix: 'pref_');
final XRegularAccount model = XRegularAccount();
var authHeader = await model.getAuthHeader(prefs);
final authHeader = await TwitterHeaders.getAuthHeader();

if (authHeader != null) {
return await model.fetch(uri, headers: headers, log: log, prefs: prefs, authHeader: authHeader);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefs was unused, so I removed it

return await model.fetch(uri, headers: headers, log: log, authHeader: authHeader);
} else {
return await fetchUnauthenticated(uri, headers: headers, log: log);
}
Expand Down
33 changes: 4 additions & 29 deletions lib/client/client_regular_account.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:pref/pref.dart';
import 'package:quacker/client/headers.dart';
import 'dart:async';
import "dart:math";
import 'package:quacker/constants.dart';
import 'package:quacker/database/entities.dart';
import 'package:quacker/database/repository.dart';

class XRegularAccount extends ChangeNotifier {
Expand All @@ -17,42 +13,21 @@ class XRegularAccount extends ChangeNotifier {
Future<http.Response?> fetch(Uri uri,
{Map<String, String>? headers,
required Logger log,
required BasePrefService prefs,
required Map<dynamic, dynamic> authHeader}) async {
log.info('Fetching $uri');

final baseHeaders = await TwitterHeaders.getHeaders(uri);

var response = await http.get(uri, headers: {
...?headers,
...authHeader,
...userAgentHeader,
'authorization': bearerToken,
'x-twitter-active-user': 'yes',
'user-agent': userAgentHeader.toString()
...baseHeaders
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseHeaders now contains everything that was here before so it can safely replace what was previously declared. It will include more headers than before however. Tell me if this is a problem. See headers.dart for more info

});

return response;
}

Future<List<Map<String, Object?>>> getAccounts() async {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to accounts.dart

var database = await Repository.readOnly();
return database.query(tableAccounts);
}

Future<void> deleteAccount(String username) async {
var database = await Repository.writable();
database.delete(tableAccounts, where: 'id = ?', whereArgs: [username]);
}

Future<Map<dynamic, dynamic>?> getAuthHeader(BasePrefService prefs) async {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAuthHeader was moved to headers.dart

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefs argument was unused so I removed it

final accounts = await getAccounts();

if (accounts.isNotEmpty) {
Account account = Account.fromMap(accounts[Random().nextInt(accounts.length)]);
final authHeader = Map.castFrom<String, dynamic, String, String>(json.decode(account.authHeader));

return authHeader;
} else {
return null;
}
}
}
70 changes: 70 additions & 0 deletions lib/client/headers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:pref/pref.dart';
import 'dart:math';
import 'package:quacker/database/entities.dart';
import 'package:quacker/constants.dart';

import 'accounts.dart';

class TwitterHeaders {
static final Map<String, String> _baseHeaders = {
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
'authorization': bearerToken,
'cache-control': 'no-cache',
'content-type': 'application/json',
'pragma': 'no-cache',
'priority': 'u=1, i',
'referer': 'https://x.com/',
'user-agent': userAgentHeader['user-agent']!,
'x-twitter-active-user': 'yes',
'x-twitter-client-language': 'en',
};

static Future<Map<String, String>?> getXClientTransactionIdHeader(Uri? uri) async {
if (uri == null) {
return null;
}

final path = uri.path;
final prefs = await PrefServiceShared.init(prefix: 'pref_');
final xClientTransactionIdDomain = prefs.get(optionXClientTransactionIdProvider) ?? optionXClientTransactionIdProviderDefaultDomain;
final xClientTransactionUriEndPoint = Uri.http(xClientTransactionIdDomain, '/generate-x-client-transaction-id', {'path': path});

try {
final response = await http.get(xClientTransactionUriEndPoint);

if (response.statusCode == 200) {
final xClientTransactionId = jsonDecode(response.body)['x-client-transaction-id'];
return {
'x-client-transaction-id': xClientTransactionId
};
} else {
throw Exception('Failed to get x-client-transaction-id. Status code: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting x-client-transaction-id: $e');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception will make it easy to know that x-client-transaction-id.xyz does not work anymore with this message for instance if the server is down.

If the transaction id is correctly generated but not valid on X's side, we will get the same 404 error as before. If 404 happen again in the future, we should consider taking a close look at the validity of the x-client-transaction-id and maybe we will have to update the server that provides it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm the owner of the server & domain, I didn't want to block the app if any of these is down/sold. This is why I added an option to change the URL of the provider in the settings. Anybody could create its own instance. Of course in this case you should update the default URL in Quacker, but users would have a way to change it themselves without any app update

}
}

static Future<Map<String, String>> getHeaders(Uri? uri) async {
final authHeader = await getAuthHeader();
final xClientTransactionIdHeader = await getXClientTransactionIdHeader(uri);
return {
..._baseHeaders,
...?authHeader,
...?xClientTransactionIdHeader
};
}

static Future<Map<dynamic, dynamic>?> getAuthHeader() async {
final accounts = await getAccounts();
if(accounts.isEmpty) {
return null;
}
Account account = Account.fromMap(accounts[Random().nextInt(accounts.length)]);
final authHeader = Map.castFrom<String, dynamic, String, String>(json.decode(account.authHeader));
return authHeader;
}
}
6 changes: 6 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ const optionUserTrendsLocations = 'trends.locations';

const optionNonConfirmationBiasMode = 'other.improve_non_confirmation_bias';

// Default instance of https://github.com/Teskann/x-client-transaction-id-generator
const String optionXClientTransactionIdProviderDefaultDomain = 'x-client-transaction-id-generator.xyz';

const String optionXClientTransactionIdProvider = 'x_client_transaction_id_provider';


final Map<String, String> userAgentHeader = {
'user-agent':
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.3",
Expand Down
7 changes: 6 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -430,5 +430,10 @@
"foryou": "For You",
"clickToShowMore": " \nClick to show more..",
"show_navigation_labels": "Show navigation labels?",
"go_to_profile": "Go to profile: @"
"go_to_profile": "Go to profile: @",
"x_client_transaction_id_provider": "x-client-transaction-id provider",
"@x_client_transaction_id_provider": {},
"x_client_transaction_id_provider_description": "Set the x-client-transaction-id provider. It must he a domain name, without http/https prefix. For more information about it, check https://github.com/Teskann/x-client-transaction-id-generator",
"@x_client_transaction_id_provider_description": {}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell me if these strings look clear to you.

I could use an LLM to translate them to all languages Quacker supports. I didn't because I don't know what you think about it for this project. Tell me if you want me to do it

}
7 changes: 6 additions & 1 deletion lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -490,5 +490,10 @@
"functionality_unsupported": "Cette fonctionnalité n'est plus prise en charge par Twitter !",
"@functionality_unsupported": {},
"add_subscriptions": "Ajouter des abonnements",
"@add_subscriptions": {}
"@add_subscriptions": {},
"x_client_transaction_id_provider": "Fournisseur de x-client-transaction-id",
"@x_client_transaction_id_provider": {},
"x_client_transaction_id_provider_description": "Définir le fournisseur de x-client-transaction-id. Ce doit être un nom de domaine, sans préfixe http/https. Pour plus d'informations, consultez https://github.com/Teskann/x-client-transaction-id-generator",
"@x_client_transaction_id_provider_description": {}

}
3 changes: 2 additions & 1 deletion lib/settings/_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:quacker/client/client_regular_account.dart';
import 'package:quacker/client/login_webview.dart';
import 'package:quacker/generated/l10n.dart';
import 'package:quacker/client/accounts.dart';

class SettingsAccountFragment extends StatefulWidget {
const SettingsAccountFragment({super.key});
Expand All @@ -24,7 +25,7 @@ class _SettingsAccountFragment extends State<SettingsAccountFragment> {
],
),
body: FutureBuilder(
future: model.getAccounts(),
future: getAccounts(),
builder: (BuildContext listContext, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const LinearProgressIndicator();
Expand Down
44 changes: 44 additions & 0 deletions lib/settings/_general.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ class SettingsGeneralFragment extends StatelessWidget {
]);
}

PrefDialog _createXClientTransactionIdDialog(BuildContext context, BasePrefService prefs) {
var mediaQuery = MediaQuery.of(context);
final controller = TextEditingController(
text: prefs.get(optionXClientTransactionIdProvider) ?? optionXClientTransactionIdProviderDefaultDomain
);

return PrefDialog(
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(L10n.of(context).cancel)
),
TextButton(
onPressed: () async {
await prefs.set(optionXClientTransactionIdProvider,
controller.text.isEmpty ? optionXClientTransactionIdProviderDefaultDomain : controller.text);
if (context.mounted) {
Navigator.pop(context);
}
},
child: Text(L10n.of(context).save)
)
],
title: Text(L10n.of(context).x_client_transaction_id_provider),
children: [
SizedBox(
width: mediaQuery.size.width,
child: TextFormField(
controller: controller,
decoration: InputDecoration(
hintText: optionXClientTransactionIdProviderDefaultDomain
),
),
)
]
);
}


@override
Widget build(BuildContext context) {
var prefs = PrefService.of(context);
Expand Down Expand Up @@ -165,6 +204,11 @@ class SettingsGeneralFragment extends StatelessWidget {
pref: optionNonConfirmationBiasMode,
subtitle: Text(L10n.of(context).activate_non_confirmation_bias_mode_description),
),
PrefDialogButton(
title: Text(L10n.of(context).x_client_transaction_id_provider),
subtitle: Text(L10n.of(context).x_client_transaction_id_provider_description),
dialog: _createXClientTransactionIdDialog(context, prefs),
),
]),
),
);
Expand Down