瀏覽代碼

Merge branch 'staging' into dev

daniel 3 年之前
父節點
當前提交
f79486c448
共有 55 個文件被更改,包括 781 次插入468 次删除
  1. 16 1
      CHANGELOG.md
  2. 4 6
      app/Http/Controllers/Api/ApiV1Controller.php
  3. 9 3
      app/Http/Controllers/FederationController.php
  4. 6 1
      app/Http/Controllers/FollowerController.php
  5. 7 4
      app/Http/Controllers/Import/Instagram.php
  6. 7 4
      app/Http/Controllers/Import/Mastodon.php
  7. 0 4
      app/Http/Controllers/ImportController.php
  8. 1 1
      app/Http/Controllers/InstanceActorController.php
  9. 3 5
      app/Http/Controllers/PollController.php
  10. 78 80
      app/Http/Controllers/PublicApiController.php
  11. 1 1
      app/Models/InstanceActor.php
  12. 0 64
      app/Observers/FollowerObserver.php
  13. 0 3
      app/Providers/AppServiceProvider.php
  14. 6 7
      app/Services/ActivityPubFetchService.php
  15. 2 0
      app/Services/FollowerService.php
  16. 7 0
      app/Services/InstanceService.php
  17. 18 4
      app/Services/NotificationService.php
  18. 86 0
      app/Services/RelationshipService.php
  19. 7 0
      app/Services/StatusService.php
  20. 1 1
      app/Transformer/ActivityPub/Verb/Note.php
  21. 38 72
      app/Transformer/Api/NotificationTransformer.php
  22. 1 2
      app/Util/ActivityPub/HttpSignature.php
  23. 3 0
      app/Util/ActivityPub/Inbox.php
  24. 18 23
      app/Util/Site/Nodeinfo.php
  25. 326 140
      composer.lock
  26. 1 0
      config/auth.php
  27. 二進制
      public/js/activity.js
  28. 二進制
      public/js/admin.js
  29. 二進制
      public/js/app.js
  30. 二進制
      public/js/components.js
  31. 二進制
      public/js/direct.js
  32. 二進制
      public/js/memoryprofile.js
  33. 二進制
      public/js/profile.js
  34. 二進制
      public/js/rempos.js
  35. 二進制
      public/js/rempro.js
  36. 二進制
      public/js/status.js
  37. 二進制
      public/js/theme-monokai.js
  38. 二進制
      public/js/timeline.js
  39. 二進制
      public/js/vendor.js
  40. 二進制
      public/mix-manifest.json
  41. 9 8
      resources/assets/js/app.js
  42. 13 6
      resources/assets/js/components/Activity.vue
  43. 7 7
      resources/assets/js/components/RemotePost.vue
  44. 1 1
      resources/assets/js/components/RemoteProfile.vue
  45. 26 2
      resources/assets/js/components/Timeline.vue
  46. 54 0
      resources/assets/js/components/partials/StatusCard.vue
  47. 2 2
      resources/assets/js/components/presenter/MixedAlbumPresenter.vue
  48. 3 3
      resources/assets/js/components/presenter/VideoAlbumPresenter.vue
  49. 1 1
      resources/assets/js/components/presenter/VideoPresenter.vue
  50. 0 2
      resources/lang/cs/site.php
  51. 11 0
      resources/lang/de/exception.php
  52. 1 2
      resources/lang/de/navmenu.php
  53. 1 1
      resources/lang/de/site.php
  54. 6 0
      routes/api.php
  55. 0 7
      routes/web.php

+ 16 - 1
CHANGELOG.md

@@ -2,6 +2,22 @@
 
 
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.1...dev)
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.1...dev)
 
 
+### Updated
+- Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3))
+- Updated HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21))
+- Updated NoteTransformer, fix tag array. ([7b3e672d](https://github.com/pixelfed/pixelfed/commit/7b3e672d))
+- Updated video presenters, add playsinline attribute to video tags. ([0299aa5b](https://github.com/pixelfed/pixelfed/commit/0299aa5b))
+- Updated RemotePost, RemoteProfile components, add fallback avatars. ([754151dc](https://github.com/pixelfed/pixelfed/commit/754151dc))
+- Updated FederationController, move well-known to api middleware and cache webfinger lookups. ([4505d1f0](https://github.com/pixelfed/pixelfed/commit/4505d1f0))
+- Updated InstanceActorController, improve json seralization by not escaping slashes. ([0a8eb81b](https://github.com/pixelfed/pixelfed/commit/0a8eb81b))
+- Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries. ([80d9b939](https://github.com/pixelfed/pixelfed/commit/80d9b939))
+- Updated PublicApiController, use AccountService in accountStatuses method. ([bef959f4](https://github.com/pixelfed/pixelfed/commit/bef959f4))
+- Updated auth config, add throttle limit to password resets. ([2609c86a](https://github.com/pixelfed/pixelfed/commit/2609c86a))
+- Updated StatusCard component, add relationship state button. ([0436b124](https://github.com/pixelfed/pixelfed/commit/0436b124))
+- Updated Timeline component, cascade relationship state change. ([f4bd5672](https://github.com/pixelfed/pixelfed/commit/f4bd5672))
+- Updated Activity component, only show context button for actionable activities. ([7886fd59](https://github.com/pixelfed/pixelfed/commit/7886fd59))
+-  ([](https://github.com/pixelfed/pixelfed/commit/))
+
 ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
 ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1)
 ### Added
 ### Added
 - WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a))
 - WebP Support ([069a0e4a](https://github.com/pixelfed/pixelfed/commit/069a0e4a))
@@ -112,7 +128,6 @@
 - Updated DirectMessageController, fix autocomplete bug. ([0f00be4d](https://github.com/pixelfed/pixelfed/commit/0f00be4d))
 - Updated DirectMessageController, fix autocomplete bug. ([0f00be4d](https://github.com/pixelfed/pixelfed/commit/0f00be4d))
 - Updated StoryService, fix division by zero bug. ([6ae1ba0a](https://github.com/pixelfed/pixelfed/commit/6ae1ba0a))
 - Updated StoryService, fix division by zero bug. ([6ae1ba0a](https://github.com/pixelfed/pixelfed/commit/6ae1ba0a))
 - Updated ApiV1Controller, fix empty public timeline bug. ([0584f9ee](https://github.com/pixelfed/pixelfed/commit/0584f9ee))
 - Updated ApiV1Controller, fix empty public timeline bug. ([0584f9ee](https://github.com/pixelfed/pixelfed/commit/0584f9ee))
--  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 
 ## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
 ## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
 ### Added
 ### Added

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

@@ -55,6 +55,7 @@ use App\Services\{
 	MediaPathService,
 	MediaPathService,
 	PublicTimelineService,
 	PublicTimelineService,
 	ProfileService,
 	ProfileService,
+	RelationshipService,
 	SearchApiV2Service,
 	SearchApiV2Service,
 	StatusService,
 	StatusService,
 	MediaBlocklistService
 	MediaBlocklistService
@@ -551,7 +552,7 @@ class ApiV1Controller extends Controller
 	 *
 	 *
 	 * @param  array|integer  $id
 	 * @param  array|integer  $id
 	 *
 	 *
-	 * @return \App\Transformer\Api\RelationshipTransformer
+	 * @return \App\Services\RelationshipService
 	 */
 	 */
 	public function accountRelationshipsById(Request $request)
 	public function accountRelationshipsById(Request $request)
 	{
 	{
@@ -563,12 +564,9 @@ class ApiV1Controller extends Controller
 		]);
 		]);
 		$pid = $request->user()->profile_id ?? $request->user()->profile->id;
 		$pid = $request->user()->profile_id ?? $request->user()->profile->id;
 		$ids = collect($request->input('id'));
 		$ids = collect($request->input('id'));
-		$filtered = $ids->filter(function($v) use($pid) {
-			return $v != $pid;
+		$res = $ids->map(function($id) use($pid) {
+			return RelationshipService::get($pid, $id);
 		});
 		});
-		$relations = Profile::whereNull('status')->findOrFail($filtered->values());
-		$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
-		$res = $this->fractal->createData($fractal)->toArray();
 		return response()->json($res);
 		return response()->json($res);
 	}
 	}
 
 

+ 9 - 3
app/Http/Controllers/FederationController.php

@@ -35,14 +35,14 @@ class FederationController extends Controller
 	public function nodeinfoWellKnown()
 	public function nodeinfoWellKnown()
 	{
 	{
 		abort_if(!config('federation.nodeinfo.enabled'), 404);
 		abort_if(!config('federation.nodeinfo.enabled'), 404);
-		return response()->json(Nodeinfo::wellKnown())
+		return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
 			->header('Access-Control-Allow-Origin','*');
 			->header('Access-Control-Allow-Origin','*');
 	}
 	}
 
 
 	public function nodeinfo()
 	public function nodeinfo()
 	{
 	{
 		abort_if(!config('federation.nodeinfo.enabled'), 404);
 		abort_if(!config('federation.nodeinfo.enabled'), 404);
-		return response()->json(Nodeinfo::get())
+		return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
 			->header('Access-Control-Allow-Origin','*');
 			->header('Access-Control-Allow-Origin','*');
 	}
 	}
 
 
@@ -53,6 +53,11 @@ class FederationController extends Controller
 		abort_if(!$request->filled('resource'), 400);
 		abort_if(!$request->filled('resource'), 400);
 
 
 		$resource = $request->input('resource');
 		$resource = $request->input('resource');
+		$hash = hash('sha256', $resource);
+		$key = 'federation:webfinger:sha256:' . $hash;
+		if($cached = Cache::get($key)) {
+			return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
+		}
 		$parsed = Nickname::normalizeProfileUrl($resource);
 		$parsed = Nickname::normalizeProfileUrl($resource);
 		if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
 		if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
 			abort(404);
 			abort(404);
@@ -63,8 +68,9 @@ class FederationController extends Controller
 			return ProfileController::accountCheck($profile);
 			return ProfileController::accountCheck($profile);
 		}
 		}
 		$webfinger = (new Webfinger($profile))->generate();
 		$webfinger = (new Webfinger($profile))->generate();
+		Cache::put($key, $webfinger, 43200);
 
 
-		return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
+		return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
 			->header('Access-Control-Allow-Origin','*');
 			->header('Access-Control-Allow-Origin','*');
 	}
 	}
 
 

+ 6 - 1
app/Http/Controllers/FollowerController.php

@@ -12,6 +12,7 @@ use Auth, Cache;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use App\Jobs\FollowPipeline\FollowPipeline;
 use App\Jobs\FollowPipeline\FollowPipeline;
 use App\Util\ActivityPub\Helpers;
 use App\Util\ActivityPub\Helpers;
+use App\Services\FollowerService;
 
 
 class FollowerController extends Controller
 class FollowerController extends Controller
 {
 {
@@ -70,7 +71,9 @@ class FollowerController extends Controller
             ]);
             ]);
             if($remote == true && config('federation.activitypub.remoteFollow') == true) {
             if($remote == true && config('federation.activitypub.remoteFollow') == true) {
                 $this->sendFollow($user, $target);
                 $this->sendFollow($user, $target);
-            } 
+            }
+
+            FollowerService::add($user->id, $target->id);
         } elseif ($private == false && $isFollowing == 0) {
         } elseif ($private == false && $isFollowing == 0) {
             if($user->following()->count() >= Follower::MAX_FOLLOWING) {
             if($user->following()->count() >= Follower::MAX_FOLLOWING) {
                 abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
                 abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
@@ -87,6 +90,7 @@ class FollowerController extends Controller
             if($remote == true && config('federation.activitypub.remoteFollow') == true) {
             if($remote == true && config('federation.activitypub.remoteFollow') == true) {
                 $this->sendFollow($user, $target);
                 $this->sendFollow($user, $target);
             } 
             } 
+            FollowerService::add($user->id, $target->id);
             FollowPipeline::dispatch($follower);
             FollowPipeline::dispatch($follower);
         } else {
         } else {
             if($force == true) {
             if($force == true) {
@@ -101,6 +105,7 @@ class FollowerController extends Controller
                 Follower::whereProfileId($user->id)
                 Follower::whereProfileId($user->id)
                     ->whereFollowingId($target->id)
                     ->whereFollowingId($target->id)
                     ->delete();
                     ->delete();
+                FollowerService::remove($user->id, $target->id);
             }
             }
         }
         }
 
 

+ 7 - 4
app/Http/Controllers/Import/Instagram.php

@@ -15,10 +15,13 @@ use App\Jobs\ImportPipeline\ImportInstagram;
 
 
 trait Instagram
 trait Instagram
 {
 {
-    public function instagram()
-    {
-      return view('settings.import.instagram.home');
-    }
+	public function instagram()
+	{
+		if(config_cache('pixelfed.import.instagram.enabled') != true) {
+			abort(404, 'Feature not enabled');
+		}
+		return view('settings.import.instagram.home');
+	}
 
 
     public function instagramStart(Request $request)
     public function instagramStart(Request $request)
     {	
     {	

+ 7 - 4
app/Http/Controllers/Import/Mastodon.php

@@ -6,8 +6,11 @@ use Illuminate\Http\Request;
 
 
 trait Mastodon
 trait Mastodon
 {
 {
-    public function mastodon()
-    {
-      return view('settings.import.mastodon.home');
-    }
+	public function mastodon()
+	{
+		if(config_cache('pixelfed.import.instagram.enabled') != true) {
+			abort(404, 'Feature not enabled');
+		}
+		return view('settings.import.mastodon.home');
+	}
 }
 }

+ 0 - 4
app/Http/Controllers/ImportController.php

@@ -11,10 +11,6 @@ class ImportController extends Controller
 	public function __construct()
 	public function __construct()
 	{
 	{
 		$this->middleware('auth');
 		$this->middleware('auth');
-
-		if(config_cache('pixelfed.import.instagram.enabled') != true) {
-			abort(404, 'Feature not enabled');
-		}
 	}
 	}
 
 
 }
 }

+ 1 - 1
app/Http/Controllers/InstanceActorController.php

@@ -12,7 +12,7 @@ class InstanceActorController extends Controller
 	{
 	{
 		$res = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
 		$res = Cache::rememberForever(InstanceActor::PROFILE_KEY, function() {
 			$res = (new InstanceActor())->first()->getActor();
 			$res = (new InstanceActor())->first()->getActor();
-			return json_encode($res);
+			return json_encode($res, JSON_UNESCAPED_SLASHES);
 		});
 		});
 		return response($res)->header('Content-Type', 'application/json');
 		return response($res)->header('Content-Type', 'application/json');
 	}
 	}

+ 3 - 5
app/Http/Controllers/PollController.php

@@ -11,14 +11,10 @@ use App\Services\FollowerService;
 
 
 class PollController extends Controller
 class PollController extends Controller
 {
 {
-
-	public function __construct()
+	public function getPoll(Request $request, $id)
 	{
 	{
 		abort_if(!config_cache('instance.polls.enabled'), 404);
 		abort_if(!config_cache('instance.polls.enabled'), 404);
-	}
 
 
-	public function getPoll(Request $request, $id)
-	{
 		$poll = Poll::findOrFail($id);
 		$poll = Poll::findOrFail($id);
 		$status = Status::findOrFail($poll->status_id);
 		$status = Status::findOrFail($poll->status_id);
 		if($status->scope != 'public') {
 		if($status->scope != 'public') {
@@ -34,6 +30,8 @@ class PollController extends Controller
 
 
     public function vote(Request $request, $id)
     public function vote(Request $request, $id)
     {
     {
+		abort_if(!config_cache('instance.polls.enabled'), 404);
+
     	abort_unless($request->user(), 403);
     	abort_unless($request->user(), 403);
 
 
     	$this->validate($request, [
     	$this->validate($request, [

+ 78 - 80
app/Http/Controllers/PublicApiController.php

@@ -51,11 +51,11 @@ class PublicApiController extends Controller
 
 
     protected function getUserData($user)
     protected function getUserData($user)
     {
     {
-    	if(!$user) {
-    		return [];
-    	} else {
+        if(!$user) {
+            return [];
+        } else {
             return AccountService::get($user->profile_id);
             return AccountService::get($user->profile_id);
-    	}
+        }
     }
     }
 
 
     protected function getLikes($status)
     protected function getLikes($status)
@@ -94,12 +94,12 @@ class PublicApiController extends Controller
         $status = Status::whereProfileId($profile->id)->findOrFail($postid);
         $status = Status::whereProfileId($profile->id)->findOrFail($postid);
         $this->scopeCheck($profile, $status);
         $this->scopeCheck($profile, $status);
         if(!$request->user()) {
         if(!$request->user()) {
-        	$res = ['status' => StatusService::get($status->id)];
+            $res = ['status' => StatusService::get($status->id)];
         } else {
         } else {
-        	$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
-	        $res = [
-	        	'status' => $this->fractal->createData($item)->toArray(),
-	        ];
+            $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
+            $res = [
+                'status' => $this->fractal->createData($item)->toArray(),
+            ];
         }
         }
 
 
         return response()->json($res);
         return response()->json($res);
@@ -200,14 +200,14 @@ class PublicApiController extends Controller
 
 
     public function statusLikes(Request $request, $username, $id)
     public function statusLikes(Request $request, $username, $id)
     {
     {
-    	abort_if(!$request->user(), 404);
+        abort_if(!$request->user(), 404);
         $status = Status::findOrFail($id);
         $status = Status::findOrFail($id);
         $this->scopeCheck($status->profile, $status);
         $this->scopeCheck($status->profile, $status);
         $page = $request->input('page');
         $page = $request->input('page');
         if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
         if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
-        	return response()->json([
-        		'data' => []
-        	]);
+            return response()->json([
+                'data' => []
+            ]);
         }
         }
         $likes = $this->getLikes($status);
         $likes = $this->getLikes($status);
         return response()->json([
         return response()->json([
@@ -217,15 +217,15 @@ class PublicApiController extends Controller
 
 
     public function statusShares(Request $request, $username, $id)
     public function statusShares(Request $request, $username, $id)
     {
     {
-    	abort_if(!$request->user(), 404);
+        abort_if(!$request->user(), 404);
         $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
         $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
         $status = Status::whereProfileId($profile->id)->findOrFail($id);
         $status = Status::whereProfileId($profile->id)->findOrFail($id);
         $this->scopeCheck($profile, $status);
         $this->scopeCheck($profile, $status);
         $page = $request->input('page');
         $page = $request->input('page');
         if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
         if($page && $page >= 3 && $request->user()->profile_id != $status->profile_id) {
-        	return response()->json([
-        		'data' => []
-        	]);
+            return response()->json([
+                'data' => []
+            ]);
         }
         }
         $shares = $this->getShares($status);
         $shares = $this->getShares($status);
         return response()->json([
         return response()->json([
@@ -300,7 +300,7 @@ class PublicApiController extends Controller
                         'scope',
                         'scope',
                         'local'
                         'local'
                       )
                       )
-            		  ->where('id', $dir, $id)
+                      ->where('id', $dir, $id)
                       ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
                       ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
                       ->whereNotIn('profile_id', $filtered)
                       ->whereNotIn('profile_id', $filtered)
                       ->whereLocal(true)
                       ->whereLocal(true)
@@ -309,7 +309,7 @@ class PublicApiController extends Controller
                       ->limit($limit)
                       ->limit($limit)
                       ->get()
                       ->get()
                       ->map(function($s) use ($user) {
                       ->map(function($s) use ($user) {
-                           $status = StatusService::get($s->id);
+                           $status = StatusService::getFull($s->id, $user->profile_id);
                            $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
                            $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
                            return $status;
                            return $status;
                       });
                       });
@@ -335,16 +335,21 @@ class PublicApiController extends Controller
                         'reblogs_count',
                         'reblogs_count',
                         'updated_at'
                         'updated_at'
                       )
                       )
-            		  ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+                      ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
                       ->whereNotIn('profile_id', $filtered)
                       ->whereNotIn('profile_id', $filtered)
                       ->with('profile', 'hashtags', 'mentions')
                       ->with('profile', 'hashtags', 'mentions')
                       ->whereLocal(true)
                       ->whereLocal(true)
                       ->whereScope('public')
                       ->whereScope('public')
                       ->orderBy('id', 'desc')
                       ->orderBy('id', 'desc')
-                      ->simplePaginate($limit);
+                      ->limit($limit)
+                      ->get()
+                      ->map(function($s) use ($user) {
+                           $status = StatusService::getFull($s->id, $user->profile_id);
+                           $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
+                           return $status;
+                      });
 
 
-	        $fractal = new Fractal\Resource\Collection($timeline, new StatusTransformer());
-	        $res = $this->fractal->createData($fractal)->toArray();
+            $res = $timeline->toArray();
         }
         }
 
 
         return response()->json($res);
         return response()->json($res);
@@ -389,12 +394,12 @@ class PublicApiController extends Controller
         });
         });
 
 
         if($recentFeed == true) {
         if($recentFeed == true) {
-			$key = 'profile:home-timeline-cursor:'.$user->id;
-			$ttl = now()->addMinutes(30);
-			$min = Cache::remember($key, $ttl, function() use($pid) {
-        		$res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
-        		return $res ? $res->status_id : null;
-			});
+            $key = 'profile:home-timeline-cursor:'.$user->id;
+            $ttl = now()->addMinutes(30);
+            $min = Cache::remember($key, $ttl, function() use($pid) {
+                $res = StatusView::whereProfileId($pid)->orderByDesc('status_id')->first();
+                return $res ? $res->status_id : null;
+            });
         }
         }
 
 
         $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
         $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
@@ -403,16 +408,16 @@ class PublicApiController extends Controller
         $textOnlyReplies = false;
         $textOnlyReplies = false;
 
 
         if(config('exp.top')) {
         if(config('exp.top')) {
-	        $textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
-	        $textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
+            $textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
+            $textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
 
 
-	        if($textOnlyPosts) {
-	        	array_push($types, 'text');
-	        }
+            if($textOnlyPosts) {
+                array_push($types, 'text');
+            }
         }
         }
 
 
         if(config('exp.polls') == true) {
         if(config('exp.polls') == true) {
-        	array_push($types, 'poll');
+            array_push($types, 'poll');
         }
         }
 
 
         if($min || $max) {
         if($min || $max) {
@@ -438,10 +443,10 @@ class PublicApiController extends Controller
                         'created_at',
                         'created_at',
                         'updated_at'
                         'updated_at'
                       )
                       )
-            		  ->whereIn('type', $types)
+                      ->whereIn('type', $types)
                       ->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
                       ->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
-                      	return $q->whereNull('in_reply_to_id');
-                  	  })
+                        return $q->whereNull('in_reply_to_id');
+                      })
                       ->with('profile', 'hashtags', 'mentions')
                       ->with('profile', 'hashtags', 'mentions')
                       ->where('id', $dir, $id)
                       ->where('id', $dir, $id)
                       ->whereIn('profile_id', $following)
                       ->whereIn('profile_id', $following)
@@ -471,10 +476,10 @@ class PublicApiController extends Controller
                         'created_at',
                         'created_at',
                         'updated_at'
                         'updated_at'
                       )
                       )
-            		  ->whereIn('type', $types)
-            		  ->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
-                      	return $q->whereNull('in_reply_to_id');
-                  	  })
+                      ->whereIn('type', $types)
+                      ->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
+                        return $q->whereNull('in_reply_to_id');
+                      })
                       ->with('profile', 'hashtags', 'mentions')
                       ->with('profile', 'hashtags', 'mentions')
                       ->whereIn('profile_id', $following)
                       ->whereIn('profile_id', $following)
                       ->whereNotIn('profile_id', $filtered)
                       ->whereNotIn('profile_id', $filtered)
@@ -527,7 +532,7 @@ class PublicApiController extends Controller
                         'scope',
                         'scope',
                         'created_at',
                         'created_at',
                       )
                       )
-            		  ->where('id', $dir, $id)
+                      ->where('id', $dir, $id)
                       ->whereNotIn('profile_id', $filtered)
                       ->whereNotIn('profile_id', $filtered)
                       ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
                       ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
                       ->whereNotNull('uri')
                       ->whereNotNull('uri')
@@ -543,19 +548,19 @@ class PublicApiController extends Controller
                       });
                       });
             $res = $timeline->toArray();
             $res = $timeline->toArray();
         } else {
         } else {
-	            $timeline = Status::select(
-	                        'id',
-	                        'uri',
-	                        'type',
-	                        'scope',
-	                        'created_at',
-	                      )
-                      	  ->whereNotIn('profile_id', $filtered)
-	            		  ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
-	                      ->whereNotNull('uri')
-	                      ->whereScope('public')
-                      	  ->where('id', '>', $amin)
-	                      ->orderBy('created_at', 'desc')
+                $timeline = Status::select(
+                            'id',
+                            'uri',
+                            'type',
+                            'scope',
+                            'created_at',
+                          )
+                          ->whereNotIn('profile_id', $filtered)
+                          ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+                          ->whereNotNull('uri')
+                          ->whereScope('public')
+                          ->where('id', '>', $amin)
+                          ->orderBy('created_at', 'desc')
                           ->limit($limit)
                           ->limit($limit)
                           ->get()
                           ->get()
                           ->map(function($s) use ($user) {
                           ->map(function($s) use ($user) {
@@ -563,7 +568,7 @@ class PublicApiController extends Controller
                                 $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
                                 $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
                                 return $status;
                                 return $status;
                           });
                           });
-	          	$res = $timeline->toArray();
+                $res = $timeline->toArray();
         }
         }
 
 
         return response()->json($res);
         return response()->json($res);
@@ -605,10 +610,10 @@ class PublicApiController extends Controller
             return response()->json([]);
             return response()->json([]);
         }
         }
         if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
         if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
-        	return response()->json([]);
+            return response()->json([]);
         }
         }
         if(!$owner && $request->page > 5) {
         if(!$owner && $request->page > 5) {
-        	return [];
+            return [];
         }
         }
 
 
         $res = Follower::select('id', 'profile_id', 'following_id')
         $res = Follower::select('id', 'profile_id', 'following_id')
@@ -639,11 +644,11 @@ class PublicApiController extends Controller
         abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
         abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
 
 
         if(!$profile->domain) {
         if(!$profile->domain) {
-        	abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
+            abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
         }
         }
 
 
         if(!$owner && $request->page > 5) {
         if(!$owner && $request->page > 5) {
-        	return [];
+            return [];
         }
         }
 
 
         if($search) {
         if($search) {
@@ -676,14 +681,15 @@ class PublicApiController extends Controller
         ]);
         ]);
 
 
         $user = $request->user();
         $user = $request->user();
-        $profile = Profile::whereNull('status')->findOrFail($id);
+        $profile = AccountService::get($id);
+        abort_if(!$profile, 404);
 
 
         $limit = $request->limit ?? 9;
         $limit = $request->limit ?? 9;
         $max_id = $request->max_id;
         $max_id = $request->max_id;
         $min_id = $request->min_id;
         $min_id = $request->min_id;
         $scope = ['photo', 'photo:album', 'video', 'video:album'];
         $scope = ['photo', 'photo:album', 'video', 'video:album'];
 
 
-        if($profile->is_private) {
+        if($profile['locked']) {
             if(!$user) {
             if(!$user) {
                 return response()->json([]);
                 return response()->json([]);
             }
             }
@@ -700,7 +706,7 @@ class PublicApiController extends Controller
                     $following = Follower::whereProfileId($pid)->pluck('following_id');
                     $following = Follower::whereProfileId($pid)->pluck('following_id');
                     return $following->push($pid)->toArray();
                     return $following->push($pid)->toArray();
                 });
                 });
-                $visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
+                $visibility = true == in_array($profile['id'], $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
             } else {
             } else {
                 $visibility = ['public', 'unlisted'];
                 $visibility = ['public', 'unlisted'];
             }
             }
@@ -708,15 +714,7 @@ class PublicApiController extends Controller
 
 
         $dir = $min_id ? '>' : '<';
         $dir = $min_id ? '>' : '<';
         $id = $min_id ?? $max_id;
         $id = $min_id ?? $max_id;
-        $res = Status::select(
-            'id',
-            'profile_id',
-            'type',
-            'scope',
-            'local',
-            'created_at'
-          )
-        ->whereProfileId($profile->id)
+        $res = Status::whereProfileId($profile['id'])
         ->whereNull('in_reply_to_id')
         ->whereNull('in_reply_to_id')
         ->whereNull('reblog_of_id')
         ->whereNull('reblog_of_id')
         ->whereIn('type', $scope)
         ->whereIn('type', $scope)
@@ -726,18 +724,18 @@ class PublicApiController extends Controller
         ->orderByDesc('id')
         ->orderByDesc('id')
         ->get()
         ->get()
         ->map(function($s) use($user) {
         ->map(function($s) use($user) {
-        	try {
-            	$status = StatusService::get($s->id, false);
-        	} catch (\Exception $e) {
-        		$status = false;
-        	}
+            try {
+                $status = StatusService::get($s->id, false);
+            } catch (\Exception $e) {
+                $status = false;
+            }
             if($user && $status) {
             if($user && $status) {
-            	$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
+                $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
             }
             }
             return $status;
             return $status;
         })
         })
         ->filter(function($s) {
         ->filter(function($s) {
-        	return $s;
+            return $s;
         })
         })
         ->values();
         ->values();
 
 

+ 1 - 1
app/Models/InstanceActor.php

@@ -11,7 +11,7 @@ class InstanceActor extends Model
 
 
 	const PROFILE_BASE = '/i/actor';
 	const PROFILE_BASE = '/i/actor';
 	const KEY_ID = '/i/actor#main-key';
 	const KEY_ID = '/i/actor#main-key';
-	const PROFILE_KEY = 'federation:_v2:instance:actor:profile';
+	const PROFILE_KEY = 'federation:_v3:instance:actor:profile';
 	const PKI_PUBLIC = 'federation:_v1:instance:actor:profile:pki_public';
 	const PKI_PUBLIC = 'federation:_v1:instance:actor:profile:pki_public';
 	const PKI_PRIVATE = 'federation:_v1:instance:actor:profile:pki_private';
 	const PKI_PRIVATE = 'federation:_v1:instance:actor:profile:pki_private';
 
 

+ 0 - 64
app/Observers/FollowerObserver.php

@@ -1,64 +0,0 @@
-<?php
-
-namespace App\Observers;
-
-use App\Follower;
-use App\Services\FollowerService;
-
-class FollowerObserver
-{
-    /**
-     * Handle the Follower "created" event.
-     *
-     * @param  \App\Models\Follower  $follower
-     * @return void
-     */
-    public function created(Follower $follower)
-    {
-        FollowerService::add($follower->profile_id, $follower->following_id);
-    }
-
-    /**
-     * Handle the Follower "updated" event.
-     *
-     * @param  \App\Models\Follower  $follower
-     * @return void
-     */
-    public function updated(Follower $follower)
-    {
-        FollowerService::add($follower->profile_id, $follower->following_id);
-    }
-
-    /**
-     * Handle the Follower "deleted" event.
-     *
-     * @param  \App\Models\Follower  $follower
-     * @return void
-     */
-    public function deleted(Follower $follower)
-    {
-        FollowerService::remove($follower->profile_id, $follower->following_id);
-    }
-
-    /**
-     * Handle the Follower "restored" event.
-     *
-     * @param  \App\Models\Follower  $follower
-     * @return void
-     */
-    public function restored(Follower $follower)
-    {
-        FollowerService::add($follower->profile_id, $follower->following_id);
-    }
-
-    /**
-     * Handle the Follower "force deleted" event.
-     *
-     * @param  \App\Models\Follower  $follower
-     * @return void
-     */
-    public function forceDeleted(Follower $follower)
-    {
-        FollowerService::remove($follower->profile_id, $follower->following_id);
-    }
-}

+ 0 - 3
app/Providers/AppServiceProvider.php

@@ -4,7 +4,6 @@ namespace App\Providers;
 
 
 use App\Observers\{
 use App\Observers\{
 	AvatarObserver,
 	AvatarObserver,
-	FollowerObserver,
 	LikeObserver,
 	LikeObserver,
 	NotificationObserver,
 	NotificationObserver,
 	ModLogObserver,
 	ModLogObserver,
@@ -15,7 +14,6 @@ use App\Observers\{
 };
 };
 use App\{
 use App\{
 	Avatar,
 	Avatar,
-	Follower,
 	Like,
 	Like,
 	Notification,
 	Notification,
 	ModLog,
 	ModLog,
@@ -50,7 +48,6 @@ class AppServiceProvider extends ServiceProvider
 		StatusHashtag::observe(StatusHashtagObserver::class);
 		StatusHashtag::observe(StatusHashtagObserver::class);
 		User::observe(UserObserver::class);
 		User::observe(UserObserver::class);
 		UserFilter::observe(UserFilterObserver::class);
 		UserFilter::observe(UserFilterObserver::class);
-		Follower::observe(FollowerObserver::class);
 		Horizon::auth(function ($request) {
 		Horizon::auth(function ($request) {
 			return Auth::check() && $request->user()->is_admin;
 			return Auth::check() && $request->user()->is_admin;
 		});
 		});

+ 6 - 7
app/Services/ActivityPubFetchService.php

@@ -2,7 +2,7 @@
 
 
 namespace App\Services;
 namespace App\Services;
 
 
-use Zttp\Zttp;
+use Illuminate\Support\Facades\Http;
 use App\Profile;
 use App\Profile;
 use App\Util\ActivityPub\Helpers;
 use App\Util\ActivityPub\Helpers;
 use App\Util\ActivityPub\HttpSignature;
 use App\Util\ActivityPub\HttpSignature;
@@ -15,14 +15,13 @@ class ActivityPubFetchService
 			return 0;
 			return 0;
 		}
 		}
 
 
-		$headers = HttpSignature::instanceActorSign($url, false, [
-			'Accept'		=> 'application/activity+json, application/json',
-			'User-Agent'	=> '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
-		]);
+		$headers = HttpSignature::instanceActorSign($url, false);
+		$headers['Accept'] = 'application/activity+json, application/json';
+		$headers['User-Agent'] = '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
 
 
-		return Zttp::withHeaders($headers)
+		return Http::withHeaders($headers)
 			->timeout(30)
 			->timeout(30)
 			->get($url)
 			->get($url)
 			->body();
 			->body();
 	}
 	}
-}
+}

+ 2 - 0
app/Services/FollowerService.php

@@ -17,12 +17,14 @@ class FollowerService
 
 
 	public static function add($actor, $target)
 	public static function add($actor, $target)
 	{
 	{
+		RelationshipService::refresh($actor, $target);
 		Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
 		Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
 		Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
 		Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
 	}
 	}
 
 
 	public static function remove($actor, $target)
 	public static function remove($actor, $target)
 	{
 	{
+		RelationshipService::refresh($actor, $target);
 		Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
 		Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
 		Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
 		Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
 		Cache::forget('pf:services:follow:audience:' . $actor);
 		Cache::forget('pf:services:follow:audience:' . $actor);

+ 7 - 0
app/Services/InstanceService.php

@@ -7,6 +7,13 @@ use App\Instance;
 
 
 class InstanceService
 class InstanceService
 {
 {
+	public static function getByDomain($domain)
+	{
+		return Cache::remember('pf:services:instance:by_domain:'.$domain, 3600, function() use($domain) {
+			return Instance::whereDomain($domain)->first();
+		});
+	}
+
 	public static function getBannedDomains()
 	public static function getBannedDomains()
 	{
 	{
 		return Cache::remember('instances:banned:domains', now()->addHours(12), function() {
 		return Cache::remember('instances:banned:domains', now()->addHours(12), function() {

+ 18 - 4
app/Services/NotificationService.php

@@ -27,7 +27,10 @@ class NotificationService {
 			$ids = self::coldGet($id, $start, $stop);
 			$ids = self::coldGet($id, $start, $stop);
 		}
 		}
 		foreach($ids as $id) {
 		foreach($ids as $id) {
-			$res->push(self::getNotification($id));
+			$n = self::getNotification($id);
+			if($n != null) {
+				$res->push($n);
+			}
 		}
 		}
 		return $res;
 		return $res;
 	}
 	}
@@ -56,7 +59,10 @@ class NotificationService {
 
 
 		$res = collect([]);
 		$res = collect([]);
 		foreach($ids as $id) {
 		foreach($ids as $id) {
-			$res->push(self::getNotification($id));
+			$n = self::getNotification($id);
+			if($n != null) {
+				$res->push($n);
+			}
 		}
 		}
 		return $res->toArray();
 		return $res->toArray();
 	}
 	}
@@ -71,7 +77,10 @@ class NotificationService {
 
 
 		$res = collect([]);
 		$res = collect([]);
 		foreach($ids as $id) {
 		foreach($ids as $id) {
-			$res->push(self::getNotification($id));
+			$n = self::getNotification($id);
+			if($n != null) {
+				$res->push($n);
+			}
 		}
 		}
 		return $res->toArray();
 		return $res->toArray();
 	}
 	}
@@ -129,7 +138,12 @@ class NotificationService {
 	public static function getNotification($id)
 	public static function getNotification($id)
 	{
 	{
 		return Cache::remember('service:notification:'.$id, now()->addDays(3), function() use($id) {
 		return Cache::remember('service:notification:'.$id, now()->addDays(3), function() use($id) {
-			$n = Notification::with('item')->findOrFail($id);
+			$n = Notification::with('item')->find($id);
+
+			if(!$n) {
+				return null;
+			}
+
 			$fractal = new Fractal\Manager();
 			$fractal = new Fractal\Manager();
 			$fractal->setSerializer(new ArraySerializer());
 			$fractal->setSerializer(new ArraySerializer());
 			$resource = new Fractal\Resource\Item($n, new NotificationTransformer());
 			$resource = new Fractal\Resource\Item($n, new NotificationTransformer());

+ 86 - 0
app/Services/RelationshipService.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Cache;
+use App\Follower;
+use App\FollowRequest;
+use App\Profile;
+use App\UserFilter;
+
+class RelationshipService
+{
+	const CACHE_KEY = 'pf:services:urel:';
+
+	public static function get($aid, $tid)
+	{
+		$actor = AccountService::get($aid);
+		$target = AccountService::get($tid);
+		if(!$actor || !$target) {
+			return self::defaultRelation($tid);
+		}
+
+		if($actor['id'] === $target['id']) {
+			return self::defaultRelation($tid);
+		}
+
+		return Cache::remember(self::key("a_{$aid}:t_{$tid}"), 1209600, function() use($aid, $tid) {
+			return [
+				'id' => (string) $tid,
+				'following' => Follower::whereProfileId($aid)->whereFollowingId($tid)->exists(),
+				'followed_by' => Follower::whereProfileId($tid)->whereFollowingId($aid)->exists(),
+				'blocking' => UserFilter::whereUserId($aid)
+					->whereFilterableType('App\Profile')
+					->whereFilterableId($tid)
+					->whereFilterType('block')
+					->exists(),
+				'muting' => UserFilter::whereUserId($aid)
+					->whereFilterableType('App\Profile')
+					->whereFilterableId($tid)
+					->whereFilterType('mute')
+					->exists(),
+				'muting_notifications' => null,
+				'requested' => FollowRequest::whereFollowerId($aid)
+					->whereFollowingId($tid)
+					->exists(),
+				'domain_blocking' => null,
+				'showing_reblogs' => null,
+				'endorsed' => false
+			];
+		});
+	}
+
+	public static function delete($aid, $tid)
+	{
+		return Cache::forget(self::key("a_{$aid}:t_{$tid}"));
+	}
+
+	public static function refresh($aid, $tid)
+	{
+		self::delete($tid, $aid);
+		self::delete($aid, $tid);
+		self::get($tid, $aid);
+		return self::get($aid, $tid);
+	}
+
+	public static function defaultRelation($tid)
+	{
+		return [
+            'id' => (string) $tid,
+            'following' => false,
+            'followed_by' => false,
+            'blocking' => false,
+            'muting' => false,
+            'muting_notifications' => null,
+            'requested' => false,
+            'domain_blocking' => null,
+            'showing_reblogs' => null,
+            'endorsed' => false
+        ];
+	}
+
+	protected static function key($suffix)
+	{
+		return self::CACHE_KEY . $suffix;
+	}
+}

+ 7 - 0
app/Services/StatusService.php

@@ -40,6 +40,13 @@ class StatusService {
 		});
 		});
 	}
 	}
 
 
+	public static function getFull($id, $pid, $publicOnly = true)
+	{
+		$res = self::get($id, $publicOnly);
+		$res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
+		return $res;
+	}
+
 	public static function del($id)
 	public static function del($id)
 	{
 	{
 		$status = self::get($id);
 		$status = self::get($id);

+ 1 - 1
app/Transformer/ActivityPub/Verb/Note.php

@@ -35,7 +35,7 @@ class Note extends Fractal\TransformerAbstract
 					'href' => $parent->permalink(),
 					'href' => $parent->permalink(),
 					'name' => $name
 					'name' => $name
 				];
 				];
-				$mentions = array_merge($reply, $mentions);
+				array_push($mentions, $reply);
 			}
 			}
 		}
 		}
 		
 		

+ 38 - 72
app/Transformer/Api/NotificationTransformer.php

@@ -2,50 +2,51 @@
 
 
 namespace App\Transformer\Api;
 namespace App\Transformer\Api;
 
 
-use App\{
-	Notification,
-	Status
-};
+use App\Notification;
+use App\Services\AccountService;
 use App\Services\HashidService;
 use App\Services\HashidService;
+use App\Services\RelationshipService;
+use App\Services\StatusService;
 use League\Fractal;
 use League\Fractal;
 
 
 class NotificationTransformer extends Fractal\TransformerAbstract
 class NotificationTransformer extends Fractal\TransformerAbstract
 {
 {
-	protected $defaultIncludes = [
-		'account',
-		'status',
-		'relationship',
-		'modlog',
-		'tagged'
-	];
-
 	public function transform(Notification $notification)
 	public function transform(Notification $notification)
 	{
 	{
-		return [
+		$res = [
 			'id'       		=> (string) $notification->id,
 			'id'       		=> (string) $notification->id,
 			'type'       	=> $this->replaceTypeVerb($notification->action),
 			'type'       	=> $this->replaceTypeVerb($notification->action),
 			'created_at' 	=> (string) $notification->created_at->format('c'),
 			'created_at' 	=> (string) $notification->created_at->format('c'),
 		];
 		];
-	}
 
 
-	public function includeAccount(Notification $notification)
-	{
-		return $this->item($notification->actor, new AccountTransformer());
-	}
+		$n = $notification;
 
 
-	public function includeStatus(Notification $notification)
-	{
-		$item = $notification;
-		if($item->item_id && $item->item_type == 'App\Status') {
-			$status = Status::with('media')->find($item->item_id);
-			if($status) {
-				return $this->item($status, new StatusTransformer());
-			} else {
-				return null;
-			}
-		} else {
-			return null;
+		if($n->actor_id) {
+			$res['account'] = AccountService::get($n->actor_id);
+			$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
+		}
+
+		if($n->item_id && $n->item_type == 'App\Status') {
+			$res['status'] = StatusService::get($n->item_id, false);
 		}
 		}
+
+		if($n->item_id && $n->item_type == 'App\ModLog') {
+			$ml = $n->item;
+			$res['modlog'] = [
+				'id' => $ml->object_uid,
+				'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
+			];
+		}
+
+		if($n->item_id && $n->item_type == 'App\MediaTag') {
+			$ml = $n->item;
+			$res['tagged'] = [
+				'username' => $ml->tagged_username,
+				'post_url' => '/p/'.HashidService::encode($ml->status_id)
+			];
+		}
+
+		return $res;
 	}
 	}
 
 
 	public function replaceTypeVerb($verb)
 	public function replaceTypeVerb($verb)
@@ -57,56 +58,21 @@ class NotificationTransformer extends Fractal\TransformerAbstract
 			'reblog' => 'share',
 			'reblog' => 'share',
 			'share' => 'share',
 			'share' => 'share',
 			'like' => 'favourite',
 			'like' => 'favourite',
+			'group:like' => 'favourite',
 			'comment' => 'comment',
 			'comment' => 'comment',
 			'admin.user.modlog.comment' => 'modlog',
 			'admin.user.modlog.comment' => 'modlog',
 			'tagged' => 'tagged',
 			'tagged' => 'tagged',
 			'group:comment' => 'group:comment',
 			'group:comment' => 'group:comment',
 			'story:react' => 'story:react',
 			'story:react' => 'story:react',
-			'story:comment' => 'story:comment'
+			'story:comment' => 'story:comment',
+			'group:join:approved' => 'group:join:approved',
+			'group:join:rejected' => 'group:join:rejected'
 		];
 		];
-		return $verbs[$verb];
-	}
-
-	public function includeRelationship(Notification $notification)
-	{
-		return $this->item($notification->actor, new RelationshipTransformer());
-	}
 
 
-	public function includeModlog(Notification $notification)
-	{
-		$n = $notification;
-		if($n->item_id && $n->item_type == 'App\ModLog') {
-			$ml = $n->item;
-			if(!empty($ml)) {
-				$res = $this->item($ml, function($ml) {
-					return [
-						'id' => $ml->object_uid,
-						'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
-					];
-				});
-				return $res;
-			} else {
-				return null;
-			}
-		} else {
-			return null;
+		if(!isset($verbs[$verb])) {
+			return $verb;
 		}
 		}
-	}
 
 
-	public function includeTagged(Notification $notification)
-	{
-		$n = $notification;
-		if($n->item_id && $n->item_type == 'App\MediaTag') {
-			$ml = $n->item;
-			$res = $this->item($ml, function($ml) {
-				return [
-					'username' => $ml->tagged_username,
-					'post_url' => '/p/'.HashidService::encode($ml->status_id)
-				];
-			});
-			return $res;
-		} else {
-			return null;
-		}
+		return $verbs[$verb];
 	}
 	}
 }
 }

+ 1 - 2
app/Util/ActivityPub/HttpSignature.php

@@ -43,7 +43,7 @@ class HttpSignature {
       $digest = self::_digest($body);
       $digest = self::_digest($body);
     }
     }
     $headers = self::_headersToSign($url, $body ? $digest : false);
     $headers = self::_headersToSign($url, $body ? $digest : false);
-    $headers = array_unique(array_merge($headers, $addlHeaders));
+    $headers = array_merge($headers, $addlHeaders);
     $stringToSign = self::_headersToSigningString($headers);
     $stringToSign = self::_headersToSigningString($headers);
     $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
     $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
     $key = openssl_pkey_get_private($privateKey);
     $key = openssl_pkey_get_private($privateKey);
@@ -133,7 +133,6 @@ class HttpSignature {
       'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
       'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
       'Host' => parse_url($url, PHP_URL_HOST),
       'Host' => parse_url($url, PHP_URL_HOST),
       'Accept' => 'application/activity+json, application/json',
       'Accept' => 'application/activity+json, application/json',
-      'Content-Type' => 'application/activity+json'
     ];
     ];
 
 
     if($digest) {
     if($digest) {

+ 3 - 0
app/Util/ActivityPub/Inbox.php

@@ -455,6 +455,7 @@ class Inbox
 			Cache::forget('profile:follower_count:'.$actor->id);
 			Cache::forget('profile:follower_count:'.$actor->id);
 			Cache::forget('profile:following_count:'.$target->id);
 			Cache::forget('profile:following_count:'.$target->id);
 			Cache::forget('profile:following_count:'.$actor->id);
 			Cache::forget('profile:following_count:'.$actor->id);
+			FollowerService::add($actor->id, $target->id);
 
 
 		} else {
 		} else {
 			$follower = new Follower;
 			$follower = new Follower;
@@ -464,6 +465,7 @@ class Inbox
 			$follower->save();
 			$follower->save();
 
 
 			FollowPipeline::dispatch($follower);
 			FollowPipeline::dispatch($follower);
+			FollowerService::add($actor->id, $target->id);
 
 
 			// send Accept to remote profile
 			// send Accept to remote profile
 			$accept = [
 			$accept = [
@@ -722,6 +724,7 @@ class Inbox
 					->whereItemId($following->id)
 					->whereItemId($following->id)
 					->whereItemType('App\Profile')
 					->whereItemType('App\Profile')
 					->forceDelete();
 					->forceDelete();
+				FollowerService::remove($profile->id, $following->id);
 				break;
 				break;
 
 
 			case 'Like':
 			case 'Like':

+ 18 - 23
app/Util/Site/Nodeinfo.php

@@ -10,34 +10,29 @@ class Nodeinfo {
 
 
 	public static function get()
 	public static function get()
 	{
 	{
-		$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
-			$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
-				// todo: replace with last_active_at after July 9, 2021 (96afc3e781)
-				$count = collect([]);
-				$likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
-				$count = $count->merge($likes);
-				$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
-				$count = $count->merge($statuses);
-				$profiles = User::select('profile_id', 'last_active_at')
-					->whereNotNull('last_active_at')
+		$res = Cache::remember('api:nodeinfo', 300, function () {
+			$activeHalfYear = Cache::remember('api:nodeinfo:ahy', 172800, function() {
+				return User::select('last_active_at')
 					->where('last_active_at', '>', now()->subMonths(6))
 					->where('last_active_at', '>', now()->subMonths(6))
-					->pluck('profile_id')
-					->toArray();
-				$newProfiles = User::select('profile_id', 'last_active_at', 'created_at')
-					->whereNull('last_active_at')
-					->where('created_at', '>', now()->subMonths(6))
-					->pluck('profile_id')
-					->toArray();
-				$count = $count->merge($newProfiles);
-				$count = $count->merge($profiles);
-				return $count->unique()->count();
+					->orWhere('created_at', '>', now()->subMonths(6))
+					->count();
 			});
 			});
-			$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
+
+			$activeMonth = Cache::remember('api:nodeinfo:am', 172800, function() {
 				return User::select('last_active_at')
 				return User::select('last_active_at')
 					->where('last_active_at', '>', now()->subMonths(1))
 					->where('last_active_at', '>', now()->subMonths(1))
 					->orWhere('created_at', '>', now()->subMonths(1))
 					->orWhere('created_at', '>', now()->subMonths(1))
 					->count();
 					->count();
 			});
 			});
+
+			$users = Cache::remember('api:nodeinfo:users', 43200, function() {
+				return User::count();
+			});
+
+			$statuses = Cache::remember('api:nodeinfo:statuses', 21600, function() {
+				return Status::whereLocal(true)->count();
+			});
+
 			return [
 			return [
 				'metadata' => [
 				'metadata' => [
 					'nodeName' => config_cache('app.name'),
 					'nodeName' => config_cache('app.name'),
@@ -59,10 +54,10 @@ class Nodeinfo {
 					'version'       => config('pixelfed.version'),
 					'version'       => config('pixelfed.version'),
 				],
 				],
 				'usage' => [
 				'usage' => [
-					'localPosts'    => Status::whereLocal(true)->count(),
+					'localPosts'    => $statuses,
 					'localComments' => 0,
 					'localComments' => 0,
 					'users'         => [
 					'users'         => [
-						'total'          => User::count(),
+						'total'          => $users,
 						'activeHalfyear' => (int) $activeHalfYear,
 						'activeHalfyear' => (int) $activeHalfYear,
 						'activeMonth'    => (int) $activeMonth,
 						'activeMonth'    => (int) $activeMonth,
 					],
 					],

文件差異過大導致無法顯示
+ 326 - 140
composer.lock


+ 1 - 0
config/auth.php

@@ -96,6 +96,7 @@ return [
             'provider' => 'users',
             'provider' => 'users',
             'table'    => 'password_resets',
             'table'    => 'password_resets',
             'expire'   => 60,
             'expire'   => 60,
+            'throttle' => 60,
         ],
         ],
     ],
     ],
 
 

二進制
public/js/activity.js


二進制
public/js/admin.js


二進制
public/js/app.js


二進制
public/js/components.js


二進制
public/js/direct.js


二進制
public/js/memoryprofile.js


二進制
public/js/profile.js


二進制
public/js/rempos.js


二進制
public/js/rempro.js


二進制
public/js/status.js


二進制
public/js/theme-monokai.js


二進制
public/js/timeline.js


二進制
public/js/vendor.js


二進制
public/mix-manifest.json


+ 9 - 8
resources/assets/js/app.js

@@ -103,31 +103,31 @@ window.App.util = {
 			}
 			}
 			return Math.floor(seconds) + "s";
 			return Math.floor(seconds) + "s";
 		}),
 		}),
-		timeAhead: (function(ts) {
+		timeAhead: (function(ts, short = true) {
 			let date = Date.parse(ts);
 			let date = Date.parse(ts);
 			let diff = date - Date.parse(new Date());
 			let diff = date - Date.parse(new Date());
 			let seconds = Math.floor((diff) / 1000);
 			let seconds = Math.floor((diff) / 1000);
 			let interval = Math.floor(seconds / 63072000);
 			let interval = Math.floor(seconds / 63072000);
 			if (interval >= 1) {
 			if (interval >= 1) {
-				return interval + "y";
+				return interval + (short ? "y" : " years");
 			}
 			}
 			interval = Math.floor(seconds / 604800);
 			interval = Math.floor(seconds / 604800);
 			if (interval >= 1) {
 			if (interval >= 1) {
-				return interval + "w";
+				return interval + (short ? "w" : " weeks");
 			}
 			}
 			interval = Math.floor(seconds / 86400);
 			interval = Math.floor(seconds / 86400);
 			if (interval >= 1) {
 			if (interval >= 1) {
-				return interval + "d";
+				return interval + (short ? "d" : " days");
 			}
 			}
 			interval = Math.floor(seconds / 3600);
 			interval = Math.floor(seconds / 3600);
 			if (interval >= 1) {
 			if (interval >= 1) {
-				return interval + "h";
+				return interval + (short ? "h" : " hours");
 			}
 			}
 			interval = Math.floor(seconds / 60);
 			interval = Math.floor(seconds / 60);
 			if (interval >= 1) {
 			if (interval >= 1) {
-				return interval + "m";
+				return interval + (short ? "m" : " minutes");
 			}
 			}
-			return Math.floor(seconds) + "s";
+			return Math.floor(seconds) + (short ? "s" : " seconds");
 		}),
 		}),
 		rewriteLinks: (function(i) {
 		rewriteLinks: (function(i) {
 
 
@@ -234,7 +234,8 @@ window.App.util = {
 		'filter-willow': 'brightness(1.2) contrast(.85) saturate(.05) sepia(.2)',
 		'filter-willow': 'brightness(1.2) contrast(.85) saturate(.05) sepia(.2)',
 		'filter-xpro-ii': 'sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)'
 		'filter-xpro-ii': 'sepia(.45) contrast(1.25) brightness(1.75) saturate(1.3) hue-rotate(-5deg)'
 	},
 	},
-	emoji: ['😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
+	emoji: [
+		'😂','💯','❤️','🙌','👏','👌','😍','😯','😢','😅','😁','🙂','😎','😀','🤣','😃','😄','😆','😉','😊','😋','😘','😗','😙','😚','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','🙁','😖','😞','😟','😤','😭','😦','😧','😨','😩','🤯','😬','😰','😱','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾','🤲','👐','🤝','👍','👎','👊','✊','🤛','🤜','🤞','✌️','🤟','🤘','👈','👉','👆','👇','☝️','✋','🤚','🖐','🖖','👋','🤙','💪','🖕','✍️','🙏','💍','💄','💋','👄','👅','👂','👃','👣','👁','👀','🧠','🗣','👤','👥'
 	],
 	],
 	embed: {
 	embed: {
 		post: (function(url, caption = true, likes = false, layout = 'full') {
 		post: (function(url, caption = true, likes = false, layout = 'full') {

+ 13 - 6
resources/assets/js/components/Activity.vue

@@ -22,56 +22,60 @@
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'comment'">
 							<div v-else-if="n.type == 'comment'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'group:comment'">
 							<div v-else-if="n.type == 'group:comment'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.group_post_url">group post</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.group_post_url">group post</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'story:react'">
 							<div v-else-if="n.type == 'story:react'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> reacted to your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> reacted to your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'story:comment'">
 							<div v-else-if="n.type == 'story:comment'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">story</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'mention'">
 							<div v-else-if="n.type == 'mention'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'follow'">
 							<div v-else-if="n.type == 'follow'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> followed you.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> followed you.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'share'">
 							<div v-else-if="n.type == 'share'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="getPostUrl(n.status)">post</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'modlog'">
 							<div v-else-if="n.type == 'modlog'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> updated a <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{truncate(n.account.username)}}</a> updated a <a class="font-weight-bold" v-bind:href="n.modlog.url">modlog</a>.
 								</p>
 								</p>
 							</div>
 							</div>
+
 							<div v-else-if="n.type == 'tagged'">
 							<div v-else-if="n.type == 'tagged'">
 								<p class="my-0">
 								<p class="my-0">
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
 								</p>
 								</p>
 							</div>
 							</div>
-							<div v-else-if="n.type == 'direct'">
-							<p class="my-0">
-								<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
-							</p>
-						</div>
 							<div class="align-items-center">
 							<div class="align-items-center">
 								<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
 								<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
 							</div>
 							</div>
@@ -105,7 +109,7 @@
 								</a>
 								</a>
 							</div> -->
 							</div> -->
 							<div v-else>
 							<div v-else>
-								<a class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
+								<a v-if="viewContext(n) != '/'" class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
 							</div>
 							</div>
 						</div>
 						</div>
 					</div>
 					</div>
@@ -209,6 +213,9 @@ export default {
 						}
 						}
 						return true;
 						return true;
 					});
 					});
+
+					let ids = data.map(n => n.id);
+					this.notificationMaxId = Math.max(...ids);
 					this.notifications.push(...data);
 					this.notifications.push(...data);
 					this.notificationCursor++;
 					this.notificationCursor++;
 					$state.loaded();
 					$state.loaded();

+ 7 - 7
resources/assets/js/components/RemotePost.vue

@@ -35,7 +35,7 @@
 				<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
 				<div class="d-flex d-md-none align-items-center justify-content-between card-header bg-white w-100">
 					<div class="d-flex">
 					<div class="d-flex">
 						<div class="status-avatar mr-2" @click="redirect(profileUrl)">
 						<div class="status-avatar mr-2" @click="redirect(profileUrl)">
-							<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
+							<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 						</div>
 						</div>
 						<div class="username">
 						<div class="username">
 							<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
 							<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
@@ -94,7 +94,7 @@
 						<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
 						<div class="d-md-flex d-none align-items-center justify-content-between card-header py-3 bg-white">
 							<div class="d-flex align-items-center status-username text-truncate">
 							<div class="d-flex align-items-center status-username text-truncate">
 								<div class="status-avatar mr-2" @click="redirect(profileUrl)">
 								<div class="status-avatar mr-2" @click="redirect(profileUrl)">
-									<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer">
+									<img :src="statusAvatar" width="24px" height="24px" style="border-radius:12px;" class="cursor-pointer" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 								</div>
 								</div>
 								<div class="username">
 								<div class="username">
 									<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
 									<span class="username-link font-weight-bold text-dark cursor-pointer" @click="redirect(profileUrl)">{{ statusUsername }}</span>
@@ -157,7 +157,7 @@
 											</p>
 											</p>
 											<div class="comments mt-3">
 											<div class="comments mt-3">
 												<div v-for="(reply, index) in results" class="pb-4 media" :key="'tl' + reply.id + '_' + index">
 												<div v-for="(reply, index) in results" class="pb-4 media" :key="'tl' + reply.id + '_' + index">
-													<img :src="reply.account.avatar" class="rounded-circle border mr-3" width="42px" height="42px">
+													<img :src="reply.account.avatar" class="rounded-circle border mr-3" width="42px" height="42px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 													<div class="media-body">
 													<div class="media-body">
 														<div v-if="reply.sensitive == true">
 														<div v-if="reply.sensitive == true">
 															<span class="py-3">
 															<span class="py-3">
@@ -190,7 +190,7 @@
 															</div>
 															</div>
 															<div v-if="reply.thread == true" class="comment-thread">
 															<div v-if="reply.thread == true" class="comment-thread">
 																<div v-for="(s, sindex) in reply.replies" class="pb-3 media" :key="'cr' + s.id + '_' + index">
 																<div v-for="(s, sindex) in reply.replies" class="pb-3 media" :key="'cr' + s.id + '_' + index">
-																	<img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px">
+																	<img :src="s.account.avatar" class="rounded-circle border mr-3" width="25px" height="25px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 																	<div class="media-body">
 																	<div class="media-body">
 																		<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
 																		<p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
 																			<span>
 																			<span>
@@ -315,7 +315,7 @@
 			<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
 			<div class="list-group-item border-0 py-1" v-for="(user, index) in likes" :key="'modal_likes_'+index">
 				<div class="media">
 				<div class="media">
 					<a :href="user.url">
 					<a :href="user.url">
-						<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
+						<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 					</a>
 					</a>
 					<div class="media-body">
 					<div class="media-body">
 						<p class="mb-0" style="font-size: 14px">
 						<p class="mb-0" style="font-size: 14px">
@@ -348,7 +348,7 @@
 			<div class="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
 			<div class="list-group-item border-0 py-1" v-for="(user, index) in shares" :key="'modal_shares_'+index">
 				<div class="media">
 				<div class="media">
 					<a :href="user.url">
 					<a :href="user.url">
-						<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px">
+						<img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 					</a>
 					</a>
 					<div class="media-body">
 					<div class="media-body">
 						<div class="d-inline-block">
 						<div class="d-inline-block">
@@ -382,7 +382,7 @@
 			<div class="list-group-item border-0 py-1" v-for="(taguser, index) in status.taggedPeople" :key="'modal_taggedpeople_'+index">
 			<div class="list-group-item border-0 py-1" v-for="(taguser, index) in status.taggedPeople" :key="'modal_taggedpeople_'+index">
 				<div class="media">
 				<div class="media">
 					<a :href="'/'+taguser.username">
 					<a :href="'/'+taguser.username">
-						<img class="mr-3 rounded-circle box-shadow" :src="taguser.avatar" :alt="taguser.username + '’s avatar'" width="30px">
+						<img class="mr-3 rounded-circle box-shadow" :src="taguser.avatar" :alt="taguser.username + '’s avatar'" width="30px" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 					</a>
 					</a>
 					<div class="media-body">
 					<div class="media-body">
 						<p class="pt-1 d-flex justify-content-between" style="font-size: 14px">
 						<p class="pt-1 d-flex justify-content-between" style="font-size: 14px">

+ 1 - 1
resources/assets/js/components/RemoteProfile.vue

@@ -19,7 +19,7 @@
 					</div>
 					</div>
 					<div class="card-body pb-0">
 					<div class="card-body pb-0">
 						<div class="mt-n5 mb-3">
 						<div class="mt-n5 mb-3">
-							<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;">
+							<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
 							<span class="float-right mt-n1">
 							<span class="float-right mt-n1">
 								<span>
 								<span>
 									<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>
 									<button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>

+ 26 - 2
resources/assets/js/components/Timeline.vue

@@ -12,7 +12,7 @@
 					<div style="margin-top:-2px;">
 					<div style="margin-top:-2px;">
 						<story-component v-if="config.features.stories" :scope="scope"></story-component>
 						<story-component v-if="config.features.stories" :scope="scope"></story-component>
 					</div>
 					</div>
-					<div>
+					<div class="pt-4">
 						<div v-if="loading" class="text-center" style="padding-top:10px;">
 						<div v-if="loading" class="text-center" style="padding-top:10px;">
 							<div class="spinner-border" role="status">
 							<div class="spinner-border" role="status">
 								<span class="sr-only">Loading...</span>
 								<span class="sr-only">Loading...</span>
@@ -106,6 +106,8 @@
 								size="small"
 								size="small"
 								v-on:status-delete="deleteStatus"
 								v-on:status-delete="deleteStatus"
 								v-on:comment-focus="commentFocus"
 								v-on:comment-focus="commentFocus"
+								v-on:followed="followedAccount"
+								v-on:unfollowed="unfollowedAccount"
 							/>
 							/>
 						</div>
 						</div>
 
 
@@ -1067,7 +1069,29 @@
 				this.feed = this.feed.filter(s => {
 				this.feed = this.feed.filter(s => {
 					return s.id != status;
 					return s.id != status;
 				});
 				});
-			}
+			},
+
+			followedAccount(id) {
+				this.feed = this.feed.map(s => {
+					if(s.account.id == id) {
+						if(s.hasOwnProperty('relationship') && s.relationship.following == false) {
+							s.relationship.following = true;
+						}
+					}
+					return s;
+				});
+			},
+
+			unfollowedAccount(id) {
+				this.feed = this.feed.map(s => {
+					if(s.account.id == id) {
+						if(s.hasOwnProperty('relationship') && s.relationship.following == true) {
+							s.relationship.following = false;
+						}
+					}
+					return s;
+				});
+			},
 		},
 		},
 
 
 		beforeDestroy () {
 		beforeDestroy () {

+ 54 - 0
resources/assets/js/components/partials/StatusCard.vue

@@ -71,6 +71,14 @@
 						<a v-if="status.place" class="small text-decoration-none text-muted" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
 						<a v-if="status.place" class="small text-decoration-none text-muted" :href="'/discover/places/'+status.place.id+'/'+status.place.slug" title="Location" data-toggle="tooltip"><i class="fas fa-map-marked-alt"></i> {{status.place.name}}, {{status.place.country}}</a>
 					</div>
 					</div>
 				</div>
 				</div>
+				<div v-if="canFollow(status)">
+					<span class="px-2"></span>
+					<button class="btn btn-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="follow(status.account.id)"><i class="far fa-user-plus mr-1"></i> Follow</button>
+				</div>
+				<div v-if="status.hasOwnProperty('relationship') && status.relationship.hasOwnProperty('following') && status.relationship.following">
+					<span class="px-2"></span>
+					<button class="btn btn-outline-primary btn-sm font-weight-bold py-1 px-3 rounded-lg" @click="unfollow(status.account.id)"><i class="far fa-user-check mr-1"></i> Following</button>
+				</div>
 				<div class="text-right" style="flex-grow:1;">
 				<div class="text-right" style="flex-grow:1;">
 					<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
 					<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
 						<span class="fas fa-ellipsis-h text-lighter"></span>
 						<span class="fas fa-ellipsis-h text-lighter"></span>
@@ -382,6 +390,52 @@
 
 
 			statusDeleted(status) {
 			statusDeleted(status) {
 				this.$emit('status-delete', status);
 				this.$emit('status-delete', status);
+			},
+
+			canFollow(status) {
+				if(!status.hasOwnProperty('relationship')) {
+					return false;
+				}
+
+				if(!status.hasOwnProperty('account') || !status.account.hasOwnProperty('id')) {
+					return false;
+				}
+
+				if(status.account.id === this.profile.id) {
+					return false;
+				}
+
+				return !status.relationship.following;
+			},
+
+			follow(id) {
+				event.currentTarget.blur();
+
+				axios.post('/i/follow', {
+					item: id
+				}).then(res => {
+					this.status.relationship.following = true;
+					this.$emit('followed', id);
+				}).catch(err => {
+					if(err.response.data.message) {
+						swal('Error', err.response.data.message, 'error');
+					}
+				});
+			},
+
+			unfollow(id) {
+				event.currentTarget.blur();
+
+				axios.post('/i/follow', {
+					item: id
+				}).then(res => {
+					this.status.relationship.following = false;
+					this.$emit('unfollowed', id);
+				}).catch(err => {
+					if(err.response.data.message) {
+						swal('Error', err.response.data.message, 'error');
+					}
+				});
 			}
 			}
 		}
 		}
 	}
 	}

+ 2 - 2
resources/assets/js/components/presenter/MixedAlbumPresenter.vue

@@ -14,7 +14,7 @@
 			>
 			>
 				<b-carousel-slide v-for="(media, index) in status.media_attachments" :key="media.id + '-media'">
 				<b-carousel-slide v-for="(media, index) in status.media_attachments" :key="media.id + '-media'">
 
 
-					<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
+					<video v-if="media.type == 'Video'" slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="media.description" width="100%" height="100%" :poster="media.preview_url">
 						<source :src="media.url" :type="media.mime">
 						<source :src="media.url" :type="media.mime">
 					</video>
 					</video>
 
 
@@ -72,4 +72,4 @@
 	export default {
 	export default {
 		props: ['status']
 		props: ['status']
 	}
 	}
-</script>
+</script>

+ 3 - 3
resources/assets/js/components/presenter/VideoAlbumPresenter.vue

@@ -13,7 +13,7 @@
 				:interval="0"
 				:interval="0"
 			>
 			>
 				<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
 				<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
-					<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
+					<video slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
 						<source :src="vid.url" :type="vid.mime">
 						<source :src="vid.url" :type="vid.mime">
 					</video>
 					</video>
 				</b-carousel-slide>
 				</b-carousel-slide>
@@ -29,7 +29,7 @@
 			:interval="0"
 			:interval="0"
 		>
 		>
 			<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
 			<b-carousel-slide v-for="(vid, index) in status.media_attachments" :key="vid.id + '-media'">
-				<video slot="img" class="embed-responsive-item" preload="none" controls loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
+				<video slot="img" class="embed-responsive-item" preload="none" controls playsinline loop :alt="vid.description" width="100%" height="100%" :poster="vid.preview_url">
 					<source :src="vid.url" :type="vid.mime">
 					<source :src="vid.url" :type="vid.mime">
 				</video>
 				</video>
 			</b-carousel-slide>
 			</b-carousel-slide>
@@ -41,4 +41,4 @@
 	export default {
 	export default {
 		props: ['status']
 		props: ['status']
 	}
 	}
-</script>
+</script>

+ 1 - 1
resources/assets/js/components/presenter/VideoPresenter.vue

@@ -22,7 +22,7 @@
 			:alt="altText(status)"/>
 			:alt="altText(status)"/>
 	</div>
 	</div>
 	<div v-else class="embed-responsive embed-responsive-16by9">
 	<div v-else class="embed-responsive embed-responsive-16by9">
-		<video class="video" controls preload="metadata" loop :poster="status.media_attachments[0].preview_url" :data-id="status.id">
+		<video class="video" controls playsinline preload="metadata" loop :poster="status.media_attachments[0].preview_url" :data-id="status.id">
 			<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
 			<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
 		</video>
 		</video>
 	</div>
 	</div>

+ 0 - 2
resources/lang/cs/site.php

@@ -1,7 +1,6 @@
 <?php
 <?php
 
 
 return [
 return [
-
     'about'             => 'O nás',
     'about'             => 'O nás',
     'help'              => 'Nápověda',
     'help'              => 'Nápověda',
     'language'          => 'Jazyk',
     'language'          => 'Jazyk',
@@ -16,5 +15,4 @@ return [
     'contact-us'        => 'Kontaktujte nás',
     'contact-us'        => 'Kontaktujte nás',
     'places'            => 'Místa',
     'places'            => 'Místa',
     'profiles'          => 'Profily',
     'profiles'          => 'Profily',
-
 ];
 ];

+ 11 - 0
resources/lang/de/exception.php

@@ -0,0 +1,11 @@
+<?php
+
+return [
+
+	'compose' => [
+		'invalid' => [
+			'album' => 'Mindestens 1 Foto oder Video muss enthalten sein.',
+		],
+	],
+
+];

+ 1 - 2
resources/lang/de/navmenu.php

@@ -1,7 +1,6 @@
 <?php
 <?php
 
 
 return [
 return [
-
     'search'         => 'Suche',
     'search'         => 'Suche',
     'home'           => 'Heim',
     'home'           => 'Heim',
     'local'          => 'Lokal',
     'local'          => 'Lokal',
@@ -16,5 +15,5 @@ return [
     'admin'          => 'Administration',
     'admin'          => 'Administration',
     'logout'         => 'Abmelden',
     'logout'         => 'Abmelden',
     'directMessages' => 'Privatnachrichten',
     'directMessages' => 'Privatnachrichten',
-
+    'composePost'	 => 'Neu',
 ];
 ];

+ 1 - 1
resources/lang/de/site.php

@@ -15,5 +15,5 @@ return [
 	'contact'		=> 'Kontakt',
 	'contact'		=> 'Kontakt',
 	'contact-us'	=> 'Kontaktiere uns',
 	'contact-us'	=> 'Kontaktiere uns',
 	'places'		=> 'Orte',
 	'places'		=> 'Orte',
-
+	'profiles'		=> 'Profile',
 ];
 ];

+ 6 - 0
routes/api.php

@@ -11,6 +11,12 @@ Route::post('i/actor/inbox', 'InstanceActorController@inbox');
 Route::get('i/actor/outbox', 'InstanceActorController@outbox');
 Route::get('i/actor/outbox', 'InstanceActorController@outbox');
 Route::get('/stories/{username}/{id}', 'StoryController@getActivityObject');
 Route::get('/stories/{username}/{id}', 'StoryController@getActivityObject');
 
 
+Route::get('.well-known/webfinger', 'FederationController@webfinger')->name('well-known.webfinger');
+Route::get('.well-known/nodeinfo', 'FederationController@nodeinfoWellKnown')->name('well-known.nodeinfo');
+Route::get('.well-known/host-meta', 'FederationController@hostMeta')->name('well-known.hostMeta');
+Route::redirect('.well-known/change-password', '/settings/password');
+Route::get('api/nodeinfo/2.0.json', 'FederationController@nodeinfo');
+
 Route::group(['prefix' => 'api'], function() use($middleware) {
 Route::group(['prefix' => 'api'], function() use($middleware) {
 
 
 	Route::group(['prefix' => 'v1'], function() use($middleware) {
 	Route::group(['prefix' => 'v1'], function() use($middleware) {

+ 0 - 7
routes/web.php

@@ -90,11 +90,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
 
 	Auth::routes();
 	Auth::routes();
 
 
-	Route::get('.well-known/webfinger', 'FederationController@webfinger')->name('well-known.webfinger');
-	Route::get('.well-known/nodeinfo', 'FederationController@nodeinfoWellKnown')->name('well-known.nodeinfo');
-	Route::get('.well-known/host-meta', 'FederationController@hostMeta')->name('well-known.hostMeta');
-	Route::redirect('.well-known/change-password', '/settings/password');
-
 	Route::get('/home', 'HomeController@index')->name('home');
 	Route::get('/home', 'HomeController@index')->name('home');
 
 
 	Route::get('discover/c/{slug}', 'DiscoverController@showCategory');
 	Route::get('discover/c/{slug}', 'DiscoverController@showCategory');
@@ -105,7 +100,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
 
 	Route::group(['prefix' => 'api'], function () {
 	Route::group(['prefix' => 'api'], function () {
 		Route::get('search', 'SearchController@searchAPI');
 		Route::get('search', 'SearchController@searchAPI');
-		Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
 		Route::post('status/view', 'StatusController@storeView');
 		Route::post('status/view', 'StatusController@storeView');
 		Route::get('v1/polls/{id}', 'PollController@getPoll');
 		Route::get('v1/polls/{id}', 'PollController@getPoll');
 		Route::post('v1/polls/{id}/votes', 'PollController@vote');
 		Route::post('v1/polls/{id}/votes', 'PollController@vote');
@@ -251,7 +245,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 			Route::post('v1/publish', 'StoryController@publishStory');
 			Route::post('v1/publish', 'StoryController@publishStory');
 			Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
 			Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete');
 		});
 		});
-
 	});
 	});
 
 
 	Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');
 	Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags');

部分文件因文件數量過多而無法顯示