From 58fb7a6f43ce30cdc2ad8fd4dec2494b37aecb3c Mon Sep 17 00:00:00 2001 From: Alessandro Maggio Date: Tue, 12 Jan 2021 19:17:06 +0100 Subject: [PATCH] Move class inside Classes folder and rename all file with the same name of the Class - Nothing work yet --- .gitignore | 138 ++++++++++++++++++++++++ exceptions.py => classes/Exceptions.py | 0 classes/PubsubTopic.py | 13 +++ classes/Raid.py | 10 ++ classes/RequestInfo.py | 4 + classes/TwitchLogin.py | 143 +++++++++++++++++++++++++ classes/WebsocketsPool.py | 56 ++++++++++ classes/__init__.py | 0 login.py | 142 +----------------------- pubsub.py | 66 +----------- raid.py | 9 -- watch_minute.py | 4 - 12 files changed, 368 insertions(+), 217 deletions(-) create mode 100644 .gitignore rename exceptions.py => classes/Exceptions.py (100%) create mode 100644 classes/PubsubTopic.py create mode 100644 classes/Raid.py create mode 100644 classes/RequestInfo.py create mode 100644 classes/TwitchLogin.py create mode 100644 classes/WebsocketsPool.py create mode 100644 classes/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5391d87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# 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/ +share/python-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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# 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/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/exceptions.py b/classes/Exceptions.py similarity index 100% rename from exceptions.py rename to classes/Exceptions.py diff --git a/classes/PubsubTopic.py b/classes/PubsubTopic.py new file mode 100644 index 0000000..7f21227 --- /dev/null +++ b/classes/PubsubTopic.py @@ -0,0 +1,13 @@ +class PubsubTopic: + def __init__(self, topic, channel_login=None): + self.topic = topic + self.channel_login = channel_login + + def is_user_topic(self): + return self.channel_login is None + + def __str__(self): + if self.is_user_topic(): + return f"{self.topic}.{get_user_id()}" + else: + return f"{self.topic}.{get_channel_id(self.channel_login)}" \ No newline at end of file diff --git a/classes/Raid.py b/classes/Raid.py new file mode 100644 index 0000000..1e2a362 --- /dev/null +++ b/classes/Raid.py @@ -0,0 +1,10 @@ +class Raid: + def __init__(self, raid_id, target_login): + self.raid_id = raid_id + self.target_login = target_login + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.raid_id == other.raid_id + else: + return False \ No newline at end of file diff --git a/classes/RequestInfo.py b/classes/RequestInfo.py new file mode 100644 index 0000000..b88f478 --- /dev/null +++ b/classes/RequestInfo.py @@ -0,0 +1,4 @@ +class RequestInfo: + def __init__(self, url, payload): + self.url = url + self.payload = payload \ No newline at end of file diff --git a/classes/TwitchLogin.py b/classes/TwitchLogin.py new file mode 100644 index 0000000..5943711 --- /dev/null +++ b/classes/TwitchLogin.py @@ -0,0 +1,143 @@ +# Based on https://github.com/derrod/twl.py +# Original Copyright (c) 2020 Rodney +# The MIT License (MIT) + +import logging +import getpass +import browser_cookie3 +import requests + +from cookies import save_cookies_to_file +from twitch_data import get_client_id + +logger = logging.getLogger(__name__) + +class TwitchLogin: + def __init__(self): + self.token = None + self.login_check_result = False + self.session = requests.session() + self.session.headers.update({'Client-ID': get_client_id()}) + self.username = None + self.user_id = None + self.email = None + + def login_flow(self): + logger.info("You'll have to login to Twitch!") + + post_data = { + 'client_id': get_client_id(), + 'undelete_user': False, + 'remember_me': True + } + + use_backup_flow = False + + while True: + self.username = input('Enter Twitch username: ') + password = getpass.getpass('Enter Twitch password: ') + + post_data['username'] = self.username + post_data['password'] = password + + while True: + # Try login without 2FA + login_response = self.send_login_request(post_data) + + if 'captcha_proof' in login_response: + post_data['captcha'] = dict(proof=login_response['captcha_proof']) + + if 'error_code' in login_response: + err_code = login_response['error_code'] + if err_code == 3011 or err_code == 3012: # missing 2fa token + if err_code == 3011: + logger.info('Two factor authentication enabled, please enter token below.') + else: + logger.info('Invalid two factor token, please try again.') + + twofa = input('2FA token: ') + post_data['authy_token'] = twofa.strip() + continue + + elif err_code == 3022 or err_code == 3023: # missing 2fa token + if err_code == 3022: + logger.info('Login Verification code required.') + self.email = login_response['obscured_email'] + else: + logger.info('Invalid Login Verification code entered, please try again.') + + twofa = input(f'Please enter the 6-digit code sent to {self.email}: ') + post_data['twitchguard_code'] = twofa.strip() + continue + + elif err_code == 3001: # invalid password + logger.info('Invalid username or password, please try again.') + break + elif err_code == 1000: + logger.info('Console login unavailable (CAPTCHA solving required).') + use_backup_flow = True + break + else: + logger.info(f'Unknown error: {login_response}') + raise NotImplementedError(f'Unknown TwitchAPI error code: {err_code}') + + if 'access_token' in login_response: + self.set_token(login_response['access_token']) + return self.check_login() + + if use_backup_flow: + break + + if use_backup_flow: + self.set_token(self.login_flow_backup()) + return self.check_login() + + return False + + def set_token(self, new_token): + self.token = new_token + self.session.headers.update({'Authorization': f'Bearer {self.token}'}) + + def send_login_request(self, json_data): + r = self.session.post('https://passport.twitch.tv/login', json=json_data) + j = r.json() + return j + + def login_flow_backup(self): + """Backup OAuth login flow in case manual captcha solving is required""" + browser = input("What browser do you use? Chrome (1), Firefox (2), Other (3): ").strip() + if browser not in ("1", "2"): + logger.info("Your browser is unsupported, sorry.") + return None + + input("Please login inside your browser of choice (NOT incognito mode) and press Enter...") + logger.info("Loading cookies saved on your computer...") + twitch_domain = ".twitch.tv" + if browser == "1": # chrome + cookie_jar = browser_cookie3.chrome(domain_name=twitch_domain) + else: + cookie_jar = browser_cookie3.firefox(domain_name=twitch_domain) + cookies_dict = requests.utils.dict_from_cookiejar(cookie_jar) + self.username = cookies_dict.get("login") + return cookies_dict.get("auth-token") + + def check_login(self): + if self.login_check_result: + return self.login_check_result + if self.token is None: + return False + + r = self.session.get(f'https://api.twitch.tv/helix/users?login={self.username}') + response = r.json() + if "data" in response: + self.login_check_result = True + self.user_id = response["data"][0]["id"] + + return self.login_check_result + + def save_cookies(self): + cookies_dict = self.session.cookies.get_dict() + cookies_dict["auth-token"] = self.token + if "persistent" not in cookies_dict: # saving user id cookies + cookies_dict["persistent"] = self.user_id + save_cookies_to_file(cookies_dict) diff --git a/classes/WebsocketsPool.py b/classes/WebsocketsPool.py new file mode 100644 index 0000000..8ae23e9 --- /dev/null +++ b/classes/WebsocketsPool.py @@ -0,0 +1,56 @@ +import logging +import threading +import websocket +import time + +logger = logging.getLogger(__name__) + +class WebsocketsPool: # you can't listen for more than 50 topics at once + def __init__(self): + self.free_websocket = None + + def submit(self, pubsub_topic): + if self.free_websocket is None or len(self.free_websocket.topics) >= 50: + self.create_new_websocket() + self.free_websocket.topics.append(pubsub_topic) + if not self.free_websocket.is_opened: + self.free_websocket.pending_topics.append(pubsub_topic) + else: + listen_for_topic(self.free_websocket, pubsub_topic) + + def create_new_websocket(self): + self.free_websocket = websocket.WebSocketApp("wss://pubsub-edge.twitch.tv/v1", + on_message=on_message, + on_open=WebsocketsPool.on_open, + on_close=WebsocketsPool.handle_websocket_reconnection + ) + self.free_websocket.parent_pool = self + self.free_websocket.is_closed = False + self.free_websocket.is_opened = False + self.free_websocket.topics = [] + self.free_websocket.pending_topics = [] + threading.Thread(target=lambda: self.free_websocket.run_forever()).start() + + @staticmethod + def on_open(ws): + def run(): + ws.is_opened = True + ping(ws) + for topic in ws.pending_topics: + listen_for_topic(ws, topic) + while not ws.is_closed: + ping(ws) + time.sleep(30) + + threading.Thread(target=run).start() + + @staticmethod + def handle_websocket_reconnection(ws): + ws.is_closed = True + logger.info("Reconnecting to Twitch PubSub server in 30 seconds") + time.sleep(30) + self = ws.parent_pool + if self.free_websocket == ws: + self.free_websocket = None + for topic in ws.topics: + self.submit(topic) diff --git a/classes/__init__.py b/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/login.py b/login.py index f5c02e7..fc895e6 100644 --- a/login.py +++ b/login.py @@ -1,15 +1,6 @@ -import getpass import os -import browser_cookie3 -import requests -from cookies import get_cookies_path, save_cookies_to_file -from twitch_data import get_client_id - - -# Based on https://github.com/derrod/twl.py -# Original Copyright (c) 2020 Rodney -# The MIT License (MIT) - +from cookies import get_cookies_path +from classes.TwitchLogin import TwitchLogin def check_login(): if not os.path.exists(get_cookies_path()): @@ -23,132 +14,3 @@ def check_login(): return True -class TwitchLogin: - def __init__(self): - self.token = None - self.login_check_result = False - self.session = requests.session() - self.session.headers.update({'Client-ID': get_client_id()}) - self.username = None - self.user_id = None - self.email = None - - def login_flow(self): - print("You'll have to login to Twitch!") - - post_data = { - 'client_id': get_client_id(), - 'undelete_user': False, - 'remember_me': True - } - - use_backup_flow = False - - while True: - self.username = input('Enter Twitch username: ') - password = getpass.getpass('Enter Twitch password: ') - - post_data['username'] = self.username - post_data['password'] = password - - while True: - # Try login without 2FA - login_response = self.send_login_request(post_data) - - if 'captcha_proof' in login_response: - post_data['captcha'] = dict(proof=login_response['captcha_proof']) - - if 'error_code' in login_response: - err_code = login_response['error_code'] - if err_code == 3011 or err_code == 3012: # missing 2fa token - if err_code == 3011: - print('Two factor authentication enabled, please enter token below.') - else: - print('Invalid two factor token, please try again.') - - twofa = input('2FA token: ') - post_data['authy_token'] = twofa.strip() - continue - - elif err_code == 3022 or err_code == 3023: # missing 2fa token - if err_code == 3022: - print('Login Verification code required.') - self.email = login_response['obscured_email'] - else: - print('Invalid Login Verification code entered, please try again.') - - twofa = input(f'Please enter the 6-digit code sent to {self.email}: ') - post_data['twitchguard_code'] = twofa.strip() - continue - - elif err_code == 3001: # invalid password - print('Invalid username or password, please try again.') - break - elif err_code == 1000: - print('Console login unavailable (CAPTCHA solving required).') - use_backup_flow = True - break - else: - print(f'Unknown error: {login_response}') - raise NotImplementedError(f'Unknown TwitchAPI error code: {err_code}') - - if 'access_token' in login_response: - self.set_token(login_response['access_token']) - return self.check_login() - - if use_backup_flow: - break - - if use_backup_flow: - self.set_token(self.login_flow_backup()) - return self.check_login() - - return False - - def set_token(self, new_token): - self.token = new_token - self.session.headers.update({'Authorization': f'Bearer {self.token}'}) - - def send_login_request(self, json_data): - r = self.session.post('https://passport.twitch.tv/login', json=json_data) - j = r.json() - return j - - def login_flow_backup(self): - """Backup OAuth login flow in case manual captcha solving is required""" - browser = input("What browser do you use? Chrome (1), Firefox (2), Other (3): ").strip() - if browser not in ("1", "2"): - print("Your browser is unsupported, sorry.") - return None - - input("Please login inside your browser of choice (NOT incognito mode) and press Enter...") - print("Loading cookies saved on your computer...") - twitch_domain = ".twitch.tv" - if browser == "1": # chrome - cookie_jar = browser_cookie3.chrome(domain_name=twitch_domain) - else: - cookie_jar = browser_cookie3.firefox(domain_name=twitch_domain) - cookies_dict = requests.utils.dict_from_cookiejar(cookie_jar) - self.username = cookies_dict.get("login") - return cookies_dict.get("auth-token") - - def check_login(self): - if self.login_check_result: - return self.login_check_result - if self.token is None: - return False - - r = self.session.get(f'https://api.twitch.tv/helix/users?login={self.username}') - response = r.json() - if "data" in response: - self.login_check_result = True - self.user_id = response["data"][0]["id"] - - return self.login_check_result - - def save_cookies(self): - cookies_dict = self.session.cookies.get_dict() - cookies_dict["auth-token"] = self.token - if "persistent" not in cookies_dict: # saving user id cookies - cookies_dict["persistent"] = self.user_id - save_cookies_to_file(cookies_dict) diff --git a/pubsub.py b/pubsub.py index daa4fa6..03e893b 100644 --- a/pubsub.py +++ b/pubsub.py @@ -7,6 +7,8 @@ from claim_bonus import claim_channel_points_bonus from raid import update_raid, Raid from twitch_data import * +from classes.WebsocketsPool import WebsocketsPool +from classes.PubsubTopic import PubsubTopic # For documentation on Twitch PubSub API, see https://dev.twitch.tv/docs/pubsub @@ -107,69 +109,6 @@ def get_reason_name(code): return code.replace("_", " ").replace("CLAIM", "bonus claimed").lower() -class PubsubTopic: - def __init__(self, topic, channel_login=None): - self.topic = topic - self.channel_login = channel_login - - def is_user_topic(self): - return self.channel_login is None - - def __str__(self): - if self.is_user_topic(): - return f"{self.topic}.{get_user_id()}" - else: - return f"{self.topic}.{get_channel_id(self.channel_login)}" - - -class WebsocketsPool: # you can't listen for more than 50 topics at once - def __init__(self): - self.free_websocket = None - - def submit(self, pubsub_topic): - if self.free_websocket is None or len(self.free_websocket.topics) >= 50: - self.create_new_websocket() - self.free_websocket.topics.append(pubsub_topic) - if not self.free_websocket.is_opened: - self.free_websocket.pending_topics.append(pubsub_topic) - else: - listen_for_topic(self.free_websocket, pubsub_topic) - - def create_new_websocket(self): - self.free_websocket = websocket.WebSocketApp("wss://pubsub-edge.twitch.tv/v1", - on_message=on_message, on_open=WebsocketsPool.on_open, on_close=WebsocketsPool.handle_websocket_reconnection) - self.free_websocket.parent_pool = self - self.free_websocket.is_closed = False - self.free_websocket.is_opened = False - self.free_websocket.topics = [] - self.free_websocket.pending_topics = [] - threading.Thread(target=lambda: self.free_websocket.run_forever()).start() - - @staticmethod - def on_open(ws): - def run(): - ws.is_opened = True - ping(ws) - for topic in ws.pending_topics: - listen_for_topic(ws, topic) - while not ws.is_closed: - ping(ws) - time.sleep(30) - - threading.Thread(target=run).start() - - @staticmethod - def handle_websocket_reconnection(ws): - ws.is_closed = True - print("Reconnecting to Twitch PubSub server in 30 seconds") - time.sleep(30) - self = ws.parent_pool - if self.free_websocket == ws: - self.free_websocket = None - for topic in ws.topics: - self.submit(topic) - - def listen_for_topic(ws, topic): data = {"topics": [str(topic)]} if topic.is_user_topic: @@ -187,7 +126,6 @@ def send(ws, request): request_str = json.dumps(request, separators=(',', ':')) ws.send(request_str) - # https://en.wikipedia.org/wiki/Cryptographic_nonce def create_nonce(length=30): nonce = "" diff --git a/raid.py b/raid.py index d9e4822..7d22119 100644 --- a/raid.py +++ b/raid.py @@ -1,16 +1,7 @@ from claim_bonus import post_gql_request -class Raid: - def __init__(self, raid_id, target_login): - self.raid_id = raid_id - self.target_login = target_login - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.raid_id == other.raid_id - else: - return False raid_id_for_streamer = {} diff --git a/watch_minute.py b/watch_minute.py index 343ac48..ea89d73 100644 --- a/watch_minute.py +++ b/watch_minute.py @@ -12,10 +12,6 @@ from twitch_data import get_streamer_url, USER_AGENT # "minute-watched" requests are what Twitch uses to know if you're watching a stream, and to grant you channel points. -class RequestInfo: - def __init__(self, url, payload): - self.url = url - self.payload = payload # "minute-watched" request for each streamer