mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-18 10:01:37 +00:00
Merge pull request #60 from Abhijitgo08/fix/stale-elements-and-offline-fallbacks
Fix StaleElementReferenceException in Daily Set, add Ollama offline fallback & PIL visual search generator
This commit is contained in:
@@ -143,11 +143,20 @@ class ElementSelectionUtils:
|
|||||||
return self.driver.find_element(By.CSS_SELECTOR, '[id$="-tab-/dashboard"]')
|
return self.driver.find_element(By.CSS_SELECTOR, '[id$="-tab-/dashboard"]')
|
||||||
|
|
||||||
def get_sidebar_section(self):
|
def get_sidebar_section(self):
|
||||||
for section in self.driver.find_elements(By.TAG_NAME, "section"):
|
sections = self.driver.find_elements(By.TAG_NAME, "section")
|
||||||
|
for section in sections:
|
||||||
try:
|
try:
|
||||||
# get_dom_attribute returns None for sections without an id,
|
sec_id = section.get_dom_attribute("id") or ""
|
||||||
# so normalise before comparing.
|
if sec_id.startswith("react-aria") and section.is_displayed():
|
||||||
if (section.get_dom_attribute("id") or "").startswith("react-aria"):
|
if section.find_elements(By.TAG_NAME, "a") or section.find_elements(By.TAG_NAME, "button"):
|
||||||
|
return section
|
||||||
|
except StaleElementReferenceException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
try:
|
||||||
|
sec_id = section.get_dom_attribute("id") or ""
|
||||||
|
if sec_id.startswith("react-aria"):
|
||||||
return section
|
return section
|
||||||
except StaleElementReferenceException:
|
except StaleElementReferenceException:
|
||||||
continue
|
continue
|
||||||
@@ -236,6 +245,12 @@ class ElementSelectionUtils:
|
|||||||
for marker in ("bing.com/search", "bing.com/rewards", "rewards.bing.com/")
|
for marker in ("bing.com/search", "bing.com/rewards", "rewards.bing.com/")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_daily_set_element_by_index(self, index: int):
|
||||||
|
elements = self.get_daily_set_elements()
|
||||||
|
if index < len(elements):
|
||||||
|
return elements[index]
|
||||||
|
raise NoSuchElementException(f"daily set element at index {index} not found")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# explore on bing (absent in en-US, present in some other markets)
|
# explore on bing (absent in en-US, present in some other markets)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
+31
-4
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
@@ -41,13 +42,19 @@ _CLIENT = ollama.Client(timeout=180)
|
|||||||
MAX_EMPTY_RETRIES = 5
|
MAX_EMPTY_RETRIES = 5
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaOfflineException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_ollama_response(messages: list[dict[str, str]], model: str="gemma4:cloud") -> str:
|
def get_ollama_response(messages: list[dict[str, str]], model: str="gemma4:cloud") -> str:
|
||||||
|
try:
|
||||||
response = _CLIENT.chat(
|
response = _CLIENT.chat(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages
|
messages=messages
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.message.content
|
return response.message.content
|
||||||
|
except Exception as exc:
|
||||||
|
raise OllamaOfflineException(f"Ollama service error: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
def get_nonempty_ollama_response(messages: list[dict[str, str]]) -> str:
|
def get_nonempty_ollama_response(messages: list[dict[str, str]]) -> str:
|
||||||
@@ -62,6 +69,7 @@ def get_nonempty_ollama_response(messages: list[dict[str, str]]) -> str:
|
|||||||
|
|
||||||
raise RuntimeError(f"LLM returned nothing usable after {MAX_EMPTY_RETRIES} attempts")
|
raise RuntimeError(f"LLM returned nothing usable after {MAX_EMPTY_RETRIES} attempts")
|
||||||
|
|
||||||
|
|
||||||
def get_search_query_from_task_description(task_description: str) -> str:
|
def get_search_query_from_task_description(task_description: str) -> str:
|
||||||
# compat
|
# compat
|
||||||
if "lyrics of your favorite song" in task_description.lower(): return "sweet caroline lyrics"
|
if "lyrics of your favorite song" in task_description.lower(): return "sweet caroline lyrics"
|
||||||
@@ -77,9 +85,15 @@ def get_search_query_from_task_description(task_description: str) -> str:
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
response = get_nonempty_ollama_response(messages)
|
response = get_nonempty_ollama_response(messages)
|
||||||
|
|
||||||
return response.lower()
|
return response.lower()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Ollama is offline or unavailable (%s). Using fallback search query generator.", exc)
|
||||||
|
words = [w for w in re.sub(r"[^\w\s]", "", task_description).split() if len(w) > 3 and w.lower() not in {"search", "bing", "find", "about", "with", "from", "that", "this"}]
|
||||||
|
fallback_query = " ".join(words[:4]) if words else f"{get_random_noun()} search"
|
||||||
|
return fallback_query.lower()
|
||||||
|
|
||||||
|
|
||||||
def get_related_search_queries(seed_word: str, num_queries: int=20) -> Generator[str, None, None]:
|
def get_related_search_queries(seed_word: str, num_queries: int=20) -> Generator[str, None, None]:
|
||||||
messages = [
|
messages = [
|
||||||
@@ -93,9 +107,12 @@ def get_related_search_queries(seed_word: str, num_queries: int=20) -> Generator
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for _ in range(num_queries):
|
use_fallback = False
|
||||||
response = get_nonempty_ollama_response(messages)
|
|
||||||
|
|
||||||
|
for _ in range(num_queries):
|
||||||
|
if not use_fallback:
|
||||||
|
try:
|
||||||
|
response = get_nonempty_ollama_response(messages)
|
||||||
yield response.lower()
|
yield response.lower()
|
||||||
|
|
||||||
messages.append({
|
messages.append({
|
||||||
@@ -107,11 +124,21 @@ def get_related_search_queries(seed_word: str, num_queries: int=20) -> Generator
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": USER_PROMPT_FOR_SEARCH_QUERY_CONTINUATION
|
"content": USER_PROMPT_FOR_SEARCH_QUERY_CONTINUATION
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Ollama is offline or unavailable (%s). Using built-in generator for remaining queries.", exc)
|
||||||
|
use_fallback = True
|
||||||
|
|
||||||
|
noun1 = get_random_noun()
|
||||||
|
noun2 = get_random_noun()
|
||||||
|
yield f"{seed_word} {noun1} {noun2}".lower()
|
||||||
|
|
||||||
|
|
||||||
NOUNS = [
|
NOUNS = [
|
||||||
noun.strip().lower() for noun in open("nouns.txt", "r").read().splitlines()
|
noun.strip().lower() for noun in open("nouns.txt", "r").read().splitlines()
|
||||||
if len(noun.strip()) >= 3
|
if len(noun.strip()) >= 3
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_random_noun() -> str:
|
def get_random_noun() -> str:
|
||||||
return random.choice(NOUNS)
|
return random.choice(NOUNS)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
@@ -7,6 +8,8 @@ from urllib.parse import urlsplit, urlunsplit
|
|||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# CONFIG
|
# CONFIG
|
||||||
@@ -93,10 +96,7 @@ def wait_after_429(response, attempt):
|
|||||||
|
|
||||||
wait_time = max(5, wait_time)
|
wait_time = max(5, wait_time)
|
||||||
|
|
||||||
print(
|
logger.warning("Rate limited. Waiting %d seconds...", wait_time)
|
||||||
f"Rate limited. Waiting "
|
|
||||||
f"{wait_time} seconds..."
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@ def download_image(url):
|
|||||||
allow_redirects=True,
|
allow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except (requests.RequestException, ConnectionResetError, OSError) as e:
|
||||||
print(f"Download failed: {e}")
|
logger.warning("Download failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
@@ -126,13 +126,13 @@ def download_image(url):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if response.status_code == 403:
|
if response.status_code == 403:
|
||||||
print("Wikimedia returned 403 Forbidden.")
|
logger.warning("Wikimedia returned 403 Forbidden.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"HTTP error: {e}")
|
logger.warning("HTTP error: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
content_type = response.headers.get(
|
content_type = response.headers.get(
|
||||||
@@ -141,13 +141,11 @@ def download_image(url):
|
|||||||
).lower()
|
).lower()
|
||||||
|
|
||||||
if not content_type.startswith("image/"):
|
if not content_type.startswith("image/"):
|
||||||
print(
|
logger.warning("Not an image: %s", content_type)
|
||||||
f"Not an image: {content_type}"
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not response.content:
|
if not response.content:
|
||||||
print("Downloaded image is empty.")
|
logger.warning("Downloaded image is empty.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return response.content
|
return response.content
|
||||||
@@ -188,12 +186,35 @@ def convert_to_jpeg(image_data):
|
|||||||
return output.getvalue()
|
return output.getvalue()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
logger.warning("JPEG conversion failed: %s", e)
|
||||||
f"JPEG conversion failed: {e}"
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_fallback_image():
|
||||||
|
"""Generate a synthetic local JPEG image using PIL as a fallback."""
|
||||||
|
logger.info("Generating synthetic local fallback image for visual search...")
|
||||||
|
from PIL import ImageDraw
|
||||||
|
import random
|
||||||
|
|
||||||
|
img = Image.new("RGB", (800, 600), color=(random.randint(50, 200), random.randint(50, 200), random.randint(50, 200)))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
for _ in range(10):
|
||||||
|
x0 = random.randint(0, 700)
|
||||||
|
y0 = random.randint(0, 500)
|
||||||
|
x1 = x0 + random.randint(50, 200)
|
||||||
|
y1 = y0 + random.randint(50, 200)
|
||||||
|
fill = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||||||
|
draw.rectangle([x0, y0, x1, y1], fill=fill)
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
img.save(output, format="JPEG", quality=90)
|
||||||
|
jpeg_data = output.getvalue()
|
||||||
|
|
||||||
|
OUTPUT_FILE.write_bytes(jpeg_data)
|
||||||
|
logger.info("Saved fallback image to %s", OUTPUT_FILE.absolute())
|
||||||
|
return {"title": "Fallback Synthetic Image", "width": 800, "height": 600}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# RANDOM IMAGE
|
# RANDOM IMAGE
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -208,14 +229,7 @@ def get_random_image():
|
|||||||
if attempt > 1:
|
if attempt > 1:
|
||||||
time.sleep(REQUEST_DELAY)
|
time.sleep(REQUEST_DELAY)
|
||||||
|
|
||||||
print(
|
logger.debug("Attempt %d/%d", attempt, MAX_ATTEMPTS)
|
||||||
f"\nAttempt "
|
|
||||||
f"{attempt}/{MAX_ATTEMPTS}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# RANDOM FILE
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"action": "query",
|
"action": "query",
|
||||||
@@ -241,14 +255,10 @@ def get_random_image():
|
|||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except (requests.RequestException, ConnectionResetError, OSError) as e:
|
||||||
print(f"API request failed: {e}")
|
logger.warning("API request failed: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# API RATE LIMIT
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
wait_after_429(
|
wait_after_429(
|
||||||
response,
|
response,
|
||||||
@@ -264,13 +274,9 @@ def get_random_image():
|
|||||||
requests.RequestException,
|
requests.RequestException,
|
||||||
ValueError,
|
ValueError,
|
||||||
) as e:
|
) as e:
|
||||||
print(f"API error: {e}")
|
logger.warning("API error: %s", e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# GET PAGE
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
pages = (
|
pages = (
|
||||||
data
|
data
|
||||||
.get("query", {})
|
.get("query", {})
|
||||||
@@ -278,7 +284,7 @@ def get_random_image():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not pages:
|
if not pages:
|
||||||
print("No page returned.")
|
logger.debug("No page returned.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
page = next(
|
page = next(
|
||||||
@@ -295,9 +301,7 @@ def get_random_image():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not imageinfo:
|
if not imageinfo:
|
||||||
print(
|
logger.debug("No image information.")
|
||||||
"No image information."
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
info = imageinfo[0]
|
info = imageinfo[0]
|
||||||
@@ -330,80 +334,44 @@ def get_random_image():
|
|||||||
"url"
|
"url"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# FILTER
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
if mime not in {
|
if mime not in {
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/webp",
|
"image/webp",
|
||||||
}:
|
}:
|
||||||
print(
|
logger.debug("Skipping unsupported type: %s", mime)
|
||||||
f"Skipping unsupported type: "
|
|
||||||
f"{mime}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
||||||
print(
|
logger.debug("Skipping small image: %dx%d", width, height)
|
||||||
f"Skipping small image: "
|
|
||||||
f"{width}x{height}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if size > MAX_FILE_SIZE:
|
if size > MAX_FILE_SIZE:
|
||||||
print(
|
logger.debug("Skipping large image: %.1f MB", size / 1024 / 1024)
|
||||||
f"Skipping large image: "
|
|
||||||
f"{size / 1024 / 1024:.1f} MB"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not thumbnail_url:
|
if not thumbnail_url:
|
||||||
print("No thumbnail URL.")
|
logger.debug("No thumbnail URL.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
logger.debug("Found: %s (%dx%d)", title, width, height)
|
||||||
# FOUND
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
print(f"Found: {title}")
|
|
||||||
print(
|
|
||||||
f"Size: {width}x{height}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# DOWNLOAD THUMBNAIL
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
image_data = download_image(
|
image_data = download_image(
|
||||||
thumbnail_url
|
thumbnail_url
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# FALLBACK TO ORIGINAL
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
if image_data is None and original_url:
|
if image_data is None and original_url:
|
||||||
print(
|
logger.debug("Thumbnail download failed, trying original URL...")
|
||||||
"Trying original..."
|
|
||||||
)
|
|
||||||
|
|
||||||
image_data = download_image(
|
image_data = download_image(
|
||||||
original_url
|
original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
if image_data is None:
|
if image_data is None:
|
||||||
print(
|
logger.debug("Couldn't download image.")
|
||||||
"Couldn't download image."
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
logger.debug("Converting to JPEG...")
|
||||||
# CONVERT TO JPEG
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
print("Converting to JPEG...")
|
|
||||||
|
|
||||||
jpeg_data = convert_to_jpeg(
|
jpeg_data = convert_to_jpeg(
|
||||||
image_data
|
image_data
|
||||||
@@ -412,25 +380,15 @@ def get_random_image():
|
|||||||
if jpeg_data is None:
|
if jpeg_data is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# SAVE JPEG
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
OUTPUT_FILE.write_bytes(
|
OUTPUT_FILE.write_bytes(
|
||||||
jpeg_data
|
jpeg_data
|
||||||
)
|
)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(
|
logger.warning("Couldn't save image: %s", e)
|
||||||
f"Couldn't save image: {e}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# SAVE METADATA
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"source": "Wikimedia Commons",
|
"source": "Wikimedia Commons",
|
||||||
@@ -471,37 +429,20 @@ def get_random_image():
|
|||||||
)
|
)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(
|
logger.warning("Couldn't save metadata: %s", e)
|
||||||
f"Warning: couldn't save "
|
|
||||||
f"metadata: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
logger.info(
|
||||||
# DONE
|
"Visual search image saved: %s (%s, %.1f KB, source: %s)",
|
||||||
# ----------------------------------------------------
|
OUTPUT_FILE.absolute(),
|
||||||
|
f"{width}x{height}",
|
||||||
print()
|
len(jpeg_data) / 1024,
|
||||||
print("=" * 50)
|
title,
|
||||||
print("SUCCESS")
|
|
||||||
print("=" * 50)
|
|
||||||
print(
|
|
||||||
f"Image: "
|
|
||||||
f"{OUTPUT_FILE.absolute()}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Size: "
|
|
||||||
f"{len(jpeg_data) / 1024:.1f} KB"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Source: {title}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
raise RuntimeError(
|
logger.warning("Could not download image from Wikimedia Commons. Generating local fallback image.")
|
||||||
"Unable to obtain a suitable "
|
return generate_fallback_image()
|
||||||
"Wikimedia image."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
+60
-16
@@ -84,6 +84,17 @@ class RewardsTaskUtils:
|
|||||||
self.mouse = mouse_trajectory.MouseUtils(driver)
|
self.mouse = mouse_trajectory.MouseUtils(driver)
|
||||||
self.keyboard = mimic_typing.KeyboardUtils(driver)
|
self.keyboard = mimic_typing.KeyboardUtils(driver)
|
||||||
self.elements = element_selectors.ElementSelectionUtils(driver)
|
self.elements = element_selectors.ElementSelectionUtils(driver)
|
||||||
|
self.verify_signed_in_state()
|
||||||
|
|
||||||
|
def verify_signed_in_state(self):
|
||||||
|
try:
|
||||||
|
time.sleep(2)
|
||||||
|
url = self.driver.current_url.lower()
|
||||||
|
if "login.live.com" in url or "account.microsoft.com" in url or "signup" in url:
|
||||||
|
logger.warning("Microsoft Rewards is NOT signed in on rewards.bing.com for this profile!")
|
||||||
|
logger.warning("Please sign in once on rewards.bing.com in this Edge profile window.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def find_element(self, xpath: str):
|
def find_element(self, xpath: str):
|
||||||
return self.driver.find_element(By.XPATH, xpath)
|
return self.driver.find_element(By.XPATH, xpath)
|
||||||
@@ -135,13 +146,26 @@ class RewardsTaskUtils:
|
|||||||
def switch_to_dashboard(self):
|
def switch_to_dashboard(self):
|
||||||
self.move_to_and_click(self.elements.get_dashboard_tab())
|
self.move_to_and_click(self.elements.get_dashboard_tab())
|
||||||
|
|
||||||
def move_to_and_click(self, elem: WebElement):
|
def move_to_and_click(self, elem_or_getter: WebElement | Callable[[], WebElement], retries: int = 3):
|
||||||
self.mouse.move_to_element(elem)
|
if callable(elem_or_getter):
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
target_elem = elem_or_getter()
|
||||||
|
self.mouse.move_to_element(target_elem)
|
||||||
|
self.mouse.human_like_click()
|
||||||
|
return
|
||||||
|
except StaleElementReferenceException as exc:
|
||||||
|
if attempt == retries - 1:
|
||||||
|
raise exc
|
||||||
|
logger.warning("StaleElementReferenceException during click attempt %d/%d, retrying...", attempt + 1, retries)
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
self.mouse.move_to_element(elem_or_getter)
|
||||||
self.mouse.human_like_click()
|
self.mouse.human_like_click()
|
||||||
|
|
||||||
def wait_for_then_click(self, element_getter: Callable[[], WebElement], timeout: int = 10):
|
def wait_for_then_click(self, element_getter: Callable[[], WebElement], timeout: int = 10):
|
||||||
elem = self.wait_for_element(element_getter, timeout)
|
self.wait_for_element(element_getter, timeout)
|
||||||
self.move_to_and_click(elem)
|
self.move_to_and_click(element_getter)
|
||||||
|
|
||||||
def complete_bing_daily_set(self, expected_activities: int = 3):
|
def complete_bing_daily_set(self, expected_activities: int = 3):
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
@@ -168,19 +192,24 @@ class RewardsTaskUtils:
|
|||||||
len(daily_set_links), expected_activities
|
len(daily_set_links), expected_activities
|
||||||
)
|
)
|
||||||
|
|
||||||
# Re-read the panel per index: clicking an activity can re-render it and
|
main_tab = self.driver.current_window_handle
|
||||||
|
|
||||||
|
# Re-read the panel per index immediately before interaction: clicking an activity can re-render it and
|
||||||
# stale the captured references.
|
# stale the captured references.
|
||||||
for index in range(len(daily_set_links)):
|
for index in range(len(daily_set_links)):
|
||||||
activities = self.elements.get_daily_set_elements()
|
def get_activity_elem(idx=index):
|
||||||
|
return self.elements.get_daily_set_element_by_index(idx)
|
||||||
|
|
||||||
if index >= len(activities):
|
try:
|
||||||
break
|
self.move_to_and_click(get_activity_elem)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to click daily set activity %d: %s", index + 1, exc)
|
||||||
|
continue
|
||||||
|
|
||||||
self.move_to_and_click(activities[index])
|
|
||||||
time.sleep(random.uniform(2, 3))
|
time.sleep(random.uniform(2, 3))
|
||||||
self.driver.switch_to.window(self.driver.current_window_handle) # refocus on the main tab
|
self.tab_utils.close_all_other_tabs(exceptions=[main_tab])
|
||||||
|
|
||||||
self.tab_utils.close_all_other_tabs()
|
self.tab_utils.close_all_other_tabs(exceptions=[main_tab])
|
||||||
|
|
||||||
def complete_explore_on_bing_tasks(self):
|
def complete_explore_on_bing_tasks(self):
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
@@ -224,6 +253,11 @@ class RewardsTaskUtils:
|
|||||||
def complete_visual_search(self):
|
def complete_visual_search(self):
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
|
|
||||||
|
if not os.path.exists(VISUAL_SEARCH_IMAGE_PATH):
|
||||||
|
logger.info("visual_search.jpg not found. Generating visual search image...")
|
||||||
|
import random_image_for_visual_search
|
||||||
|
random_image_for_visual_search.get_random_image()
|
||||||
|
|
||||||
self.wait_for_then_click(self.elements.get_open_visual_search_sidebar)
|
self.wait_for_then_click(self.elements.get_open_visual_search_sidebar)
|
||||||
|
|
||||||
self.wait_for_then_click(self.elements.get_search_now_link_from_visual_search_sidebar)
|
self.wait_for_then_click(self.elements.get_search_now_link_from_visual_search_sidebar)
|
||||||
@@ -243,25 +277,35 @@ class RewardsTaskUtils:
|
|||||||
|
|
||||||
def complete_misc_cards(self):
|
def complete_misc_cards(self):
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
|
main_tab = self.driver.current_window_handle
|
||||||
|
|
||||||
misc_cards: list[WebElement] = self.wait_for_element(self.elements.get_all_misc_cards)
|
misc_cards: list[WebElement] = self.wait_for_element(self.elements.get_all_misc_cards)
|
||||||
|
|
||||||
for card in misc_cards:
|
for index in range(len(misc_cards)):
|
||||||
|
cards = self.elements.get_all_misc_cards()
|
||||||
|
if index >= len(cards):
|
||||||
|
break
|
||||||
|
card = cards[index]
|
||||||
|
|
||||||
|
try:
|
||||||
self.mouse.wheel_scroll_element_into_view(card)
|
self.mouse.wheel_scroll_element_into_view(card)
|
||||||
|
|
||||||
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
|
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
|
||||||
self.move_to_and_click(card)
|
self.move_to_and_click(card)
|
||||||
time.sleep(random.uniform(1, 2))
|
time.sleep(random.uniform(1, 2))
|
||||||
self.driver.switch_to.window(self.driver.current_window_handle)
|
self.tab_utils.close_all_other_tabs(exceptions=[main_tab])
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Misc Card [%d] interaction failed: %s", index, exc)
|
||||||
|
continue
|
||||||
|
|
||||||
for card in misc_cards:
|
for card in self.elements.get_all_misc_cards():
|
||||||
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
|
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Misc Card [desc=%r] is not complete after clicking. Please check manually.",
|
"Misc Card [desc=%r] is not complete after clicking. Please check manually.",
|
||||||
self.elements.extract_card_descriptions(card)
|
self.elements.extract_card_descriptions(card)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tab_utils.close_all_other_tabs()
|
self.tab_utils.close_all_other_tabs(exceptions=[main_tab])
|
||||||
|
|
||||||
self.mouse.wheel_scroll_to_top()
|
self.mouse.wheel_scroll_to_top()
|
||||||
|
|
||||||
@@ -342,7 +386,7 @@ class RewardsTaskUtils:
|
|||||||
):
|
):
|
||||||
self.keyboard.send_keys(f"{query} -noai{Keys.ENTER}")
|
self.keyboard.send_keys(f"{query} -noai{Keys.ENTER}")
|
||||||
|
|
||||||
time.sleep(random.uniform(0.5, 1))
|
time.sleep(random.uniform(5.5, 7.5))
|
||||||
|
|
||||||
try: self.wait_for_then_click(self.elements.get_clear_bing_search_query_button)
|
try: self.wait_for_then_click(self.elements.get_clear_bing_search_query_button)
|
||||||
except StaleElementReferenceException:
|
except StaleElementReferenceException:
|
||||||
|
|||||||
+18
-3
@@ -41,12 +41,18 @@ document.dispatchEvent(new Event('visibilitychange'));
|
|||||||
|
|
||||||
def close_all_other_tabs(self, exceptions: list[str] = None):
|
def close_all_other_tabs(self, exceptions: list[str] = None):
|
||||||
if exceptions is None:
|
if exceptions is None:
|
||||||
|
try:
|
||||||
exceptions = [self.driver.current_window_handle]
|
exceptions = [self.driver.current_window_handle]
|
||||||
|
except WebDriverException:
|
||||||
|
handles = self.driver.window_handles
|
||||||
|
exceptions = [handles[0]] if handles else []
|
||||||
|
|
||||||
switch_back_to = exceptions[0]
|
switch_back_to = exceptions[0] if exceptions else None
|
||||||
|
|
||||||
for handle in self.driver.window_handles:
|
for handle in list(self.driver.window_handles):
|
||||||
if handle not in exceptions and handle not in self.problematic_tabs:
|
if handle not in exceptions and handle not in self.problematic_tabs:
|
||||||
|
tab_url = None
|
||||||
|
try:
|
||||||
self.driver.switch_to.window(handle)
|
self.driver.switch_to.window(handle)
|
||||||
|
|
||||||
if self.driver.current_url in GHOST_TAB_URLS:
|
if self.driver.current_url in GHOST_TAB_URLS:
|
||||||
@@ -55,7 +61,6 @@ document.dispatchEvent(new Event('visibilitychange'));
|
|||||||
|
|
||||||
tab_url = self.driver.current_url
|
tab_url = self.driver.current_url
|
||||||
|
|
||||||
try:
|
|
||||||
self.driver.close()
|
self.driver.close()
|
||||||
# Routine bookkeeping, one line per tab. At info it drowned
|
# Routine bookkeeping, one line per tab. At info it drowned
|
||||||
# the task summary: 19 of the 33 records in a full run were
|
# the task summary: 19 of the 33 records in a full run were
|
||||||
@@ -68,4 +73,14 @@ document.dispatchEvent(new Event('visibilitychange'));
|
|||||||
self.problematic_tabs.add(handle)
|
self.problematic_tabs.add(handle)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
handles = self.driver.window_handles
|
||||||
|
if switch_back_to and switch_back_to in handles:
|
||||||
|
try:
|
||||||
self.driver.switch_to.window(switch_back_to)
|
self.driver.switch_to.window(switch_back_to)
|
||||||
|
except WebDriverException:
|
||||||
|
if handles:
|
||||||
|
self.driver.switch_to.window(handles[0])
|
||||||
|
elif handles:
|
||||||
|
self.driver.switch_to.window(handles[0])
|
||||||
|
|
||||||
|
self.ensure_focus()
|
||||||
Reference in New Issue
Block a user