1# nitter-guest-account.py
2# cross-platform port of https://github.com/zedeus/nitter/issues/983#issuecomment-1681199357
3# thank you!
4import sys
5import json
6import typing
7import traceback
8from base64 import b64encode
9from argparse import ArgumentParser
10
11try:
12 import requests
13except ImportError:
14 print("\x1b[31m[!] Could not import `requests`.")
15 print("\x1b[31m[!] This script requires the requests module to be installed.")
16 print("\x1b[31m[!] We apologize but using plain http.client is way too painful."
17 " Please reach out with a PR if you would like to change that!")
18 sys.exit(1)
19
20verbose = False
21noprettyprint = False
22
23# Constants
24CONSUMER_KEY = "3nVuSoBZnx6U4vzUxf5w"
25CONSUMER_SECRET = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"
26EXPECTED_BEARER_TOKEN = "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"
27BASE_REQUEST_HEADERS = {
28 'Content-Type': 'application/json',
29 'User-Agent': 'TwitterAndroid/9.95.0-release.0 (29950000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)',
30 'X-Twitter-API-Version': '5',
31 'X-Twitter-Client': 'TwitterAndroid',
32 'X-Twitter-Client-Version': '9.95.0-release.0',
33 'OS-Version': '28',
34 'System-User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)',
35 'X-Twitter-Active-User': 'yes',
36}
37
38BASE_URL = "https://api.twitter.com"
39BEARER_TOKEN_ENDPOINT = "/oauth2/token"
40GUEST_TOKEN_ENDPOINT = "/1.1/guest/activate.json"
41FLOW_TOKEN_ENDPOINT = "/1.1/onboarding/task.json?flow_name=welcome&api_version=1&known_device_token=&sim_country_code=us"
42TASKS_ENDPOINT = "/1.1/onboarding/task.json"
43
44def send_req(method, endpoint, **kwargs) -> requests.Response:
45 debug(f"attempting `{endpoint}`")
46 try:
47 res = requests.request(method, BASE_URL + endpoint, **kwargs)
48 res.raise_for_status()
49 except requests.HTTPError:
50 error("HTTP request failed (non 2xx), unable to proceed.")
51 error('Please try again in a bit')
52 debug(f"request headers => {res.request.headers}")
53 debug(f"response headers => {res.headers}")
54 debug(f'response body => {res.content}')
55 sys.exit(1)
56 except Exception:
57 error(f"an unhandled error while sending a request to {endpoint} occurred")
58 raise
59
60 debug(f'got response body {res.content}')
61 return res
62
63B_SUBTASK_VERSIONS = {
64 "generic_urt": 3,
65 "standard": 1,
66 "open_home_timeline": 1,
67 "app_locale_update": 1,
68 "enter_date": 1,
69 "email_verification": 3,
70 "enter_password": 5,
71 "enter_text": 5,
72 "one_tap": 2,
73 "cta": 7,
74 "single_sign_on": 1,
75 "fetch_persisted_data": 1,
76 "enter_username": 3,
77 "web_modal": 2,
78 "fetch_temporary_password": 1,
79 "menu_dialog": 1,
80 "sign_up_review": 5,
81 "interest_picker": 4,
82 "user_recommendations_urt": 3,
83 "in_app_notification": 1,
84 "sign_up": 2,
85 "typeahead_search": 1,
86 "user_recommendations_list": 4,
87 "cta_inline": 1,
88 "contacts_live_sync_permission_prompt": 3,
89 "choice_selection": 5,
90 "js_instrumentation": 1,
91 "alert_dialog_suppress_client_events": 1,
92 "privacy_options": 1,
93 "topics_selector": 1,
94 "wait_spinner": 3,
95 "tweet_selection_urt": 1,
96 "end_flow": 1,
97 "settings_list": 7,
98 "open_external_link": 1,
99 "phone_verification": 5,
100 "security_key": 3,
101 "select_banner": 2,
102 "upload_media": 1,
103 "web": 2,
104 "alert_dialog": 1,
105 "open_account": 2,
106 "action_list": 2,
107 "enter_phone": 2,
108 "open_link": 1,
109 "show_code": 1,
110 "update_users": 1,
111 "check_logged_in_account": 1,
112 "enter_email": 2,
113 "select_avatar": 4,
114 "location_permission_prompt": 2,
115 "notifications_permission_prompt": 4
116}
117
118def get_flow_token_body():
119 return {
120 "flow_token": None,
121 "input_flow_data": {
122 "country_code": None,
123 "flow_context": {
124 "start_location": {
125 "location": "splash_screen"
126 }
127 },
128 "requested_variant": None,
129 "target_user_id": 0
130 },
131 "subtask_versions": B_SUBTASK_VERSIONS
132 }
133
134def get_tasks_body(flow_token: str) -> dict:
135 return {
136 "flow_token": flow_token,
137 "subtask_inputs": [{
138 "open_link": {
139 "link": "next_link"
140 },
141 "subtask_id": "NextTaskOpenLink"
142 }],
143 "subtask_versions": B_SUBTASK_VERSIONS
144 }
145
146# Functions
147def format_json(object) -> str:
148 global noprettyprint
149 return json.dumps(object, indent=None if noprettyprint else 4)
150
151def debug(msg, *arg, override=False, **kwarg) -> None:
152 global verbose
153 if verbose or override:
154 print("\x1b[37m[*]", msg, *arg, "\x1b[0m", file=sys.stderr, **kwarg)
155
156def info(msg, *arg, **kwarg) -> None:
157 print("\x1b[34m[i]", msg, *arg, "\x1b[0m", file=sys.stderr, **kwarg)
158
159
160def success(msg, *arg, **kwarg) -> None:
161 print("\x1b[32m[i]", msg, *arg, "\x1b[0m", file=sys.stderr, **kwarg)
162
163
164def warn(msg, *arg, **kwarg) -> None:
165 print("\x1b[33m[!]", msg, *arg, "\x1b[0m", file=sys.stderr, **kwarg)
166
167
168def error(msg, *arg, **kwarg) -> None:
169 print("\x1b[31m[x]", msg, *arg, "\x1b[0m", file=sys.stderr, **kwarg)
170
171
172def prompt_bool(msg, default: typing.Optional[bool] = True) -> bool:
173 """
174 Prompt the user for a y/n value until a valid input is entered.
175
176 :param msg: Message to display
177 :param default: What should the default value be if the user pressed enter. Pass in None to force user to pick one.
178 """
179 resolved = False
180 p_string = f"({'Y' if default else 'y'}/{'N' if not default else 'n'})" if default is not None else "(y/n)"
181 while not resolved:
182 print("\x1b[35m[?]", msg, f"\x1b[0m{p_string}", file=sys.stderr, end=" ")
183 try:
184 r = input()
185 except EOFError:
186 debug("^D", override=True)
187 debug("gracefully handling EOF")
188 error("invalid input, please try again.")
189 continue
190
191 if default is None and r.strip() == "":
192 error('a response is required. please enter either y or n.')
193 continue
194 if r.strip() == "":
195 return default
196 # if r.strip()[0].lower() not in ['y', 'n']:
197 # error('invalid input. please enter either y or n.')
198 # continue
199 match r.strip()[0].lower():
200 case "y":
201 return True
202 case "n":
203 return False
204 case _:
205 error('invalid input. please enter either y or n.')
206 continue
207
208
209# Arguments
210parser = ArgumentParser()
211parser.add_argument('-v', '--verbose', action='store_true', help="be more noisy")
212parser.add_argument('-P', '--no-pretty', action='store_true', help="disable pretty-printing of json data")
213parser.add_argument(
214 'outfile',
215 nargs="?",
216 default="-",
217 help="the json output file to put/append received account data to."
218)
219
220
221def main() -> int:
222 global verbose, noprettyprint
223 args = parser.parse_args()
224 verbose = args.verbose
225 noprettyprint = args.no_pretty
226
227 info("nitter-guest-account.py (2023-08-25)")
228 info("This is free software: you are free to change and redistribute it, under the terms of the Apache-2.0 license")
229 info("There is NO WARRANTY, to the extent permitted by law.")
230
231 info("Fetching bearer token...")
232 bt_raw = send_req('post', BEARER_TOKEN_ENDPOINT,
233 auth=requests.auth.HTTPBasicAuth(CONSUMER_KEY, CONSUMER_SECRET),
234 data={'grant_type': "client_credentials"}
235 ).json()
236 bearer_token = ' '.join(bt_raw.values())
237 if bearer_token.lower() != EXPECTED_BEARER_TOKEN.lower():
238 warn('Received bearer token does not match expected value. Continuing anyways, but beware of errors.')
239 info(f'bearer token => {bearer_token}')
240 else:
241 success('Received bearer token matches expected value.')
242
243 info("Fetching guest token...")
244 guest_token = send_req('post', GUEST_TOKEN_ENDPOINT, headers={'Authorization': bearer_token}).json()['guest_token']
245 success(f'guest token => {guest_token}')
246
247 debug('updating header with acquried credentials')
248 request_headers = BASE_REQUEST_HEADERS.copy()
249 request_headers.update({
250 "authorization": bearer_token,
251 "X-Guest-Token": guest_token
252 })
253
254 info('Fetching flow token...')
255 flow_token = send_req('post', FLOW_TOKEN_ENDPOINT, headers=request_headers, json=get_flow_token_body()).json()['flow_token']
256 success(f'flow token => {flow_token}')
257
258 info('Fetching final account object...')
259 tasks = send_req('post', TASKS_ENDPOINT, headers=request_headers, json=get_tasks_body(flow_token)).json()
260 try:
261 try:
262 open_account_task = next(filter(lambda i: i.get('subtask_id') == "OpenAccount", tasks['subtasks']))
263 account = open_account_task['open_account']
264 except StopIteration:
265 debug("resulting tasks =>", format_json(tasks), override=True)
266 error("Unable to acquire guest account credentials as it isn't present in the API response.")
267 error("This might be because of a wide variety of reasons, but it most likely is due to your IP being rate-limited.")
268 error("Try again with a new IP address or in 24 hours after this attempt.")
269 return 1
270
271 if args.outfile == "-":
272 debug("outfile is `-`, printing to stdout")
273 print(format_json(account))
274 return 0
275
276 # Sanity checks
277 try:
278 debug(f"attempting to read file: {args.outfile}")
279 with open(args.outfile) as f:
280 old_data = json.load(f)
281 except FileNotFoundError:
282 # that's okay, we might be able to create it later.
283 old_data = []
284 except PermissionError:
285 # that's not okay. we will need to access the file later anyways.
286 error("unable to read file due to a permission error.")
287 error("please make sure this script has read and write access to the file.")
288 print(format_json(account))
289 return 1
290 except json.JSONDecodeError:
291 error("could not parse the provided JSON file.")
292 if not prompt_bool("Do you want to overwrite the file?", default=None):
293 warn("Not overwriting file, printing to stdout instead.")
294 print(format_json(account))
295 return 1
296 debug('assuming old data is an empty array because we are overwriting')
297 old_data = []
298 if type(old_data) != list:
299 error("top-level object of the existing JSON file is not a list.")
300 error("due to the implementation, the file must be overwritten.")
301 if not (prompt_bool("Do you want to overwrite?", default=None)):
302 warn("Not overwriting existing data, printing to stdout instead.")
303 print(format_json(account))
304 return 1
305 debug("assuming old data is an empty array because we are overwriting")
306 old_data = []
307
308 old_data.append(account)
309
310 try:
311 debug("attempting to write file")
312 with open(args.outfile, 'w+') as f:
313 f.write(format_json(old_data))
314 success(f"successfully written to file {args.outfile}")
315 return 0
316 except PermissionError:
317 error("unable to write to file due to permission error.")
318 error("please make sure this script has write access to the file.")
319 print(format_json(account))
320 return 1
321 except Exception as e:
322 error("Unable to write to file due to an uncaught error:", e)
323 tb = ''.join(traceback.TracebackException.from_exception(e).format())
324 debug("exception stacktrace\n" + tb)
325 print(format_json(account))
326
327 except Exception:
328 debug("resulting tasks =>", format_json(tasks), override=True)
329 error("an unhandled error occurred. the tasks object is printed to avoid losing otherwise successful data.")
330 error("please file a bug report and attach the traceback below.")
331 raise
332
333 return 0
334
335
336if __name__ == "__main__":
337 rc = main()
338 sys.exit(rc)