├── .gitignore ├── README.md ├── poe_auth ├── V1.py ├── V2.py ├── __init__.py └── cli.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | main.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poe-auth 2 | 3 | poe-auth is a command line tool to automate the obtention of a session token from [Quora's Poe](https://poe.com). 4 | 5 | **UPDATE**: This library is currently **broken** as Quora's Poe can detect when sending automated queries. 6 | If someone know how to bypass that, feel free to contribute. 7 | 8 | ## Installation 9 | 10 | You can install this package using pip 11 | 12 | ```bash 13 | pip install --upgrade git+https://github.com/krishna2206/poe-auth.git 14 | ``` 15 | 16 | ## Usage 17 | ### CLI 18 | You can use the `poe-auth` command that will be installed in your system. 19 | It has two options: `--email` and `--phone`. 20 | You can use either one of them to authenticate to Poe. 21 | 22 | ```bash 23 | poe-auth --email your.email@example.com 24 | ``` 25 | 26 | or 27 | 28 | ```bash 29 | poe-auth --phone +33601234567 30 | ``` 31 | 32 | ### Module 33 | You can also use this package as a module. To do so, import the `PoeAuth` class and instantiate it. 34 | 35 | #### V1 (Reverse engineered API) 36 | ```python 37 | from poe_auth.V1 import PoeAuth 38 | 39 | auth = PoeAuth() 40 | ``` 41 | 42 | #### Login/Signup using email 43 | 44 | ##### V1 (Reverse engineered API) 45 | ```python 46 | # Send a verification code to your email 47 | email = input("Enter your email: ") 48 | status = auth.send_verification_code(email) 49 | 50 | # Authenticate by entering the verification code 51 | verification_code = input("Enter the verification code: ") 52 | if status == "user_with_confirmed_email_not_found": 53 | session_token = auth.signup_using_verification_code( 54 | verification_code=verification_code, mode="email", email=email_adress) 55 | else: 56 | session_token = auth.login_using_verification_code( 57 | verification_code=verification_code, mode="email", email=email_adress) 58 | 59 | # Print the session token 60 | print(session_token) 61 | ``` 62 | 63 | #### Login/Signup using phone number 64 | 65 | ##### V1 (Reverse engineered API) 66 | ```python 67 | phone = input("Enter your phone number: ") 68 | status = auth.send_verification_code(phone, mode="phone") 69 | 70 | # Authenticate by entering the verification code 71 | verification_code = input("Enter the verification code: ") 72 | if status == "user_with_confirmed_phone_number_not_found": 73 | session_token = auth.signup_using_verification_code( 74 | verification_code=verification_code, mode="phone", phone=phone_number) 75 | else: 76 | session_token = auth.login_using_verification_code( 77 | verification_code=verification_code, mode="phone", phone=phone_number) 78 | 79 | # Print the session token 80 | print(session_token) 81 | ``` 82 | 83 | The script will send a verification code to your email or phone number, depending on the option you choose. 84 | Enter the verification code when prompted, and the script will authenticate to Poe and display the session token. 85 | You can now use this token for this [API](https://github.com/ading2210/poe-api). 86 | 87 | ## License 88 | 89 | MIT License 90 | 91 | Copyright (c) 2023 Fitiavana Anhy Krishna 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy 94 | of this software and associated documentation files (the "Software"), to deal 95 | in the Software without restriction, including without limitation the rights 96 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 97 | copies of the Software, and to permit persons to whom the Software is 98 | furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in 101 | all copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 109 | THE SOFTWARE. 110 | -------------------------------------------------------------------------------- /poe_auth/V1.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import logging 4 | import hashlib 5 | 6 | from requests import Session 7 | from fake_useragent import UserAgent 8 | 9 | 10 | gql_queries = { 11 | "SendVerificationCodeForLoginMutation": """ 12 | mutation MainSignupLoginSection_sendVerificationCodeMutation_Mutation( 13 | $emailAddress: String 14 | $phoneNumber: String 15 | $recaptchaToken: String 16 | ) { 17 | sendVerificationCode( 18 | verificationReason: login 19 | emailAddress: $emailAddress 20 | phoneNumber: $phoneNumber 21 | recaptchaToken: $recaptchaToken 22 | ) { 23 | status 24 | errorMessage 25 | } 26 | } 27 | """, 28 | "SignupWithVerificationCodeMutation": """ 29 | mutation SignupOrLoginWithCodeSection_signupWithVerificationCodeMutation_Mutation( 30 | $verificationCode: String! 31 | $emailAddress: String 32 | $phoneNumber: String 33 | ) { 34 | signupWithVerificationCode( 35 | verificationCode: $verificationCode 36 | emailAddress: $emailAddress 37 | phoneNumber: $phoneNumber 38 | ) { 39 | status 40 | errorMessage 41 | } 42 | } 43 | """, 44 | "LoginWithVerificationCodeMutation": """ 45 | mutation SignupOrLoginWithCodeSection_loginWithVerificationCodeMutation_Mutation( 46 | $verificationCode: String! 47 | $emailAddress: String 48 | $phoneNumber: String 49 | ) { 50 | loginWithVerificationCode( 51 | verificationCode: $verificationCode 52 | emailAddress: $emailAddress 53 | phoneNumber: $phoneNumber 54 | ) { 55 | status 56 | errorMessage 57 | } 58 | } 59 | """ 60 | } 61 | 62 | logging.basicConfig() 63 | logger = logging.getLogger() 64 | 65 | 66 | class PoeAuthException(Exception): 67 | pass 68 | 69 | 70 | class PoeAuth: 71 | def __init__(self) -> None: 72 | self.session = Session() 73 | self.login_url = "https://poe.com/login" 74 | self.gql_api_url = "https://poe.com/api/gql_POST" 75 | self.settings_url = "https://poe.com/api/settings" 76 | self.session.headers = { 77 | "Host": "poe.com", 78 | "User-Agent": UserAgent(browsers=["edge", "chrome", "firefox"]).random, 79 | } 80 | 81 | self.form_key = self.__get_form_key() 82 | self.tchannel = self.__get_tchannel() 83 | 84 | # CTTO: https://github.com/ading2210/poe-api/commit/59597cfb4a9c81c93e879c985f5b617a74d07f85 85 | def __get_form_key(self) -> str: 86 | response = self.session.get(self.login_url) 87 | 88 | script_regex = r'' 89 | script_text = re.search(script_regex, response.text).group(1) 90 | key_regex = r'var .="([0-9a-f]+)",' 91 | key_text = re.search(key_regex, script_text).group(1) 92 | cipher_regex = r'.\[(\d+)\]=.\[(\d+)\]' 93 | cipher_pairs = re.findall(cipher_regex, script_text) 94 | 95 | formkey_list = [""] * len(cipher_pairs) 96 | for pair in cipher_pairs: 97 | formkey_index, key_index = map(int, pair) 98 | formkey_list[formkey_index] = key_text[key_index] 99 | formkey = "".join(formkey_list) 100 | 101 | return formkey 102 | 103 | def __get_tchannel(self) -> str: 104 | response = self.session.get(self.settings_url) 105 | try: 106 | tchannel = response.json().get("tchannelData").get("channel") 107 | except Exception as e: 108 | raise PoeAuthException(f"Error while getting tchannel: {e}") 109 | return tchannel 110 | 111 | def __generate_payload(self, query_key: str, query_name: str, variables: dict) -> dict: 112 | return { 113 | "queryName": query_name, 114 | "query": gql_queries[query_key], 115 | "variables": variables 116 | } 117 | 118 | # Inspiration: https://github.com/ading2210/poe-api/pull/39 119 | def __generate_tag_id(self, form_key: str, payload: dict) -> str: 120 | payload = json.dumps(payload) 121 | 122 | base_string = payload + form_key + "WpuLMiXEKKE98j56k" 123 | 124 | return hashlib.md5(base_string.encode()).hexdigest() 125 | 126 | def send_verification_code(self, email: str = None, phone: str = None, mode: str = "email") -> dict: 127 | if mode not in ("email", "phone"): 128 | raise ValueError("Invalid mode. Must be 'email' or 'phone'.") 129 | 130 | payload = self.__generate_payload( 131 | query_key="SendVerificationCodeForLoginMutation", 132 | query_name="MainSignupLoginSection_sendVerificationCodeMutation_Mutation", 133 | variables={"emailAddress": email, "phoneNumber": None, "recaptchaToken": None} if mode == "email" 134 | else {"emailAddress": None, "phoneNumber": phone, "recaptchaToken": None} 135 | ) 136 | tag_id = self.__generate_tag_id( 137 | form_key=self.form_key, 138 | payload=payload 139 | ) 140 | 141 | logger.debug(f"Form key: {self.form_key}") 142 | logger.debug(f"Tchannel: {self.tchannel}") 143 | logger.debug(f"Tag ID: {tag_id}") 144 | 145 | self.session.headers.update({ 146 | 'Referer': 'https://poe.com/login', 147 | 'Origin': 'https://poe.com', 148 | 'Content-Type': 'application/json', 149 | 'poe-formkey': self.form_key, 150 | 'poe-tchannel': self.tchannel, 151 | 'poe-tag-id': tag_id, 152 | }) 153 | 154 | response = self.session.post(self.gql_api_url, json=payload).json() 155 | if response.get("data") is not None: 156 | error_message = response.get("data").get( 157 | "sendVerificationCode").get("errorMessage") 158 | status = response.get("data").get( 159 | "sendVerificationCode").get("status") 160 | if error_message is not None: 161 | raise PoeAuthException( 162 | f"Error while sending verification code: {error_message}") 163 | return status 164 | raise PoeAuthException( 165 | f"Error while sending verification code: {response}") 166 | 167 | def __login_or_signup( 168 | self, action: str, verification_code: str, mode: str, 169 | email: str = None, phone: str = None, 170 | ) -> str: 171 | 172 | if mode not in ("email", "phone"): 173 | raise ValueError("Invalid mode. Must be 'email' or 'phone'.") 174 | 175 | payload = self.__generate_payload( 176 | query_key="LoginWithVerificationCodeMutation" if action == "login" 177 | else "SignupWithVerificationCodeMutation", 178 | query_name="SignupOrLoginWithCodeSection_loginWithVerificationCodeMutation_Mutation" if action == "login" 179 | else "SignupOrLoginWithCodeSection_signupWithVerificationCodeMutation_Mutation", 180 | variables={"verificationCode": verification_code, "emailAddress": email, "phoneNumber": None} if mode == "email" 181 | else {"verificationCode": verification_code, "emailAddress": None, "phoneNumber": phone} 182 | ) 183 | 184 | tag_id = self.__generate_tag_id( 185 | form_key=self.form_key, 186 | payload=payload 187 | ) 188 | 189 | logger.debug(f"Form key: {self.form_key}") 190 | logger.debug(f"Tchannel: {self.tchannel}") 191 | logger.debug(f"Tag ID: {tag_id}") 192 | 193 | self.session.headers.update({ 194 | 'poe-tag-id': tag_id, 195 | }) 196 | 197 | response = self.session.post(self.gql_api_url, json=payload).json() 198 | if response.get("data") is not None: 199 | status = response.get("data").get( 200 | "loginWithVerificationCode" if action == "login" 201 | else "signupWithVerificationCode").get("status") 202 | 203 | if status != "success": 204 | raise PoeAuthException( 205 | f"Error while login in using verification code: {status}") 206 | return self.session.cookies.get_dict().get("p-b") 207 | raise PoeAuthException( 208 | f"Error while login in using verification code: {response}") 209 | 210 | def signup_using_verification_code( 211 | self, verification_code: str, mode: str, 212 | email: str = None, phone: str = None 213 | ) -> str: 214 | return self.__login_or_signup("signup", verification_code, mode, email, phone) 215 | 216 | def login_using_verification_code( 217 | self, verification_code: str, mode: str, 218 | email: str = None, phone: str = None 219 | ) -> str: 220 | return self.__login_or_signup("login", verification_code, mode, email, phone) 221 | -------------------------------------------------------------------------------- /poe_auth/V2.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from playwright.sync_api._generated import Playwright 4 | 5 | logging.basicConfig() 6 | logger = logging.getLogger() 7 | 8 | 9 | class PoeAuthException(Exception): 10 | pass 11 | 12 | 13 | class PoeAuth: 14 | def __init__(self, playwright: Playwright) -> None: 15 | self.browser = playwright.firefox.launch(headless=True) 16 | context = self.browser.new_context() 17 | self.api_req_context = context.request 18 | self.api_req_context_headers = {} 19 | self.current_page = context.new_page() 20 | 21 | self.login_url = "https://poe.com/login" 22 | self.gql_api_url = "https://poe.com/api/gql_POST" 23 | 24 | self.init_login_page() 25 | 26 | def init_login_page(self): 27 | self.current_page.goto(self.login_url) 28 | 29 | def send_verification_code(self, email: str = None) -> dict: 30 | logger.debug("Filling form...") 31 | self.current_page.locator("button[class='Button_buttonBase__0QP_m Button_flat__1hj0f undefined']").click() 32 | self.current_page.fill("input[type='email']", email) 33 | 34 | logger.debug("Clicking send verification code button...") 35 | with self.current_page.expect_response(self.gql_api_url) as response_info: 36 | self.current_page.locator("button[class='Button_buttonBase__0QP_m Button_primary__pIDjn undefined']").click() 37 | self.api_req_context_headers = response_info.value.request.headers 38 | response = response_info.value.json() 39 | 40 | if response.get("data") is not None: 41 | error_message = response.get("data").get( 42 | "sendVerificationCode").get("errorMessage") 43 | status = response.get("data").get( 44 | "sendVerificationCode").get("status") 45 | if error_message is not None: 46 | raise PoeAuthException( 47 | f"Error while sending verification code: {error_message}") 48 | if status == "success": 49 | return response_info.value.headers.get("set-cookie").split(";")[0].split("=")[1] 50 | return status 51 | raise PoeAuthException( 52 | f"Error while sending verification code: {response}") 53 | 54 | def __login_or_signup( 55 | self, action: str, verification_code: str 56 | ) -> str: 57 | 58 | logger.debug("Filling form using verification code...") 59 | self.current_page.fill("input[class='VerificationCodeInput_verificationCodeInput__YD3KV']", verification_code) 60 | with self.current_page.expect_response(self.gql_api_url) as response_info: 61 | logger.debug("Clicking verify button...") 62 | self.current_page.locator("button[class='Button_buttonBase__0QP_m Button_primary__pIDjn undefined']").click() 63 | response = response_info.value.json() 64 | 65 | if response.get("data") is not None: 66 | status = response.get("data").get( 67 | "loginWithVerificationCode" if action == "login" 68 | else "signupWithVerificationCode").get("status") 69 | 70 | if status != "success": 71 | raise PoeAuthException( 72 | f"Error while login in using verification code: {status}") 73 | return response_info.value.headers.get("set-cookie").split(";")[0].split("=")[1] 74 | raise PoeAuthException( 75 | f"Error while login in using verification code: {response}") 76 | 77 | def signup_using_verification_code( 78 | self, verification_code: str 79 | ) -> str: 80 | return self.__login_or_signup("signup", verification_code) 81 | 82 | def login_using_verification_code( 83 | self, verification_code: str 84 | ) -> str: 85 | return self.__login_or_signup("login", verification_code) 86 | -------------------------------------------------------------------------------- /poe_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishna2206/poe-auth/664dc165792630a023a3b0c3400e2412065c8c4b/poe_auth/__init__.py -------------------------------------------------------------------------------- /poe_auth/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from playwright.sync_api import sync_playwright 3 | 4 | from poe_auth.V1 import PoeAuth as PoeAuthV1, PoeAuthException as PoeAuthExceptionV1 5 | from poe_auth.V2 import PoeAuth as PoeAuthV2, PoeAuthException as PoeAuthExceptionV2 6 | 7 | 8 | def cli_V1(email, phone): 9 | poeauth = PoeAuthV1() 10 | 11 | if (email is None) and (phone is None): 12 | click.echo("Email address or phone number is required.") 13 | return 14 | 15 | try: 16 | if email: 17 | status = poeauth.send_verification_code(email=email) 18 | elif phone: 19 | status = poeauth.send_verification_code(phone=phone, mode="phone") 20 | except PoeAuthExceptionV1 as e: 21 | click.echo(str(e)) 22 | return 23 | 24 | verification_code = click.prompt( 25 | f"Enter the verification code sent to {email if email else phone}", type=str) 26 | 27 | try: 28 | if email: 29 | if status == "user_with_confirmed_email_not_found": 30 | auth_session = poeauth.signup_using_verification_code( 31 | verification_code=verification_code, mode="email", email=email) 32 | else: 33 | auth_session = poeauth.login_using_verification_code( 34 | verification_code=verification_code, mode="email", email=email) 35 | elif phone: 36 | if status == "user_with_confirmed_phone_number_not_found": 37 | auth_session = poeauth.signup_using_verification_code( 38 | verification_code=verification_code, mode="phone", phone=phone) 39 | else: 40 | auth_session = poeauth.login_using_verification_code( 41 | verification_code=verification_code, mode="phone", phone=phone) 42 | except PoeAuthExceptionV1 as e: 43 | click.echo(str(e)) 44 | return 45 | 46 | click.echo(f"Successful authentication. Session cookie: {auth_session}") 47 | 48 | 49 | def cli_V2(email): 50 | with sync_playwright() as playwright: 51 | poeauth = PoeAuthV2(playwright) 52 | 53 | if email is None: 54 | click.echo("Email address or phone number is required.") 55 | return 56 | 57 | try: 58 | if email: 59 | status = poeauth.send_verification_code(email=email) 60 | except PoeAuthExceptionV2 as e: 61 | click.echo(str(e)) 62 | return 63 | 64 | verification_code = click.prompt( 65 | f"Enter the verification code sent to {email}", type=str) 66 | 67 | try: 68 | if email: 69 | if status == "user_with_confirmed_email_not_found": 70 | auth_session = poeauth.signup_using_verification_code( 71 | verification_code=verification_code, mode="email") 72 | else: 73 | auth_session = poeauth.login_using_verification_code( 74 | verification_code=verification_code, mode="email") 75 | except PoeAuthExceptionV2 as e: 76 | click.echo(str(e)) 77 | return 78 | 79 | click.echo(f"Successful authentication. Session cookie: {auth_session}") 80 | 81 | 82 | @click.command() 83 | @click.option("--email", help="User email address") 84 | @click.option("--phone", help="User phone number") 85 | @click.option("--help", is_flag=True, help="Show help message") 86 | @click.option("--browser", is_flag=True, help="Use browser") 87 | def cli(email, phone, help, browser): 88 | if help: 89 | click.echo("Usage: poe-auth [OPTIONS]") 90 | click.echo("Options:") 91 | click.echo(" --email TEXT User email address") 92 | click.echo(" --phone TEXT User phone number") 93 | click.echo(" --help Show help message") 94 | click.echo(" --browser Use browser") 95 | return 96 | 97 | if browser: 98 | if phone: 99 | click.echo("Phone number is not supported in browser mode.") 100 | return 101 | cli_V2(email, phone) 102 | else: 103 | cli_V1(email, phone) 104 | 105 | 106 | if __name__ == "__main__": 107 | cli() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | requests 3 | fake-useragent 4 | playwright -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = '2.0.5' 4 | DESCRIPTION = 'A reverse-engineered Python library for the Quora\'s Poe authentication API.' 5 | setup( 6 | name="poe-auth", 7 | version=VERSION, 8 | author="Anhy Krishna Fitiavana", 9 | author_email="fitiavana.krishna@gmail.com", 10 | description=DESCRIPTION, 11 | long_description=open('README.md').read(), 12 | packages=find_packages(), 13 | install_requires=['requests', 'click', 'fake-useragent', 'playwright'], 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'poe-auth=poe_auth.cli:cli', 17 | ], 18 | }, 19 | classifiers=[ 20 | "Development Status :: 5 - Production/Stable", 21 | "Intended Audience :: Developers", 22 | "Programming Language :: Python :: 3", 23 | "Operating System :: OS Independent", 24 | ] 25 | ) 26 | --------------------------------------------------------------------------------