Skip to content

Commit 3703923

Browse files
committed
Add ActivityPub cache
1 parent e824242 commit 3703923

6 files changed

Lines changed: 189 additions & 22 deletions

File tree

app/Http/Controllers/ObjectController.php

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
use App\Models\Profile;
99
use App\Models\QuoteAuthorization;
1010
use App\Models\Video;
11+
use App\Services\ActivityPubCacheService;
1112
use App\Services\ConfigService;
1213
use App\Services\HashidService;
1314
use App\Services\VideoService;
1415
use Illuminate\Http\Request;
1516

1617
class ObjectController extends Controller
1718
{
19+
protected $apCache;
20+
21+
public function __construct(ActivityPubCacheService $apCache)
22+
{
23+
$this->apCache = $apCache;
24+
}
25+
1826
public function showProfile(Request $request, $username)
1927
{
2028
$config = app(ConfigService::class);
@@ -24,11 +32,7 @@ public function showProfile(Request $request, $username)
2432
return abort(404, 'Not found');
2533
}
2634

27-
if ($config->federationAuthorizedFetch()) {
28-
return abort(403, 'You do not have permission to view this.');
29-
}
30-
31-
$profile = Profile::whereUsername($username)->whereLocal(true)->first();
35+
$profile = Profile::active()->whereUsername($username)->whereLocal(true)->first();
3236

3337
if (! $profile) {
3438
return $this->activityPubError('Record not found', 404);
@@ -38,7 +42,12 @@ public function showProfile(Request $request, $username)
3842
return $this->activityPubError('Record not available', 400);
3943
}
4044

41-
return $this->activityPubResponse($profile->toActivityPub());
45+
$cacheKey = $this->apCache->profileKey($profile->id);
46+
$activityPub = $this->apCache->getOrCache($cacheKey, function () use ($profile) {
47+
return $profile->toActivityPub();
48+
});
49+
50+
return $this->activityPubResponseWithCache($activityPub);
4251
}
4352

4453
$profile = Profile::whereUsername($username)->first();
@@ -69,10 +78,6 @@ public function showVideo(Request $request, $hashId)
6978
return abort(404, 'Not found');
7079
}
7180

72-
if ($config->federationAuthorizedFetch()) {
73-
return abort(403, 'You do not have permission to view this.');
74-
}
75-
7681
if ($video->visibility !== 1) {
7782
return $this->activityPubError('You do not have permission to access this resource.', 403);
7883
}
@@ -108,9 +113,13 @@ public function showVideo(Request $request, $hashId)
108113

109114
protected function renderVideoObject($video, $hashId)
110115
{
111-
$res = CreateActivityBuilder::buildForVideoFlat($video->profile, $video);
116+
$cacheKey = $this->apCache->videoKey($video->id);
117+
118+
$res = $this->apCache->getOrCache($cacheKey, function () use ($video) {
119+
return CreateActivityBuilder::buildForVideoFlat($video->profile, $video);
120+
});
112121

113-
return $this->activityPubResponse($res);
122+
return $this->activityPubResponseWithCache($res);
114123
}
115124

116125
public function showVideoObject(Request $request, $actor, $id)
@@ -171,18 +180,27 @@ protected function renderVideoCommentObject($video, $videoHashId, $hashId, $vid,
171180
{
172181
$comment = Comment::with('profile')->where('visibility', 1)->whereNull('ap_id')->where('status', 'active')->whereVideoId($vid)->findOrFail($cid);
173182
abort_if($comment->profile->status != 1, 403, 'Resource not available');
174-
$res = CreateActivityBuilder::buildForCommentFlat($comment->profile, $comment);
175183

176-
return $this->activityPubResponse($res);
184+
$cacheKey = $this->apCache->commentKey($comment->id);
185+
186+
$res = $this->apCache->getOrCache($cacheKey, function () use ($comment) {
187+
return CreateActivityBuilder::buildForCommentFlat($comment->profile, $comment);
188+
});
189+
190+
return $this->activityPubResponseWithCache($res);
177191
}
178192

179193
protected function renderVideoCommentReplyObject($video, $comment, $videoHashId, $hashId, $vid, $cid)
180194
{
181195
abort_if($comment->profile->status != 1, 403, 'Resource not available');
182196

183-
$res = CreateActivityBuilder::buildForCommentReplyFlat($comment->profile, $comment);
197+
$cacheKey = $this->apCache->replyKey($comment->id);
198+
199+
$res = $this->apCache->getOrCache($cacheKey, function () use ($comment) {
200+
return CreateActivityBuilder::buildForCommentReplyFlat($comment->profile, $comment);
201+
});
184202

185-
return $this->activityPubResponse($res);
203+
return $this->activityPubResponseWithCache($res);
186204
}
187205

188206
public function getQuoteAuthorization($profileId, $authId)
@@ -191,8 +209,27 @@ public function getQuoteAuthorization($profileId, $authId)
191209
->where('quoted_profile_id', $profileId)
192210
->firstOrFail();
193211

194-
return response()->json($authorization->toActivityPub())
212+
$cacheKey = "quote_auth:{$authId}";
213+
214+
$activityPub = $this->apCache->getOrCache($cacheKey, function () use ($authorization) {
215+
return $authorization->toActivityPub();
216+
}, 900);
217+
218+
return $this->activityPubResponseWithCache($activityPub);
219+
}
220+
221+
/**
222+
* Return ActivityPub response with proper cache headers
223+
*/
224+
protected function activityPubResponseWithCache($data)
225+
{
226+
$json = is_array($data) ? json_encode($data) : $data;
227+
$etag = $this->apCache->getETag($json);
228+
229+
return response($json)
195230
->header('Content-Type', 'application/activity+json; charset=utf-8')
196-
->header('Cache-Control', 'public, max-age=300');
231+
->header('Cache-Control', 'public, max-age=3600, s-maxage=7200, stale-while-revalidate=86400')
232+
->header('ETag', $etag)
233+
->header('Vary', 'Accept');
197234
}
198235
}

app/Observers/CommentObserver.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44

55
use App\Models\Comment;
66
use App\Models\Notification;
7+
use App\Services\ActivityPubCacheService;
78
use App\Services\NotificationService;
89
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
910

1011
class CommentObserver implements ShouldHandleEventsAfterCommit
1112
{
13+
protected $apCache;
14+
15+
public function __construct(ActivityPubCacheService $apCache)
16+
{
17+
$this->apCache = $apCache;
18+
}
19+
1220
/**
1321
* Handle the Comment "created" event.
1422
*/
@@ -30,7 +38,7 @@ public function created(Comment $comment): void
3038
*/
3139
public function updated(Comment $comment): void
3240
{
33-
//
41+
$this->apCache->forget($this->apCache->commentKey($comment->id));
3442
}
3543

3644
/**
@@ -51,14 +59,16 @@ public function deleted(Comment $comment): void
5159
->whereProfileId($comment->profile_id)
5260
->whereCommentId($comment->id)
5361
->delete();
62+
63+
$this->apCache->forget($this->apCache->commentKey($comment->id));
5464
}
5565

5666
/**
5767
* Handle the Comment "restored" event.
5868
*/
5969
public function restored(Comment $comment): void
6070
{
61-
//
71+
$this->apCache->forget($this->apCache->commentKey($comment->id));
6272
}
6373

6474
/**
@@ -79,5 +89,7 @@ public function forceDeleted(Comment $comment): void
7989
->whereProfileId($comment->profile_id)
8090
->whereCommentId($comment->id)
8191
->delete();
92+
93+
$this->apCache->forget($this->apCache->commentKey($comment->id));
8294
}
8395
}

app/Observers/CommentReplyObserver.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44

55
use App\Models\CommentReply;
66
use App\Models\Notification;
7+
use App\Services\ActivityPubCacheService;
78
use App\Services\NotificationService;
89
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
910

1011
class CommentReplyObserver implements ShouldHandleEventsAfterCommit
1112
{
13+
protected $apCache;
14+
15+
public function __construct(ActivityPubCacheService $apCache)
16+
{
17+
$this->apCache = $apCache;
18+
}
19+
1220
/**
1321
* Handle the CommentReply "created" event.
1422
*/
@@ -49,7 +57,7 @@ public function created(CommentReply $commentReply): void
4957
*/
5058
public function updated(CommentReply $commentReply): void
5159
{
52-
//
60+
$this->apCache->forget($this->apCache->replyKey($commentReply->id));
5361
}
5462

5563
/**
@@ -71,14 +79,16 @@ public function deleted(CommentReply $commentReply): void
7179
->whereProfileId($commentReply->profile_id)
7280
->whereCommentReplyId($commentReply->id)
7381
->delete();
82+
83+
$this->apCache->forget($this->apCache->replyKey($commentReply->id));
7484
}
7585

7686
/**
7787
* Handle the CommentReply "restored" event.
7888
*/
7989
public function restored(CommentReply $commentReply): void
8090
{
81-
//
91+
$this->apCache->forget($this->apCache->replyKey($commentReply->id));
8292
}
8393

8494
/**
@@ -100,5 +110,7 @@ public function forceDeleted(CommentReply $commentReply): void
100110
->whereProfileId($commentReply->profile_id)
101111
->whereCommentReplyId($commentReply->id)
102112
->delete();
113+
114+
$this->apCache->forget($this->apCache->replyKey($commentReply->id));
103115
}
104116
}

app/Observers/ProfileObserver.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@
55
use App\Jobs\Federation\FetchRemoteAvatarJob;
66
use App\Models\Profile;
77
use App\Services\AccountService;
8+
use App\Services\ActivityPubCacheService;
89
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
910

1011
class ProfileObserver implements ShouldHandleEventsAfterCommit
1112
{
13+
protected $apCache;
14+
15+
public function __construct(ActivityPubCacheService $apCache)
16+
{
17+
$this->apCache = $apCache;
18+
}
19+
1220
/**
1321
* Handle the Profile "created" event.
1422
*/
@@ -31,6 +39,8 @@ public function updated(Profile $profile): void
3139
if (! $profile->local && $profile->uri && $profile->avatar) {
3240
FetchRemoteAvatarJob::dispatch($profile)->onQueue('actor-update');
3341
}
42+
43+
$this->apCache->forget($this->apCache->profileKey($profile->id));
3444
}
3545

3646
/**
@@ -39,6 +49,7 @@ public function updated(Profile $profile): void
3949
public function deleted(Profile $profile): void
4050
{
4151
AccountService::del($profile->id);
52+
$this->apCache->forget($this->apCache->profileKey($profile->id));
4253
}
4354

4455
/**
@@ -47,6 +58,7 @@ public function deleted(Profile $profile): void
4758
public function restored(Profile $profile): void
4859
{
4960
AccountService::del($profile->id);
61+
$this->apCache->forget($this->apCache->profileKey($profile->id));
5062
}
5163

5264
/**
@@ -55,5 +67,6 @@ public function restored(Profile $profile): void
5567
public function forceDeleted(Profile $profile): void
5668
{
5769
AccountService::del($profile->id);
70+
$this->apCache->forget($this->apCache->profileKey($profile->id));
5871
}
5972
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Support\Facades\Cache;
6+
7+
class ActivityPubCacheService
8+
{
9+
protected $ttl = 3600;
10+
11+
protected $enabled = true;
12+
13+
public function __construct()
14+
{
15+
$this->enabled = true;
16+
$this->ttl = config('loops.federation.cache_ttl', 3600);
17+
}
18+
19+
/**
20+
* Get or cache an ActivityPub object
21+
*/
22+
public function getOrCache(string $key, callable $callback, ?int $ttl = null)
23+
{
24+
if (! $this->enabled) {
25+
return $callback();
26+
}
27+
28+
$cacheKey = "ap:object:{$key}";
29+
$ttl = $ttl ?? $this->ttl;
30+
31+
return Cache::remember($cacheKey, $ttl, $callback);
32+
}
33+
34+
/**
35+
* Invalidate cache for a specific object
36+
*/
37+
public function forget(string $key)
38+
{
39+
$cacheKey = "ap:object:{$key}";
40+
Cache::forget($cacheKey);
41+
}
42+
43+
/**
44+
* Invalidate multiple cache keys at once
45+
*/
46+
public function forgetMultiple(array $keys)
47+
{
48+
foreach ($keys as $key) {
49+
$this->forget($key);
50+
}
51+
}
52+
53+
/**
54+
* Generate cache key for profile
55+
*/
56+
public function profileKey(int $profileId): string
57+
{
58+
return "profile:{$profileId}";
59+
}
60+
61+
/**
62+
* Generate cache key for video
63+
*/
64+
public function videoKey(int $videoId): string
65+
{
66+
return "video:{$videoId}";
67+
}
68+
69+
/**
70+
* Generate cache key for comment
71+
*/
72+
public function commentKey(int $commentId): string
73+
{
74+
return "comment:{$commentId}";
75+
}
76+
77+
/**
78+
* Generate cache key for comment reply
79+
*/
80+
public function replyKey(int $replyId): string
81+
{
82+
return "reply:{$replyId}";
83+
}
84+
85+
/**
86+
* Get ETag for response
87+
*/
88+
public function getETag(string $content): string
89+
{
90+
return md5($content);
91+
}
92+
}

config/loops.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
],
5151
'inbox_dispatch_chunk_size' => env('LOOPS_FED_INBOX_DIS_CHUNKER', 100),
5252
'inbox_max_followers' => env('LOOPS_FED_INBOX_MAX_FOLLOWERS', 5000),
53+
'cache_ttl' => env('LOOPS_FEDI_CACHE_TTL', 3600),
5354
],
5455

5556
'registration' => [

0 commit comments

Comments
 (0)