twscrape/twapi/account.py

289 строки
9.8 KiB
Python

import json
from datetime import datetime, timedelta, timezone
from enum import Enum
from fake_useragent import UserAgent
from httpx import AsyncClient, HTTPStatusError, Response
from .constants import LOGIN_URL, TOKEN
from .imap import get_email_code
from .logger import logger
from .utils import raise_for_status
class Status(str, Enum):
NEW = "new"
ACTIVE = "active"
LOGIN_ERROR = "login_error"
class Account:
@classmethod
def load_from_file(cls, filepath: str):
try:
with open(filepath) as f:
data = json.load(f)
return cls(**data)
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.error(f"Failed to load account {filepath}: {e}")
return None
def __init__(
self,
username: str,
password: str,
email: str,
email_password: str,
user_agent: str | None = None,
proxy: str | None = None,
headers={},
cookies={},
limits={},
status=Status.NEW,
):
self.username = username
self.password = password
self.email = email
self.email_password = email_password
self.user_agent = user_agent or UserAgent().safari
self.proxy = proxy or None
self.client = AsyncClient(proxies=self.proxy)
self.client.cookies.update(cookies)
self.client.headers.update(headers)
self.client.headers["user-agent"] = self.user_agent
self.client.headers["content-type"] = "application/json"
self.client.headers["authorization"] = TOKEN
self.client.headers["x-twitter-active-user"] = "yes"
self.client.headers["x-twitter-client-language"] = "en"
self.status: Status = status
self.locked: dict[str, bool] = {}
self.limits: dict[str, datetime] = {
k: datetime.fromisoformat(v) for k, v in limits.items()
}
def dump(self):
return {
"username": self.username,
"password": self.password,
"email": self.email,
"email_password": self.email_password,
"user_agent": self.user_agent,
"proxy": self.proxy,
"cookies": {k: v for k, v in self.client.cookies.items()},
"headers": {k: v for k, v in self.client.headers.items()},
"limits": {k: v.isoformat() for k, v in self.limits.items()},
"status": self.status,
}
def update_limit(self, queue: str, reset_ts: int):
self.limits[queue] = datetime.fromtimestamp(reset_ts, tz=timezone.utc)
def can_use(self, queue: str):
if self.locked.get(queue, False) or self.status != Status.ACTIVE:
return False
limit = self.limits.get(queue)
return not limit or limit <= datetime.now(timezone.utc)
def lock(self, queue: str):
self.locked[queue] = True
def unlock(self, queue: str):
self.locked[queue] = False
async def login(self, fresh=False):
log_id = f"{self.username} - {self.email}"
if self.status != "new" and not fresh:
logger.debug(f"logged in already {log_id}")
return
logger.debug(f"logging in {log_id}")
guest_token = await self.get_guest_token()
self.client.headers["x-guest-token"] = guest_token
rep = await self.login_initiate()
while True:
try:
if rep:
rep = await self.next_login_task(rep)
else:
break
except HTTPStatusError as e:
if e.response.status_code == 403:
logger.error(f"403 error {log_id}")
self.status = Status.LOGIN_ERROR
return
self.client.headers["x-csrf-token"] = self.client.cookies["ct0"]
self.client.headers["x-twitter-auth-type"] = "OAuth2Session"
logger.info(f"logged in success {log_id}")
self.status = Status.ACTIVE
async def get_guest_token(self):
rep = await self.client.post("https://api.twitter.com/1.1/guest/activate.json")
raise_for_status(rep, "guest_token")
return rep.json()["guest_token"]
async def login_initiate(self) -> Response:
payload = {
"input_flow_data": {
"flow_context": {"debug_overrides": {}, "start_location": {"location": "unknown"}}
},
"subtask_versions": {},
}
rep = await self.client.post(LOGIN_URL, params={"flow_name": "login"}, json=payload)
raise_for_status(rep, "login_initiate")
return rep
async def next_login_task(self, rep: Response):
ct0 = self.client.cookies.get("ct0", None)
if ct0:
self.client.headers["x-csrf-token"] = ct0
self.client.headers["x-twitter-auth-type"] = "OAuth2Session"
data = rep.json()
assert "flow_token" in data, f"flow_token not in {rep.text}"
# logger.debug(f"login tasks: {[x['subtask_id'] for x in data['subtasks']]}")
for x in data["subtasks"]:
task_id = x["subtask_id"]
try:
if task_id == "LoginSuccessSubtask":
return await self.login_success(data)
if task_id == "LoginAcid":
is_code = x["enter_text"]["hint_text"].lower() == "confirmation code"
# logger.debug(f"is login code: {is_code}")
fn = self.login_confirm_email_code if is_code else self.login_confirm_email
return await fn(data)
if task_id == "AccountDuplicationCheck":
return await self.login_duplication_check(data)
if task_id == "LoginEnterPassword":
return await self.login_enter_password(data)
if task_id == "LoginEnterUserIdentifierSSO":
return await self.login_enter_username(data)
if task_id == "LoginJsInstrumentationSubtask":
return await self.login_instrumentation(data)
except Exception as e:
logger.error(f"Error in {task_id}: {e}")
logger.error(e)
return None
async def login_instrumentation(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "LoginJsInstrumentationSubtask",
"js_instrumentation": {"response": "{}", "link": "next_link"},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_instrumentation")
return rep
async def login_enter_username(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "LoginEnterUserIdentifierSSO",
"settings_list": {
"setting_responses": [
{
"key": "user_identifier",
"response_data": {"text_data": {"result": self.username}},
}
],
"link": "next_link",
},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_username")
return rep
async def login_enter_password(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "LoginEnterPassword",
"enter_password": {"password": self.password, "link": "next_link"},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_password")
return rep
async def login_duplication_check(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "AccountDuplicationCheck",
"check_logged_in_account": {"link": "AccountDuplicationCheck_false"},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_duplication_check")
return rep
async def login_confirm_email(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "LoginAcid",
"enter_text": {"text": self.email, "link": "next_link"},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_confirm_email")
return rep
async def login_confirm_email_code(self, prev: dict):
now_time = datetime.now(timezone.utc) - timedelta(seconds=30)
value = await get_email_code(self.email, self.email_password, now_time)
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [
{
"subtask_id": "LoginAcid",
"enter_text": {"text": value, "link": "next_link"},
}
],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_confirm_email")
return rep
async def login_success(self, prev: dict) -> Response:
payload = {
"flow_token": prev["flow_token"],
"subtask_inputs": [],
}
rep = await self.client.post(LOGIN_URL, json=payload)
raise_for_status(rep, "login_success")
return rep