Commit Graph
72 Commits
Author SHA1 Message Date
Ethan Stoner e17053bd48 keep the rewards tab when a task fails, and put it back on the rewards page
The cleanup after each task called close_all_other_tabs() with no arguments.
That defaults `exceptions` to the currently focused handle, so when a task died
with a Bing tab in focus the cleanup kept the Bing tab and closed the Rewards
one. Every later task then looked for Rewards controls on a search results page
and reported [SKIP] for a UI that was present and working the whole time. One
failure cost the rest of the run.

Measured in the container rather than reasoned about: with a Rewards tab and a
Bing tab open and the Bing one focused, close_all_other_tabs() leaves
['https://www.bing.com/search?q=weather'] behind and the Rewards handle is gone.

Two separate things have to hold before the next task can work, and they failed
independently. The right tab has to survive, so restore_main_tab names the
handle recorded at startup instead of trusting whatever happens to be focused.
The surviving tab also has to be showing the right page, which the tab fix alone
does not give you: run_search_batch navigates the main tab to bing.com itself,
so a failure part way through that batch strands the main tab on a search page
with no second tab involved at all.

Navigating only happens when the step did not finish, and is skipped when the
browser is already on the Rewards host, so a clean run navigates nowhere and a
panel that merely failed to render does not cost a page load.

Both recoveries swallow their own errors. Raising here would replace the real
failure with the tidy-up's, which is why the close_all_other_tabs call was
wrapped in the first place.

End to end in a real browser: two tabs with the Bing one focused become one tab,
the original handle, on rewards.bing.com/about (the logged out redirect), and a
main tab stranded on bing.com recovers too. Fifteen unit tests cover the tab
choice, the missing-main-tab fallback, the failure and skip paths, a clean run
navigating nowhere, and a driver too broken to do either.

Refs #63

Claude-Session: https://claude.ai/code/session_0113hp36oUBeMqG7twfuC8rE
2026-09-09 08:25:38 -07:00
Carl Furtado 914d910922 clean up constants.py 2026-09-08 17:04:21 -04:00
abhijitgorde5-crypto 03636ac984 Replace print() with logger calls in llm_utils, random_image_for_visual_search, and rewards_tasks 2026-09-08 23:02:34 +05:30
abhijitgorde5-crypto 049a119e36 Pacing Bing searches to 5.5-7.5s delay to prevent MS Rewards rate-limiting throttling 2026-09-08 00:23:24 +05:30
abhijitgorde5-crypto 7686537dc1 Address code review: refactor move_to_and_click retries, simplify wait_for_then_click, resolve conflicts with main 2026-09-07 23:55:04 +05:30
Carl Furtado 4121c42552 Merge pull request #69 from TheMallyGuy/main
feature: spoof the reward app's `X-Rewards-Source` to unlock rewards app only quests
2026-09-07 10:23:08 -04:00
Carl Furtado de669da145 normalize indentation to tabs 2026-09-07 10:13:24 -04:00
Mally Dang 3c6b29e857 chore: move header setting thing to RewardsTaskUtils 2026-09-07 18:06:43 +07:00
mardausdennis 15782d1836 tell absence and a slow render apart when a task fails
Every failed task printed the same reason, because the blanket catch in
complete_all_tasks treated NoSuchElementException and TimeoutException as one
finding. A wait that expires says nothing about whether the section was there,
since wait_for_element swallowed whatever the getter raised and returned False.
A market that does not ship visual search and a points panel that was on screen
and slow both arrived as TimeoutException

wait_for_element now keeps the last reason the getter gave and raises
ElementNeverAppeared when nothing was in the DOM for the whole wait.
_container_by_id raises ElementNotReady when the id is present but no visible
copy has content yet, which is the hydrating case it used to report as missing.
Both subclass what they refine, so claim_bonus_points and
complete_bing_daily_set keep working untouched

A task that was reached and then ran out of time is now a [FAIL] rather than a
[SKIP], because it may have left points behind. Absence stays a [SKIP]

56 tests pass, 20 of them new. A mutation check backs them, forcing every
expired wait to count as absence fails 2, and collapsing the two branches back
into one [SKIP] fails 4

Refs #52
2026-09-03 22:38:01 +02:00
abhijitgorde5-crypto 4c67f3f46e Merge upstream main and resolve merge conflict in src/tab_utils.py 2026-09-02 18:22:39 +05:30
Mally Dang afccf81f8f feat: spoof the rewards app headers 2026-09-02 12:54:14 +07:00
Carl Furtado 0a3ab5ba6f Merge pull request #36 from ethanstoner/feat/query-sources
logging, a query source without an LLM, multi-account and docker
2026-08-31 09:33:29 -04:00
Carl Furtado ddbc6512a3 Merge pull request #55 from mardausdennis/fix/search-close-button
read the search row before waiting on the panel's close button
2026-08-31 09:12:25 -04:00
mardausdennis 8ff99c0e9d cover the rewards urls the daily set actually uses
Matching on bing.com/search alone was too narrow. Turn referrals into rewards is a real daily set activity that awards points and it points at a rewards url, so the filter skipped it and the run came up short without saying why.

Three shapes now count, bing.com/search, bing.com/rewards and rewards.bing.com. The bing app promo from #45 sits on bingapp.microsoft.com and stays out of all three, which is what keeps that fix intact. Five tests, one per shape plus a mixed panel.
2026-08-30 18:15:24 +02:00
mardausdennis 28f3bdb101 Merge remote-tracking branch 'origin/main' into fix/daily-set-promo-links 2026-08-30 18:15:23 +02:00
Ethan Stoner cdafcc59da merge upstream: NoSuchWindowException when closing a tab
Upstream widened the except in close_all_tabs_except to catch
NoSuchWindowException; this branch had rewritten the same two lines from print
to logging. Both are wanted, so the resolution keeps the wider except and the
logger call, and takes the new import.

Claude-Session: https://claude.ai/code/session_01RCncv1HC4UvoJcYqBtwxrt
2026-08-30 08:47:35 -07:00
Carl Furtado 619b282fd8 catch NoSuchWindowException in tab_utils 2026-08-30 11:44:04 -04:00
abhijitgorde5-crypto 27a98d9b93 Fix StaleElementReferenceException, add Ollama offline fallback, and PIL visual search generator 2026-08-29 23:41:57 +05:30
mardausdennis 806156c6c6 do not click a different streak when the daily set label is missing
The positional fallback returned whatever sat at index 3 of the streaks section. On a partially rendered page that is not the daily set: observed live returning 'Mobile App | Check-in: 0/1', and clicking it opens the app store page instead of the panel, which is what #45 and #46 describe. It now checks the label before handing the button back and raises otherwise, so the task is skipped rather than the wrong streak clicked.
2026-08-29 18:46:47 +02:00
mardausdennis dd92cface3 read the search row before waiting on the panel's close button
read_search_points waited for the close button before reading anything, so a panel that rendered its content but not its button failed the whole search task while the number was already on screen. Traced to that wait with a stacktrace. Closing is best effort now.
2026-08-28 23:46:41 +02:00
mardausdennis 238050ba9a wait for the daily set panel to fill before checking it
The panel hydrates progressively, so waiting for the section only tells you it opened, not that it filled. A check running on the first non-empty state reports whatever happened to be rendered at that moment, which is why the daily set came out differently run to run. Same wait as complete_bing_daily_set: hold out for the full set, report what is there if it never arrives.
2026-08-28 22:44:01 +02:00
Ethan Stoner 0f18795f51 refuse profile names that Win32 resolves onto another account's directory
Win32 strips a trailing dot off a path component and python's normalisation
does not, so such a name is not the directory it reads as. REWARDS_ACCOUNTS=...
resolved to data-dir itself, which is the default profile the resolved-path
check was added to keep named accounts out of, and personal,personal. passed
the duplicate check as two entries while sharing one profile on disk. Both
break the one-directory-per-account guarantee this module exists for.

Reject the shape by name, and resolve with realpath rather than abspath so a
link or a junction under data-dir is followed to where it really goes.
2026-08-28 08:53:37 -07:00
Ethan Stoner 3e9e953c75 keep one account's failure from ending a batched run
Only SessionNotCreatedException was isolated. Every other way an account can
fail reached main() and took the accounts after it with them: a driver that
will not start for another reason, an unwritable profile directory, the first
page not loading, the browser dying mid-run, or quit() raising because it was
already gone. With REWARDS_ACCOUNTS=one,two,three and the middle one failing,
three never ran and main() exited on a traceback instead of an exit code.

Catch it around run_account, report it the way a failed task is reported, and
carry on. KeyboardInterrupt is left alone so Ctrl-C still stops the run. The
quit() in run_account is guarded too, so a tidy-up that raises no longer hides
the failure it was tidying up after.
2026-08-28 08:53:28 -07:00
mardausdennis f23afb7568 match daily set activities by target instead of by position
get_daily_set_elements returned everything after the first link in the panel. The panel also carries promotional links, so position hands one back as an activity. Clicking it leaves rewards.bing.com and every element captured before that goes stale, which is the exception reported in #45.

Activities always point at a Bing search, so match on that. When nothing matches it returns nothing rather than falling back to position, since clicking a promo is worse than skipping the task and complete_bing_daily_set already reports the shortfall.
2026-08-28 11:00:44 +02:00
Ethan Stoner 1a224919b2 reject profile names that resolve outside data-dir
The character check allowed "." and "..", which are made entirely of allowed
characters and still walk out of the directory, so the traversal guard only
stopped the cases containing a separator. Reject both by name and check the
resolved path against the profile root as well, since the character set
constrains the characters rather than where they point.

Also drop the MouseUtils and KeyboardUtils built in run_account and never
used, RewardsTaskUtils builds its own, along with the two imports that
leaves unused, and say "lowercased" in the no-source warning, which is what
the code returns.
2026-08-27 15:26:01 -07:00
Ethan Stoner b697ea198b merge upstream: the random image downloader
Both sides added a line after *.txt in .gitignore, so keep both: *.log
from the logging change here and visual_search.json from upstream.
2026-08-27 15:13:01 -07:00
RealEvoranz 31426c174a Merge branch 'main' into random_image_downloader 2026-08-27 09:00:08 -04:00
RealEvoranz b97613b5b8 Merge remote-tracking branch 'upstream/main' 2026-08-27 08:59:48 -04:00
Ethan Stoner 817d5bb63d carry the daily set warning from #37 into logging 2026-08-27 00:27:29 -07:00
Ethan Stoner 8059e4a6eb merge upstream: daily set hydration and the readme image name 2026-08-27 00:21:29 -07:00
Carl Furtado aa0d07841b Merge pull request #37 from mardausdennis/fix/daily-set-hydration
wait for the daily set panel to fill, and give the breakdown time to render
2026-08-26 22:45:49 -04:00
Ethan Stoner dc9860189b merge upstream: #24's wheel scrolling 2026-08-26 19:41:22 -07:00
Carl Furtado 3f5e02c83d Merge pull request #33 from ethanstoner/fix/breakdown-wait
wait for the breakdown panel's content, not just its container
2026-08-26 22:40:56 -04:00
mardausdennis efe4b07eb8 wait for the daily set panel to fill, and give the breakdown time to render
complete_bing_daily_set indexed [0] [1] [2] on whatever wait_for_element returned first. The panel hydrates progressively, so that can be a single activity, and the task died with IndexError before touching the other two. Wait for the full set, fall back to what is there, and re-read per index since a click can re-render the panel.

read_search_points ran into the default 10s timeout because it starts after the earlier tasks navigated away, so the earn page re-renders from scratch first. That skipped the whole search task while points were still available.
2026-08-27 00:38:58 +02:00
Ethan Stoner 823dcbfc75 merge the logging work into the feature branch
Both change main.py and README.md, and three separate pull requests touching
the same entry point is worse for review than one. Resolved by keeping every
section of the README and folding the logging setup into the new multi-account
main, so the profile-in-use message from #35 is now a logger.error and uses
log_utils.exception_summary rather than repeating the truncation inline.

queries.py moves to logging with the rest of the runtime.
2026-08-26 15:19:04 -07:00
Ethan Stoner bfe30dd9a0 run in docker, and work through more than one account
Both taken from TheNetsky/Microsoft-Rewards-Script, which packages a container
and handles several accounts. Approach only: that project is GPL-3.0 and this
one MIT, so no code crosses over.

**Accounts.** Rewards is per Microsoft account and the browser profile holds
the sign-in, so an account here is a profile directory. REWARDS_ACCOUNTS takes
a comma separated list and gives each its own directory under the configured
one. They run in sequence, and a profile that will not start is reported and
skipped rather than ending the run. Left unset, a run uses the single profile
exactly as before.

Names are validated rather than trusted: they become directory names, so
"../escape" is refused instead of quietly writing outside data-dir.

**Docker.** The image carries only what main.py actually reaches, selenium and
numpy. pygetwindow, keyboard, matplotlib and pygame are used solely by the
recording and visualisation scripts, and two of those are Windows-only, so
none of them belong in a container. msedgedriver is pinned at build time to
the Edge the image installed rather than to latest, which drifts from it
between releases.

QUERY_SOURCE defaults to trends in the image, so a container needs no Ollama
account and no model download at all.

That default turned out to require a fix. queries.py imported llm_utils at
module scope, which imports ollama, so a trends-only install still needed the
ollama package: exactly what running in a minimal image is good at exposing.
The import is now made inside the llm branch, and the wordlist fallback reads
nouns.txt directly rather than borrowing llm_utils.get_random_noun.

REWARDS_HEADLESS drives the headless flags. The window size is set explicitly
because the pointer code works in viewport coordinates and the default
headless window is small enough to put cards out of reach, which is the
MoveTargetOutOfBoundsException from #19. Verified on the host that
move_to_element and human_like_click both work headless before relying on it.

Verified in the built image: Edge 151.0.4129.107 with a driver of exactly the
same build, the trends feed reachable from inside, Edge driven to bing.com and
rewards.bing.com at 1920x1080, and REWARDS_ACCOUNTS producing separate profile
directories with traversal refused.
2026-08-26 15:13:02 -07:00
Ethan Stoner af03afcc6d add a query source that does not need a language model
Idea taken from TheNetsky/Microsoft-Rewards-Script, which builds search terms
from public feeds rather than a model. No code from it: that project is
GPL-3.0 and this one is MIT, so only the approach crosses over.

The LLM has exactly two call sites here, both producing a short string to type
into Bing. Everything the dependency costs, an Ollama account, cloud usage and
the provider work in #15, is paid for search strings. Three keyless sources
answer the same question:

  Google Trends RSS    queries people are actually typing right now
  Wikipedia most-read  topic seeds when trends is unavailable
  Bing autosuggest     expands a seed into related queries

Autosuggest is what makes the chaining work. Asking Bing what follows a term
returns queries Bing already expects, which is nearer to what the prompt in
llm_utils was reaching for than a model guessing unaided.

Selected with QUERY_SOURCE=trends. The default stays llm, so no existing setup
changes. stdlib only, no new dependencies.

Measured against the LLM on the same cards from a live account:

  card                     llm                                    trends
  airport parking          best rates airport parking reservations reserve airport parking best rates
  checking vs savings      compare checking vs savings accounts    compare checking savings account options
  cruise deals             best cruise deals and destinations      cruise deals destinations

Verified live with OLLAMA_HOST pointed at a dead port, so nothing could reach
a model: five queries generated from feeds and three typed into Bing, each
landing on a real results page.

Every source degrades to an empty list rather than raising, and both entry
points fall back, to the trimmed task description and to nouns.txt. A search
that does not happen costs points; a run that dies costs the rest of the day.
2026-08-26 15:05:09 -07:00
Ethan Stoner 92159cbc13 wait for the breakdown panel's content, not just its container
check_selectors reports FAILED for two selectors that are fine.

The wait before them is satisfied by a placeholder. get_sidebar_section
returns the first section whose id starts with react-aria, and that section is
in the DOM as soon as the panel opens, holding a "Loading..." placeholder. So
`wait_until(get_sidebar_section() is not None)` returns immediately, and the
two selectors that read the panel's text then read "Loading..." and raise.

The report's own output shows it: the section that resolves OK has the text
"Loading...", and the two entries under it fail.

Before, on a healthy en-US account:

    OK       get_sidebar_section        'Loading...'
    FAILED   get_points_earned_from_searches_on_points_breakdown
    FAILED   get_close_button_on_points_breakdown
    OK=10  ABSENT=1  FAILED=2

After:

    OK       get_sidebar_section        "Points breakdown | Today's points | 480 | To"
    OK       get_points_earned_from_searches_on_points_breakdown (25, 25)
    OK       get_close_button_on_points_breakdown
    OK=12  ABSENT=1  FAILED=0

This matters more than a cosmetic miscount. The README asks people to paste
this output into bug reports and says FAILED is what needs fixing, so a false
FAILED sends both the reporter and whoever triages it after selectors that
work. The bot itself was never affected, read_search_points reaches the same
selector through wait_for_element and so does wait.

Waiting on the content rather than the container keeps a genuine breakage
reporting FAILED; it just costs the timeout first.
2026-08-26 14:38:46 -07:00
Ethan Stoner 78a4aff513 quieten routine tab logging and harden the failure summary
Follow-ups from running the conversion against a live account.

Tab open/close bookkeeping moves from info to debug. It was 19 of the 33
records in a full run, so the six task outcomes that are the point of the
summary were outnumbered three to one by tab handles and query strings. The
"could not close" case stays at warning, a tab that will not close is a real
problem rather than bookkeeping.

The [FAIL] summary moves into log_utils.exception_summary, which takes the
first line, drops the "(Session info: ...)" fragment and caps the result. A
selenium exception embeds the whole msedgedriver stacktrace in str(), and the
cap means a pathological message cannot push a screenful of text into one
record. The cut marker is ASCII because this can land on a Windows console
whose encoding cannot represent an ellipsis.

The suppressed-library list was checked rather than guessed: with the root
logger wide open, a real browser session plus one ollama call produced records
from httpx, httpcore, urllib3 and selenium only, and nothing else. That set is
already pinned. Worth noting selenium alone emits 45 records for a single page
load, so without the pinning the debug mode this PR recommends for bug reports
would be unusable.
2026-08-26 11:51:58 -07:00
Ethan Stoner 5c475cab05 switch the bot runtime from print to logging
Closes #14.

The runtime modules now log through the stdlib logging module. A new
log_utils.setup_logging is called once from main.py, and each module holds
its own logging.getLogger(__name__) so every line says which module it came
from.

The [INFO] and [WARNING] prefixes are gone, since the level field carries
that now. [OK], [SKIP] and [FAIL] stay in the message text: they are the
per-task outcome summary from complete_all_tasks rather than severities, and
folding them into the level would erase the run summary. They map to info,
warning and error, which is the one thing print could not express, a real
failure now sorts above a task the current UI variant simply does not ship.

Two things fall out of having levels at all:

- REWARDS_FARMER_LOG_LEVEL=DEBUG attaches the traceback to every [FAIL],
  which is the stack trace that bug reports keep having to be asked for.
- REWARDS_FARMER_LOG_FILE writes the same output to a file, so an unattended
  run can be read after the fact.

Both are off by default, so a normal run looks the same as before apart from
the timestamp and level columns.

The [FAIL] summary keeps only the first line of the exception message. A
selenium exception carries the whole msedgedriver stacktrace inside str(),
tens of lines of it, which would turn one task into one screenful and make
the log file impossible to scan. The full detail is still there with the
traceback on debug.

The console stream is stdout rather than the StreamHandler default of stderr,
so anyone already redirecting stdout keeps getting the output there, and its
error handler is set to replace. Card descriptions are scraped from the page
and are not ASCII outside the en-US market, and the Windows console encoding
raises on them.

check_selectors.py, fitts_law.py and analyze_keypresses.py are left on print.
Their output is formatted report text, and prefixing every row of a
diagnostic table with a timestamp and a level makes it harder to read.
2026-08-26 11:12:20 -07:00
RealEvoranz 37b0050166 renamed 2026-08-25 22:28:51 -04:00
RealEvoranz 44ae02eb2a refining 2026-08-25 22:14:13 -04:00
RealEvoranz 255c8b05fc outputs as jpg not png 2026-08-25 22:09:50 -04:00
RealEvoranz cafc5097bd Merge remote-tracking branch 'upstream/main' into random_image_downloader 2026-08-25 22:02:13 -04:00
mardausdennis 43aadf7ded drive the misc cards scrolling with wheel input instead of fixed bursts
The card loop fired fixed 100px scroll events back to back with no pauses, which is the jumpy scrolling, and its while-not-in-viewport loop was unbounded, so a card that never fits the viewport completely would hang the run forever. The way back up unwound a counted number of steps, which lands wrong when the page height changes while cards update.

Scrolling is now wheel input with varying step sizes and short pauses, bounded, aimed at centering the target. The return reads the actual scroll position instead of counting.
2026-08-26 00:28:01 +02:00
Carl Furtado e2a06275f3 Merge pull request #4 from mardausdennis/fix/market-independent-selectors
fix selectors that only resolve in the en-US market
2026-08-25 16:50:13 -04:00
Carl Furtado 0ee5e1c9bb Merge pull request #20 from mardausdennis/fix/measure-search-quota
fill the search quota by measuring instead of assuming a rate
2026-08-25 16:40:42 -04:00
mardausdennis 77d9ec2a0f fill the search quota by measuring instead of assuming a rate
searches_needed was computed once as (max - earned) // 5 and never re-checked. Two assumptions fail in practice: some markets award 3 points per search rather than 5, and the daily maximum itself is not stable, observed as 15, 30 and 60 on one account within a day with the counter resetting. The run therefore stopped around 18/30 and still reported success.

Search in rounds instead: measure, run a batch sized on the lower known rate, measure again, stop when the quota is full or a round gains nothing, and warn instead of claiming success when it is not filled.

Also give the ollama client a timeout and bound the empty-response retry, since both were unbounded and an unattended run hung for 14 minutes with 2.3 CPU-seconds. The bare while-not-response loop spins forever on empty responses.
2026-08-25 22:28:14 +02:00
mardausdennis bf79e188f2 address review: scroll only when needed and smoothly, tighten the visual search needle
scrollIntoView re-centered every element unconditionally and instantly, which is the page jump between the daily set and explore on bing. The pointer move now scrolls only when the element is actually outside the viewport, smoothly, and polls the rect until the async scroll settles before computing the path.

Visual search matches the full 'visual search streak' label. Plain 'visual search' can also hit a dashboard element that goes stale mid-interaction, and the guessed 'image search' needle is gone.
2026-08-25 22:23:13 +02:00
Carl Furtado 31d4031477 append -noai to search queries 2026-08-25 11:48:07 -04:00