diff --git a/CHANGELOG.md b/CHANGELOG.md index 94395c279..d373fb025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,12 @@ - Updated admin diagnostics, add more configuration data to help diagnose potential issues. ([eab96fc3](https://github.com/pixelfed/pixelfed/commit/eab96fc3)) - Updated ConfigCacheService, fix discover features. ([ad48521a](https://github.com/pixelfed/pixelfed/commit/ad48521a)) - Updated MediaTransformer, fix type case bug. Fixes #3281. ([c1669253](https://github.com/pixelfed/pixelfed/commit/c1669253)) +- Updated SpaController, redirect web ui hashtags to legacy page for unauthenticated users. ([a44b812b](https://github.com/pixelfed/pixelfed/commit/a44b812b)) +- Updated ApiV1Controller, fixes #3288. ([3e670774](https://github.com/pixelfed/pixelfed/commit/3e670774)) +- Updated AP Helpers, fixes #3287. ([b78bff72](https://github.com/pixelfed/pixelfed/commit/b78bff72)) +- Updated AP Helpers, fixes #3290. ([53975206](https://github.com/pixelfed/pixelfed/commit/53975206)) +- Updated AccountController, refresh relationship after handling follow request. ([fe768785](https://github.com/pixelfed/pixelfed/commit/fe768785)) +- Updated CollectionController, fixes #3289. ([c7e1e473](https://github.com/pixelfed/pixelfed/commit/c7e1e473)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.2 (2022-01-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.1...v0.11.2) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 073bbfa81..fc3ff69d3 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -406,6 +406,7 @@ class AccountController extends Controller Cache::forget('profile:follower_count:'.$pid); Cache::forget('profile:following_count:'.$pid); + RelationshipService::refresh($pid, $follower->id); return response()->json(['msg' => 'success'], 200); } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 7e11a6591..f01bb1a7c 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2547,7 +2547,7 @@ class ApiV1Controller extends Controller $sortBy = $request->input('sort', 'all'); - if($sortBy == 'all' && $status['replies_count'] && $request->has('refresh_cache')) { + if($sortBy == 'all' && isset($status['replies_count']) && $status['replies_count'] && $request->has('refresh_cache')) { if(!Cache::has('status:replies:all-rc:' . $id)) { Cache::forget('status:replies:all:' . $id); Cache::put('status:replies:all-rc:' . $id, true, 300); diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index 992aa97e6..6ddba8239 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -17,6 +17,8 @@ use App\Transformer\Api\{ }; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Services\CollectionService; +use App\Services\FollowerService; use App\Services\StatusService; class CollectionController extends Controller @@ -30,18 +32,25 @@ class CollectionController extends Controller 'profile_id' => $profile->id, 'published_at' => null ]); + $collection->visibility = 'draft'; + $collection->save(); return view('collection.create', compact('collection')); } public function show(Request $request, int $id) { $user = $request->user(); - $collection = Collection::findOrFail($id); - if($collection->published_at == null || $collection->visibility != 'public') { - if(!$user || $user->profile_id != $collection->profile_id) { - abort_unless($user && $user->is_admin, 404); - } - } + $collection = CollectionService::getCollection($id); + abort_if(!$collection, 404); + if($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_if(!$user, 404); + if($user->profile_id != $collection['pid']) { + if(!$user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } return view('collection.show', compact('collection')); } @@ -57,7 +66,7 @@ class CollectionController extends Controller $this->validate($request, [ 'title' => 'nullable', 'description' => 'nullable', - 'visibility' => 'nullable|string|in:public,private' + 'visibility' => 'nullable|string|in:public,private,draft' ]); $profile = Auth::user()->profile; @@ -67,7 +76,7 @@ class CollectionController extends Controller $collection->visibility = e($request->input('visibility')); $collection->save(); - return 200; + return CollectionService::setCollection($collection->id, $collection); } public function publish(Request $request, int $id) @@ -76,7 +85,7 @@ class CollectionController extends Controller $this->validate($request, [ 'title' => 'nullable', 'description' => 'nullable', - 'visibility' => 'required|alpha|in:public,private' + 'visibility' => 'required|alpha|in:public,private,draft' ]); $profile = Auth::user()->profile; $collection = Collection::whereProfileId($profile->id)->findOrFail($id); @@ -88,8 +97,7 @@ class CollectionController extends Controller $collection->visibility = e($request->input('visibility')); $collection->published_at = now(); $collection->save(); - - return $collection->url(); + return CollectionService::setCollection($collection->id, $collection); } public function delete(Request $request, int $id) @@ -105,6 +113,8 @@ class CollectionController extends Controller return 200; } + CollectionService::deleteCollection($id); + return redirect('/'); } @@ -139,82 +149,98 @@ class CollectionController extends Controller 'order' => $count, ]); - return 200; + CollectionService::addItem( + $collection->id, + $status->id, + $count + ); + + return StatusService::get($status->id); } - public function get(Request $request, $id) + public function getCollection(Request $request, $id) { - $user = $request->user(); - $collection = Collection::findOrFail($id); - if($collection->published_at == null || $collection->visibility != 'public') { - if(!$user || $user->profile_id != $collection->profile_id) { - abort_unless($user && $user->is_admin, 404); - } - } + $user = $request->user(); + $collection = CollectionService::getCollection($id); + if($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_unless($user, 404); + if($user->profile_id != $collection['pid']) { + if(!$user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } - return [ - 'id' => (string) $collection->id, - 'visibility' => $collection->visibility, - 'title' => $collection->title, - 'description' => $collection->description, - 'thumb' => $collection->posts()->first()->thumb(), - 'url' => $collection->url(), - 'post_count' => $collection->posts()->count(), - 'published_at' => $collection->published_at - ]; + return $collection; } public function getItems(Request $request, int $id) { - $collection = Collection::findOrFail($id); - if($collection->visibility !== 'public') { - abort_if(!Auth::check() || Auth::user()->profile_id != $collection->profile_id, 404); - } + $user = $request->user(); + $collection = CollectionService::getCollection($id); + if($collection['published_at'] == null || $collection['visibility'] != 'public') { + abort_unless($user, 404); + if($user->profile_id != $collection['pid']) { + if(!$user->is_admin) { + abort_if($collection['visibility'] != 'private', 404); + abort_if(!FollowerService::follows($user->profile_id, $collection['pid']), 404); + } + } + } + $page = $request->input('page') ?? 1; + $start = $page == 1 ? 0 : ($page * 10 - 10); + $end = $start + 10; + $items = CollectionService::getItems($id, $start, $end); - $res = CollectionItem::whereCollectionId($id) - ->pluck('object_id') + return collect($items) ->map(function($id) { return StatusService::get($id); }) - ->filter(function($post) { - return $post && isset($post['account']); + ->filter(function($item) { + return $item && isset($item['account'], $item['media_attachments']); }) ->values(); - - return response()->json($res); } public function getUserCollections(Request $request, int $id) { $user = $request->user(); $pid = $user ? $user->profile_id : null; + $follows = false; + $visibility = ['public']; $profile = Profile::whereNull('status') ->whereNull('domain') ->findOrFail($id); - if($profile->is_private) { - abort_if(!$pid, 404); - abort_if(!$profile->id != $pid, 404); + if($pid) { + $follows = FollowerService::follows($pid, $profile->id); } - $visibility = $pid == $profile->id ? ['public', 'private'] : ['public']; + if($profile->is_private) { + abort_if(!$pid, 404); + if(!$user->is_admin) { + abort_if($profile->id != $pid && $follows == false, 404); + } + } + + $owner = $pid ? $pid == $profile->id : false; + + if($follows) { + $visibility = ['public', 'private']; + } + + if($pid && $pid == $profile->id) { + $visibility = ['public', 'private', 'draft']; + } return Collection::whereProfileId($profile->id) ->whereIn('visibility', $visibility) ->orderByDesc('id') ->paginate(9) ->map(function($collection) { - return [ - 'id' => (string) $collection->id, - 'visibility' => $collection->visibility, - 'title' => $collection->title, - 'description' => $collection->description, - 'thumb' => $collection->posts()->first()->thumb(), - 'url' => $collection->url(), - 'post_count' => $collection->posts()->count(), - 'published_at' => $collection->published_at - ]; + return CollectionService::getCollection($collection->id); }); } @@ -240,6 +266,11 @@ class CollectionController extends Controller ->whereIn('type', ['photo', 'photo:album', 'video']) ->findOrFail($postId); + CollectionService::removeItem( + $collection->id, + $status->id + ); + $item = CollectionItem::whereCollectionId($collection->id) ->whereObjectType('App\Status') ->whereObjectId($status->id) diff --git a/app/Http/Controllers/SpaController.php b/app/Http/Controllers/SpaController.php index 664286f78..a64a880b5 100644 --- a/app/Http/Controllers/SpaController.php +++ b/app/Http/Controllers/SpaController.php @@ -126,4 +126,13 @@ class SpaController extends Controller } return redirect('/i/web/profile/' . $id); } + + public function hashtagRedirect(Request $request, $tag) + { + if(!$request->user()) { + return redirect('/discover/tags/' . $tag); + } + + return view('layouts.spa'); + } } diff --git a/app/Services/CollectionService.php b/app/Services/CollectionService.php new file mode 100644 index 000000000..33a3303ff --- /dev/null +++ b/app/Services/CollectionService.php @@ -0,0 +1,141 @@ +orderBy('order') + ->get() + ->each(function($item) use ($id) { + self::addItem($id, $item->object_id, $item->order); + }) + ->map(function($item) { + return (string) $item->object_id; + }) + ->values() + ->toArray(); + }); + } + + public static function count($id) + { + $count = Redis::zcard(self::CACHE_KEY . 'items:' . $id); + if(!$count) { + self::coldBootItems($id); + $count = Redis::zcard(self::CACHE_KEY . 'items:' . $id); + } + return $count; + } + + public static function getCollection($id) + { + $collection = Cache::remember(self::CACHE_KEY . 'get:' . $id, 86400, function() use($id) { + $collection = Collection::find($id); + if(!$collection) { + return false; + } + $account = AccountService::get($collection->profile_id); + if(!$account) { + return false; + } + return [ + 'id' => (string) $collection->id, + 'pid' => (string) $collection->profile_id, + 'username' => $account['username'], + 'visibility' => $collection->visibility, + 'title' => $collection->title, + 'description' => $collection->description, + 'thumb' => self::getThumb($id), + 'url' => $collection->url(), + 'published_at' => $collection->published_at + ]; + }); + + if($collection) { + $collection['post_count'] = self::count($id); + } + + return $collection; + } + + public static function setCollection($id, $collection) + { + $account = AccountService::get($collection->profile_id); + if(!$account) { + return false; + } + $res = [ + 'id' => (string) $collection->id, + 'pid' => (string) $collection->profile_id, + 'username' => $account['username'], + 'visibility' => $collection->visibility, + 'title' => $collection->title, + 'description' => $collection->description, + 'thumb' => self::getThumb($id), + 'url' => $collection->url(), + 'published_at' => $collection->published_at + ]; + Cache::put(self::CACHE_KEY . 'get:' . $id, $res, 86400); + $res['post_count'] = self::count($id); + return $res; + } + + public static function deleteCollection($id) + { + Redis::del(self::CACHE_KEY . 'items:' . $id); + Cache::forget(self::CACHE_KEY . 'get:' . $id); + } + + public static function getThumb($id) + { + $item = self::getItems($id, 0, 1); + if(!$item || empty($item)) { + return '/storage/no-preview.png'; + } + $status = StatusService::get($item[0]); + if(!$status) { + return '/storage/no-preview.png'; + } + + if(!isset($status['media_attachments']) || empty($status['media_attachments'])) { + return '/storage/no-preview.png'; + } + + return $status['media_attachments'][0]['url']; + } +} diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index a18a57ff0..b9a4f26e1 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -71,11 +71,25 @@ class Helpers { $mimeTypes = explode(',', config_cache('pixelfed.media_types')); $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image']; + // Peertube + // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image']; + if(!isset($activity['attachment']) || empty($activity['attachment'])) { return false; } + // peertube + // $attachment = is_array($activity['url']) ? + // collect($activity['url']) + // ->filter(function($media) { + // return $media['type'] == 'Link' && $media['mediaType'] == 'video/mp4'; + // }) + // ->take(1) + // ->values() + // ->toArray()[0] : $activity['attachment']; + $attachment = $activity['attachment']; + $valid = Validator::make($attachment, [ '*.type' => [ 'required', @@ -88,7 +102,7 @@ class Helpers { 'string', Rule::in($mimeTypes) ], - '*.name' => 'nullable|string|max:255' + '*.name' => 'sometimes|nullable|string|max:255' ])->passes(); return $valid; @@ -247,6 +261,19 @@ class Helpers { return self::fetchFromUrl($url); } + public static function pluckval($val) + { + if(is_string($val)) { + return $val; + } + + if(is_array($val)) { + return !empty($val) ? $val[0] : null; + } + + return null; + } + public static function statusFirstOrFetch($url, $replyTo = false) { $url = self::validateUrl($url); @@ -330,7 +357,7 @@ class Helpers { } } - $id = isset($res['id']) ? $res['id'] : $url; + $id = isset($res['id']) ? self::pluckval($res['id']) : self::pluckval($url); $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); @@ -338,9 +365,20 @@ class Helpers { return; } - if(isset($activity['object']['attributedTo'])) { - $actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST); - if(!self::validateUrl($activity['object']['attributedTo']) || + $attributedTo = is_string($activity['object']['attributedTo']) ? + $activity['object']['attributedTo'] : + (is_array($activity['object']['attributedTo']) ? + collect($activity['object']['attributedTo']) + ->filter(function($o) { + return $o && isset($o['type']) && $o['type'] == 'Person'; + }) + ->pluck('id') + ->first() : null + ); + + if($attributedTo) { + $actorDomain = parse_url($attributedTo, PHP_URL_HOST); + if(!self::validateUrl($attributedTo) || $idDomain !== $actorDomain || $actorDomain !== $urlDomain ) @@ -353,14 +391,14 @@ class Helpers { return; } - $profile = self::profileFirstOrNew($activity['object']['attributedTo']); + $profile = self::profileFirstOrNew($attributedTo); if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) { - $reply_to = self::statusFirstOrFetch($activity['object']['inReplyTo'], false); + $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false); $reply_to = optional($reply_to)->id; } else { $reply_to = null; } - $ts = is_array($res['published']) ? $res['published'][0] : $res['published']; + $ts = self::pluckval($res['published']); if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { $scope = 'unlisted'; @@ -399,8 +437,8 @@ class Helpers { return DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) { $status = new Status; $status->profile_id = $profile->id; - $status->url = isset($res['url']) ? $res['url'] : $url; - $status->uri = isset($res['url']) ? $res['url'] : $url; + $status->url = isset($res['url']) && is_string($res['url']) ? $res['url'] : $url; + $status->uri = isset($res['url']) && is_string($res['url']) ? $res['url'] : $url; $status->object_url = $id; $status->caption = strip_tags($res['content']); $status->rendered = Purify::clean($res['content']); @@ -486,10 +524,16 @@ class Helpers { public static function importNoteAttachment($data, Status $status) { if(self::verifyAttachments($data) == false) { + // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]); $status->viewType(); return; } $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment']; + // peertube + // if(!$attachments) { + // $obj = isset($data['object']) ? $data['object'] : $data; + // $attachments = is_array($obj['url']) ? $obj['url'] : null; + // } $user = $status->profile; $storagePath = MediaPathService::get($user, 2); $allowed = explode(',', config_cache('pixelfed.media_types')); @@ -585,7 +629,7 @@ class Helpers { $profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null; $profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null; $profile->inbox_url = $res['inbox']; - $profile->outbox_url = $res['outbox']; + $profile->outbox_url = isset($res['outbox']) ? $res['outbox'] : null; $profile->remote_url = $res['id']; $profile->public_key = $res['publicKey']['publicKeyPem']; $profile->key_id = $res['publicKey']['id']; diff --git a/public/js/collectioncompose.js b/public/js/collectioncompose.js index 0cb69fed1..60a393b6c 100644 Binary files a/public/js/collectioncompose.js and b/public/js/collectioncompose.js differ diff --git a/public/js/collections.js b/public/js/collections.js index 1a1759102..39304b0dd 100644 Binary files a/public/js/collections.js and b/public/js/collections.js differ diff --git a/public/js/profile-chunk-uopy3z.js b/public/js/profile-chunk-uopy3z.js index a6d9b2c99..bce0b77fa 100644 Binary files a/public/js/profile-chunk-uopy3z.js and b/public/js/profile-chunk-uopy3z.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index e11291084..78eb83387 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/views/collection/show.blade.php b/resources/views/collection/show.blade.php index 5d8ef0fec..c835068f5 100644 --- a/resources/views/collection/show.blade.php +++ b/resources/views/collection/show.blade.php @@ -4,12 +4,12 @@
@@ -30,4 +30,4 @@ -@endpush \ No newline at end of file +@endpush diff --git a/routes/web.php b/routes/web.php index 0cb0c9507..d9436a21e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -237,7 +237,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('collection/items/{id}', 'CollectionController@getItems'); Route::post('collection/item', 'CollectionController@storeId'); Route::delete('collection/item', 'CollectionController@deleteId'); - Route::get('collection/{id}', 'CollectionController@get'); + Route::get('collection/{id}', 'CollectionController@getCollection'); Route::post('collection/{id}', 'CollectionController@store'); Route::delete('collection/{id}', 'CollectionController@delete'); Route::post('collection/{id}/publish', 'CollectionController@publish'); @@ -351,6 +351,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('warning', 'AccountInterstitialController@read'); Route::get('my2020', 'SeasonalController@yearInReview'); + Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect'); Route::get('web/username/{id}', 'SpaController@usernameRedirect'); Route::get('web/post/{id}', 'SpaController@webPost'); Route::get('web/profile/{id}', 'SpaController@webProfile');