Merge pull request #69 from TheMallyGuy/main

feature: spoof the reward app's `X-Rewards-Source` to unlock rewards app only quests
This commit is contained in:
Carl Furtado
2026-09-07 10:23:08 -04:00
committed by GitHub
4 changed files with 426 additions and 407 deletions
+15 -6
View File
@@ -8,7 +8,11 @@ import rewards_tasks
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException from selenium.common.exceptions import SessionNotCreatedException
HEADLESS = os.environ.get("REWARDS_HEADLESS", "").strip().lower() in ("1", "true", "yes") HEADLESS = os.environ.get("REWARDS_HEADLESS", "").strip().lower() in (
"1",
"true",
"yes",
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,7 +21,7 @@ def build_options(account: accounts.Account) -> webdriver.EdgeOptions:
options = webdriver.EdgeOptions() options = webdriver.EdgeOptions()
options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False) options.add_experimental_option("useAutomationExtension", False)
options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(f"--user-data-dir={account.user_data_dir}") options.add_argument(f"--user-data-dir={account.user_data_dir}")
options.add_argument(f"--profile-directory={account.profile_name}") options.add_argument(f"--profile-directory={account.profile_name}")
@@ -45,7 +49,9 @@ def run_account(account: accounts.Account) -> bool:
# the profile nor the other window. # the profile nor the other window.
logger.error("[FAIL] %s: could not start Edge with this profile.", account.name) logger.error("[FAIL] %s: could not start Edge with this profile.", account.name)
logger.error(" profile directory: %s", account.user_data_dir) logger.error(" profile directory: %s", account.user_data_dir)
logger.error(" The usual cause is that this profile is already open in another") logger.error(
" The usual cause is that this profile is already open in another"
)
logger.error(" Edge window, including one left over from a previous run.") logger.error(" Edge window, including one left over from a previous run.")
logger.error(" driver said: %s", log_utils.exception_summary(exc)) logger.error(" driver said: %s", log_utils.exception_summary(exc))
@@ -63,7 +69,8 @@ def run_account(account: accounts.Account) -> bool:
# own error, and the process it is meant to end is dead anyway. # own error, and the process it is meant to end is dead anyway.
logger.warning( logger.warning(
"%s: the driver did not shut down cleanly: %s", "%s: the driver did not shut down cleanly: %s",
account.name, log_utils.exception_summary(exc) account.name,
log_utils.exception_summary(exc),
) )
return True return True
@@ -97,8 +104,10 @@ def main() -> int:
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"[FAIL] %s: %s: %s", "[FAIL] %s: %s: %s",
account.name, type(exc).__name__, log_utils.exception_summary(exc), account.name,
exc_info=logger.isEnabledFor(logging.DEBUG) type(exc).__name__,
log_utils.exception_summary(exc),
exc_info=logger.isEnabledFor(logging.DEBUG),
) )
if len(configured) > 1: if len(configured) > 1:
+338 -338
View File
@@ -43,10 +43,10 @@ MAX_BACKOFF = 300
session = requests.Session() session = requests.Session()
session.headers.update({ session.headers.update({
"User-Agent": ( "User-Agent": (
"RandomVisualSearchImage/1.1 " "RandomVisualSearchImage/1.1 "
"(contact: 12345rfdz@gmail.com)" "(contact: 12345rfdz@gmail.com)"
) )
}) })
@@ -55,17 +55,17 @@ session.headers.update({
# ============================================================ # ============================================================
def clean_url(url): def clean_url(url):
"""Remove query parameters from Wikimedia URLs.""" """Remove query parameters from Wikimedia URLs."""
parts = urlsplit(url) parts = urlsplit(url)
return urlunsplit(( return urlunsplit((
parts.scheme, parts.scheme,
parts.netloc, parts.netloc,
parts.path, parts.path,
"", "",
"", "",
)) ))
# ============================================================ # ============================================================
@@ -73,32 +73,32 @@ def clean_url(url):
# ============================================================ # ============================================================
def wait_after_429(response, attempt): def wait_after_429(response, attempt):
"""Wait according to Wikimedia's Retry-After header.""" """Wait according to Wikimedia's Retry-After header."""
retry_after = response.headers.get("Retry-After") retry_after = response.headers.get("Retry-After")
if retry_after: if retry_after:
try: try:
wait_time = int(retry_after) wait_time = int(retry_after)
except ValueError: except ValueError:
wait_time = min( wait_time = min(
2 ** attempt, 2 ** attempt,
MAX_BACKOFF, MAX_BACKOFF,
) )
else: else:
wait_time = min( wait_time = min(
2 ** attempt, 2 ** attempt,
MAX_BACKOFF, MAX_BACKOFF,
) )
wait_time = max(5, wait_time) wait_time = max(5, wait_time)
print( print(
f"Rate limited. Waiting " f"Rate limited. Waiting "
f"{wait_time} seconds..." f"{wait_time} seconds..."
) )
time.sleep(wait_time) time.sleep(wait_time)
# ============================================================ # ============================================================
@@ -106,51 +106,51 @@ def wait_after_429(response, attempt):
# ============================================================ # ============================================================
def download_image(url): def download_image(url):
"""Download image bytes from Wikimedia.""" """Download image bytes from Wikimedia."""
url = clean_url(url) url = clean_url(url)
try: try:
response = session.get( response = session.get(
url, url,
timeout=30, timeout=30,
allow_redirects=True, allow_redirects=True,
) )
except requests.RequestException as e: except requests.RequestException as e:
print(f"Download failed: {e}") print(f"Download failed: {e}")
return None return None
if response.status_code == 429: if response.status_code == 429:
wait_after_429(response, 1) wait_after_429(response, 1)
return None return None
if response.status_code == 403: if response.status_code == 403:
print("Wikimedia returned 403 Forbidden.") print("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}") print(f"HTTP error: {e}")
return None return None
content_type = response.headers.get( content_type = response.headers.get(
"Content-Type", "Content-Type",
"", "",
).lower() ).lower()
if not content_type.startswith("image/"): if not content_type.startswith("image/"):
print( print(
f"Not an image: {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.") print("Downloaded image is empty.")
return None return None
return response.content return response.content
# ============================================================ # ============================================================
@@ -158,40 +158,40 @@ def download_image(url):
# ============================================================ # ============================================================
def convert_to_jpeg(image_data): def convert_to_jpeg(image_data):
"""Convert downloaded image bytes to JPEG.""" """Convert downloaded image bytes to JPEG."""
try: try:
with Image.open( with Image.open(
io.BytesIO(image_data) io.BytesIO(image_data)
) as image: ) as image:
# JPEG does not support alpha (transparency). # JPEG does not support alpha (transparency).
# If the image has transparency (RGBA or LA), paste it over a white background. # If the image has transparency (RGBA or LA), paste it over a white background.
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info): if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
background = Image.new("RGB", image.size, (255, 255, 255)) background = Image.new("RGB", image.size, (255, 255, 255))
if image.mode == "P": if image.mode == "P":
image = image.convert("RGBA") image = image.convert("RGBA")
background.paste(image, mask=image.split()[-1]) background.paste(image, mask=image.split()[-1])
jpeg_image = background jpeg_image = background
else: else:
jpeg_image = image.convert("RGB") jpeg_image = image.convert("RGB")
output = io.BytesIO() output = io.BytesIO()
jpeg_image.save( jpeg_image.save(
output, output,
format="JPEG", format="JPEG",
quality=90, quality=90,
optimize=True, optimize=True,
) )
return output.getvalue() return output.getvalue()
except Exception as e: except Exception as e:
print( print(
f"JPEG conversion failed: {e}" f"JPEG conversion failed: {e}"
) )
return None return None
# ============================================================ # ============================================================
@@ -200,308 +200,308 @@ def convert_to_jpeg(image_data):
def get_random_image(): def get_random_image():
for attempt in range( for attempt in range(
1, 1,
MAX_ATTEMPTS + 1, MAX_ATTEMPTS + 1,
): ):
if attempt > 1: if attempt > 1:
time.sleep(REQUEST_DELAY) time.sleep(REQUEST_DELAY)
print( print(
f"\nAttempt " f"\nAttempt "
f"{attempt}/{MAX_ATTEMPTS}" f"{attempt}/{MAX_ATTEMPTS}"
) )
# ---------------------------------------------------- # ----------------------------------------------------
# RANDOM FILE # RANDOM FILE
# ---------------------------------------------------- # ----------------------------------------------------
params = { params = {
"action": "query", "action": "query",
"format": "json", "format": "json",
"generator": "random", "generator": "random",
"grnnamespace": 6, "grnnamespace": 6,
"grnlimit": 1, "grnlimit": 1,
"prop": "imageinfo", "prop": "imageinfo",
"iiprop": ( "iiprop": (
"url|size|mime|dimensions" "url|size|mime|dimensions"
), ),
"iiurlwidth": THUMBNAIL_WIDTH, "iiurlwidth": THUMBNAIL_WIDTH,
} }
try: try:
response = session.get( response = session.get(
API_URL, API_URL,
params=params, params=params,
timeout=20, timeout=20,
) )
except requests.RequestException as e: except requests.RequestException as e:
print(f"API request failed: {e}") print(f"API request failed: {e}")
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# API RATE LIMIT # API RATE LIMIT
# ---------------------------------------------------- # ----------------------------------------------------
if response.status_code == 429: if response.status_code == 429:
wait_after_429( wait_after_429(
response, response,
attempt, attempt,
) )
continue continue
try: try:
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
except ( except (
requests.RequestException, requests.RequestException,
ValueError, ValueError,
) as e: ) as e:
print(f"API error: {e}") print(f"API error: {e}")
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# GET PAGE # GET PAGE
# ---------------------------------------------------- # ----------------------------------------------------
pages = ( pages = (
data data
.get("query", {}) .get("query", {})
.get("pages", {}) .get("pages", {})
) )
if not pages: if not pages:
print("No page returned.") print("No page returned.")
continue continue
page = next( page = next(
iter(pages.values()) iter(pages.values())
) )
title = page.get( title = page.get(
"title", "title",
"Unknown", "Unknown",
) )
imageinfo = page.get( imageinfo = page.get(
"imageinfo" "imageinfo"
) )
if not imageinfo: if not imageinfo:
print( print(
"No image information." "No image information."
) )
continue continue
info = imageinfo[0] info = imageinfo[0]
mime = info.get( mime = info.get(
"mime", "mime",
"", "",
) )
width = info.get( width = info.get(
"width", "width",
0, 0,
) )
height = info.get( height = info.get(
"height", "height",
0, 0,
) )
size = info.get( size = info.get(
"size", "size",
0, 0,
) )
thumbnail_url = info.get( thumbnail_url = info.get(
"thumburl" "thumburl"
) )
original_url = info.get( original_url = info.get(
"url" "url"
) )
# ---------------------------------------------------- # ----------------------------------------------------
# FILTER # FILTER
# ---------------------------------------------------- # ----------------------------------------------------
if mime not in { if mime not in {
"image/jpeg", "image/jpeg",
"image/png", "image/png",
"image/webp", "image/webp",
}: }:
print( print(
f"Skipping unsupported type: " f"Skipping unsupported type: "
f"{mime}" f"{mime}"
) )
continue continue
if width < MIN_WIDTH or height < MIN_HEIGHT: if width < MIN_WIDTH or height < MIN_HEIGHT:
print( print(
f"Skipping small image: " f"Skipping small image: "
f"{width}x{height}" f"{width}x{height}"
) )
continue continue
if size > MAX_FILE_SIZE: if size > MAX_FILE_SIZE:
print( print(
f"Skipping large image: " f"Skipping large image: "
f"{size / 1024 / 1024:.1f} MB" f"{size / 1024 / 1024:.1f} MB"
) )
continue continue
if not thumbnail_url: if not thumbnail_url:
print("No thumbnail URL.") print("No thumbnail URL.")
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# FOUND # FOUND
# ---------------------------------------------------- # ----------------------------------------------------
print(f"Found: {title}") print(f"Found: {title}")
print( print(
f"Size: {width}x{height}" f"Size: {width}x{height}"
) )
# ---------------------------------------------------- # ----------------------------------------------------
# DOWNLOAD THUMBNAIL # DOWNLOAD THUMBNAIL
# ---------------------------------------------------- # ----------------------------------------------------
image_data = download_image( image_data = download_image(
thumbnail_url thumbnail_url
) )
# ---------------------------------------------------- # ----------------------------------------------------
# FALLBACK TO ORIGINAL # FALLBACK TO ORIGINAL
# ---------------------------------------------------- # ----------------------------------------------------
if image_data is None and original_url: if image_data is None and original_url:
print( print(
"Trying original..." "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( print(
"Couldn't download image." "Couldn't download image."
) )
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# CONVERT TO JPEG # CONVERT TO JPEG
# ---------------------------------------------------- # ----------------------------------------------------
print("Converting to JPEG...") print("Converting to JPEG...")
jpeg_data = convert_to_jpeg( jpeg_data = convert_to_jpeg(
image_data image_data
) )
if jpeg_data is None: if jpeg_data is None:
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# SAVE JPEG # 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( print(
f"Couldn't save image: {e}" f"Couldn't save image: {e}"
) )
continue continue
# ---------------------------------------------------- # ----------------------------------------------------
# SAVE METADATA # SAVE METADATA
# ---------------------------------------------------- # ----------------------------------------------------
metadata = { metadata = {
"title": title, "title": title,
"source": "Wikimedia Commons", "source": "Wikimedia Commons",
"output_format": "JPEG", "output_format": "JPEG",
"width": width, "width": width,
"height": height, "height": height,
"original_mime": mime, "original_mime": mime,
"original_size": size, "original_size": size,
"jpeg_size": len( "jpeg_size": len(
jpeg_data jpeg_data
), ),
"original_url": ( "original_url": (
clean_url(original_url) clean_url(original_url)
if original_url if original_url
else None else None
), ),
"thumbnail_url": ( "thumbnail_url": (
clean_url(thumbnail_url) clean_url(thumbnail_url)
if thumbnail_url if thumbnail_url
else None else None
), ),
} }
try: try:
METADATA_FILE.write_text( METADATA_FILE.write_text(
json.dumps( json.dumps(
metadata, metadata,
indent=4, indent=4,
ensure_ascii=False, ensure_ascii=False,
), ),
encoding="utf-8", encoding="utf-8",
) )
except OSError as e: except OSError as e:
print( print(
f"Warning: couldn't save " f"Warning: couldn't save "
f"metadata: {e}" f"metadata: {e}"
) )
# ---------------------------------------------------- # ----------------------------------------------------
# DONE # DONE
# ---------------------------------------------------- # ----------------------------------------------------
print() print()
print("=" * 50) print("=" * 50)
print("SUCCESS") print("SUCCESS")
print("=" * 50) print("=" * 50)
print( print(
f"Image: " f"Image: "
f"{OUTPUT_FILE.absolute()}" f"{OUTPUT_FILE.absolute()}"
) )
print( print(
f"Size: " f"Size: "
f"{len(jpeg_data) / 1024:.1f} KB" f"{len(jpeg_data) / 1024:.1f} KB"
) )
print( print(
f"Source: {title}" f"Source: {title}"
) )
return metadata return metadata
raise RuntimeError( raise RuntimeError(
"Unable to obtain a suitable " "Unable to obtain a suitable "
"Wikimedia image." "Wikimedia image."
) )
# ============================================================ # ============================================================
@@ -509,4 +509,4 @@ def get_random_image():
# ============================================================ # ============================================================
if __name__ == "__main__": if __name__ == "__main__":
get_random_image() get_random_image()
+10
View File
@@ -66,6 +66,16 @@ class RewardsTaskUtils:
def __init__(self, driver: webdriver.Edge): def __init__(self, driver: webdriver.Edge):
self.driver = driver self.driver = driver
# Set headers to spoof the rewards app for the rewards only quests
self.driver.execute_cdp_cmd("Network.enable", {})
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0 MSRewards/Desktop/1.1.0",
"X-Rewards-Source": "msrewards-desktop",
}
self.driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": headers})
self.driver.get("https://rewards.bing.com/") self.driver.get("https://rewards.bing.com/")
self.tab_utils = tab_utils.TabUtils(driver) self.tab_utils = tab_utils.TabUtils(driver)
+63 -63
View File
@@ -19,87 +19,87 @@ Point = tuple[int, int]
def make_paths() -> tuple[callable, callable]: def make_paths() -> tuple[callable, callable]:
"""Return independent paths with the same start and end points.""" """Return independent paths with the same start and end points."""
start = (150, 535) start = (150, 535)
end = (950, 535) end = (950, 535)
base_path = get_bezier_path(start, end, intermediate_radius_interval=(150, 210)) base_path = get_bezier_path(start, end, intermediate_radius_interval=(150, 210))
distorted_path = get_distorted_bezier_path( distorted_path = get_distorted_bezier_path(
start, start,
end, end,
intermediate_radius_interval=(150, 210), intermediate_radius_interval=(150, 210),
distortion_zone_time_length=0.08, distortion_zone_time_length=0.08,
distortion_frequency=1.0, distortion_frequency=1.0,
deviation_interval=(10, 18), deviation_interval=(10, 18),
) )
return base_path, distorted_path return base_path, distorted_path
def sample_path(path: callable, steps: int = 360) -> list[Point]: def sample_path(path: callable, steps: int = 360) -> list[Point]:
return [ return [
(round(point[0]), round(point[1])) (round(point[0]), round(point[1]))
for point in (path(index / (steps - 1)) for index in range(steps)) for point in (path(index / (steps - 1)) for index in range(steps))
] ]
def draw_label(screen: pygame.Surface, font: pygame.font.Font, text: str, position: tuple[int, int], color: tuple[int, int, int]) -> None: def draw_label(screen: pygame.Surface, font: pygame.font.Font, text: str, position: tuple[int, int], color: tuple[int, int, int]) -> None:
screen.blit(font.render(text, True, color), position) screen.blit(font.render(text, True, color), position)
def main() -> None: def main() -> None:
pygame.init() pygame.init()
screen = pygame.display.set_mode(WINDOW_SIZE) screen = pygame.display.set_mode(WINDOW_SIZE)
pygame.display.set_caption("Bezier Path Distortion") pygame.display.set_caption("Bezier Path Distortion")
clock = pygame.time.Clock() clock = pygame.time.Clock()
title_font = pygame.font.SysFont("Segoe UI", 28, bold=True) title_font = pygame.font.SysFont("Segoe UI", 28, bold=True)
body_font = pygame.font.SysFont("Segoe UI", 19) body_font = pygame.font.SysFont("Segoe UI", 19)
button_font = pygame.font.SysFont("Segoe UI", 18, bold=True) button_font = pygame.font.SysFont("Segoe UI", 18, bold=True)
base_path, distorted_path = make_paths() base_path, distorted_path = make_paths()
base_points = sample_path(base_path) base_points = sample_path(base_path)
distorted_points = sample_path(distorted_path) distorted_points = sample_path(distorted_path)
show_distorted = False show_distorted = False
button = pygame.Rect(405, 595, 290, 52) button = pygame.Rect(405, 595, 290, 52)
running = True running = True
while running: while running:
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
running = False running = False
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if button.collidepoint(event.pos): if button.collidepoint(event.pos):
show_distorted = not show_distorted show_distorted = not show_distorted
screen.fill(BACKGROUND) screen.fill(BACKGROUND)
draw_label(screen, title_font, "Bezier path comparison", (38, 28), PATH_COLOR) draw_label(screen, title_font, "Bezier path comparison", (38, 28), PATH_COLOR)
draw_label( draw_label(
screen, screen,
body_font, body_font,
"The red path adds temporary offsets to the same underlying curve.", "The red path adds temporary offsets to the same underlying curve.",
(40, 70), (40, 70),
(91, 97, 102), (91, 97, 102),
) )
pygame.draw.lines(screen, (184, 188, 190), False, base_points, 1) pygame.draw.lines(screen, (184, 188, 190), False, base_points, 1)
pygame.draw.lines(screen, DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR, False, distorted_points if show_distorted else base_points, 4) pygame.draw.lines(screen, DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR, False, distorted_points if show_distorted else base_points, 4)
pygame.draw.circle(screen, POINT_COLOR, base_points[0], 10) pygame.draw.circle(screen, POINT_COLOR, base_points[0], 10)
pygame.draw.circle(screen, POINT_COLOR, base_points[-1], 10) pygame.draw.circle(screen, POINT_COLOR, base_points[-1], 10)
draw_label(screen, body_font, "A", (base_points[0][0] - 8, base_points[0][1] + 18), PATH_COLOR) draw_label(screen, body_font, "A", (base_points[0][0] - 8, base_points[0][1] + 18), PATH_COLOR)
draw_label(screen, body_font, "B", (base_points[-1][0] - 8, base_points[-1][1] + 18), PATH_COLOR) draw_label(screen, body_font, "B", (base_points[-1][0] - 8, base_points[-1][1] + 18), PATH_COLOR)
draw_label(screen, body_font, "DISTORTED" if show_distorted else "UNDISTORTED", (20, 535), DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR) draw_label(screen, body_font, "DISTORTED" if show_distorted else "UNDISTORTED", (20, 535), DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR)
button_color = BUTTON_HOVER_COLOR if button.collidepoint(pygame.mouse.get_pos()) else BUTTON_COLOR button_color = BUTTON_HOVER_COLOR if button.collidepoint(pygame.mouse.get_pos()) else BUTTON_COLOR
pygame.draw.rect(screen, button_color, button, border_radius=7) pygame.draw.rect(screen, button_color, button, border_radius=7)
button_text = "Show undistorted path" if show_distorted else "Show distorted path" button_text = "Show undistorted path" if show_distorted else "Show distorted path"
text_surface = button_font.render(button_text, True, BUTTON_TEXT_COLOR) text_surface = button_font.render(button_text, True, BUTTON_TEXT_COLOR)
screen.blit(text_surface, text_surface.get_rect(center=button.center)) screen.blit(text_surface, text_surface.get_rect(center=button.center))
pygame.display.flip() pygame.display.flip()
clock.tick(60) clock.tick(60)
pygame.quit() pygame.quit()
if __name__ == "__main__": if __name__ == "__main__":
main() main()