commit
0debf35541
@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
chromedriver.exe
|
||||||
|
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# add
|
||||||
|
.idea/
|
@ -0,0 +1,201 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
# import pyotp
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
from retry import retry
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
|
|
||||||
|
# def generate_authenticator_token(secret):
|
||||||
|
# totp = pyotp.TOTP(secret)
|
||||||
|
# return totp.now()
|
||||||
|
|
||||||
|
def login(userid, username, password, authentication_secret=None):
|
||||||
|
if not username or not password:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
|
||||||
|
options.add_argument("--headless")
|
||||||
|
driver = webdriver.Chrome(options=options)
|
||||||
|
driver.get("https://x.com/i/flow/login")
|
||||||
|
|
||||||
|
WebDriverWait(driver, 10).until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, 'input[autocomplete="username"]')))
|
||||||
|
username_field = driver.find_element(By.CSS_SELECTOR, 'input[autocomplete="username"]')
|
||||||
|
username_field.send_keys(username)
|
||||||
|
buttons = driver.find_elements(By.TAG_NAME, 'button')
|
||||||
|
buttons[2].click()
|
||||||
|
|
||||||
|
WebDriverWait(driver, 10).until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, 'input[autocomplete="on"]')))
|
||||||
|
username_field = driver.find_element(By.CSS_SELECTOR, 'input[autocomplete="on"]')
|
||||||
|
username_field.send_keys(userid)
|
||||||
|
buttons = driver.find_elements(By.TAG_NAME, 'button')
|
||||||
|
buttons[1].click()
|
||||||
|
|
||||||
|
WebDriverWait(driver, 10).until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, 'input[autocomplete="current-password"]')))
|
||||||
|
password_field = driver.find_element(By.CSS_SELECTOR, 'input[autocomplete="current-password"]')
|
||||||
|
password_field.send_keys(password)
|
||||||
|
login_button = driver.find_element(By.CSS_SELECTOR, 'button[data-testid="LoginForm_Login_Button"]')
|
||||||
|
login_button.click()
|
||||||
|
|
||||||
|
# # 如果需要两步验证
|
||||||
|
# if authentication_secret:
|
||||||
|
# WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input[inputmode="numeric"]')))
|
||||||
|
# token = generate_authenticator_token(authentication_secret) # 需要实现的函数
|
||||||
|
# auth_field = driver.find_element(By.CSS_SELECTOR, 'input[inputmode="numeric"]')
|
||||||
|
# auth_field.send_keys(token)
|
||||||
|
# next_button = driver.find_element(By.CSS_SELECTOR, 'button[data-testid="ocfEnterTextNextButton"]')
|
||||||
|
# next_button.click()
|
||||||
|
|
||||||
|
WebDriverWait(driver, 300).until(ec.url_contains('/home'))
|
||||||
|
cookies = driver.get_cookies()
|
||||||
|
cookie_string = "; ".join([f"{cookie['name']}={cookie['value']}" for cookie in cookies])
|
||||||
|
logger.success(f"Twitter login success for username {username}\n{cookie_string}")
|
||||||
|
return driver
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Twitter login failed for username {username}: {e}")
|
||||||
|
driver.quit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@retry(tries=10, delay=10)
|
||||||
|
def get_timeline(driver, url):
|
||||||
|
logger.info(f"check timeline {url}")
|
||||||
|
driver.get(url)
|
||||||
|
WebDriverWait(driver, 60).until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, 'div[aria-label="Timeline: List"]')))
|
||||||
|
for packet in driver.get_log("performance"):
|
||||||
|
message = json.loads(packet["message"])["message"]
|
||||||
|
if (message["method"] == "Network.responseReceived" and
|
||||||
|
"ListLatestTweetsTimeline" in message["params"]["response"]["url"]):
|
||||||
|
request_id = message["params"]["requestId"]
|
||||||
|
resp = driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
|
||||||
|
return json.loads(resp["body"])
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timeline(data):
|
||||||
|
entries = data["data"]["list"]["tweets_timeline"]["timeline"]["instructions"][0]["entries"]
|
||||||
|
result = []
|
||||||
|
for entry in entries:
|
||||||
|
result += parse_entry(entry)
|
||||||
|
result.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def parse_entry(entry):
|
||||||
|
result = []
|
||||||
|
entry_id = entry["entryId"]
|
||||||
|
if "list-conversation" in entry_id and not "tweet" in entry_id:
|
||||||
|
for item in entry["content"]["items"]:
|
||||||
|
result.append(parse_content(item["item"]))
|
||||||
|
elif entry["content"]["__typename"] != 'TimelineTimelineCursor':
|
||||||
|
result.append(parse_content(entry["content"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def parse_content(content):
|
||||||
|
tweet = content["itemContent"]["tweet_results"]["result"]
|
||||||
|
data = parse_tweet(tweet)
|
||||||
|
if "quoted_status_result" in tweet:
|
||||||
|
data["quoted"] = parse_tweet(tweet["quoted_status_result"]["result"])
|
||||||
|
if "retweeted_status_result" in tweet["legacy"]:
|
||||||
|
data["retweeted"] = parse_tweet(tweet["legacy"]["retweeted_status_result"]["result"])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse_media(media):
|
||||||
|
data = {
|
||||||
|
"url": media["media_url_https"] + "?name=orig",
|
||||||
|
"video": ""
|
||||||
|
}
|
||||||
|
if media["type"] in ["video", "animated_gif"]:
|
||||||
|
variants = [i for i in media["video_info"]["variants"] if "bitrate" in i]
|
||||||
|
variants.sort(key=lambda x: x["bitrate"], reverse=True)
|
||||||
|
if variants: data["video"] = variants[0]["url"]
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse_tweet(tweet):
|
||||||
|
data = {
|
||||||
|
"rest_id": tweet["rest_id"],
|
||||||
|
"name": tweet["core"]["user_results"]["result"]["legacy"]["name"],
|
||||||
|
"screen_name": tweet["core"]["user_results"]["result"]["legacy"]["screen_name"],
|
||||||
|
"profile_image": tweet["core"]["user_results"]["result"]["legacy"]["profile_image_url_https"],
|
||||||
|
"profile_image_shape": tweet["core"]["user_results"]["result"]["profile_image_shape"],
|
||||||
|
"full_text": tweet["legacy"]["full_text"],
|
||||||
|
"created_at": tweet["legacy"]["created_at"],
|
||||||
|
"timestamp": int(datetime.strptime(tweet["legacy"]["created_at"], '%a %b %d %H:%M:%S %z %Y').timestamp()),
|
||||||
|
"media": [],
|
||||||
|
"quoted": {},
|
||||||
|
"retweeted": {}
|
||||||
|
}
|
||||||
|
for m in tweet["legacy"]["entities"].get("media", []):
|
||||||
|
data["media"].append(parse_media(m))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LATEST_TWEET_ID_DICT = {}
|
||||||
|
def check_new_tweets(tweets, url):
|
||||||
|
global LATEST_TWEET_ID_DICT
|
||||||
|
|
||||||
|
if url in LATEST_TWEET_ID_DICT:
|
||||||
|
new_tweets = []
|
||||||
|
for tweet in tweets:
|
||||||
|
if tweet["rest_id"] == LATEST_TWEET_ID_DICT[url]:
|
||||||
|
LATEST_TWEET_ID_DICT[url] = tweets[0]["rest_id"]
|
||||||
|
return new_tweets
|
||||||
|
new_tweets.append(tweet)
|
||||||
|
|
||||||
|
LATEST_TWEET_ID_DICT[url] = tweets[0]["rest_id"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def check_timeline(driver, url):
|
||||||
|
data = get_timeline(driver, url)
|
||||||
|
tweets = parse_timeline(data)
|
||||||
|
return check_new_tweets(tweets, url)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main(userid, username, password, config):
|
||||||
|
driver = login(userid, username, password)
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
json_data = {}
|
||||||
|
for group_id, url in config.items():
|
||||||
|
new_tweets = check_timeline(driver, url)
|
||||||
|
if new_tweets:
|
||||||
|
json_data[group_id] = new_tweets
|
||||||
|
|
||||||
|
if json_data:
|
||||||
|
pprint(json_data)
|
||||||
|
try:
|
||||||
|
requests.post("http://localhost:8520/twitter", json=json_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
|
||||||
|
time.sleep(55)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
userid = "<userid>"
|
||||||
|
username = "<username>"
|
||||||
|
password = "<password>"
|
||||||
|
config = {
|
||||||
|
"<qq_group_id>": "https://x.com/i/lists/<...>",
|
||||||
|
}
|
||||||
|
main(userid, username, password, config)
|
||||||
|
# with open("lovelive.json", 'r') as f: pprint(parse_timeline(json.load(f)))
|
Loading…
Reference in new issue