Skip to content

Commit c7d3a86

Browse files
committed
Merge branch 'ca/feat_185'
2 parents cf89583 + 9f1989e commit c7d3a86

39 files changed

+1532
-362
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
* Feature #185 Using regular Twitter/X accounts.
2+
To add the information of an existing regular Twitter/X account, go to Settings / Account, tap on the plus icon.
3+
Only the username and password are mandatory. When saved an authentication token is created. It is automatically renewed every 30 days.
4+
There can be many existing regular Twitter/X accounts.
5+
To delete a regular Twitter/X account information (and its authenticated token), swipe to the left and tap on delete.
6+
The project's wiki describe how to create a (regular) Twitter/X account.
7+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
* Feature #185 Using regular Twitter/X accounts.
2+
To add the information of an existing regular Twitter/X account, go to Settings / Account, tap on the plus icon.
3+
Only the username and password are mandatory. When saved an authentication token is created. It is automatically renewed every 30 days.
4+
There can be many existing regular Twitter/X accounts.
5+
To delete a regular Twitter/X account information (and its authenticated token), swipe to the left and tap on delete.
6+
The project's wiki describe how to create a (regular) Twitter/X account.

lib/client.dart renamed to lib/client/client.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:squawker/profile/profile_model.dart';
99
import 'package:squawker/user.dart';
1010
import 'package:squawker/utils/cache.dart';
1111
import 'package:squawker/utils/iterables.dart';
12-
import 'package:squawker/client_account.dart';
12+
import 'package:squawker/client/client_account.dart';
1313
import 'package:http/http.dart' as http;
1414
import 'package:logging/logging.dart';
1515
import 'package:quiver/iterables.dart';
@@ -38,7 +38,7 @@ class _SquawkerTwitterClient extends TwitterClient {
3838
}
3939
}
4040
on Exception catch (err) {
41-
if (err is! GuestAccountException && err is! RateLimitException) {
41+
if (err is! TwitterAccountException && err is! RateLimitException) {
4242
log.severe('The request ${uri.path} has an error: ${err.toString()}');
4343
}
4444
return Future.error(ExceptionResponse(err));
Lines changed: 305 additions & 284 deletions
Large diffs are not rendered by default.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'dart:convert';
2+
import 'package:http/http.dart' as http;
3+
import 'package:logging/logging.dart';
4+
import 'package:squawker/client/client_account.dart';
5+
import 'package:squawker/database/entities.dart';
6+
import 'package:squawker/utils/iterables.dart';
7+
8+
class TwitterGuestAccount {
9+
static final log = Logger('TwitterGuestAccount');
10+
11+
static Future<String> _getWelcomeFlowToken(Map<String,String> headers, String accessToken, String guestToken) async {
12+
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json?flow_name=welcome');
13+
headers.addAll({
14+
'Authorization': 'Bearer $accessToken',
15+
'X-Guest-Token': guestToken
16+
});
17+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json?flow_name=welcome'),
18+
headers: headers,
19+
body: json.encode({
20+
'flow_token': null,
21+
'input_flow_data': {
22+
'flow_context': {
23+
'start_location': {
24+
'location': 'splash_screen'
25+
}
26+
}
27+
}
28+
})
29+
);
30+
31+
if (response.statusCode == 200) {
32+
var result = jsonDecode(response.body);
33+
if (result.containsKey('flow_token')) {
34+
return result['flow_token'];
35+
}
36+
}
37+
38+
throw TwitterAccountException('Unable to get the welcome flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
39+
}
40+
41+
static Future<TwitterTokenEntity> _getGuestTwitterTokenFromTwitter(Map<String,String> headers, String flowToken) async {
42+
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json');
43+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
44+
headers: headers,
45+
body: json.encode({
46+
'flow_token': flowToken,
47+
'subtask_inputs': [{
48+
'open_link': {
49+
'link': 'next_link'
50+
},
51+
'subtask_id': 'NextTaskOpenLink'
52+
}]
53+
})
54+
);
55+
56+
if (response.statusCode == 200) {
57+
var result = jsonDecode(response.body);
58+
List? subtasks = result['subtasks'];
59+
if (subtasks != null) {
60+
var accountElm = subtasks.firstWhereOrNull((task) => task['subtask_id'] == 'OpenAccount');
61+
if (accountElm != null) {
62+
var account = accountElm['open_account'];
63+
log.info("Guest Twitter/X token created! oauth_token=${account['oauth_token']} oauth_token_secret=${account['oauth_token_secret']}");
64+
return TwitterTokenEntity(
65+
guest: true,
66+
idStr: account['user']?['id_str'],
67+
screenName: account['user']?['screen_name'],
68+
oauthToken: account['oauth_token'],
69+
oauthTokenSecret: account['oauth_token_secret'],
70+
createdAt: DateTime.now()
71+
);
72+
}
73+
}
74+
}
75+
76+
throw TwitterAccountException('Unable to create the guest Twitter/X token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
77+
}
78+
79+
static Future<TwitterTokenEntity> createGuestTwitterToken() async {
80+
String accessToken = await TwitterAccount.getAccessToken();
81+
String guestToken = await TwitterAccount.getGuestToken(accessToken);
82+
Map<String,String> headers = TwitterAccount.initHeaders();
83+
String flowToken = await _getWelcomeFlowToken(headers, accessToken, guestToken);
84+
TwitterTokenEntity guestTwitterToken = await _getGuestTwitterTokenFromTwitter(headers, flowToken);
85+
86+
TwitterAccount.addTwitterToken(guestTwitterToken);
87+
return guestTwitterToken;
88+
}
89+
90+
}
91+
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import 'dart:convert';
2+
import 'package:squawker/database/entities.dart';
3+
import 'package:http/http.dart' as http;
4+
import 'package:logging/logging.dart';
5+
import 'package:squawker/client/client_account.dart';
6+
7+
class TwitterRegularAccount {
8+
static final log = Logger('TwitterRegularAccount');
9+
10+
static Future<String> _getLoginFlowToken(Map<String,String> headers, String accessToken, String guestToken) async {
11+
log.info('Posting https://api.twitter.com/1.1/onboarding/task.json?flow_name=login&api_version=1&known_device_token=&sim_country_code=us');
12+
headers.addAll({
13+
'Authorization': 'Bearer $accessToken',
14+
'X-Guest-Token': guestToken
15+
});
16+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json?flow_name=login&api_version=1&known_device_token=&sim_country_code=us'),
17+
headers: headers,
18+
body: json.encode({
19+
'flow_token': null,
20+
'input_flow_data': {
21+
'country_code': null,
22+
'flow_context': {
23+
'referrer_context': {
24+
'referral_details': 'utm_source=google-play&utm_medium=organic',
25+
'referrer_url': ''
26+
},
27+
'start_location': {
28+
'location': 'deeplink'
29+
}
30+
},
31+
'requested_variant': null,
32+
'target_user_id': 0
33+
}
34+
})
35+
);
36+
37+
if (response.statusCode == 200) {
38+
var result = jsonDecode(response.body);
39+
if (response.headers.containsKey('att')) {
40+
headers.addAll({
41+
'att': response.headers['att'] as String,
42+
'cookie': 'att=${response.headers['att']}'
43+
});
44+
}
45+
if (result.containsKey('flow_token')) {
46+
return result['flow_token'];
47+
}
48+
}
49+
50+
throw TwitterAccountException('Unable to get the login flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
51+
}
52+
53+
static Future<String> _getUsernameFlowToken(Map<String,String> headers, String flowToken, String username) async {
54+
log.info('Posting (username) https://api.twitter.com/1.1/onboarding/task.json');
55+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
56+
headers: headers,
57+
body: json.encode({
58+
'flow_token': flowToken,
59+
'subtask_inputs': [
60+
{
61+
'enter_text': {
62+
'suggestion_id': null,
63+
'text': username,
64+
'link': 'next_link'
65+
},
66+
'subtask_id': 'LoginEnterUserIdentifier'
67+
}
68+
]
69+
})
70+
);
71+
72+
if (response.statusCode == 200) {
73+
var result = jsonDecode(response.body);
74+
if (result.containsKey('flow_token')) {
75+
return result['flow_token'];
76+
}
77+
}
78+
79+
throw TwitterAccountException('Unable to get the username flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
80+
}
81+
82+
static Future<String> _getPasswordFlowToken(Map<String,String> headers, String flowToken, String password) async {
83+
log.info('Posting (password) https://api.twitter.com/1.1/onboarding/task.json');
84+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
85+
headers: headers,
86+
body: json.encode({
87+
'flow_token': flowToken,
88+
'subtask_inputs': [
89+
{
90+
'enter_password': {
91+
'password': password,
92+
'link': 'next_link'
93+
},
94+
'subtask_id': 'LoginEnterPassword'
95+
}
96+
]
97+
})
98+
);
99+
100+
if (response.statusCode == 200) {
101+
var result = jsonDecode(response.body);
102+
if (result.containsKey('flow_token')) {
103+
return result['flow_token'];
104+
}
105+
}
106+
107+
throw TwitterAccountException('Unable to get the password flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
108+
}
109+
110+
static Future<Map<String,dynamic>> _getDuplicationCheckFlowToken(Map<String,String> headers, String flowToken) async {
111+
log.info('Posting (duplication check) https://api.twitter.com/1.1/onboarding/task.json');
112+
var response = await http.post(Uri.parse('https://api.twitter.com/1.1/onboarding/task.json'),
113+
headers: headers,
114+
body: json.encode({
115+
'flow_token': flowToken,
116+
'subtask_inputs': [
117+
{
118+
'check_logged_in_account': {
119+
'link': 'AccountDuplicationCheck_false'
120+
},
121+
'subtask_id': 'AccountDuplicationCheck'
122+
}
123+
]
124+
})
125+
);
126+
127+
if (response.statusCode == 200) {
128+
List<String> cookies = [];
129+
if (headers.containsKey('cookie')) {
130+
String attCookie = headers['cookie'] as String;
131+
cookies.add(attCookie);
132+
headers.remove('cookie');
133+
}
134+
if (response.headers.containsKey('auth_token')) {
135+
cookies.add('auth_token=${response.headers['auth_token']}');
136+
}
137+
if (response.headers.containsKey('ct0')) {
138+
cookies.add('ct0=${response.headers['ct0']}');
139+
}
140+
if (cookies.isNotEmpty) {
141+
headers['cookie'] = cookies.join(';');
142+
}
143+
var result = jsonDecode(response.body);
144+
return result;
145+
}
146+
147+
throw TwitterAccountException('Unable to get the duplication check flow token. The response (${response.statusCode}) from Twitter/X was: ${response.body}');
148+
}
149+
150+
static Future<TwitterTokenEntity> createRegularTwitterToken(String username, String password, String? name, String? email, String? phone) async {
151+
String accessToken = await TwitterAccount.getAccessToken();
152+
String guestToken = await TwitterAccount.getGuestToken(accessToken);
153+
Map<String,String> headers = TwitterAccount.initHeaders();
154+
String flowToken = await _getLoginFlowToken(headers, accessToken, guestToken);
155+
flowToken = await _getUsernameFlowToken(headers, flowToken, username);
156+
flowToken = await _getPasswordFlowToken(headers, flowToken, password);
157+
Map<String,dynamic> res = await _getDuplicationCheckFlowToken(headers, flowToken);
158+
if (res['subtasks']?[0]?['open_account'] != null) {
159+
Map<String,dynamic> openAccount = res['subtasks'][0]['open_account'] as Map<String,dynamic>;
160+
TwitterTokenEntity tte = TwitterTokenEntity(
161+
guest: false,
162+
idStr: (openAccount['user'] as Map<String,dynamic>)['id_str'] as String,
163+
screenName: (openAccount['user'] as Map<String,dynamic>)['screen_name'] as String,
164+
oauthToken: openAccount['oauth_token'] as String,
165+
oauthTokenSecret: openAccount['oauth_token_secret'] as String,
166+
createdAt: DateTime.now(),
167+
profile: await TwitterAccount.getOrCreateProfile(username, password, name, email, phone)
168+
);
169+
await TwitterAccount.addTwitterToken(tte);
170+
171+
return tte;
172+
}
173+
throw TwitterAccountException('Unable to create the regular Twitter/X token. The response from Twitter/X was: $res');
174+
}
175+
}
176+

0 commit comments

Comments
 (0)