From b73a988f09c30cee0ffe63d8e557db59d2273f66 Mon Sep 17 00:00:00 2001 From: 3mora2 <66757189+3mora2@users.noreply.github.com> Date: Fri, 21 Jun 2024 04:30:02 +0300 Subject: [PATCH] improve improve detect scan qrcode or in chat add loginByCode --- WPP_Whatsapp/api/layers/HostLayer.py | 79 +++++++++++++++++++---- WPP_Whatsapp/api/layers/RetrieverLayer.py | 45 ++++++------- WPP_Whatsapp/api/layers/SenderLayer.py | 23 +++++++ WPP_Whatsapp/controllers/browser.py | 22 +++++++ WPP_Whatsapp/controllers/initializer.py | 62 ++++++------------ examples/get_messages.py | 2 +- setup.py | 2 +- 7 files changed, 159 insertions(+), 76 deletions(-) diff --git a/WPP_Whatsapp/api/layers/HostLayer.py b/WPP_Whatsapp/api/layers/HostLayer.py index 1dc5770..2837e7d 100644 --- a/WPP_Whatsapp/api/layers/HostLayer.py +++ b/WPP_Whatsapp/api/layers/HostLayer.py @@ -6,6 +6,8 @@ import re from datetime import datetime from pathlib import Path +from typing import Callable + from playwright.async_api import Page from WPP_Whatsapp.api.const import whatsappUrl, Logger from WPP_Whatsapp.api.helpers.function import asciiQr @@ -40,8 +42,11 @@ class HostLayer: version: str wa_js_version: str loop: object + catchLinkCode: Callable[[str], None] = None def __init__(self): + self.isInChat = False + self.isLogged = False self.__initialize() def catchQR(self, **kwargs): @@ -103,8 +108,18 @@ async def _afterPageScriptInjectedHost(self): page=self.page) except: Logger.exception("window.checkQrCode") - await self.__checkQrCode() - await self.__checkInChat() + + self.logger.info(f'{self.session}: Wait First selector (INTRO_IMG, INTRO_QRCODE)') + INTRO_IMG_SELECTOR = '[data-icon=\'search\']' + INTRO_QRCODE_SELECTOR = 'div[data-ref] canvas' + result = await self.ThreadsafeBrowser.wait_for_first_selectors(INTRO_IMG_SELECTOR, INTRO_QRCODE_SELECTOR) + needAuthentication = True if result == INTRO_QRCODE_SELECTOR else False + + self.logger.info(f'{self.session}: {needAuthentication=}') + if needAuthentication: + await self.__checkQrCode() + else: + await self.__checkInChat() async def start(self): if self.isStarted: @@ -118,8 +133,9 @@ async def start(self): await self.ThreadsafeBrowser.expose_function('checkInChat', self.__checkInChat, page=self.page) # ToDo: self.logger.info(f'{self.session}: setInterval__checkStart') + # Clear in whatsapp self.checkStartInterval = setInterval(self.loop, self.__checkStart, 10) - # self.page.on('close', lambda: self.clearInterval(self.checkStartInterval)) + # return True ############################### initWhatsapp #################################################### @@ -185,7 +201,8 @@ async def __checkStart(self): async def __checkQrCode(self): need_scan = await self.__needsToScan() - self.isLogged = not need_scan + Logger.info(f"{self.session}: __checkQrCode {need_scan=}") + self.isLogged = not need_scan if need_scan is not None else need_scan if not need_scan: self.attempt = 0 return @@ -216,6 +233,20 @@ async def __checkQrCode(self): urlCode=result.get("urlCode") ) + async def __loginByCode(self, phone: str): + code = self.ThreadsafeBrowser.page_evaluate("""async ({ phone }) => { + return JSON.parse( + JSON.stringify(await WPP.conn.genLinkDeviceCodeForPhoneNumber(phone)) + ); + }""", {"phone": phone}) + + if self.logQR: + Logger.info(f'Waiting for Login By Code (Code: {code})\n') + else: + Logger.info("Waiting for Login By Code") + if self.catchLinkCode: + self.catchLinkCode(code) + async def __checkInChat(self): in_chat = await self.isInsideChat() self.isInChat = in_chat @@ -300,8 +331,12 @@ def cancelAutoClose(self): # self.autoCloseInterval = None async def __getQrCode(self): - qr_result = await self.scrapeImg() - return qr_result + try: + qr_result = await self.scrapeImg() + return qr_result + except: + Logger.exception("__getQrCode") + return def waitForQrCodeScan(self): if not self.isStarted: @@ -372,7 +407,8 @@ def waitForPageLoad(self): # TODO:: self.ThreadsafeBrowser.sleep(.2) - self.ThreadsafeBrowser.page_wait_for_function_sync("() => window.WPP.isReady", page=self.page) + self.ThreadsafeBrowser.page_wait_for_function_sync("() => window.WPP.isReady", timeout=120 * 1000, + page=self.page) async def waitForPageLoad_(self): while not self.isInjected: @@ -381,7 +417,7 @@ async def waitForPageLoad_(self): # TODO:: await asyncio.sleep(.2) - await self.ThreadsafeBrowser.page_wait_for_function("() => window.WPP.isReady", page=self.page) + await self.ThreadsafeBrowser.page_wait_for_function("() => window.WPP.isReady", timeout=120 * 1000, page=self.page) async def waitForLogin_(self): self.logger.info(f'{self.session}: http => Waiting page load') @@ -557,12 +593,12 @@ def isMultiDevice(self): async def isAuthenticated(self): try: if self.page.is_closed(): - return False + return None return await self.ThreadsafeBrowser.page_evaluate( "() => typeof window.WPP !== 'undefined' && window.WPP.conn.isRegistered()", page=self.page) except Exception as e: self.logger.debug(e) - return False + return None def sync_isAuthenticated(self): try: @@ -575,7 +611,8 @@ def sync_isAuthenticated(self): return False async def __needsToScan(self): - return not await self.isAuthenticated() + rs = await self.isAuthenticated() + return not rs if rs is not None else rs def __sync_needsToScan(self): return not self.sync_isAuthenticated() @@ -634,6 +671,15 @@ def sync_isInsideChat(self): "() => typeof window.WPP !== 'undefined' && window.WPP.conn.isMainReady()", page=self.page) return result if result else False + # /** + # * Returns the version of WhatsApp Web currently being run + # * @returns {Promise} + # */ + async def getWWebVersion(self): + return await self.ThreadsafeBrowser.page_evaluate("""() => { + return window.Debug.VERSION; + }""") + async def inject_api(self): self.logger.debug(f'{self.session}: start inject') try: @@ -648,6 +694,17 @@ async def inject_api(self): return self.logger.info(f'{self.session}: injected state: {injected}') + + await self.ThreadsafeBrowser.page_wait_for_function('window.Debug?.VERSION != undefined') + # TODO:: + # version = await self.getWWebVersion() + # isCometOrAbove = int(version.split('.')[1]) >= 3000 + # Logger.info(f"version {version}, {isCometOrAbove=}") + # if isCometOrAbove: + # await self.ThreadsafeBrowser.page_evaluate(ExposeAuthStore, page=self.pupPage) + # else: + # await self.ThreadsafeBrowser.page_evaluate(ExposeLegacyAuthStore, ModuleRaid, page=self.pupPage) + # self.logger.info(f'{self.session}: wait for load webpackChunkwhatsapp_web_client') # try: # # await self.ThreadsafeBrowser.page_evaluate( diff --git a/WPP_Whatsapp/api/layers/RetrieverLayer.py b/WPP_Whatsapp/api/layers/RetrieverLayer.py index e152c16..66ffb38 100644 --- a/WPP_Whatsapp/api/layers/RetrieverLayer.py +++ b/WPP_Whatsapp/api/layers/RetrieverLayer.py @@ -147,28 +147,29 @@ async def getAllChats_(self, withNewMessageOnly=False): async def checkNumberStatus_(self, contactId): # @returns contact detial as promise contactId = self.valid_chatId(contactId) - # return await self.ThreadsafeBrowser.page_evaluate("(contactId) => WAPI.checkNumberStatus(contactId)", contactId, page=self.page) - result = await self.ThreadsafeBrowser.page_evaluate( - "(contactId) => WPP.contact.queryExists(contactId)", - contactId, page=self.page) - if not result: - return { - "id": contactId, - "isBusiness": False, - "canReceiveMessage": False, - "numberExists": False, - "status": 404, - "result": result - } - else: - return { - "id": result.get("wid"), - "isBusiness": result.get("biz"), - "canReceiveMessage": True, - "numberExists": True, - "status": 200, - "result": result - } + return await self.ThreadsafeBrowser.page_evaluate( + "(contactId) => WAPI.checkNumberStatus(contactId)", contactId, page=self.page) + # result = await self.ThreadsafeBrowser.page_evaluate( + # "(contactId) => WPP.contact.queryExists(contactId)", + # contactId, page=self.page) + # if not result: + # return { + # "id": contactId, + # "isBusiness": False, + # "canReceiveMessage": False, + # "numberExists": False, + # "status": 404, + # "result": result + # } + # else: + # return { + # "id": result.get("wid"), + # "isBusiness": result.get("biz"), + # "canReceiveMessage": True, + # "numberExists": True, + # "status": 200, + # "result": result + # } async def getAllChatsWithMessages_(self, withNewMessageOnly=False): # @returns array of [Chat] diff --git a/WPP_Whatsapp/api/layers/SenderLayer.py b/WPP_Whatsapp/api/layers/SenderLayer.py index ce1e48b..4b52147 100644 --- a/WPP_Whatsapp/api/layers/SenderLayer.py +++ b/WPP_Whatsapp/api/layers/SenderLayer.py @@ -415,6 +415,29 @@ async def forwardMessages_(self, to, messages, skipMyMessages): {"to": to, "messages": messages, "skipMyMessages": skipMyMessages}, page=self.page) + async def sendImageAsStickerGif_(self, to: str,pathOrBase64: str,options={}): + """ + /** + * Generates sticker from the provided animated gif image and sends it (Send image as animated sticker) + * + * @example + * ```javascript + * client.sendImageAsStickerGif('000000000000@c.us', 'base64....'); + * ``` + * + * @example + * Send Sticker with reply + * ```javascript + * client.sendImageAsStickerGif('000000000000@c.us', 'base64....', { + * quotedMsg: 'msgId', + * }); + * ``` + * @category Chat + * @param pathOrBase64 image path imageBase64 A valid gif image is required. You can also send via http/https (http://www.website.com/img.gif) + * @param to chatId '000000000000@c.us' + */ + """ + ... async def sendLocation_(self, to, options): to = self.valid_chatId(to) options = { diff --git a/WPP_Whatsapp/controllers/browser.py b/WPP_Whatsapp/controllers/browser.py index 13c1a5d..c6ebe06 100644 --- a/WPP_Whatsapp/controllers/browser.py +++ b/WPP_Whatsapp/controllers/browser.py @@ -1,5 +1,6 @@ import asyncio +import playwright from PlaywrightSafeThread.browser.threadsafe_browser import ThreadsafeBrowser as Tb, BrowserName, SUPPORTED_BROWSERS, \ Logger from playwright.async_api import Error @@ -50,3 +51,24 @@ def run_threadsafe(self, func, *args, timeout_=120, **kwargs): if not asyncio.iscoroutine(func): func = func(*args, **kwargs) return super().run_threadsafe(func, timeout_=timeout_) + + async def wait_for_first_selectors(self, *selectors, timeout=0): + async def wa(selector): + try: + await self.page.wait_for_selector(selector, timeout=timeout) + return selector + except playwright._impl._errors.TimeoutError: + return + + tasks = [self.loop.create_task(wa(selector)) for selector in selectors] + while True: + task_done = next(filter(lambda task: task.done(), tasks), None) + pending = list(filter(lambda task: not task.done(), tasks)) + if not task_done: + await asyncio.sleep(.5) + continue + + for p in pending: + self.loop.call_soon_threadsafe(p.cancel) + + return task_done.result() diff --git a/WPP_Whatsapp/controllers/initializer.py b/WPP_Whatsapp/controllers/initializer.py index b6125a4..ce3e39e 100644 --- a/WPP_Whatsapp/controllers/initializer.py +++ b/WPP_Whatsapp/controllers/initializer.py @@ -15,7 +15,7 @@ class Create: def __init__( self, session: str, user_data_dir='', folderNameToken="", catchQR=None, - statusFind=None, onLoadingScreen=None, + statusFind=None, onLoadingScreen=None, catchLinkCode=None, onStateChange=None, waitForLogin: bool = True, logQR: bool = False, autoClose: int = 0, version=None, wa_js_version=None, *args, **kwargs) -> None: """ @@ -56,6 +56,9 @@ class Create: self.statusFind = statusFind if type(statusFind) in [types.FunctionType, types.MethodType] else self.statusFind self.onLoadingScreen = onLoadingScreen if type( onLoadingScreen) in [types.FunctionType, types.MethodType] else self.onLoadingScreen + + self.catchLinkCode = catchLinkCode + self.onStateChange = onStateChange if type(onStateChange) in [types.FunctionType, types.MethodType] else None self.logger = Logger self.waitForLogin = waitForLogin @@ -80,7 +83,8 @@ def _onStateChange(self, state): self.state = state if hasattr(self, "ThreadsafeBrowser") and not self.client.page.is_closed(): # TODO:: - connected = self.ThreadsafeBrowser.page_evaluate_sync("() => WPP.conn.isRegistered()", page=self.client.page) + connected = self.ThreadsafeBrowser.page_evaluate_sync("() => WPP.conn.isRegistered()", + page=self.client.page) if not connected: self.ThreadsafeBrowser.sleep(2) if not self.waitLoginPromise: @@ -142,38 +146,8 @@ async def start_(self) -> "Whatsapp": return self.client def create_sync(self) -> Whatsapp: - self.state = "STARTING" - default = { - "no_viewport": True, "bypass_csp": True, "headless": False, - "browser": "chromium", "install": True, "user_agent": useragentOverride - } - default.update(self.__kwargs) - self.__kwargs = default - # for key in default: - # if key not in self.__kwargs: - # self.__kwargs[key] = default[key] - - # Use Default channel as chrome - if self.__kwargs.get("browser") == "chrome" or self.__kwargs.get("browser") not in SUPPORTED_BROWSERS: - self.__kwargs["browser"] = "chromium" - self.__kwargs["channel"] = "chrome" - - self.ThreadsafeBrowser = ThreadsafeBrowser(user_data_dir=self.user_data_dir, **self.__kwargs) - - self.ThreadsafeBrowser.page.on("close", self.close) - self.ThreadsafeBrowser.page.on("crash", self.close) - self.ThreadsafeBrowser.browser.on("disconnected", lambda: self.statusFind('browserClose', self.session)) - - self.client = Whatsapp(self.session, - threadsafe_browser=self.ThreadsafeBrowser, page=self.ThreadsafeBrowser.page, - loop=self.loop, logQR=self.logQR, - autoClose=self.autoClose, version=self.version, wa_js_version=self.wa_js_version) - - self.client.catchQR = self.catchQR - self.client.statusFind = self.statusFind - self.client.onLoadingScreen = self.onLoadingScreen + self.__create() self.ThreadsafeBrowser.run_threadsafe(self.client.start, timeout_=120) - self.client.onStateChange(self._onStateChange) if self.waitForLogin: is_logged = self.client.waitForLogin() if not is_logged: @@ -184,6 +158,18 @@ def create_sync(self) -> Whatsapp: return self.client async def create(self) -> Whatsapp: + self.__create() + await self.client.start() + if self.waitForLogin: + is_logged = await self.client.waitForLogin_() + if not is_logged: + raise Exception('Not Logged') + self.state = "CONNECTED" + self.setup() + + return self.client + + def __create(self): self.state = "STARTING" default = { "no_viewport": True, "bypass_csp": True, "headless": False, @@ -212,17 +198,11 @@ async def create(self) -> Whatsapp: self.client.catchQR = self.catchQR self.client.statusFind = self.statusFind self.client.onLoadingScreen = self.onLoadingScreen - await self.client.start() + self.client.catchLinkCode = self.catchLinkCode + # self.ThreadsafeBrowser.run_threadsafe(self.client.start, timeout_=120) self.client.onStateChange(self._onStateChange) - if self.waitForLogin: - is_logged = await self.client.waitForLogin_() - if not is_logged: - raise Exception('Not Logged') - self.state = "CONNECTED" - self.setup() - return self.client def get_state(self) -> dict: return { diff --git a/examples/get_messages.py b/examples/get_messages.py index 1e097ae..e27fccb 100644 --- a/examples/get_messages.py +++ b/examples/get_messages.py @@ -16,4 +16,4 @@ # messages = client.getMessages(phone_number) # messages = client. getGroupMembers("120363022378011811@g.us") # print(messages) -# client.joinGroup("KOtrjvEwQk8DlDw8mv9IAE") \ No newline at end of file +# client.joinGroup("KOtrjvEwQk8DlDw8mv9IAE") diff --git a/setup.py b/setup.py index 9866d04..578b8f3 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ "the creation of any interaction, such as customer service, media sending, intelligence recognition " "based on phrases artificial and many other things, use your imagination") -version = "0.3.5" +version = "0.4.0" setup( name="WPP_Whatsapp",