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

pull/5914/head
Daniel Supernault 8 months ago
parent d5582bcedf
commit 0f1819125c
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -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))

@ -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();

@ -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();
}
}

@ -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;

@ -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;
}
}
}

@ -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) {

@ -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);

@ -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');

Loading…
Cancel
Save