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