diff --git a/README.md b/README.md index 41fb0ee..ee7be11 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,16 @@ # Strava Kudos Giver 👍👍👍 -[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) ![Build](https://github.com/isaac-chung/strava-kudos/actions/workflows/build.yml/badge.svg) [![Give Strava Kudos](https://github.com/isaac-chung/strava-kudos/actions/workflows/give_kudos.yml/badge.svg)](https://github.com/isaac-chung/strava-kudos/actions/workflows/give_kudos.yml) +Originally from [isaac-chung](https://github.com/isaac-chung/strava-kudos) which supports an run env via github workflows. Modified to run via systemd timer locally on a server. A Python tool to automatically give [Strava](https://www.strava.com) Kudos to recent activities on your feed. There are a few repos that uses JavaScript like [strava-kudos-lambda](https://github.com/mjad-org/strava-kudos-lambda) and [strava-kudos](https://github.com/rnvo/strava-kudos). -The repo is set up so that the script runs on a set schedule via Github Actions. Github suggests in their [docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule) to not run cron jobs at the start of every hour to avoid delays so minute30 was chosen here. Feel free to change it to whenever you want. There is also a `max_run_duration` parameter which is 9 minutes by default so that we don't exceed the [monthly Github Action free tier minutes](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes) when the action is triggered a few times a day. ## 🏃 Usage 1. Fork the repo -2. Setup the environment variables in secrets -3. Give kudos automatically! - -Alternatively, you can run the script manually with -``` -python3 give_kudos.py -``` - -## 🛠️Setup - -### Playwright -[Playwright](https://github.com/microsoft/playwright-python) is used, so be sure to follow instructions to install it properly. - -### Environment Variables - -Set the environment variables for your email and password as follows: -``` -export STRAVA_EMAIL=YOUR_EMAIL -export STRAVA_PASSWORD=YOUR_PASSWORD -``` - -### Github Actions -To add secrets for GH actions, navigate to Settings -> Security -> Secrets and Variables -> Actions. Enter your email and password within `Repository Secrets`. - - -## Contributions -Let me know if you wish to add anything or if there are any issues :) - -[![ForTheBadge built-with-love](http://ForTheBadge.com/images/badges/built-with-love.svg)](https://GitHub.com/Naereen/) +2. Setup pyenv and/or install playwright +3. Fill strava credentials inside env file +4. Adjust paths in service file +5. Copy service/timer to /etc/systemd/system +6. `systemctl daemon-reload` +7. `systemctl enable --now give_kudos.timer` +8. Give kudos automatically! diff --git a/give_kudos.env b/give_kudos.env new file mode 100644 index 0000000..3447f55 --- /dev/null +++ b/give_kudos.env @@ -0,0 +1,2 @@ +STRAVA_EMAIL=example@example.com +STRAVA_PASSWORD=VerySecurePassword11elf diff --git a/give_kudos.py b/give_kudos.py index 6996906..f18eb2a 100644 --- a/give_kudos.py +++ b/give_kudos.py @@ -2,6 +2,8 @@ import time from playwright.sync_api import sync_playwright +from os.path import exists +from os.path import getsize BASE_URL = "https://www.strava.com/" @@ -22,10 +24,12 @@ def __init__(self, max_run_duration=540) -> None: self.start_time = time.time() self.num_entries = 100 self.web_feed_entry_pattern = '[data-testid=web-feed-entry]' + self.storage_file = 'session.json' p = sync_playwright().start() self.browser = p.firefox.launch() # does not work in chrome - self.page = self.browser.new_page() + self.context = self.browser.new_context() + self.page = self.context.new_page() def email_login(self): @@ -37,24 +41,18 @@ def email_login(self): self.page.fill("#password", self.PASSWORD) self.page.click("button[type='submit']") print("---Logged in!!---") - self._run_with_retries(func=self._get_page_and_own_profile) - - def _run_with_retries(self, func, retries=3): + + def set_session(self): """ - Retry logic with sleep in between tries. + Reads the browser session from a file """ - for i in range(retries): - if i == retries - 1: - raise Exception(f"Retries {retries} times failed.") - try: - func() - return - except: - time.sleep(1) + self.context = self.browser.new_context(storage_state=f"{self.storage_file}") + self.page = self.context.new_page() + print("---Session loaded!---") - def _get_page_and_own_profile(self): + def goto_dashboard(self): """ - Limit activities count by GET parameter and get own profile ID. + Opens the Strava Dashboard page """ self.page.goto(os.path.join(BASE_URL, f"dashboard?num_entries={self.num_entries}")) @@ -62,7 +60,10 @@ def _get_page_and_own_profile(self): for _ in range(5): self.page.keyboard.press('PageDown') time.sleep(0.5) + + for _ in range(5): self.page.keyboard.press('PageUp') + time.sleep(0.5) try: self.own_profile_id = self.page.locator(".user-menu > a").get_attribute('href').split("/athletes/")[1] @@ -70,6 +71,11 @@ def _get_page_and_own_profile(self): except: print("can't find own profile ID") + print("saving session data") + self.context.storage_state(path=f"{self.storage_file}") + print(f"own_profile_id: {self.own_profile_id}") + + def locate_kudos_buttons_and_maybe_give_kudos(self, web_feed_entry_locator) -> int: """ input: playwright.locator class @@ -77,6 +83,16 @@ def locate_kudos_buttons_and_maybe_give_kudos(self, web_feed_entry_locator) -> i """ w_count = web_feed_entry_locator.count() given_count = 0 + + if w_count == 0: + print("No data found, try relogin") + fp = open(self.storage_file, 'w') + fp.close() + self.email_login() + self.goto_dashboard() + web_feed_entry_locator = self.page.locator(self.web_feed_entry_pattern) + w_count = web_feed_entry_locator.count() + print(f"web feeds found: {w_count}") for i in range(w_count): # run condition check @@ -95,6 +111,7 @@ def locate_kudos_buttons_and_maybe_give_kudos(self, web_feed_entry_locator) -> i # check if activity has multiple participants if p_count > 1: + print(f"Found multiple list entries: {p_count}") for j in range(p_count): participant = web_feed.get_by_test_id("entry-header").nth(j) # ignore own activities @@ -103,25 +120,28 @@ def locate_kudos_buttons_and_maybe_give_kudos(self, web_feed_entry_locator) -> i button = self.find_unfilled_kudos_button(kudos_container) given_count += self.click_kudos_button(unfilled_kudos_container=button) else: + # skip if webfeed is not an activity entry + if web_feed.get_by_test_id("owners-name").count() == 0: + continue # ignore own activities if not self.is_participant_me(web_feed): button = self.find_unfilled_kudos_button(web_feed) given_count += self.click_kudos_button(unfilled_kudos_container=button) print(f"\nKudos given: {given_count}") return given_count - + def is_club_post(self, container) -> bool: """ Returns true if the container is a club post """ if(container.get_by_test_id("group-header").count() > 0): return True - + if(container.locator(".clubMemberPostHeaderLinks").count() > 0): return True return False - + def is_participant_me(self, container) -> bool: """ Returns true is the container's owner is logged-in user. @@ -134,7 +154,7 @@ def is_participant_me(self, container) -> bool: except: print("Some issue with getting owners-name container.") return owner == self.own_profile_id - + def find_unfilled_kudos_button(self, container): """ Returns button as a playwright.locator class @@ -153,7 +173,7 @@ def click_kudos_button(self, unfilled_kudos_container) -> int: """ if unfilled_kudos_container.count() == 1: unfilled_kudos_container.click(timeout=0, no_wait_after=True) - print('=', end='') + print("Kudos button clicked") time.sleep(1) return 1 return 0 @@ -170,7 +190,11 @@ def give_kudos(self): def main(): kg = KudosGiver() - kg.email_login() + if exists(kg.storage_file) and getsize(kg.storage_file) > 250: + kg.set_session() + else: + kg.email_login() + kg.goto_dashboard() kg.give_kudos() diff --git a/give_kudos.service b/give_kudos.service new file mode 100644 index 0000000..3861efc --- /dev/null +++ b/give_kudos.service @@ -0,0 +1,8 @@ +[Unit] +Description=Strava give kudos + +[Service] +Type=oneshot +WorkingDirectory=/path/to/clone/dir +EnvironmentFile=/path/to/clone/dir/give_kudos.env +ExecStart=/path/to/python give_kudos.py \ No newline at end of file diff --git a/give_kudos.timer b/give_kudos.timer new file mode 100644 index 0000000..080237c --- /dev/null +++ b/give_kudos.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Strava kudos timer + +[Timer] +OnCalendar=8..21:0/15 +RandomizedDelaySec=15min +Persistent=true + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/session.json b/session.json new file mode 100644 index 0000000..e69de29