Merge pull request #6348 from pixelfed/staging

More federation fixes
pull/6349/head
dansup 8 months ago committed by GitHub
commit df9bd88a1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -24,6 +24,7 @@ use App\Jobs\FollowPipeline\UnfollowPipeline;
use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\LikePipeline\UnlikePipeline;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
use App\Jobs\NotificationPipeline\NotificationWarmUserCache;
@ -1382,17 +1383,16 @@ class ApiV1Controller extends Controller
*/
public function statusFavouriteById(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
$napi = $request->has(self::PF_API_ENTITY_KEY);
$status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
abort_unless($status, 404);
abort_if(isset($status['moved'], $status['moved']['id']), 422, 'Cannot like a post from an account that has migrated');
if ($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) {
@ -1406,9 +1406,9 @@ class ApiV1Controller extends Controller
if (intval($spid) !== intval($user->profile_id)) {
if ($status['visibility'] == 'private') {
abort_if(! FollowerService::follows($user->profile_id, $spid), 403);
abort_if(!FollowerService::follows($user->profile_id, $spid), 403);
} else {
abort_if(! in_array($status['visibility'], ['public', 'unlisted']), 403);
abort_if(!in_array($status['visibility'], ['public', 'unlisted']), 403);
}
}
@ -1424,23 +1424,47 @@ class ApiV1Controller extends Controller
abort(422);
}
$like = Like::firstOrCreate([
'profile_id' => $user->profile_id,
'status_id' => $status['id'],
]);
$like = DB::transaction(function () use ($user, $status, $spid, $id) {
$statusModel = Status::lockForUpdate()->find($status['id']);
if ($like->wasRecentlyCreated == true) {
$like->status_profile_id = $spid;
$like->is_comment = ! empty($status['in_reply_to_id']);
$like->save();
Status::findOrFail($status['id'])->update([
'likes_count' => ($status['favourites_count'] ?? 0) + 1,
]);
LikePipeline::dispatch($like)->onQueue('feed');
if (!$statusModel) {
abort(404, 'Status not found');
}
$like = Like::firstOrCreate(
[
'profile_id' => $user->profile_id,
'status_id' => $status['id'],
],
[
'status_profile_id' => $spid,
'is_comment' => !empty($status['in_reply_to_id']),
]
);
if ($like->wasRecentlyCreated) {
$statusModel->increment('likes_count');
DB::afterCommit(function () use ($like) {
LikePipeline::dispatch($like)->onQueue('feed');
});
}
return $like;
});
StatusService::del($status['id']);
$freshStatus = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
if ($freshStatus) {
$freshStatus['favourited'] = true;
$freshStatus['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$freshStatus['reblogged'] = ReblogService::get($user->profile_id, $status['id']);
return $this->json($freshStatus);
}
$status['favourited'] = true;
$status['favourites_count'] = $status['favourites_count'] + 1;
$status['favourites_count'] = ($status['favourites_count'] ?? 0) + ($like->wasRecentlyCreated ? 1 : 0);
$status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$status['reblogged'] = ReblogService::get($user->profile_id, $status['id']);
@ -1455,11 +1479,11 @@ class ApiV1Controller extends Controller
*/
public function statusUnfavouriteById(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
$napi = $request->has(self::PF_API_ENTITY_KEY);
$status = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
@ -1478,29 +1502,42 @@ class ApiV1Controller extends Controller
if (intval($spid) !== intval($user->profile_id)) {
if ($status['visibility'] == 'private') {
abort_if(! FollowerService::follows($user->profile_id, $spid), 403);
abort_if(!FollowerService::follows($user->profile_id, $spid), 403);
} else {
abort_if(! in_array($status['visibility'], ['public', 'unlisted']), 403);
abort_if(!in_array($status['visibility'], ['public', 'unlisted']), 403);
}
}
$like = Like::whereProfileId($user->profile_id)
->whereStatusId($status['id'])
->first();
$didUnlike = DB::transaction(function () use ($user, $status) {
$like = Like::with(['actor', 'status'])
->lockForUpdate()
->whereProfileId($user->profile_id)
->whereStatusId($status['id'])
->first();
if ($like) {
$like->forceDelete();
$ogStatus = Status::find($status['id']);
if ($ogStatus) {
$ogStatus->likes_count = $ogStatus->likes_count > 1 ? $ogStatus->likes_count - 1 : 0;
$ogStatus->save();
if (!$like) {
return false;
}
}
DB::afterCommit(function () use ($like) {
UnlikePipeline::dispatch($like)->onQueue('feed');
});
return true;
});
StatusService::del($status['id']);
$freshStatus = $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
if ($freshStatus) {
$freshStatus['favourited'] = false;
$freshStatus['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$freshStatus['reblogged'] = ReblogService::get($user->profile_id, $status['id']);
return $this->json($freshStatus);
}
$status['favourited'] = false;
$status['favourites_count'] = isset($ogStatus) ? $ogStatus->likes_count : $status['favourites_count'] - 1;
$status['favourites_count'] = max(0, ($status['favourites_count'] ?? 0) - ($didUnlike ? 1 : 0));
$status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$status['reblogged'] = ReblogService::get($user->profile_id, $status['id']);

@ -15,8 +15,9 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
@ -26,122 +27,101 @@ class LikePipeline implements ShouldQueue
protected $like;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
public $timeout = 5;
public $timeout = 30;
public $tries = 1;
public $tries = 3;
public $maxExceptions = 2;
public $backoff = [3, 10];
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Like $like)
{
$this->like = $like;
}
/**
* Execute the job.
*
* @return void
*/
public function middleware()
{
return [
(new WithoutOverlapping("like:{$this->like->status_id}:{$this->like->profile_id}"))
->releaseAfter(10)
->expireAfter(60),
];
}
public function uniqueId()
{
return "like:{$this->like->status_id}:{$this->like->profile_id}";
}
public function handle()
{
$like = $this->like;
$status = $like->status;
$actor = $like->actor;
// Check if like still exists (in case it was soft deleted or removed)
if (!$like) {
Log::info("LikePipeline: Like no longer exists, skipping job");
if (! $status) {
return;
}
$status = $this->like->status;
$actor = $this->like->actor;
if ($status->url && $actor->domain == null) {
$this->remoteLikeDeliver();
StatusService::refresh($status->id);
// Verify both status and actor exist
if (!$status) {
Log::info("LikePipeline: Status no longer exists for like {$like->id}, skipping job");
return;
}
if (!$actor) {
Log::info("LikePipeline: Actor no longer exists for like {$like->id}, skipping job");
return;
}
StatusService::refresh($status->id);
if ($actor->id === $status->profile_id) {
StatusService::refresh($status->id);
if ($status->url && $actor->domain == null) {
return $this->remoteLikeDeliver();
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
->whereItemType('App\Status')
->count();
if ($actor->id === $status->profile_id || $exists) {
return true;
return;
}
if ($status->uri === null && $status->object_url === null && $status->url === null) {
try {
$notification = new Notification;
$notification->profile_id = $status->profile_id;
$notification->actor_id = $actor->id;
$notification->action = 'like';
$notification->item_id = $status->id;
$notification->item_type = "App\Status";
$notification->save();
} catch (\Exception $e) {
Log::warning("LikePipeline: Failed to create notification for like {$like->id}: " . $e->getMessage());
}
if (NotificationAppGatewayService::enabled()) {
if (PushNotificationService::check('like', $status->profile_id)) {
$user = User::whereProfileId($status->profile_id)->first();
if ($user && $user->expo_token && $user->notify_enabled) {
LikePushNotifyPipeline::dispatchSync($user->expo_token, $actor->username);
}
DB::transaction(function () use ($status, $actor) {
$notification = Notification::firstOrCreate(
[
'profile_id' => $status->profile_id,
'actor_id' => $actor->id,
'action' => 'like',
'item_id' => $status->id,
'item_type' => 'App\Status',
]
);
if ($notification->wasRecentlyCreated) {
$this->sendPushNotification($status, $actor);
}
}
});
}
StatusService::refresh($status->id);
}
public function remoteLikeDeliver()
protected function sendPushNotification($status, $actor)
{
$like = $this->like;
$status = $this->like->status;
$actor = $this->like->actor;
// Verify all required models exist before attempting delivery
if (!$like) {
Log::info("LikePipeline: Like missing for remote delivery, skipping");
if (! NotificationAppGatewayService::enabled()) {
return;
}
if (!$status) {
Log::info("LikePipeline: status missing for remote delivery, skipping");
return;
}
if (!$actor) {
Log::info("LikePipeline: actor missing for remote delivery, skipping");
if (! PushNotificationService::check('like', $status->profile_id)) {
return;
}
// Verify status has a profile
if (!$status->profile) {
Log::info("LikePipeline: Status profile missing for like {$like->id}, skipping remote delivery");
return;
$user = User::whereProfileId($status->profile_id)->first();
if ($user && $user->expo_token && $user->notify_enabled) {
LikePushNotifyPipeline::dispatchSync($user->expo_token, $actor->username);
}
}
public function remoteLikeDeliver()
{
$like = $this->like;
$status = $like->status;
$actor = $like->actor;
$fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer);
@ -150,11 +130,6 @@ class LikePipeline implements ShouldQueue
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
if (!$url) {
Log::info("LikePipeline: No inbox URL available for like {$like->id}, skipping remote delivery");
return;
}
Helpers::sendSignedObject($actor, $url, $activity);
}
}

@ -2,143 +2,105 @@
namespace App\Jobs\LikePipeline;
use Cache, DB, Log;
use Illuminate\Support\Facades\Redis;
use App\{Like, Notification};
use App\Like;
use App\Notification;
use App\Services\StatusService;
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
use App\Util\ActivityPub\Helpers;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\DB;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
use App\Services\StatusService;
class UnlikePipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $like;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
public $timeout = 5;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Like $like)
{
$this->like = $like;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$like = $this->like;
// Check if like still exists (in case it was already deleted)
if (!$like) {
Log::info("UnlikePipeline: Like no longer exists, skipping job");
return;
}
$status = $this->like->status;
$actor = $this->like->actor;
// Verify both status and actor exist
if (!$status) {
Log::info("UnlikePipeline: Status no longer exists for like {$like->id}, skipping job");
return;
}
if (!$actor) {
Log::info("UnlikePipeline: Actor no longer exists for like {$like->id}, skipping job");
return;
}
$status->likes_count = DB::table('likes')->whereStatusId($status->id)->count();
$status->save();
StatusService::refresh($status->id);
if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
$this->remoteLikeDeliver();
}
$exists = Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
if($exists) {
$exists->delete();
}
$like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first();
if(!$like) {
return;
}
$like->forceDelete();
return;
}
public function remoteLikeDeliver()
{
$like = $this->like;
$status = $this->like->status;
$actor = $this->like->actor;
// Verify all required models exist before attempting delivery
if (!$like) {
Log::info("UnlikePipeline: Like missing for remote delivery, skipping");
return;
}
if (!$status) {
Log::info("UnlikePipeline: Status missing for remote delivery, skipping");
return;
}
if (!$actor) {
Log::info("UnlikePipeline: Actor missing for remote delivery, skipping");
return;
}
// Verify status has a profile
if (!$status->profile) {
Log::info("UnlikePipeline: Status profile missing for like {$like->id}, skipping remote delivery");
return;
}
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
$activity = $fractal->createData($resource)->toArray();
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
if (!$url) {
Log::info("UnlikePipeline: No inbox URL available for like {$like->id}, skipping remote delivery");
return;
}
Helpers::sendSignedObject($actor, $url, $activity);
}
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $like;
public $deleteWhenMissingModels = true;
public $timeout = 30;
public $tries = 3;
public $maxExceptions = 2;
public $backoff = [3, 10];
public function __construct(Like $like)
{
$this->like = $like;
}
public function middleware()
{
return [
(new WithoutOverlapping("unlike:{$this->like->status_id}:{$this->like->profile_id}"))
->releaseAfter(10)
->expireAfter(60),
];
}
public function uniqueId()
{
return "unlike:{$this->like->status_id}:{$this->like->profile_id}";
}
public function handle()
{
$like = $this->like;
$status = $like->status;
$actor = $like->actor;
if (! $status) {
return;
}
DB::transaction(function () use ($status, $actor, $like) {
if ($status->likes_count > 0) {
$status->decrement('likes_count');
}
Notification::whereProfileId($status->profile_id)
->whereActorId($actor->id)
->whereAction('like')
->whereItemId($status->id)
->whereItemType('App\Status')
->chunkById(100, function ($notifications) {
foreach ($notifications as $notification) {
$notification->forceDelete();
}
});
$like->forceDelete();
});
if ($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
$this->remoteLikeDeliver();
}
StatusService::refresh($status->id);
}
public function remoteLikeDeliver()
{
$like = $this->like;
$status = $like->status;
$actor = $like->actor;
$fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer);
$resource = new Fractal\Resource\Item($like, new LikeTransformer);
$activity = $fractal->createData($resource)->toArray();
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
Helpers::sendSignedObject($actor, $url, $activity);
}
}

@ -17,7 +17,7 @@ class Like extends Model
* @var array
*/
protected $casts = [
'deleted_at' => 'datetime'
'deleted_at' => 'datetime',
];
protected $fillable = ['profile_id', 'status_id', 'status_profile_id'];

@ -775,6 +775,13 @@ class Inbox
if (! $profile || $profile->private_key != null) {
return;
}
Notification::whereActorId($profile->id)
->chunkById(100, function ($notifications) {
foreach ($notifications as $notification) {
$notification->forceDelete();
}
});
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox');
return;
@ -795,6 +802,14 @@ class Inbox
if ($status->profile_id != $profile->id) {
return;
}
$notifications = Notification::whereActorId($status->profile_id)
->whereItemId($status->id)
->whereItemType('App\Status')
->get();
foreach ($notifications as $notification) {
$notification->forceDelete();
}
if ($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) {
if ($status->type && ! in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) {
FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
@ -916,12 +931,17 @@ class Inbox
FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
Status::whereProfileId($profile->id)
->whereReblogOfId($status->id)
->delete();
->forceDelete();
if ($status->reblogs_count) {
$status->reblogs_count = $status->reblogs_count - 1;
$status->saveQuietly();
}
ReblogService::removePostReblog($profile->id, $status->id);
$notifications = Notification::whereProfileId($status->profile_id)
->whereActorId($profile->id)
->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemId($status->id)
->whereItemType('App\Status')
->get();
foreach ($notifications as $notification) {

Loading…
Cancel
Save