Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
improve detect scan qrcode or in chat
add loginByCode
  • Loading branch information
3mora2 committed Jun 21, 2024
1 parent 073ad4e commit b73a988
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 76 deletions.
79 changes: 68 additions & 11 deletions WPP_Whatsapp/api/layers/HostLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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 ####################################################
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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<string>}
# */
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:
Expand All @@ -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(
Expand Down
45 changes: 23 additions & 22 deletions WPP_Whatsapp/api/layers/RetrieverLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 23 additions & 0 deletions WPP_Whatsapp/api/layers/SenderLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
22 changes: 22 additions & 0 deletions WPP_Whatsapp/controllers/browser.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
62 changes: 21 additions & 41 deletions WPP_Whatsapp/controllers/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit b73a988

Please sign in to comment.