mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-16 01:31:36 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
|
||||||
*.txt
|
*.txt
|
||||||
visual_search.json
|
visual_search.json
|
||||||
!nouns.txt
|
!nouns.txt
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ You should also have an Ollama account created (for the LLM), the `ollama` tool
|
|||||||
You must also provide an image for the script to upload to complete the visual search task. A helper script is included at `src/random_image.py` that will download an image from Wikipedia named `random_image.png` into the project root for you. You may also provide an image of your own, just ensure that the absolute path of the image is placed in the `VISUAL_SEARCH_IMAGE_PATH` constant at the top of `rewards_tasks.py`.
|
You must also provide an image for the script to upload to complete the visual search task. A helper script is included at `src/random_image.py` that will download an image from Wikipedia named `random_image.png` into the project root for you. You may also provide an image of your own, just ensure that the absolute path of the image is placed in the `VISUAL_SEARCH_IMAGE_PATH` constant at the top of `rewards_tasks.py`.
|
||||||
|
|
||||||
Activate the virtual environment & install dependencies (you may have to use `python -m poetry` instead of `poetry`).
|
Activate the virtual environment & install dependencies (you may have to use `python -m poetry` instead of `poetry`).
|
||||||
You must have Python 3.14+ and Poetry installed.
|
You must have Python 3.12+ and Poetry installed.
|
||||||
|
|
||||||
|
If `iex (poetry env activate)` fails with *"Cannot bind argument to parameter 'Command' because it is null"*, `poetry install` did not create an environment. Run `python --version` first: an older Python leaves poetry with nothing to activate, and the message explaining that goes to stderr rather than into `iex`.
|
||||||
|
|
||||||
Windows (PowerShell)
|
Windows (PowerShell)
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
Generated
+3
-2
@@ -26,6 +26,7 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
idna = ">=2.8"
|
idna = ">=2.8"
|
||||||
|
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
trio = ["trio (>=0.32.0)"]
|
trio = ["trio (>=0.32.0)"]
|
||||||
@@ -5247,5 +5248,5 @@ h11 = ">=0.16.0,<1"
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.14"
|
python-versions = ">=3.12"
|
||||||
content-hash = "2d00394186a132c3c58f8d396d45224c4ca1fe08bdf78ea74e0bb7167ae8dcda"
|
content-hash = "fd4eaf67a2a0849a8ce66dfd1ac2be20276da3d8a1454ef9d991214912a51d61"
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ authors = [
|
|||||||
{name = "Carl Furtado",email = "user0332@duck.com"}
|
{name = "Carl Furtado",email = "user0332@duck.com"}
|
||||||
]
|
]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"selenium (>=4.46.0,<5.0.0)",
|
"selenium (>=4.46.0,<5.0.0)",
|
||||||
"matplotlib (>=3.11.1,<4.0.0)",
|
"matplotlib (>=3.11.1,<4.0.0)",
|
||||||
|
|||||||
@@ -184,6 +184,18 @@ def main():
|
|||||||
driver.execute_script("arguments[0].click();", elements.get_points_breakdown_button())
|
driver.execute_script("arguments[0].click();", elements.get_points_breakdown_button())
|
||||||
wait_until(lambda: elements.get_sidebar_section() is not None, 30)
|
wait_until(lambda: elements.get_sidebar_section() is not None, 30)
|
||||||
|
|
||||||
|
# The section exists before it has content: the panel renders a
|
||||||
|
# "Loading..." placeholder inside it first, and that satisfies the
|
||||||
|
# presence check above immediately. Waiting only for the section leaves
|
||||||
|
# the two selectors below reading an empty panel, so they report FAILED
|
||||||
|
# for markup that is fine, on a page that is merely slow. Wait for the
|
||||||
|
# content itself. A selector that really is broken still reports FAILED,
|
||||||
|
# it just costs the timeout first.
|
||||||
|
wait_until(
|
||||||
|
lambda: elements.get_points_earned_from_searches_on_points_breakdown() is not None,
|
||||||
|
30,
|
||||||
|
)
|
||||||
|
|
||||||
report.check("get_sidebar_section", elements.get_sidebar_section)
|
report.check("get_sidebar_section", elements.get_sidebar_section)
|
||||||
report.check(
|
report.check(
|
||||||
"get_points_earned_from_searches_on_points_breakdown",
|
"get_points_earned_from_searches_on_points_breakdown",
|
||||||
|
|||||||
@@ -311,6 +311,57 @@ class MouseUtils:
|
|||||||
self.driver.execute_script(f"window.moveVisualCursor({point[0]}, {point[1]});")
|
self.driver.execute_script(f"window.moveVisualCursor({point[0]}, {point[1]});")
|
||||||
|
|
||||||
|
|
||||||
|
def wheel_scroll_element_into_view(self, element: WebElement, max_wheel_events: int = 60):
|
||||||
|
"""Scroll the element into the viewport with simulated wheel input.
|
||||||
|
|
||||||
|
Wheel steps of varying size with short pauses, the way a person scrolls,
|
||||||
|
instead of a fixed-size burst. The loop is bounded on purpose: an element
|
||||||
|
that never fits the viewport completely, for example one taller than the
|
||||||
|
window, must not hang the run forever. When the budget runs out the
|
||||||
|
caller proceeds with the element as visible as it got.
|
||||||
|
"""
|
||||||
|
for _ in range(max_wheel_events):
|
||||||
|
top, bottom, height = self.driver.execute_script(
|
||||||
|
"var r = arguments[0].getBoundingClientRect();"
|
||||||
|
"return [r.top, r.bottom, window.innerHeight];",
|
||||||
|
element
|
||||||
|
)
|
||||||
|
|
||||||
|
if top >= 0 and bottom <= height:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Aim the element at the middle of the viewport, one notch at a time.
|
||||||
|
distance = (top + bottom) / 2 - height / 2
|
||||||
|
step = max(-320, min(320, distance))
|
||||||
|
step = int(step * random.uniform(0.6, 1.0))
|
||||||
|
|
||||||
|
if abs(step) < 40:
|
||||||
|
step = 40 if distance > 0 else -40
|
||||||
|
|
||||||
|
ActionChains(self.driver).scroll_by_amount(0, step).perform()
|
||||||
|
|
||||||
|
time.sleep(random.uniform(0.04, 0.12))
|
||||||
|
|
||||||
|
def wheel_scroll_to_top(self, max_wheel_events: int = 80):
|
||||||
|
"""Scroll back to the top of the page with simulated wheel input.
|
||||||
|
|
||||||
|
Reads the actual scroll position instead of unwinding a counted number
|
||||||
|
of steps, because the page height can change while cards update and a
|
||||||
|
symmetric unwind then lands in the wrong place.
|
||||||
|
"""
|
||||||
|
for _ in range(max_wheel_events):
|
||||||
|
offset = self.driver.execute_script("return window.scrollY || window.pageYOffset;")
|
||||||
|
|
||||||
|
if offset <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
step = min(340, int(offset))
|
||||||
|
step = max(60, int(step * random.uniform(0.6, 1.0)))
|
||||||
|
|
||||||
|
ActionChains(self.driver).scroll_by_amount(0, -step).perform()
|
||||||
|
|
||||||
|
time.sleep(random.uniform(0.04, 0.12))
|
||||||
|
|
||||||
def move_to_element(self, element: WebElement, visualize: bool=True):
|
def move_to_element(self, element: WebElement, visualize: bool=True):
|
||||||
# The pointer is moved to viewport coordinates, so an element below the
|
# The pointer is moved to viewport coordinates, so an element below the
|
||||||
# fold yields a target outside the window and the driver rejects the move
|
# fold yields a target outside the window and the driver rejects the move
|
||||||
|
|||||||
+35
-21
@@ -7,7 +7,6 @@ from selenium.webdriver.common.by import By
|
|||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.remote.webelement import WebElement
|
from selenium.webdriver.remote.webelement import WebElement
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
|
||||||
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException, NoSuchElementException
|
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException, NoSuchElementException
|
||||||
import tab_utils
|
import tab_utils
|
||||||
import llm_utils
|
import llm_utils
|
||||||
@@ -58,25 +57,40 @@ class RewardsTaskUtils:
|
|||||||
elem = self.wait_for_element(element_getter, timeout)
|
elem = self.wait_for_element(element_getter, timeout)
|
||||||
self.move_to_and_click(elem)
|
self.move_to_and_click(elem)
|
||||||
|
|
||||||
def complete_bing_daily_set(self):
|
def complete_bing_daily_set(self, expected_activities: int = 3):
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
|
|
||||||
self.wait_for_then_click(self.elements.get_open_daily_set_button)
|
self.wait_for_then_click(self.elements.get_open_daily_set_button)
|
||||||
|
|
||||||
daily_set_links = self.wait_for_element(self.elements.get_daily_set_elements)
|
# The panel hydrates progressively, so the first non-empty snapshot can
|
||||||
|
# hold fewer than 3 activities. wait_for_element returns on the first
|
||||||
|
# truthy result, so a 1-element list satisfied it and indexing [1] and
|
||||||
|
# [2] then raised IndexError, taking the whole task down. Wait for the
|
||||||
|
# full set instead, and if it never fills, work with what is there.
|
||||||
|
def full_activity_list():
|
||||||
|
activities = self.elements.get_daily_set_elements()
|
||||||
|
|
||||||
self.move_to_and_click(daily_set_links[0])
|
return activities if len(activities) >= expected_activities else False
|
||||||
|
|
||||||
|
try:
|
||||||
|
daily_set_links = self.wait_for_element(full_activity_list, timeout=30)
|
||||||
|
except TimeoutException:
|
||||||
|
daily_set_links = self.elements.get_daily_set_elements()
|
||||||
|
|
||||||
|
print(f"[WARNING] Daily set panel only shows {len(daily_set_links)} of {expected_activities} activities")
|
||||||
|
|
||||||
|
# Re-read the panel per index: clicking an activity can re-render it and
|
||||||
|
# stale the captured references.
|
||||||
|
for index in range(len(daily_set_links)):
|
||||||
|
activities = self.elements.get_daily_set_elements()
|
||||||
|
|
||||||
|
if index >= len(activities):
|
||||||
|
break
|
||||||
|
|
||||||
|
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.driver.switch_to.window(self.driver.current_window_handle) # refocus on the main tab
|
||||||
|
|
||||||
self.move_to_and_click(daily_set_links[1])
|
|
||||||
time.sleep(random.uniform(2, 3))
|
|
||||||
self.driver.switch_to.window(self.driver.current_window_handle)
|
|
||||||
|
|
||||||
self.move_to_and_click(daily_set_links[2])
|
|
||||||
time.sleep(random.uniform(2, 3))
|
|
||||||
self.driver.switch_to.window(self.driver.current_window_handle)
|
|
||||||
|
|
||||||
self.tab_utils.close_all_other_tabs()
|
self.tab_utils.close_all_other_tabs()
|
||||||
|
|
||||||
def complete_explore_on_bing_tasks(self):
|
def complete_explore_on_bing_tasks(self):
|
||||||
@@ -140,12 +154,8 @@ class RewardsTaskUtils:
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
scroll_times = 0
|
|
||||||
|
|
||||||
for card in misc_cards:
|
for card in misc_cards:
|
||||||
while not self.elements.element_is_fully_in_viewport(card): # this should work for top-down iteration
|
self.mouse.wheel_scroll_element_into_view(card)
|
||||||
ActionChains(self.driver).scroll_by_amount(0, 100).perform()
|
|
||||||
scroll_times+=1
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -158,8 +168,7 @@ class RewardsTaskUtils:
|
|||||||
|
|
||||||
self.tab_utils.close_all_other_tabs()
|
self.tab_utils.close_all_other_tabs()
|
||||||
|
|
||||||
for i in range(scroll_times):
|
self.mouse.wheel_scroll_to_top()
|
||||||
ActionChains(self.driver).scroll_by_amount(0, -100).perform() # scroll back to top of page
|
|
||||||
|
|
||||||
def complete_required_searches(self, max_rounds: int = 6):
|
def complete_required_searches(self, max_rounds: int = 6):
|
||||||
# Points per search are not fixed. Some markets award 3 rather than 5,
|
# Points per search are not fixed. Some markets award 3 rather than 5,
|
||||||
@@ -198,9 +207,14 @@ class RewardsTaskUtils:
|
|||||||
def read_search_points(self):
|
def read_search_points(self):
|
||||||
"""Open the points breakdown, read the Bing search row, close it again."""
|
"""Open the points breakdown, read the Bing search row, close it again."""
|
||||||
self.switch_to_earn_page()
|
self.switch_to_earn_page()
|
||||||
self.wait_for_then_click(self.elements.get_points_breakdown_button)
|
|
||||||
|
|
||||||
close_btn = self.wait_for_element(self.elements.get_close_button_on_points_breakdown)
|
# 30s rather than the default 10s: this runs after the earlier tasks have
|
||||||
|
# navigated away, so the earn page re-renders from scratch first and the
|
||||||
|
# breakdown button regularly needs longer than 10s to appear. Timing out
|
||||||
|
# here skipped the entire search task while points were still available.
|
||||||
|
self.wait_for_then_click(self.elements.get_points_breakdown_button, timeout=30)
|
||||||
|
|
||||||
|
close_btn = self.wait_for_element(self.elements.get_close_button_on_points_breakdown, timeout=15)
|
||||||
|
|
||||||
points_earned, max_pts = self.elements.get_points_earned_from_searches_on_points_breakdown()
|
points_earned, max_pts = self.elements.get_points_earned_from_searches_on_points_breakdown()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user