瀏覽代碼

Update post pinning, and dispatch Notification cache warming to a job, and fix reblogged state on some endpoints

Daniel Supernault 3 月之前
父節點
當前提交
0f1819125c

+ 7 - 2
CHANGELOG.md

@@ -1,9 +1,14 @@
 # Release Notes
 
-## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
+## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
+
+### Added
+- Pinned Posts ([2f655d000](https://github.com/pixelfed/pixelfed/commit/2f655d000))
+
+### Updates
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
-## [v0.12.5 (2024-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
+## [v0.12.5 (2025-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)
 
 ### Added
 - Add app register email verify resends ([dbd1e17](https://github.com/pixelfed/pixelfed/commit/dbd1e17))

+ 6 - 4
app/Http/Controllers/Api/ApiV1Controller.php

@@ -26,6 +26,7 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
 use App\Jobs\LikePipeline\LikePipeline;
 use App\Jobs\MediaPipeline\MediaDeletePipeline;
 use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
+use App\Jobs\NotificationPipeline\NotificationWarmUserCache;
 use App\Jobs\SharePipeline\SharePipeline;
 use App\Jobs\SharePipeline\UndoSharePipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
@@ -2388,7 +2389,7 @@ class ApiV1Controller extends Controller
         if (empty($res)) {
             if (! Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
                 Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
-                NotificationService::warmCache($pid, 400, true);
+                NotificationWarmUserCache::dispatch($pid);
             }
         }
 
@@ -4438,11 +4439,12 @@ class ApiV1Controller extends Controller
     }
 
     /**
-     *  GET /api/v2/statuses/{id}/pin
+     *  GET /api/v1/statuses/{id}/pin
      */
     public function statusPin(Request $request, $id)
     {
         abort_if(! $request->user(), 403);
+        abort_unless($request->user()->tokenCan('write'), 403);
         $user = $request->user();
         $status = Status::whereScope('public')->find($id);
 
@@ -4469,12 +4471,12 @@ class ApiV1Controller extends Controller
     }
 
     /**
-     *  GET /api/v2/statuses/{id}/unpin
+     *  GET /api/v1/statuses/{id}/unpin
      */
     public function statusUnpin(Request $request, $id)
     {
-
         abort_if(! $request->user(), 403);
+        abort_unless($request->user()->tokenCan('write'), 403);
         $status = Status::whereScope('public')->findOrFail($id);
         $user = $request->user();
 

+ 82 - 101
app/Http/Controllers/Api/BaseApiController.php

@@ -2,46 +2,22 @@
 
 namespace App\Http\Controllers\Api;
 
-use Illuminate\Http\Request;
-use App\Http\Controllers\{
-    Controller,
-    AvatarController
-};
-use Auth, Cache, Storage, URL;
-use Carbon\Carbon;
-use App\{
-    Avatar,
-    Like,
-    Media,
-    Notification,
-    Profile,
-    Status,
-    StatusArchived
-};
-use App\Transformer\Api\{
-    AccountTransformer,
-    NotificationTransformer,
-    MediaTransformer,
-    MediaDraftTransformer,
-    StatusTransformer,
-    StatusStatelessTransformer
-};
-use League\Fractal;
-use App\Util\Media\Filter;
-use League\Fractal\Serializer\ArraySerializer;
-use League\Fractal\Pagination\IlluminatePaginatorAdapter;
+use App\Avatar;
+use App\Http\Controllers\AvatarController;
+use App\Http\Controllers\Controller;
 use App\Jobs\AvatarPipeline\AvatarOptimize;
-use App\Jobs\ImageOptimizePipeline\ImageOptimize;
-use App\Jobs\VideoPipeline\{
-    VideoOptimize,
-    VideoPostProcess,
-    VideoThumbnail
-};
+use App\Jobs\NotificationPipeline\NotificationWarmUserCache;
 use App\Services\AccountService;
 use App\Services\NotificationService;
-use App\Services\MediaPathService;
-use App\Services\MediaBlocklistService;
 use App\Services\StatusService;
+use App\Status;
+use App\StatusArchived;
+use App\Transformer\Api\StatusStatelessTransformer;
+use Auth;
+use Cache;
+use Illuminate\Http\Request;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
 
 class BaseApiController extends Controller
 {
@@ -50,47 +26,47 @@ class BaseApiController extends Controller
     public function __construct()
     {
         // $this->middleware('auth');
-        $this->fractal = new Fractal\Manager();
-        $this->fractal->setSerializer(new ArraySerializer());
+        $this->fractal = new Fractal\Manager;
+        $this->fractal->setSerializer(new ArraySerializer);
     }
 
     public function notifications(Request $request)
     {
-        abort_if(!$request->user(), 403);
-
-		$pid = $request->user()->profile_id;
-		$limit = $request->input('limit', 20);
-
-		$since = $request->input('since_id');
-		$min = $request->input('min_id');
-		$max = $request->input('max_id');
-
-		if(!$since && !$min && !$max) {
-			$min = 1;
-		}
-
-		$maxId = null;
-		$minId = null;
-
-		if($max) {
-			$res = NotificationService::getMax($pid, $max, $limit);
-			$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
-			if(!empty($ids)) {
-				$maxId = max($ids);
-				$minId = min($ids);
-			}
-		} else {
-			$res = NotificationService::getMin($pid, $min ?? $since, $limit);
-			$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
-			if(!empty($ids)) {
-				$maxId = max($ids);
-				$minId = min($ids);
-			}
-		}
-
-        if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
-        	Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
-        	NotificationService::warmCache($pid, 100, true);
+        abort_if(! $request->user(), 403);
+
+        $pid = $request->user()->profile_id;
+        $limit = $request->input('limit', 20);
+
+        $since = $request->input('since_id');
+        $min = $request->input('min_id');
+        $max = $request->input('max_id');
+
+        if (! $since && ! $min && ! $max) {
+            $min = 1;
+        }
+
+        $maxId = null;
+        $minId = null;
+
+        if ($max) {
+            $res = NotificationService::getMax($pid, $max, $limit);
+            $ids = NotificationService::getRankedMaxId($pid, $max, $limit);
+            if (! empty($ids)) {
+                $maxId = max($ids);
+                $minId = min($ids);
+            }
+        } else {
+            $res = NotificationService::getMin($pid, $min ?? $since, $limit);
+            $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
+            if (! empty($ids)) {
+                $maxId = max($ids);
+                $minId = min($ids);
+            }
+        }
+
+        if (empty($res) && ! Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
+            Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
+            NotificationWarmUserCache::dispatch($pid);
         }
 
         return response()->json($res);
@@ -98,17 +74,17 @@ class BaseApiController extends Controller
 
     public function avatarUpdate(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-            'upload'   => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
+            'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
         ]);
 
         try {
             $user = Auth::user();
             $profile = $user->profile;
             $file = $request->file('upload');
-            $path = (new AvatarController())->getPath($user, $file);
+            $path = (new AvatarController)->getPath($user, $file);
             $dir = $path['root'];
             $name = $path['name'];
             $public = $path['storage'];
@@ -129,13 +105,13 @@ class BaseApiController extends Controller
 
         return response()->json([
             'code' => 200,
-            'msg'  => 'Avatar successfully updated',
+            'msg' => 'Avatar successfully updated',
         ]);
     }
 
     public function verifyCredentials(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $user = $request->user();
         if ($user->status != null) {
@@ -143,47 +119,51 @@ class BaseApiController extends Controller
             abort(403);
         }
         $res = AccountService::get($user->profile_id);
+
         return response()->json($res);
     }
 
     public function accountLikes(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $this->validate($request, [
-        	'page' => 'sometimes|int|min:1|max:20',
-        	'limit' => 'sometimes|int|min:1|max:10'
+            'page' => 'sometimes|int|min:1|max:20',
+            'limit' => 'sometimes|int|min:1|max:10',
         ]);
 
         $user = $request->user();
         $limit = $request->input('limit', 10);
 
         $res = \DB::table('likes')
-        	->whereProfileId($user->profile_id)
-        	->latest()
-        	->simplePaginate($limit)
-        	->map(function($id) {
-        		$status = StatusService::get($id->status_id, false);
-        		$status['favourited'] = true;
-        		return $status;
-        	})
-        	->filter(function($post) {
-        		return $post && isset($post['account']);
-        	})
-        	->values();
+            ->whereProfileId($user->profile_id)
+            ->latest()
+            ->simplePaginate($limit)
+            ->map(function ($id) use ($user) {
+                $status = StatusService::get($id->status_id, false);
+                $status['favourited'] = true;
+                $status['reblogged'] = (bool) StatusService::isShared($id->status_id, $user->profile_id);
+
+                return $status;
+            })
+            ->filter(function ($post) {
+                return $post && isset($post['account']);
+            })
+            ->values();
+
         return response()->json($res);
     }
 
     public function archive(Request $request, $id)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $status = Status::whereNull('in_reply_to_id')
             ->whereNull('reblog_of_id')
             ->whereProfileId($request->user()->profile_id)
             ->findOrFail($id);
 
-        if($status->scope === 'archived') {
+        if ($status->scope === 'archived') {
             return [200];
         }
 
@@ -204,14 +184,14 @@ class BaseApiController extends Controller
 
     public function unarchive(Request $request, $id)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $status = Status::whereNull('in_reply_to_id')
             ->whereNull('reblog_of_id')
             ->whereProfileId($request->user()->profile_id)
             ->findOrFail($id);
 
-        if($status->scope !== 'archived') {
+        if ($status->scope !== 'archived') {
             return [200];
         }
 
@@ -231,16 +211,17 @@ class BaseApiController extends Controller
 
     public function archivedPosts(Request $request)
     {
-        abort_if(!$request->user(), 403);
+        abort_if(! $request->user(), 403);
 
         $statuses = Status::whereProfileId($request->user()->profile_id)
             ->whereScope('archived')
             ->orderByDesc('id')
             ->simplePaginate(10);
 
-        $fractal = new Fractal\Manager();
-        $fractal->setSerializer(new ArraySerializer());
-        $resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
+        $fractal = new Fractal\Manager;
+        $fractal->setSerializer(new ArraySerializer);
+        $resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer);
+
         return $fractal->createData($resource)->toArray();
     }
 }

+ 61 - 0
app/Http/Controllers/PublicApiController.php

@@ -35,6 +35,11 @@ class PublicApiController extends Controller
         $this->fractal->setSerializer(new ArraySerializer);
     }
 
+    public function json($res, $code = 200, $headers = [])
+    {
+        return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
+    }
+
     protected function getUserData($user)
     {
         if (! $user) {
@@ -726,6 +731,61 @@ class PublicApiController extends Controller
         return response()->json($result, 200, $headers);
     }
 
+    /**
+     *  GET /api/pixelfed/v1/statuses/{id}/pin
+     */
+    public function statusPin(Request $request, $id)
+    {
+        abort_if(! $request->user(), 403);
+        $user = $request->user();
+        $status = Status::whereScope('public')->find($id);
+
+        if (! $status) {
+            return $this->json(['error' => 'Record not found'], 404);
+        }
+
+        if ($status->profile_id != $user->profile_id) {
+            return $this->json(['error' => "Validation failed: Someone else's post cannot be pinned"], 422);
+        }
+
+        $res = StatusService::markPin($status->id);
+
+        if (! $res['success']) {
+            return $this->json([
+                'error' => $res['error'],
+            ], 422);
+        }
+
+        $statusRes = StatusService::get($status->id, true, true);
+        $status['pinned'] = true;
+
+        return $this->json($statusRes);
+    }
+
+    /**
+     *  GET /api/pixelfed/v1/statuses/{id}/unpin
+     */
+    public function statusUnpin(Request $request, $id)
+    {
+        abort_if(! $request->user(), 403);
+        $status = Status::whereScope('public')->findOrFail($id);
+        $user = $request->user();
+
+        if ($status->profile_id != $user->profile_id) {
+            return $this->json(['error' => 'Record not found'], 404);
+        }
+
+        $res = StatusService::unmarkPin($status->id);
+        if (! $res) {
+            return $this->json($res, 422);
+        }
+
+        $status = StatusService::get($status->id, true, true);
+        $status['pinned'] = false;
+
+        return $this->json($status);
+    }
+
     private function determineVisibility($profile, $user)
     {
         if ($profile['id'] == $user->profile_id) {
@@ -768,6 +828,7 @@ class PublicApiController extends Controller
 
                 if ($user) {
                     $mastodonStatus['favourited'] = (bool) LikeService::liked($user->profile_id, $status->id);
+                    $mastodonStatus['reblogged'] = (bool) StatusService::isShared($status->id, $user->profile_id);
                 }
 
                 return $mastodonStatus;

+ 90 - 0
app/Jobs/NotificationPipeline/NotificationWarmUserCache.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Jobs\NotificationPipeline;
+
+use App\Services\NotificationService;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldBeUnique;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+class NotificationWarmUserCache implements ShouldBeUnique, ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    /**
+     * The profile ID to warm cache for.
+     *
+     * @var int
+     */
+    public $pid;
+
+    /**
+     * The number of times the job may be attempted.
+     *
+     * @var int
+     */
+    public $tries = 3;
+
+    /**
+     * The number of seconds to wait before retrying the job.
+     * This creates exponential backoff: 10s, 30s, 90s
+     *
+     * @var array
+     */
+    public $backoff = [10, 30, 90];
+
+    /**
+     * The number of seconds after which the job's unique lock will be released.
+     *
+     * @var int
+     */
+    public $uniqueFor = 3600; // 1 hour
+
+    /**
+     * The maximum number of unhandled exceptions to allow before failing.
+     *
+     * @var int
+     */
+    public $maxExceptions = 2;
+
+    /**
+     * Create a new job instance.
+     *
+     * @param  int  $pid  The profile ID to warm cache for
+     * @return void
+     */
+    public function __construct(int $pid)
+    {
+        $this->pid = $pid;
+    }
+
+    /**
+     * Get the unique ID for the job.
+     */
+    public function uniqueId(): string
+    {
+        return 'notifications:profile_warm_cache:'.$this->pid;
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle(): void
+    {
+        try {
+            NotificationService::warmCache($this->pid, 100, true);
+        } catch (\Exception $e) {
+            Log::error('Failed to warm notification cache', [
+                'profile_id' => $this->pid,
+                'exception' => get_class($e),
+                'message' => $e->getMessage(),
+                'attempt' => $this->attempts(),
+            ]);
+            throw $e;
+        }
+    }
+}

+ 2 - 2
resources/assets/components/partials/post/ContextMenu.vue

@@ -999,7 +999,7 @@
 
                 this.closeModals();
 
-                axios.post('/api/v2/statuses/' + status.id.toString() + '/pin')
+                axios.post('/api/pixelfed/v1/statuses/' + status.id.toString() + '/pin')
                 .then(res => {
                     const data = res.data;
                     if(data.id && data.pinned) {
@@ -1023,7 +1023,7 @@
                 }
                 this.closeModals();
 
-                axios.post('/api/v2/statuses/' + status.id.toString() + '/unpin')
+                axios.post('/api/pixelfed/v1/statuses/' + status.id.toString() + '/unpin')
                 .then(res => {
                     const data = res.data;
                     if(data.id) {

+ 2 - 0
routes/api.php

@@ -147,6 +147,8 @@ Route::group(['prefix' => 'api'], function () use ($middleware) {
         Route::post('statuses/{id}/unreblog', 'Api\ApiV1Controller@statusUnshare')->middleware($middleware);
         Route::post('statuses/{id}/bookmark', 'Api\ApiV1Controller@bookmarkStatus')->middleware($middleware);
         Route::post('statuses/{id}/unbookmark', 'Api\ApiV1Controller@unbookmarkStatus')->middleware($middleware);
+        Route::post('statuses/{id}/pin', 'Api\ApiV1Controller@statusPin')->middleware($middleware);
+        Route::post('statuses/{id}/unpin', 'Api\ApiV1Controller@statusUnpin')->middleware($middleware);
         Route::delete('statuses/{id}', 'Api\ApiV1Controller@statusDelete')->middleware($middleware);
         Route::get('statuses/{id}', 'Api\ApiV1Controller@statusById')->middleware($middleware);
         Route::post('statuses', 'Api\ApiV1Controller@statusCreate')->middleware($middleware);

+ 2 - 2
routes/web-api.php

@@ -58,8 +58,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::get('discover/tag', 'DiscoverController@getHashtags');
             Route::get('statuses/{id}/replies', 'Api\ApiV1Controller@statusReplies');
             Route::get('statuses/{id}/state', 'Api\ApiV1Controller@statusState');
-            Route::post('statuses/{id}/pin', 'Api\ApiV1Controller@statusPin');
-            Route::post('statuses/{id}/unpin', 'Api\ApiV1Controller@statusUnpin');
         });
 
         Route::group(['prefix' => 'pixelfed'], function() {
@@ -71,6 +69,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
                 Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById');
                 Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById');
                 Route::get('statuses/{id}', 'PublicApiController@getStatus');
+                Route::post('statuses/{id}/pin', 'PublicApiController@statusPin');
+                Route::post('statuses/{id}/unpin', 'PublicApiController@statusUnpin');
                 Route::get('accounts/{id}', 'PublicApiController@account');
                 Route::post('avatar/update', 'ApiController@avatarUpdate');
                 Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis');