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