-
Notifications
You must be signed in to change notification settings - Fork 5
Implemented a way to get a valid x-client-transaction-id header for requests #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import 'package:quacker/database/repository.dart'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return await model.fetch(uri, headers: headers, log: log, authHeader: authHeader); | ||
} else { | ||
return await fetchUnauthenticated(uri, headers: headers, log: log); | ||
} | ||
|
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 { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}); | ||
|
||
return response; | ||
} | ||
|
||
Future<List<Map<String, Object?>>> getAccounts() async { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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": {} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} |
There was a problem hiding this comment.
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