Procházet zdrojové kódy

Merge pull request #3295 from pixelfed/staging

Staging
daniel před 3 roky
rodič
revize
7ab581f700

+ 6 - 0
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)

+ 1 - 0
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);
 	}

+ 1 - 1
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);

+ 85 - 54
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);
-            }
-        }
-
-        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
-        ];
+		$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 $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);
-        }
-
-        $res = CollectionItem::whereCollectionId($id)
-        	->pluck('object_id')
+    	$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);
+
+        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($pid) {
+        	$follows = FollowerService::follows($pid, $profile->id);
+        }
+
         if($profile->is_private) {
             abort_if(!$pid, 404);
-            abort_if(!$profile->id != $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'];
         }
 
-        $visibility = $pid == $profile->id ? ['public', 'private'] : ['public'];
+        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)

+ 9 - 0
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');
+	}
 }

+ 141 - 0
app/Services/CollectionService.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace App\Services;
+
+use App\Collection;
+use App\CollectionItem;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Redis;
+
+class CollectionService
+{
+	const CACHE_KEY = 'pf:services:collections:';
+
+	public static function getItems($id, $start = 0, $stop = 10)
+	{
+		if(self::count($id)) {
+			return Redis::zrangebyscore(self::CACHE_KEY . 'items:' . $id, $start, $stop);
+		}
+
+		return self::coldBootItems($id);
+	}
+
+	public static function addItem($id, $sid, $score)
+	{
+		Redis::zadd(self::CACHE_KEY . 'items:' . $id, $score, $sid);
+	}
+
+	public static function removeItem($id, $sid)
+	{
+		Redis::zrem(self::CACHE_KEY . 'items:' . $id, $sid);
+	}
+
+	public static function clearItems($id)
+	{
+		Redis::del(self::CACHE_KEY . 'items:' . $id);
+	}
+
+	public static function coldBootItems($id)
+	{
+		return Cache::remember(self::CACHE_KEY . 'items:' . $id, 86400, function() use($id) {
+			return CollectionItem::whereCollectionId($id)
+				->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'];
+	}
+}

+ 55 - 11
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'];

binární
public/js/collectioncompose.js


binární
public/js/collections.js


binární
public/js/profile-chunk-uopy3z.js


binární
public/mix-manifest.json


+ 7 - 7
resources/views/collection/show.blade.php

@@ -4,12 +4,12 @@
 
 <div class="container">
 	<collection-component 
-		collection-id="{{$collection->id}}" 
-		collection-title="{{$collection->title}}" 
-		collection-description="{{$collection->description}}" 
-		collection-visibility="{{$collection->visibility}}"
-		profile-id="{{$collection->profile_id}}" 
-		profile-username="{{$collection->profile->username}}"
+		collection-id="{{$collection['id']}}"
+		collection-title="{{$collection['title']}}"
+		collection-description="{{$collection['description']}}"
+		collection-visibility="{{$collection['visibility']}}"
+		profile-id="{{$collection['pid']}}"
+		profile-username="{{$collection['username']}}"
 	></collection-component>
 </div>
 
@@ -30,4 +30,4 @@
 <script type="text/javascript" src="{{mix('js/compose.js')}}" async></script>
 	<script type="text/javascript" src="{{mix('js/collections.js')}}"></script>
 	<script type="text/javascript">App.boot()</script>
-@endpush	
+@endpush	

+ 2 - 1
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');