Skip to content

Commit

Permalink
feat: run locally via systemd
Browse files Browse the repository at this point in the history
feat: add session logic
  • Loading branch information
bin101 committed Jul 22, 2024
1 parent 0b89ad7 commit 9b57c9e
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 52 deletions.
39 changes: 8 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -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!
2 changes: 2 additions & 0 deletions give_kudos.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STRAVA_EMAIL=example@example.com
STRAVA_PASSWORD=VerySecurePassword11elf
66 changes: 45 additions & 21 deletions give_kudos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/"

Expand All @@ -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):
Expand All @@ -37,46 +41,58 @@ 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}"))

## Scrolling for lazy loading elements.
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]
print("id", self.own_profile_id)
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
Returns count of kudos given.
"""
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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()


Expand Down
8 changes: 8 additions & 0 deletions give_kudos.service
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions give_kudos.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Strava kudos timer

[Timer]
OnCalendar=8..21:0/15
RandomizedDelaySec=15min
Persistent=true

[Install]
WantedBy=timers.target
Empty file added session.json
Empty file.

0 comments on commit 9b57c9e

Please sign in to comment.