Skip to content

Commit 4dac846

Browse files
committed
Merge branch 'main' into delete-all-threads-from-user
2 parents 5acc43e + 049b35e commit 4dac846

File tree

23 files changed

+1497
-586
lines changed

23 files changed

+1497
-586
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ TELEGRAM_CHANNEL=
4949

5050
FATHOM_SITE_ID=
5151
FATHOM_TOKEN=
52+
53+
UNSPLASH_ACCESS_KEY=
54+
5255
LOG_STACK=single
5356
SESSION_ENCRYPT=false
5457
SESSION_PATH=/

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ updates:
44
directory: "/"
55
schedule:
66
interval: weekly
7-
day: wednesday
7+
day: friday
88
groups:
99
php-dependencies:
1010
update-types:
@@ -18,7 +18,7 @@ updates:
1818
directory: "/"
1919
schedule:
2020
interval: weekly
21-
day: wednesday
21+
day: friday
2222
groups:
2323
js-dependencies:
2424
update-types:

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ We'd like to thank these **amazing companies** for sponsoring us. If you are int
2626
- [Tinkerwell](https://tinkerwell.app)
2727
- [Skynet Technologies](https://www.skynettechnologies.com/hire-laravel-developer)
2828
- [BairesDev](https://www.bairesdev.com/sponsoring-open-source-projects/)
29-
- [Remotely Works](https://www.remotely.works/sponsoring-open-source-projects)
3029
- [Dotcom-monitor](https://www.dotcom-monitor.com/sponsoring-open-source-projects/)
30+
- [N-iX](https://www.n-ix.com/)
3131

3232
## Requirements
3333

@@ -125,6 +125,20 @@ FATHOM_SITE_ID=
125125
FATHOM_TOKEN=
126126
```
127127

128+
### Unsplash (optional)
129+
130+
To make sure article and user header images get synced into the database we'll need to setup an access key from [Unsplash](https://unsplash.com/developers). Please note that your Unsplash app requires production access.
131+
132+
```
133+
UNSPLASH_ACCESS_KEY=
134+
```
135+
136+
After that you can add an Unsplash photo ID to any article row in the `hero_image_id` column and run the sync command to fetch the image url and author data:
137+
138+
```bash
139+
php artisan lio:sync-article-images
140+
```
141+
128142
## Commands
129143

130144
Command | Description
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Article;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Facades\Http;
8+
9+
final class SyncArticleImages extends Command
10+
{
11+
protected $signature = 'lio:sync-article-images';
12+
13+
protected $description = 'Updates the Unsplash image for all unsynced articles';
14+
15+
public function handle(): void
16+
{
17+
if (! config('services.unsplash.access_key')) {
18+
$this->error('Unsplash access key must be configured');
19+
20+
return;
21+
}
22+
23+
Article::unsyncedImages()->chunk(100, function ($articles) {
24+
$articles->each(function ($article) {
25+
$imageData = $this->fetchUnsplashImageDataFromId($article->hero_image_id);
26+
27+
if (! is_null($imageData)) {
28+
$article->hero_image_url = $imageData['image_url'];
29+
$article->hero_image_author_name = $imageData['author_name'];
30+
$article->hero_image_author_url = $imageData['author_url'];
31+
$article->save();
32+
}
33+
});
34+
});
35+
}
36+
37+
protected function fetchUnsplashImageDataFromId(string $imageId): ?array
38+
{
39+
$response = Http::retry(3, 100, throw: false)
40+
->withToken(config('services.unsplash.access_key'), 'Client-ID')
41+
->get("https://api.unsplash.com/photos/{$imageId}");
42+
43+
if ($response->failed()) {
44+
logger()->error('Failed to get raw image url from unsplash for', [
45+
'imageId' => $imageId,
46+
'response' => $response->json(),
47+
]);
48+
49+
return null;
50+
}
51+
52+
$response = $response->json();
53+
54+
// Trigger as download...
55+
Http::retry(3, 100, throw: false)
56+
->withToken(config('services.unsplash.access_key'), 'Client-ID')
57+
->get($response['links']['download_location']);
58+
59+
return [
60+
'image_url' => $response['urls']['raw'],
61+
'author_name' => $response['user']['name'],
62+
'author_url' => $response['user']['links']['html'],
63+
];
64+
}
65+
}

app/Http/Controllers/Forum/ThreadsController.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,14 @@ public function show(Thread $thread)
8181
return view('forum.threads.show', compact('thread', 'moderators'));
8282
}
8383

84-
public function create(): View
84+
public function create(): RedirectResponse|View
8585
{
86+
if (Auth::user()->hasTooManyThreadsToday()) {
87+
$this->error('You can only post a maximum of 5 threads per day.');
88+
89+
return redirect()->route('forum');
90+
}
91+
8692
$tags = Tag::all();
8793
$selectedTags = old('tags') ?: [];
8894

@@ -91,6 +97,12 @@ public function create(): View
9197

9298
public function store(ThreadRequest $request): RedirectResponse
9399
{
100+
if (Auth::user()->hasTooManyThreadsToday()) {
101+
$this->error('You can only post a maximum of 5 threads per day.');
102+
103+
return redirect()->route('forum');
104+
}
105+
94106
$this->dispatchSync(CreateThread::fromRequest($request, $uuid = Str::uuid()));
95107

96108
$thread = Thread::findByUuidOrFail($uuid);

app/Models/Article.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ final class Article extends Model implements Feedable
4444
'body',
4545
'original_url',
4646
'slug',
47-
'hero_image',
47+
'hero_image_id',
48+
'hero_image_url',
49+
'hero_image_author_name',
50+
'hero_image_author_url',
4851
'is_pinned',
4952
'view_count',
5053
'tweet_id',
@@ -100,13 +103,19 @@ public function excerpt(int $limit = 100): string
100103

101104
public function hasHeroImage(): bool
102105
{
103-
return $this->hero_image !== null;
106+
return $this->hero_image_url !== null;
107+
}
108+
109+
public function hasHeroImageAuthor(): bool
110+
{
111+
return $this->hero_image_author_name !== null &&
112+
$this->hero_image_author_url !== null;
104113
}
105114

106115
public function heroImage($width = 400, $height = 300): string
107116
{
108-
if ($this->hero_image) {
109-
return "https://source.unsplash.com/{$this->hero_image}/{$width}x{$height}";
117+
if ($this->hasHeroImage()) {
118+
return "{$this->hero_image_url}&fit=clip&w={$width}&h={$height}&utm_source=Laravel.io&utm_medium=referral";
110119
}
111120

112121
return asset('images/default-background.svg');
@@ -309,6 +318,12 @@ public function scopeTrending(Builder $query): Builder
309318
->orderBy('submitted_at', 'desc');
310319
}
311320

321+
public function scopeUnsyncedImages(Builder $query): Builder
322+
{
323+
return $query->whereNotNull('hero_image_id')
324+
->whereNull('hero_image_url');
325+
}
326+
312327
public function shouldBeSearchable()
313328
{
314329
return $this->isPublished();

app/Models/User.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Concerns\HasTimestamps;
66
use App\Concerns\PreparesSearch;
77
use App\Enums\NotificationType;
8+
use Carbon\Carbon;
89
use Illuminate\Contracts\Auth\MustVerifyEmail;
910
use Illuminate\Database\Eloquent\Builder;
1011
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -196,6 +197,20 @@ public function countThreads(): int
196197
return $this->threadsRelation()->count();
197198
}
198199

200+
public function countThreadsFromToday(): int
201+
{
202+
$today = Carbon::today();
203+
204+
return $this->threadsRelation()
205+
->whereBetween('created_at', [$today, $today->copy()->endOfDay()])
206+
->count();
207+
}
208+
209+
public function hasTooManyThreadsToday(): bool
210+
{
211+
return $this->countThreadsFromToday() >= 5;
212+
}
213+
199214
/**
200215
* @return \Illuminate\Database\Eloquent\Collection
201216
*/

0 commit comments

Comments
 (0)