Skip to content

Commit

Permalink
Fix issue #198 Suspicious activity ask confirmation code when adding …
Browse files Browse the repository at this point in the history
…account.
  • Loading branch information
j-fbriere committed Feb 11, 2024
1 parent 2fb08bb commit 402e521
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 61 deletions.
1 change: 1 addition & 0 deletions android/fastlane/metadata/android/en-GB/changelogs/36.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* Fix issues #177 and #196 Import only working partially.
* Fix issue #198 Suspicious activity ask confirmation code when adding account.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/36.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* Fix issues #177 and #196 Import only working partially.
* Fix issue #198 Suspicious activity ask confirmation code when adding account.
286 changes: 225 additions & 61 deletions lib/client/client_regular_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import 'package:squawker/generated/l10n.dart';
class TwitterRegularAccount {
static final log = Logger('TwitterRegularAccount');

static Future<String> _getLoginFlowToken(Map<String,String> headers, String accessToken, String guestToken) async {
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json?flow_name=login');
static Future<Map<String,dynamic>> _loginFlowToken(Map<String,String> headers, String accessToken, String guestToken, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json?flow_name=login';
if (languageCode != null) {
url = '$url&lang=$languageCode';
}
log.info('Posting (login) $url');
headers.addAll({
'Authorization': 'Bearer $accessToken',
'X-Guest-Token': guestToken
});
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json?flow_name=login'),
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
'flow_token': null,
Expand Down Expand Up @@ -44,17 +48,19 @@ class TwitterRegularAccount {
'cookie': 'att=${response.headers['att']}'
});
}
if (result.containsKey('flow_token')) {
return result['flow_token'];
}
return result;
}

throw TwitterAccountException('Unable to get the login flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<String> _getUsernameFlowToken(Map<String,String> headers, String flowToken, String username) async {
log.info('Posting (username) https://api.twitter.com/1.1/onboarding/task.json');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
static Future<Map<String,dynamic>> _userIdentifierFlowToken(Map<String,String> headers, String flowToken, String username, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (userIdentifier) $url');
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
'flow_token': flowToken,
Expand All @@ -73,17 +79,19 @@ class TwitterRegularAccount {

if (response.statusCode == 200) {
var result = jsonDecode(response.body);
if (result.containsKey('flow_token')) {
return result['flow_token'];
}
return result;
}

throw TwitterAccountException('Unable to get the username flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
throw TwitterAccountException('Unable to get the userIdentifier flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<String> _getPasswordFlowToken(Map<String,String> headers, String flowToken, String password) async {
log.info('Posting (password) https://api.twitter.com/1.1/onboarding/task.json');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
static Future<Map<String,dynamic>> _passwordFlowToken(Map<String,String> headers, String flowToken, String password, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (password) $url');
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
'flow_token': flowToken,
Expand All @@ -101,20 +109,18 @@ class TwitterRegularAccount {

if (response.statusCode == 200) {
var result = jsonDecode(response.body);
if (result.containsKey('flow_token')) {
return result['flow_token'];
}
return result;
}

throw TwitterAccountException('Unable to get the password flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<Map<String,dynamic>> _getDuplicationCheckFlowToken(Map<String,String> headers, String flowToken, String? languageCode) async {
static Future<Map<String,dynamic>> _duplicationCheckFlowToken(Map<String,String> headers, String flowToken, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (duplication check) $url');
log.info('Posting (duplicationCheck) $url');
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
Expand Down Expand Up @@ -150,22 +156,86 @@ class TwitterRegularAccount {
return result;
}

throw TwitterAccountException('Unable to get the duplication check flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
throw TwitterAccountException('Unable to get the duplicationCheck flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<Map<String,dynamic>> _get2FAFlowToken(Map<String,String> headers, String flowToken, String code) async {
log.info('Posting (2FA) https://api.twitter.com/1.1/onboarding/task.json');
static Future<Map<String,dynamic>> _twoFactorAuthChallengeFlowToken(Map<String,String> headers, String flowToken, String text, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (twoFactorAuthChallenge) $url');
var response = await http.post(Uri.parse(url),
headers: headers,
body: json.encode({
'flow_token': flowToken,
'subtask_inputs': [
{
'enter_text': {
'text': text,
'link': 'next_link'
},
'subtask_id': 'LoginTwoFactorAuthChallenge'
}
]
})
);

if (response.statusCode == 200) {
var result = jsonDecode(response.body);
return result;
}

throw TwitterAccountException('Unable to get the twoFactorAuthChallenge flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<Map<String,dynamic>> _alternateIdentifierFlowToken(Map<String,String> headers, String flowToken, String text, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (alternateIdentifier) $url');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
headers: headers,
body: json.encode({
'flow_token': flowToken,
'subtask_inputs': [
{
'enter_text': {
'text': text,
'link': 'next_link'
},
'subtask_id': 'LoginEnterAlternateIdentifierSubtask'
}
]
})
);

if (response.statusCode == 200) {
var result = jsonDecode(response.body);
return result;
}

throw TwitterAccountException('Unable to get the alternateIdentifier flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Future<Map<String,dynamic>> _loginAcidFlowToken(Map<String,String> headers, String flowToken, String text, String? languageCode) async {
String url = 'https://api.twitter.com/1.1/onboarding/task.json';
if (languageCode != null) {
url = '$url?lang=$languageCode';
}
log.info('Posting (loginAcid) $url');
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
headers: headers,
body: json.encode({
'flow_token': flowToken,
'subtask_inputs': [
{
'enter_text': {
'text': code,
'text': text,
'link': 'next_link'
},
'subtask_id': 'LoginTwoFactorAuthChallenge'
'subtask_id': 'LoginAcid'
}
]
})
Expand All @@ -176,34 +246,116 @@ class TwitterRegularAccount {
return result;
}

throw TwitterAccountException('Unable to get the 2FA flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
throw TwitterAccountException('Unable to get the loginAcid flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
}

static Map<String,dynamic>? _findSubtask(List<dynamic>? subtasks, String subtaskId) {
if (subtasks == null) {
return null;
}
for (Map<String,dynamic> subtask in subtasks) {
if (subtask['subtask_id'] == subtaskId) {
return subtask;
}
}
return null;
}

static Future<TwitterTokenEntity> createRegularTwitterToken(BuildContext? context, String? languageCode, String username, String password, String? name, String? email, String? phone) async {
String accessToken = await TwitterAccount.getAccessToken();
String guestToken = await TwitterAccount.getGuestToken(accessToken);
Map<String,String> headers = TwitterAccount.initHeaders();
String flowToken = await _getLoginFlowToken(headers, accessToken, guestToken);
flowToken = await _getUsernameFlowToken(headers, flowToken, username);
flowToken = await _getPasswordFlowToken(headers, flowToken, password);
Map<String,dynamic> res = await _getDuplicationCheckFlowToken(headers, flowToken, languageCode);
Map<String,dynamic>? openAccount;
if (res['subtasks']?[0]?['subtask_id'] == 'LoginTwoFactorAuthChallenge') {
if (context != null) {
flowToken = res['flow_token'];
Map<String,dynamic> head = res['subtasks'][0]['enter_text']['header'];
String? code = await askForTwoFactorCode(context, head['primary_text']['text'] as String, head['secondary_text']['text'] as String);
if (code != null) {
res = await _get2FAFlowToken(headers, flowToken, code);
if (res['subtasks']?[0]?['subtask_id'] == 'LoginSuccessSubtask') {
openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
Map<String,dynamic> res = await _loginFlowToken(headers, accessToken, guestToken, languageCode);
String flowToken = res['flow_token'];
Map<String,dynamic>? subtask;
while ((subtask = _findSubtask(res['subtasks'], 'LoginSuccessSubtask')) == null) {
if ((subtask = _findSubtask(res['subtasks'], 'LoginEnterUserIdentifier')) != null) {
res = await _userIdentifierFlowToken(headers, flowToken, username, languageCode);
}
else if ((subtask = _findSubtask(res['subtasks'], 'LoginEnterPassword')) != null) {
res = await _passwordFlowToken(headers, flowToken, password, languageCode);
}
else if ((subtask = _findSubtask(res['subtasks'], 'AccountDuplicationCheck')) != null) {
res = await _duplicationCheckFlowToken(headers, flowToken, languageCode);
}
else if ((subtask = _findSubtask(res['subtasks'], 'LoginTwoFactorAuthChallenge')) != null) {
if (context != null) {
Map<String,dynamic>? head = subtask!['enter_text']['header'];
String? text1 = head?['primary_text']['text'];
String? text2 = head?['secondary_text']['text'];
if ((text1?.isEmpty ?? true) && (text2?.isEmpty ?? true)) {
text1 = 'Enter code';
}
else if (text1?.isEmpty ?? true) {
text1 = text2;
text2 = null;
}
String? text = await _askForInput(context, text1!, text2);
if (text?.isNotEmpty ?? false) {
res = await _twoFactorAuthChallengeFlowToken(headers, flowToken, text!, languageCode);
}
else {
throw TwitterAccountException('No input provided for LoginTwoFactorAuthChallenge');
}
}
else {
throw TwitterAccountException('No context to ask for input for LoginTwoFactorAuthChallenge');
}
}
else if ((subtask = _findSubtask(res['subtasks'], 'LoginEnterAlternateIdentifierSubtask')) != null) {
if (context != null) {
Map<String,dynamic>? enterText = subtask!['enter_text'];
String? text1 = enterText?['primary_text']['text'];
String? text2 = enterText?['secondary_text']['text'];
if ((text1?.isEmpty ?? true) && (text2?.isEmpty ?? true)) {
text1 = 'Enter code';
}
else if (text1?.isEmpty ?? true) {
text1 = text2;
text2 = null;
}
String? text = await _askForInput(context, text1!, text2);
if (text?.isNotEmpty ?? false) {
res = await _alternateIdentifierFlowToken(headers, flowToken, text!, languageCode);
}
else {
throw TwitterAccountException('No input provided for LoginEnterAlternateIdentifierSubtask');
}
}
else {
throw TwitterAccountException('No context to ask for input for LoginEnterAlternateIdentifierSubtask');
}
}
else if ((subtask = _findSubtask(res['subtasks'], 'LoginAcid')) != null) {
if (context != null) {
Map<String,dynamic>? head = subtask!['enter_text']['header'];
String? text1 = head?['primary_text']['text'];
String? text2 = head?['secondary_text']['text'];
if ((text1?.isEmpty ?? true) && (text2?.isEmpty ?? true)) {
text1 = 'Enter code';
}
else if (text1?.isEmpty ?? true) {
text1 = text2;
text2 = null;
}
String? text = await _askForInput(context, text1!, text2);
if (text?.isNotEmpty ?? false) {
res = await _loginAcidFlowToken(headers, flowToken, text!, languageCode);
}
else {
throw TwitterAccountException('No input provided for LoginAcid');
}
}
else {
throw TwitterAccountException('No context to ask for input for LoginAcid');
}
}
else {
throw TwitterAccountException('Don''t know what to do with ${jsonEncode(res['subtasks'])}');
}
flowToken = res['flow_token'];
}
else if (res['subtasks']?[0]?['subtask_id'] == 'LoginSuccessSubtask') {
openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
}
Map<String,dynamic>? openAccount = subtask!['open_account'] as Map<String,dynamic>?;
if (openAccount != null) {
TwitterTokenEntity tte = TwitterTokenEntity(
guest: false,
Expand All @@ -218,31 +370,43 @@ class TwitterRegularAccount {

return tte;
}
throw TwitterAccountException('Unable to create the regular Twitter/X token. The response from Twitter/X was: $res');
throw TwitterAccountException('Unable to create the regular Twitter/X token. The response from Twitter/X was: ${jsonEncode(res)}');
}

static Future<String?> askForTwoFactorCode(BuildContext context, String primaryText, String secondaryText) async {
String? code;
static Future<String?> _askForInput(BuildContext context, String primaryText, String? secondaryText) async {
String? returnedText;
Widget contentWidget;
if (secondaryText?.isEmpty ?? true) {
contentWidget = TextField(
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (value) async {
returnedText = value;
},
);
}
else {
contentWidget = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(secondaryText!, style: TextStyle(fontSize: Theme.of(context).textTheme.labelMedium!.fontSize)),
SizedBox(height: 20),
TextField(
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (value) async {
returnedText = value;
},
)
]
);
}
return await showDialog<String?>(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(primaryText),
titleTextStyle: TextStyle(fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, color: Theme.of(context).textTheme.titleMedium!.color, fontWeight: FontWeight.bold),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(secondaryText, style: TextStyle(fontSize: Theme.of(context).textTheme.labelMedium!.fontSize)),
SizedBox(height: 20),
TextField(
decoration: InputDecoration(contentPadding: EdgeInsets.all(5)),
onChanged: (value) async {
code = value;
},
)
]
),
content: contentWidget,
actionsAlignment: MainAxisAlignment.center,
actions: [
ElevatedButton(
Expand All @@ -254,7 +418,7 @@ class TwitterRegularAccount {
ElevatedButton(
child: Text(L10n.current.ok),
onPressed: () {
Navigator.of(context).pop(code);
Navigator.of(context).pop(returnedText);
},
),
],
Expand Down

0 comments on commit 402e521

Please sign in to comment.