Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SyncStravaCommand
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 6
870
0.00% covered (danger)
0.00%
0 / 1
 getAllowedAppEnvs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configure
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 syncStravaForYear
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 syncStravaForUserForYear
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 syncStravaLinks
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
306
1<?php
2
3namespace Olz\Command;
4
5use Olz\Command\Common\OlzCommand;
6use Olz\Entity\Anniversary\RunRecord;
7use Olz\Entity\StravaLink;
8use Symfony\Component\Console\Attribute\AsCommand;
9use Symfony\Component\Console\Command\Command;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Output\OutputInterface;
13
14#[AsCommand(name: 'olz:sync-strava')]
15class SyncStravaCommand extends OlzCommand {
16    /** @return array<string> */
17    protected function getAllowedAppEnvs(): array {
18        return ['dev', 'test', 'staging', 'prod'];
19    }
20
21    protected function configure(): void {
22        $this->addArgument('year', InputArgument::REQUIRED, 'Year (YYYY)');
23        $this->addArgument('user', InputArgument::OPTIONAL, 'User ID');
24    }
25
26    protected function handle(InputInterface $input, OutputInterface $output): int {
27        $year = $input->getArgument('year');
28        if (!preg_match('/^[0-9]{4}$/', $year) || intval($year) < 1996) {
29            $this->logAndOutput("Invalid year: {$year}. Must be in format YYYY and 1996 or later.", level: 'notice');
30            return Command::INVALID;
31        }
32        $year = intval($year);
33        $user_id = $input->getArgument('user');
34        if ($user_id === null) {
35            $this->syncStravaForYear($year);
36            return Command::SUCCESS;
37        }
38        $int_user_id = $user_id ? intval($user_id) : null;
39        if (!preg_match('/^[0-9]+$/', $user_id) || intval($user_id) < 1 || !$int_user_id) {
40            $this->logAndOutput("Invalid user: {$user_id}. Must be a positive integer.", level: 'notice');
41            return Command::INVALID;
42        }
43        $this->syncStravaForUserForYear($int_user_id, $year);
44        return Command::SUCCESS;
45    }
46
47    protected function syncStravaForYear(int $year): void {
48        $this->logAndOutput("Syncing Strava for {$year}...");
49
50        $strava_link_repo = $this->entityManager()->getRepository(StravaLink::class);
51        $strava_links = $strava_link_repo->findAll();
52
53        $this->syncStravaLinks($strava_links);
54    }
55
56    protected function syncStravaForUserForYear(?int $user_id, int $year): void {
57        $this->logAndOutput("Syncing Strava for user {$user_id} for {$year}...");
58
59        $strava_link_repo = $this->entityManager()->getRepository(StravaLink::class);
60        $strava_links = $strava_link_repo->findBy(['user' => $user_id]);
61
62        $this->syncStravaLinks($strava_links);
63    }
64
65    /** @param array<StravaLink> $strava_links */
66    protected function syncStravaLinks(array $strava_links): void {
67        $is_sport_type_valid = [
68            'Run' => true,
69            'TrailRun' => true,
70            'Hike' => true,
71            'Walk' => true,
72        ];
73        $name_blocklist = [
74            'Michael B.' => true,
75            'Kamm M.' => true,
76            'Sandro A.' => true,
77            'Daniel G.' => true,
78        ];
79        $iso_now = $this->dateUtils()->getIsoNow();
80        $now = new \DateTime($iso_now);
81        $minus_one_month = \DateInterval::createFromDateString("-30 days");
82        $one_month_ago = (new \DateTime($iso_now))->add($minus_one_month);
83        $runs_repo = $this->entityManager()->getRepository(RunRecord::class);
84        foreach ($strava_links as $strava_link) {
85            $this->logAndOutput("Syncing {$strava_link}...");
86            $access_token = $this->stravaUtils()->getAccessToken($strava_link);
87            if (!$access_token) {
88                $this->logAndOutput("{$strava_link} has no access token...", level: 'debug');
89                continue;
90            }
91            $activities = $this->stravaUtils()->callStravaApi('GET', '/clubs/158910/activities', [], $access_token);
92            if (!is_array($activities)) {
93                $activities_str = var_export($activities, true);
94                $this->logAndOutput("Invalid activities fetched: {$activities_str}", level: 'notice');
95            }
96            $num_activities = count($activities);
97            $this->logAndOutput("Fetched {$num_activities} activities...", level: 'debug');
98            foreach ($activities as $activity) {
99                $activity_json = json_encode($activity) ?: '';
100                $this->logAndOutput("Processing activity {$activity_json}...", level: 'debug');
101                $firstname = $activity['athlete']['firstname'] ?? null;
102                $lastname = $activity['athlete']['lastname'] ?? null;
103                $is_name_blocklisted = $name_blocklist["{$firstname} {$lastname}"] ?? false;
104                $distance = $activity['distance'] ?? null;
105                $moving_time = $activity['moving_time'] ?? null;
106                $elapsed_time = $activity['elapsed_time'] ?? null;
107                $total_elevation_gain = $activity['total_elevation_gain'] ?? null;
108                $type = $activity['type'] ?? '';
109                $sport_type = $activity['sport_type'] ?? '';
110                if ($firstname === null || $lastname === null || $distance === null || $moving_time === null || $elapsed_time === null || $total_elevation_gain === null) {
111                    $this->logAndOutput("Invalid activity {$activity_json}", level: 'notice');
112                    continue;
113                }
114                $is_counting = $is_sport_type_valid[$sport_type] ?? false;
115                $pretty_sport_type = "{$type} / {$sport_type}";
116                $old_id = md5("{$firstname}-{$lastname}-{$distance}-{$total_elevation_gain}-{$moving_time}-{$elapsed_time}");
117                $id = md5("{$firstname}-{$lastname}-{$distance}-{$moving_time}-{$elapsed_time}");
118                $old_source = "strava-{$old_id}";
119                $source = "strava-{$id}";
120                $old_existing = $runs_repo->findOneBy(['source' => $old_source], ['run_at' => 'DESC']);
121                $existing = $runs_repo->findOneBy(['source' => $source], ['run_at' => 'DESC']);
122                if (
123                    ($old_existing !== null && $old_existing->getRunAt() > $one_month_ago)
124                    || ($existing !== null && $existing->getRunAt() > $one_month_ago)
125                ) {
126                    $this->logAndOutput("Duplicate activity {$activity_json}", level: 'debug');
127                    continue;
128                }
129                $this->logAndOutput("New activity: {$source} by {$firstname} {$lastname}");
130                $run = new RunRecord();
131                $this->entityUtils()->createOlzEntity($run, ['onOff' => !$is_name_blocklisted]);
132                $run->setUser(null);
133                $run->setRunnerName("{$firstname} {$lastname}");
134                $run->setRunAt($now);
135                $run->setIsCounting($is_counting);
136                $run->setDistanceMeters(intval($distance));
137                $run->setElevationMeters(intval($total_elevation_gain));
138                $run->setSportType($pretty_sport_type);
139                $run->setSource($source);
140                $run->setInfo(json_encode($activity) ?: null);
141                $this->entityManager()->persist($run);
142                $this->entityManager()->flush();
143            }
144        }
145    }
146}