diff --git a/WPP_Whatsapp/api/layers/HostLayer.py b/WPP_Whatsapp/api/layers/HostLayer.py index 2837e7d..6b8a72f 100644 --- a/WPP_Whatsapp/api/layers/HostLayer.py +++ b/WPP_Whatsapp/api/layers/HostLayer.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Callable +from playwright._impl._errors import TargetClosedError from playwright.async_api import Page from WPP_Whatsapp.api.const import whatsappUrl, Logger from WPP_Whatsapp.api.helpers.function import asciiQr @@ -27,6 +28,7 @@ class HostLayer: isInitialized: bool isInjected: bool isStarted: bool + isClosed: bool isLogged: bool isInChat: bool urlCode: str @@ -47,6 +49,7 @@ class HostLayer: def __init__(self): self.isInChat = False self.isLogged = False + self.isClosed = False self.__initialize() def catchQR(self, **kwargs): @@ -65,12 +68,21 @@ def __initialize(self): self.isInitialized = True async def on_close(self, _): + self.isClosed = True self.logger.info(f'{self.session}: Page Closed') self.cancelAutoClose() async def on_load(self, _): - self.logger.info(f'{self.session}: Page loaded') - await self._afterPageLoad() + if self.isClosed: + return + try: + self.logger.info(f'{self.session}: Page loaded') + await self._afterPageLoad() + except (RuntimeError, TargetClosedError): + # mean stop app + self.logger.info(f'{self.session}: Stop App, Auto Close') + self.isClosed = True + await self.tryAutoClose() async def _afterPageLoad(self): self.logger.info(f'{self.session}: Injecting wapi.js') @@ -197,6 +209,9 @@ async def __handel_request(self, ): ################################################################################################# async def __checkStart(self): + if self.isClosed and hasattr(self, "checkStartInterval"): + self.clearInterval(self.checkStartInterval) + return await self.__needsToScan() async def __checkQrCode(self): @@ -257,27 +272,50 @@ async def __checkInChat(self): self.statusFind('inChat', self.session) async def tryAutoClose(self): + if self.isClosed: + self.logger.info(f'{self.session}: Closing the page') + self.statusFind('autocloseCalled', self.session) + if not self.page.is_closed(): + await self.ThreadsafeBrowser.close() + + if not hasattr(self, "autoCloseInterval"): + return + if self.autoCloseInterval: self.cancelAutoClose() if (self.autoClose > 0 or self.options.get( "deviceSyncTimeout") > 0) and ( - not self.autoCloseInterval or self.autoCloseInterval.is_set()) and not self.page.is_closed(): + not self.autoCloseInterval or self.autoCloseInterval.is_set()): + self.logger.info(f'{self.session}: Closing the page') self.autoCloseCalled = True + + self.isClosed = True self.statusFind('autocloseCalled', self.session) if not self.page.is_closed(): await self.ThreadsafeBrowser.close() def sync_tryAutoClose(self): + if self.isClosed: + self.logger.info(f'{self.session}: Closing the page') + self.statusFind('autocloseCalled', self.session) + if not self.page.is_closed(): + self.ThreadsafeBrowser.sync_close() + + if not hasattr(self, "autoCloseInterval"): + return + if self.autoCloseInterval: self.cancelAutoClose() if (self.autoClose > 0 or self.options.get( "deviceSyncTimeout") > 0) and ( - not self.autoCloseInterval or self.autoCloseInterval.is_set()) and not self.page.is_closed(): + not self.autoCloseInterval or self.autoCloseInterval.is_set()): self.logger.info(f'{self.session}: Closing the page') self.autoCloseCalled = True + + self.isClosed = True self.statusFind('autocloseCalled', self.session) if not self.page.is_closed(): self.ThreadsafeBrowser.sync_close() @@ -317,6 +355,10 @@ async def autoCloseIntervalHandel(self): self.cancelAutoClose() return + if not self.isStarted or self.isClosed: + self.cancelAutoClose() + return + self.remain -= 1 if self.remain % 10 == 0 or self.remain <= 5: self.logger.info(f'{self.session}: http => Auto close remain: {self.remain}s') @@ -341,7 +383,7 @@ async def __getQrCode(self): def waitForQrCodeScan(self): if not self.isStarted: raise Exception('waitForQrCodeScan error: Session not started') - while not self.page.is_closed() and not self.isLogged: + while not self.page.is_closed() and not self.isLogged and not self.isClosed: # sleep(200 / 1000) self.ThreadsafeBrowser.sleep(0.2) needScan = self.__sync_needsToScan() @@ -350,7 +392,7 @@ def waitForQrCodeScan(self): async def waitForQrCodeScan_(self): if not self.isStarted: raise Exception('waitForQrCodeScan error: Session not started') - while not self.page.is_closed() and not self.isLogged: + while not self.page.is_closed() and not self.isLogged and not self.isClosed: # sleep(200 / 1000) await asyncio.sleep(200 / 1000) needScan = await self.__needsToScan() @@ -365,7 +407,7 @@ def waitForInChat(self): return False start = datetime.now() - while not self.page.is_closed() and self.isLogged and not self.isInChat: + while not self.page.is_closed() and self.isLogged and not self.isInChat and not self.isClosed: if 0 < self.options.get("deviceSyncTimeout") <= (datetime.now() - start).seconds: Logger.info(f"deviceSyncTimeout:{self.options.get('deviceSyncTimeout')} timeout") return False @@ -387,7 +429,7 @@ async def waitForInChat_(self): return False start = datetime.now() - while not self.page.is_closed() and self.isLogged and not self.isInChat: + while not self.page.is_closed() and self.isLogged and not self.isInChat and not self.isClosed: if 0 < self.options.get("deviceSyncTimeout") <= (datetime.now() - start).seconds: Logger.info(f"deviceSyncTimeout:{self.options.get('deviceSyncTimeout')} timeout") return False @@ -404,6 +446,9 @@ def waitForPageLoad(self): while not self.isInjected: if self.page.is_closed(): return + # Stop when close + if self.isClosed: + return # TODO:: self.ThreadsafeBrowser.sleep(.2) @@ -414,10 +459,14 @@ async def waitForPageLoad_(self): while not self.isInjected: if self.page.is_closed(): return + # Stop when close + if self.isClosed: + return # TODO:: await asyncio.sleep(.2) - await self.ThreadsafeBrowser.page_wait_for_function("() => window.WPP.isReady", timeout=120 * 1000, 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') @@ -477,7 +526,7 @@ async def waitForLogin_(self): self.logger.error(f'{self.session}: Auto Close Called') raise Exception("Auto Close Called") - if self.page.is_closed(): + if self.page.is_closed() or self.isClosed: self.logger.error(f'{self.session}: Page Closed') raise Exception("Page Closed") @@ -542,7 +591,7 @@ def waitForLogin(self): self.logger.error(f'{self.session}: Auto Close Called') raise Exception("Auto Close Called") - if self.page.is_closed(): + if self.page.is_closed() or self.isClosed: self.logger.error(f'{self.session}: Page Closed') raise Exception("Page Closed") @@ -592,7 +641,7 @@ def isMultiDevice(self): async def isAuthenticated(self): try: - if self.page.is_closed(): + if self.page.is_closed() or self.isClosed: return None return await self.ThreadsafeBrowser.page_evaluate( "() => typeof window.WPP !== 'undefined' && window.WPP.conn.isRegistered()", page=self.page) @@ -602,7 +651,7 @@ async def isAuthenticated(self): def sync_isAuthenticated(self): try: - if self.page.is_closed(): + if self.page.is_closed() or self.isClosed: return False return self.ThreadsafeBrowser.page_evaluate_sync( "() => typeof window.WPP !== 'undefined' && window.WPP.conn.isRegistered()", page=self.page) @@ -802,6 +851,7 @@ def valid_chatId(chatId): return chatId def close(self): + self.isClosed = True self.ThreadsafeBrowser.sync_close() @staticmethod diff --git a/WPP_Whatsapp/controllers/browser.py b/WPP_Whatsapp/controllers/browser.py index d73fea2..7b98664 100644 --- a/WPP_Whatsapp/controllers/browser.py +++ b/WPP_Whatsapp/controllers/browser.py @@ -1,8 +1,7 @@ import asyncio - -import playwright +import subprocess from PlaywrightSafeThread.browser.threadsafe_browser import ThreadsafeBrowser as Tb, BrowserName, SUPPORTED_BROWSERS, \ - Logger + Logger, creation_flags_dict, compute_driver_executable from playwright.async_api import Error @@ -57,7 +56,7 @@ async def wa(selector): try: await self.page.wait_for_selector(selector, timeout=timeout) return selector - except : + except: return tasks = [self.loop.create_task(wa(selector)) for selector in selectors] @@ -73,3 +72,25 @@ async def wa(selector): return task_done.result() + def run_playwright(self, *args: str): + env = self.get_driver_env() + driver_executable, driver_cli = compute_driver_executable() + + with subprocess.Popen([driver_executable, driver_cli, *args], env=env, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, **creation_flags_dict()) as process: + for line in process.stdout: + print(line.decode('utf-8'), end="\r") + + def sync_close(self, timeout_=60): + try: + self.run_threadsafe(self.__stop_playwright(), timeout_=timeout_) + except Exception as e: + print(e) + self.stop() + + async def close(self): + try: + await self.create_task(self.__stop_playwright()) + except Exception as e: + print(e) + self.stop() diff --git a/WPP_Whatsapp/controllers/initializer.py b/WPP_Whatsapp/controllers/initializer.py index ce3e39e..c88555a 100644 --- a/WPP_Whatsapp/controllers/initializer.py +++ b/WPP_Whatsapp/controllers/initializer.py @@ -70,13 +70,18 @@ def __exit__(self, *args): self.sync_close() async def close(self): + if self.client: + self.client.isClosed = True if hasattr(self, "ThreadsafeBrowser"): await self.ThreadsafeBrowser.close() self._onStateChange("CLOSED") def sync_close(self): + if self.client: + self.client.isClosed = True if hasattr(self, "ThreadsafeBrowser"): self.ThreadsafeBrowser.sync_close() + self._onStateChange("CLOSED") def _onStateChange(self, state): @@ -139,7 +144,7 @@ async def start_(self) -> "Whatsapp": await self.create() elif self.state in ["CONFLICT", "UNPAIRED", "UNLAUNCHED"]: Logger.info("client.useHere()") - self.client.useHere() + await self.client.useHere_() else: Logger.info(self.get_state()) diff --git a/examples/send_text_message.py b/examples/send_text_message.py index fd822a1..1211c62 100644 --- a/examples/send_text_message.py +++ b/examples/send_text_message.py @@ -18,6 +18,7 @@ message = "hello from wpp" phone_number = "***********" # or "+***********" +creator.sync_close() # example # Simple message # result = client.sendText(phone_number, message) diff --git a/setup.py b/setup.py index e9413af..711527a 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.4.1" +version = "0.4.5" setup( name="WPP_Whatsapp", diff --git a/test/3.py b/test/3.py index 3ac7def..a9f53e0 100644 --- a/test/3.py +++ b/test/3.py @@ -1,39 +1,40 @@ -import logging +try: + from WPP_Whatsapp import Create +except (ModuleNotFoundError, ImportError): + import sys, os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + from WPP_Whatsapp import Create -from WPP_Whatsapp import Create +import logging -logger = logging.getLogger() +logger = logging.getLogger(name="WPP_Whatsapp") logger.setLevel(logging.DEBUG) -def new_message(message): - global client - if message: - mensagem = message.get('body') - chat_id = message.get('from') - - # Extract user ID and message content - usuario_id = message.get('chatId')['user'] - - print(usuario_id, mensagem) - - if usuario_id == 'mynumber': - client.sendText(chat_id, "Here's what you said:" + mensagem) - - - +# start client with your session name your_session_name = "test" -creator = Create(session=your_session_name, browser='chrome') +creator = Create(session=your_session_name, browser="firefox") client = creator.start() +# Now scan Whatsapp Qrcode in browser # check state of login if creator.state != 'CONNECTED': raise Exception(creator.state) -print('Starting!') - -client.sendText('***********0', 'Testando docker') - -creator.client.onMessage(new_message) -# creator.loop.run_forever() -# while True: -# pass \ No newline at end of file +message = "hello from wpp" +phone_number = "***********" # or "+***********" + +creator.sync_close() +# example +# Simple message +# result = client.sendText(phone_number, message) +# print(result) +# client.forwardMessages("***********@c.us", 'true_***********@c.us_3EB07B4EABBAB75F0BD08A_out') +""" +sendText: + Sends a text message to given chat + @category Chat + @param to chat id: xxxxx@us.c + @param content text message + @option dict + return dict -> {'id': 'true_**********@c.us_*************_out', 'ack': 3, 'sendMsgResult': {}} +""" diff --git a/test/qt.py b/test/qt.py new file mode 100644 index 0000000..79bb054 --- /dev/null +++ b/test/qt.py @@ -0,0 +1,80 @@ +import threading +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QLineEdit, QVBoxLayout, QWidget +try: + from WPP_Whatsapp import Create +except (ModuleNotFoundError, ImportError): + import sys, os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + from WPP_Whatsapp import Create + +import logging + +logger = logging.getLogger(name="WPP_Whatsapp") +logger.setLevel(logging.DEBUG) + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.initUI() + self.creators = [] + + def initUI(self): + self.setWindowTitle('WhatsApp Messenger') + self.setGeometry(100, 100, 400, 300) + + self.session_input = QLineEdit(self) + self.session_input.move(20, 20) + self.session_input.resize(360, 30) + self.session_input.setPlaceholderText("Enter session name") + + self.phone_input = QLineEdit(self) + self.phone_input.move(20, 70) + self.phone_input.resize(360, 30) + self.phone_input.setPlaceholderText("Enter phone number") + + self.message_input = QLineEdit(self) + self.message_input.move(20, 120) + self.message_input.resize(360, 30) + self.message_input.setPlaceholderText("Enter message") + + self.send_button = QPushButton('Send Message', self) + self.send_button.move(20, 170) + self.send_button.clicked.connect(self.send_message) + + def send_message(self): + session_name = self.session_input.text() + phone_number = self.phone_input.text() + message = self.message_input.text() + + # Start the WhatsApp client in a separate thread + threading.Thread(target=self.start_whatsapp_client, args=(session_name, phone_number, message)).start() + + def start_whatsapp_client(self, session_name, phone_number, message): + creator = Create(session=session_name) + try: + self.creators.append(creator) + client = creator.start() + if creator.state != 'CONNECTED': + raise Exception(creator.state) + + # Send the message + result = client.sendText(phone_number, message) + print("Message sent successfully:", result) + creator.sync_close() + except Exception as e: + print("Exception", e) + finally: + creator.client.isClosed = True + + def closeEvent(self, a0): + # Ensure Close all + for creator in self.creators: + creator.client.isClosed = True + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_())