Skip to content

Commit cd4bab4

Browse files
author
Sebastian BURGIN-FIX (ext)
committed
wip
1 parent 71edf34 commit cd4bab4

26 files changed

Lines changed: 498 additions & 382 deletions

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ CLOUDINARY_API_SECRET=my-api-secret
6060
LARAVEL_DEFAULT_FATHOM_SITE_ID=WECFOADW
6161
LITELLM_URL=https://llm.codebar.net
6262
LITELLM_MASTER_KEY=
63+
GITHUB_TOKEN=

GAP_ANALYSIS.md

Lines changed: 0 additions & 81 deletions
This file was deleted.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\LocaleEnum;
6+
use App\Models\OpenSource;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Support\Facades\Http;
9+
use Illuminate\Support\Str;
10+
11+
class SyncRepositoriesCommand extends Command
12+
{
13+
protected $signature = 'sync:repositories';
14+
15+
protected $description = 'Sync public GitHub repositories from the codebar-ag organization into the open-source listing';
16+
17+
private const string ORG = 'codebar-ag';
18+
19+
private const string DEFAULT_IMAGE = 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-codebar-ch/seo/seo_codebar.webp';
20+
21+
public function handle(): int
22+
{
23+
$this->info('Fetching public repositories from GitHub...');
24+
25+
$repos = $this->fetchAllRepositories();
26+
27+
if ($repos === null) {
28+
$this->error('Failed to fetch repositories from GitHub.');
29+
30+
return self::FAILURE;
31+
}
32+
33+
$this->info(sprintf('Found %d public repositories.', count($repos)));
34+
35+
$synced = 0;
36+
37+
foreach ($repos as $repo) {
38+
if ($repo['fork']) {
39+
continue;
40+
}
41+
42+
$this->syncRepository($repo);
43+
$synced++;
44+
}
45+
46+
$this->info(sprintf('Synced %d repositories.', $synced));
47+
48+
return self::SUCCESS;
49+
}
50+
51+
/**
52+
* @return array<int, array{fork: bool, name: string, description: ?string, topics: array<int, string>, html_url: string, full_name: string, stargazers_count: int, forks_count: int, language: ?string}>|null
53+
*/
54+
private function fetchAllRepositories(): ?array
55+
{
56+
$repos = [];
57+
$page = 1;
58+
$batch = [];
59+
60+
$headers = [];
61+
$token = config('services.github.token');
62+
if (is_string($token) && $token !== '') {
63+
$headers['Authorization'] = "Bearer {$token}";
64+
}
65+
66+
do {
67+
$response = Http::withHeaders($headers)
68+
->accept('application/vnd.github+json')
69+
->get(sprintf('https://api.github.com/orgs/%s/repos', self::ORG), [
70+
'type' => 'public',
71+
'per_page' => 100,
72+
'page' => $page,
73+
]);
74+
75+
if ($response->failed()) {
76+
$this->error(sprintf('GitHub API error: %s', $response->body()));
77+
78+
return null;
79+
}
80+
81+
$json = $response->json();
82+
83+
if (! is_array($json) || $json === []) {
84+
break;
85+
}
86+
87+
$batch = array_map(
88+
fn (mixed $item): array => $this->normalizeRepo(is_array($item) ? $item : []),
89+
array_values($json),
90+
);
91+
92+
$repos = array_merge($repos, $batch);
93+
$page++;
94+
} while (count($batch) === 100);
95+
96+
return $repos;
97+
}
98+
99+
/**
100+
* @param array<array-key, mixed> $repo
101+
* @return array{fork: bool, name: string, description: ?string, topics: array<int, string>, html_url: string, full_name: string, stargazers_count: int, forks_count: int, language: ?string}
102+
*/
103+
private function normalizeRepo(array $repo): array
104+
{
105+
return [
106+
'fork' => $this->boolValue($repo['fork'] ?? null),
107+
'name' => $this->stringValue($repo['name'] ?? null),
108+
'description' => $this->nullableStringValue($repo['description'] ?? null),
109+
'topics' => $this->stringListValue($repo['topics'] ?? null),
110+
'html_url' => $this->stringValue($repo['html_url'] ?? null),
111+
'full_name' => $this->stringValue($repo['full_name'] ?? null),
112+
'stargazers_count' => $this->intValue($repo['stargazers_count'] ?? null),
113+
'forks_count' => $this->intValue($repo['forks_count'] ?? null),
114+
'language' => $this->nullableStringValue($repo['language'] ?? null),
115+
];
116+
}
117+
118+
private function boolValue(mixed $value): bool
119+
{
120+
return is_scalar($value) && $value;
121+
}
122+
123+
private function stringValue(mixed $value): string
124+
{
125+
return is_scalar($value) ? (string) $value : '';
126+
}
127+
128+
private function nullableStringValue(mixed $value): ?string
129+
{
130+
return is_scalar($value) ? (string) $value : null;
131+
}
132+
133+
private function intValue(mixed $value): int
134+
{
135+
return is_numeric($value) ? (int) $value : 0;
136+
}
137+
138+
/**
139+
* @return array<int, string>
140+
*/
141+
private function stringListValue(mixed $value): array
142+
{
143+
if (! is_array($value)) {
144+
return [];
145+
}
146+
147+
return array_values(array_map(
148+
fn (mixed $item): string => $this->stringValue($item),
149+
$value,
150+
));
151+
}
152+
153+
/**
154+
* @param array{fork: bool, name: string, description: ?string, topics: array<int, string>, html_url: string, full_name: string, stargazers_count: int, forks_count: int, language: ?string} $repo
155+
*/
156+
private function syncRepository(array $repo): void
157+
{
158+
$slug = Str::slug($repo['name']);
159+
$title = Str::of($repo['name'])->replace('-', ' ')->title()->toString();
160+
$teaser = $repo['description'] ?? '';
161+
$downloads = $this->fetchPackagistDownloads($repo['full_name']);
162+
163+
foreach (LocaleEnum::cases() as $locale) {
164+
$entry = OpenSource::query()->updateOrCreate(
165+
[
166+
'locale' => $locale->value,
167+
'slug' => $slug,
168+
],
169+
[
170+
'published' => true,
171+
'title' => $title,
172+
'teaser' => $teaser,
173+
'image' => self::DEFAULT_IMAGE,
174+
'tags' => $repo['topics'],
175+
'link' => $repo['html_url'],
176+
'downloads' => $downloads,
177+
'stars' => $repo['stargazers_count'],
178+
'forks' => $repo['forks_count'],
179+
'primary_language' => $repo['language'],
180+
'github_name' => $repo['full_name'],
181+
]
182+
);
183+
184+
$this->line(sprintf(' %s [%s] %s downloads', $entry->title, $locale->value, number_format($downloads)));
185+
}
186+
}
187+
188+
private function fetchPackagistDownloads(string $fullName): int
189+
{
190+
$response = Http::accept('application/json')
191+
->get(sprintf('https://packagist.org/packages/%s.json', $fullName));
192+
193+
if ($response->failed()) {
194+
return 0;
195+
}
196+
197+
$downloads = data_get($response->json(), 'package.downloads.total', 0);
198+
199+
return is_numeric($downloads) ? (int) $downloads : 0;
200+
}
201+
}

app/Http/Controllers/CoWorking/CoWorkingIndexController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public function __invoke(): View
3232
]);
3333
}
3434

35+
/**
36+
* @return array<int, array{category: string, title: string, teaser: string}>
37+
*/
3538
private function services(): array
3639
{
3740
return [

app/Http/Controllers/Contact/ContactIndexController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public function __invoke(): View
1919
]);
2020
}
2121

22+
/**
23+
* @return array<int, array{day: string, open: ?string, close: ?string}>
24+
*/
2225
private function openingHours(): array
2326
{
2427
return [
@@ -27,7 +30,7 @@ private function openingHours(): array
2730
['day' => 'Wednesday', 'open' => '08:00', 'close' => '18:00'],
2831
['day' => 'Thursday', 'open' => '08:00', 'close' => '18:00'],
2932
['day' => 'Friday', 'open' => '08:00', 'close' => '18:00'],
30-
['day' => 'Saturday', 'open' => '08:00', 'close' => '18:00'],
33+
['day' => 'Saturday', 'open' => '08:00', 'close' => '12:00'],
3134
['day' => 'Sunday', 'open' => null, 'close' => null],
3235
];
3336
}

app/Http/Controllers/Network/NetworkManageUpdateController.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Illuminate\Support\Facades\Storage;
1313
use Illuminate\Support\Facades\URL;
1414
use Illuminate\Support\Str;
15-
use Spatie\ResponseCache\Facades\ResponseCache;
1615

1716
class NetworkManageUpdateController extends Controller
1817
{
@@ -38,16 +37,17 @@ public function __invoke(NetworkManageUpdateRequest $request, NetworkUser $netwo
3837
's3',
3938
);
4039

40+
abort_if($path === false, 500, 'Failed to store the uploaded avatar.');
41+
4142
$attributes['avatar'] = Storage::disk('s3')->url($path);
4243
}
4344

4445
$networkUser->update($attributes);
4546

4647
Network::query()
4748
->where('key', $networkUser->network_key)
48-
->update(['website' => $request->validated('website')]);
49-
50-
ResponseCache::clear();
49+
->get()
50+
->each(fn (Network $network) => $network->update(['website' => $request->validated('website')]));
5151

5252
Mail::to(config('mail.from.address'))->send(new NetworkProfileUpdatedMail($networkUser->refresh()));
5353

0 commit comments

Comments
 (0)