1
0
Эх сурвалжийг харах

Update activitpub setting, use config_cache()

Daniel Supernault 1 жил өмнө
parent
commit
5071aaf408

+ 87 - 91
app/Http/Controllers/FederationController.php

@@ -2,57 +2,42 @@
 
 namespace App\Http\Controllers;
 
-use App\Jobs\InboxPipeline\{
-    DeleteWorker,
-    InboxWorker,
-    InboxValidator
-};
-use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
-use App\{
-    AccountLog,
-    Like,
-    Profile,
-    Status,
-    User
-};
+use App\Jobs\InboxPipeline\DeleteWorker;
+use App\Jobs\InboxPipeline\InboxValidator;
+use App\Jobs\InboxPipeline\InboxWorker;
+use App\Profile;
+use App\Services\AccountService;
+use App\Services\InstanceService;
+use App\Status;
 use App\Util\Lexer\Nickname;
+use App\Util\Site\Nodeinfo;
 use App\Util\Webfinger\Webfinger;
-use Auth;
 use Cache;
-use Carbon\Carbon;
 use Illuminate\Http\Request;
-use League\Fractal;
-use App\Util\Site\Nodeinfo;
-use App\Util\ActivityPub\{
-    Helpers,
-    HttpSignature,
-    Outbox
-};
-use Zttp\Zttp;
-use App\Services\InstanceService;
-use App\Services\AccountService;
 
 class FederationController extends Controller
 {
     public function nodeinfoWellKnown()
     {
-        abort_if(!config('federation.nodeinfo.enabled'), 404);
+        abort_if(! config('federation.nodeinfo.enabled'), 404);
+
         return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function nodeinfo()
     {
-        abort_if(!config('federation.nodeinfo.enabled'), 404);
+        abort_if(! config('federation.nodeinfo.enabled'), 404);
+
         return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function webfinger(Request $request)
     {
-        if (!config('federation.webfinger.enabled') ||
-            !$request->has('resource') ||
-            !$request->filled('resource')
+        if (! config('federation.webfinger.enabled') ||
+            ! $request->has('resource') ||
+            ! $request->filled('resource')
         ) {
             return response('', 400);
         }
@@ -60,55 +45,56 @@ class FederationController extends Controller
         $resource = $request->input('resource');
         $domain = config('pixelfed.domain.app');
 
-        if(config('federation.activitypub.sharedInbox') &&
-            $resource == 'acct:' . $domain . '@' . $domain) {
+        if (config('federation.activitypub.sharedInbox') &&
+            $resource == 'acct:'.$domain.'@'.$domain) {
             $res = [
-                'subject' => 'acct:' . $domain . '@' . $domain,
+                'subject' => 'acct:'.$domain.'@'.$domain,
                 'aliases' => [
-                    'https://' . $domain . '/i/actor'
+                    'https://'.$domain.'/i/actor',
                 ],
                 'links' => [
                     [
                         'rel' => 'http://webfinger.net/rel/profile-page',
                         'type' => 'text/html',
-                        'href' => 'https://' . $domain . '/site/kb/instance-actor'
+                        'href' => 'https://'.$domain.'/site/kb/instance-actor',
                     ],
                     [
                         'rel' => 'self',
                         'type' => 'application/activity+json',
-                        'href' => 'https://' . $domain . '/i/actor'
-                    ]
-                ]
+                        'href' => 'https://'.$domain.'/i/actor',
+                    ],
+                ],
             ];
+
             return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
         }
         $hash = hash('sha256', $resource);
-        $key = 'federation:webfinger:sha256:' . $hash;
-        if($cached = Cache::get($key)) {
+        $key = 'federation:webfinger:sha256:'.$hash;
+        if ($cached = Cache::get($key)) {
             return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
         }
-        if(strpos($resource, $domain) == false) {
+        if (strpos($resource, $domain) == false) {
             return response('', 400);
         }
         $parsed = Nickname::normalizeProfileUrl($resource);
-        if(empty($parsed) || $parsed['domain'] !== $domain) {
+        if (empty($parsed) || $parsed['domain'] !== $domain) {
             return response('', 400);
         }
         $username = $parsed['username'];
         $profile = Profile::whereNull('domain')->whereUsername($username)->first();
-        if(!$profile || $profile->status !== null) {
+        if (! $profile || $profile->status !== null) {
             return response('', 400);
         }
         $webfinger = (new Webfinger($profile))->generate();
         Cache::put($key, $webfinger, 1209600);
 
         return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
-            ->header('Access-Control-Allow-Origin','*');
+            ->header('Access-Control-Allow-Origin', '*');
     }
 
     public function hostMeta(Request $request)
     {
-        abort_if(!config('federation.webfinger.enabled'), 404);
+        abort_if(! config('federation.webfinger.enabled'), 404);
 
         $path = route('well-known.webfinger');
         $xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
@@ -118,19 +104,19 @@ class FederationController extends Controller
 
     public function userOutbox(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
 
-        if(!$request->wantsJson()) {
-            return redirect('/' . $username);
+        if (! $request->wantsJson()) {
+            return redirect('/'.$username);
         }
 
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['statuses_count']), 404);
+        abort_if(! $account || ! isset($account['statuses_count']), 404);
         $res = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
+            'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
             'type' => 'OrderedCollection',
             'totalItems' => $account['statuses_count'] ?? 0,
         ];
@@ -140,135 +126,145 @@ class FederationController extends Controller
 
     public function userInbox(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.inbox'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.inbox'), 404);
 
         $headers = $request->headers->all();
         $payload = $request->getContent();
-        if(!$payload || empty($payload)) {
+        if (! $payload || empty($payload)) {
             return;
         }
         $obj = json_decode($payload, true, 8);
-        if(!isset($obj['id'])) {
+        if (! isset($obj['id'])) {
             return;
         }
         $domain = parse_url($obj['id'], PHP_URL_HOST);
-        if(in_array($domain, InstanceService::getBannedDomains())) {
+        if (in_array($domain, InstanceService::getBannedDomains())) {
             return;
         }
 
-        if(isset($obj['type']) && $obj['type'] === 'Delete') {
-            if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
-                if($obj['object']['type'] === 'Person') {
-                    if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
+        if (isset($obj['type']) && $obj['type'] === 'Delete') {
+            if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
+                if ($obj['object']['type'] === 'Person') {
+                    if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Tombstone') {
-                    if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
+                if ($obj['object']['type'] === 'Tombstone') {
+                    if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Story') {
+                if ($obj['object']['type'] === 'Story') {
                     dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
+
                     return;
                 }
             }
+
             return;
-        } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
+        } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
             dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
         } else {
             dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
         }
-        return;
+
     }
 
     public function sharedInbox(Request $request)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.sharedInbox'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.sharedInbox'), 404);
 
         $headers = $request->headers->all();
         $payload = $request->getContent();
 
-        if(!$payload || empty($payload)) {
+        if (! $payload || empty($payload)) {
             return;
         }
 
         $obj = json_decode($payload, true, 8);
-        if(!isset($obj['id'])) {
+        if (! isset($obj['id'])) {
             return;
         }
 
         $domain = parse_url($obj['id'], PHP_URL_HOST);
-        if(in_array($domain, InstanceService::getBannedDomains())) {
+        if (in_array($domain, InstanceService::getBannedDomains())) {
             return;
         }
 
-        if(isset($obj['type']) && $obj['type'] === 'Delete') {
-            if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
-                if($obj['object']['type'] === 'Person') {
-                    if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
+        if (isset($obj['type']) && $obj['type'] === 'Delete') {
+            if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
+                if ($obj['object']['type'] === 'Person') {
+                    if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Tombstone') {
-                    if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
+                if ($obj['object']['type'] === 'Tombstone') {
+                    if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
                         dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+
                         return;
                     }
                 }
 
-                if($obj['object']['type'] === 'Story') {
+                if ($obj['object']['type'] === 'Story') {
                     dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
+
                     return;
                 }
             }
+
             return;
-        } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
+        } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
             dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
         } else {
             dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
         }
-        return;
+
     }
 
     public function userFollowing(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
 
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['following_count']), 404);
+        abort_if(! $account || ! isset($account['following_count']), 404);
         $obj = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id'       => $request->getUri(),
-            'type'     => 'OrderedCollection',
+            'id' => $request->getUri(),
+            'type' => 'OrderedCollection',
             'totalItems' => $account['following_count'] ?? 0,
         ];
+
         return response()->json($obj)->header('Content-Type', 'application/activity+json');
     }
 
     public function userFollowers(Request $request, $username)
     {
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
         $id = AccountService::usernameToId($username);
-        abort_if(!$id, 404);
+        abort_if(! $id, 404);
         $account = AccountService::get($id);
-        abort_if(!$account || !isset($account['followers_count']), 404);
+        abort_if(! $account || ! isset($account['followers_count']), 404);
         $obj = [
             '@context' => 'https://www.w3.org/ns/activitystreams',
-            'id'       => $request->getUri(),
-            'type'     => 'OrderedCollection',
+            'id' => $request->getUri(),
+            'type' => 'OrderedCollection',
             'totalItems' => $account['followers_count'] ?? 0,
         ];
+
         return response()->json($obj)->header('Content-Type', 'application/activity+json');
     }
 }

+ 353 - 354
app/Http/Controllers/SearchController.php

@@ -2,368 +2,367 @@
 
 namespace App\Http\Controllers;
 
-use Auth;
 use App\Hashtag;
 use App\Place;
 use App\Profile;
+use App\Services\WebfingerService;
 use App\Status;
-use Illuminate\Http\Request;
 use App\Util\ActivityPub\Helpers;
+use Auth;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Str;
-use App\Transformer\Api\{
-	AccountTransformer,
-	HashtagTransformer,
-	StatusTransformer,
-};
-use App\Services\WebfingerService;
 
 class SearchController extends Controller
 {
-	public $tokens = [];
-	public $term = '';
-	public $hash = '';
-	public $cacheKey = 'api:search:tag:';
-
-	public function __construct()
-	{
-		$this->middleware('auth');
-	}
-
-	public function searchAPI(Request $request)
-	{
-		$this->validate($request, [
-			'q' => 'required|string|min:3|max:120',
-			'src' => 'required|string|in:metro',
-			'v' => 'required|integer|in:2',
-			'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
-		]);
-
-		$scope = $request->input('scope') ?? 'all';
-		$this->term = e(urldecode($request->input('q')));
-		$this->hash = hash('sha256', $this->term);
-
-		switch ($scope) {
-			case 'all':
-				$this->getHashtags();
-				$this->getPosts();
-				$this->getProfiles();
-				// $this->getPlaces();
-				break;
-
-			case 'hashtag':
-				$this->getHashtags();
-				break;
-
-			case 'profile':
-				$this->getProfiles();
-				break;
-
-			case 'webfinger':
-				$this->webfingerSearch();
-				break;
-
-			case 'remote':
-				$this->remoteLookupSearch();
-				break;
-
-			case 'place':
-				$this->getPlaces();
-				break;
-
-			default:
-				break;
-		}
-
-		return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
-	}
-
-	protected function getPosts()
-	{
-		$tag = $this->term;
-		$hash = hash('sha256', $tag);
-		if( Helpers::validateUrl($tag) != false &&
-			Helpers::validateLocalUrl($tag) != true &&
-			config_cache('federation.activitypub.enabled') == true &&
-			config('federation.activitypub.remoteFollow') == true
-		) {
-			$remote = Helpers::fetchFromUrl($tag);
-			if( isset($remote['type']) &&
-				$remote['type'] == 'Note') {
-				$item = Helpers::statusFetch($tag);
-				$this->tokens['posts'] = [[
-					'count'  => 0,
-					'url'    => $item->url(),
-					'type'   => 'status',
-					'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-					'tokens' => [$item->caption],
-					'name'   => $item->caption,
-					'thumb'  => $item->thumb(),
-				]];
-			}
-		} else {
-			$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
-						->whereHas('media')
-						->whereNull('in_reply_to_id')
-						->whereNull('reblog_of_id')
-						->whereProfileId(Auth::user()->profile_id)
-						->where('caption', 'like', '%'.$tag.'%')
-						->latest()
-						->limit(10)
-						->get();
-
-			if($posts->count() > 0) {
-				$posts = $posts->map(function($item, $key) {
-					return [
-						'count'  => 0,
-						'url'    => $item->url(),
-						'type'   => 'status',
-						'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
-						'tokens' => [$item->caption],
-						'name'   => $item->caption,
-						'thumb'  => $item->thumb(),
-						'filter' => $item->firstMedia()->filter_class
-					];
-				});
-				$this->tokens['posts'] = $posts;
-			}
-		}
-	}
-
-	protected function getHashtags()
-	{
-		$tag = $this->term;
-		$key = $this->cacheKey . 'hashtags:' . $this->hash;
-		$ttl = now()->addMinutes(1);
-		$tokens = Cache::remember($key, $ttl, function() use($tag) {
-			$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
-			$hashtags = Hashtag::select('id', 'name', 'slug')
-				->where('slug', 'like', '%'.$htag.'%')
-				->whereHas('posts')
-				->limit(20)
-				->get();
-			if($hashtags->count() > 0) {
-				$tags = $hashtags->map(function ($item, $key) {
-					return [
-						'count'  => $item->posts()->count(),
-						'url'    => $item->url(),
-						'type'   => 'hashtag',
-						'value'  => $item->name,
-						'tokens' => '',
-						'name'   => null,
-					];
-				});
-				return $tags;
-			}
-		});
-		$this->tokens['hashtags'] = $tokens;
-	}
-
-	protected function getPlaces()
-	{
-		$tag = $this->term;
-		// $key = $this->cacheKey . 'places:' . $this->hash;
-		// $ttl = now()->addHours(12);
-		// $tokens = Cache::remember($key, $ttl, function() use($tag) {
-			$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
-			$hashtags = Place::select('id', 'name', 'slug', 'country')
-				->where('name', 'like', '%'.$htag[0].'%')
-				->paginate(20);
-			$tags = [];
-			if($hashtags->count() > 0) {
-				$tags = $hashtags->map(function ($item, $key) {
-					return [
-						'count'     => null,
-						'url'       => $item->url(),
-						'type'      => 'place',
-						'value'     => $item->name . ', ' . $item->country,
-						'tokens'    => '',
-						'name'      => null,
-						'city'      => $item->name,
-						'country'   => $item->country
-					];
-				});
-				// return $tags;
-			}
-		// });
-		$this->tokens['places'] = $tags;
-		$this->tokens['placesPagination'] = [
-			'total' => $hashtags->total(),
-			'current_page' => $hashtags->currentPage(),
-			'last_page' => $hashtags->lastPage()
-		];
-	}
-
-	protected function getProfiles()
-	{
-		$tag = $this->term;
-		$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
-		$key = $this->cacheKey . 'profiles:' . $this->hash;
-		$remoteTtl = now()->addMinutes(15);
-		$ttl = now()->addHours(2);
-		if( Helpers::validateUrl($tag) != false &&
-			Helpers::validateLocalUrl($tag) != true &&
-			config_cache('federation.activitypub.enabled') == true &&
-			config('federation.activitypub.remoteFollow') == true
-		) {
-			$remote = Helpers::fetchFromUrl($tag);
-			if( isset($remote['type']) &&
-				$remote['type'] == 'Person'
-			) {
-				$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
-					$item = Helpers::profileFirstOrNew($tag);
-					$tokens = [[
-						'count'  => 1,
-						'url'    => $item->url(),
-						'type'   => 'profile',
-						'value'  => $item->username,
-						'tokens' => [$item->username],
-						'name'   => $item->name,
-						'entity' => [
-							'id' => (string) $item->id,
-							'following' => $item->followedBy(Auth::user()->profile),
-							'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-							'thumb' => $item->avatarUrl(),
-							'local' => (bool) !$item->domain,
-							'post_count' => $item->statuses()->count()
-						]
-					]];
-					return $tokens;
-				});
-			}
-		}
-
-		else {
-			$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
-				if(Str::startsWith($tag, '@')) {
-					$tag = substr($tag, 1);
-				}
-				$users = Profile::select('status', 'domain', 'username', 'name', 'id')
-					->whereNull('status')
-					->where('username', 'like', '%'.$tag.'%')
-					->limit(20)
-					->orderBy('domain')
-					->get();
-
-				if($users->count() > 0) {
-					return $users->map(function ($item, $key) {
-						return [
-							'count'  => 0,
-							'url'    => $item->url(),
-							'type'   => 'profile',
-							'value'  => $item->username,
-							'tokens' => [$item->username],
-							'name'   => $item->name,
-							'avatar' => $item->avatarUrl(),
-							'id'     =>  (string) $item->id,
-							'entity' => [
-								'id' => (string) $item->id,
-								'following' => $item->followedBy(Auth::user()->profile),
-								'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
-								'thumb' => $item->avatarUrl(),
-								'local' => (bool) !$item->domain,
-								'post_count' => $item->statuses()->count()
-							]
-						];
-					});
-				}
-			});
-		}
-	}
-
-	public function results(Request $request)
-	{
-		$this->validate($request, [
-			'q' => 'required|string|min:1',
-		]);
-
-		return view('search.results');
-	}
-
-	protected function webfingerSearch()
-	{
-		$wfs = WebfingerService::lookup($this->term);
-
-		if(empty($wfs)) {
-			return;
-		}
-
-		$this->tokens['profiles'] = [
-			[
-				'count'  => 1,
-				'url'    => $wfs['url'],
-				'type'   => 'profile',
-				'value'  => $wfs['username'],
-				'tokens' => [$wfs['username']],
-				'name'   => $wfs['display_name'],
-				'entity' => [
-					'id' => (string) $wfs['id'],
-					'following' => null,
-					'follow_request' => null,
-					'thumb' => $wfs['avatar'],
-					'local' => (bool) $wfs['local']
-				]
-			]
-		];
-		return;
-	}
-
-	protected function remotePostLookup()
-	{
-		$tag = $this->term;
-		$hash = hash('sha256', $tag);
-		$local = Helpers::validateLocalUrl($tag);
-		$valid = Helpers::validateUrl($tag);
-
-		if($valid == false || $local == true) {
-			return;
-		}
-
-		if(Status::whereUri($tag)->whereLocal(false)->exists()) {
-			$item = Status::whereUri($tag)->first();
-			$media = $item->firstMedia();
-			$url = null;
-			if($media) {
-				$url = $media->remote_url;
-			}
-			$this->tokens['posts'] = [[
-				'count'  => 0,
-				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-				'type'   => 'status',
-				'username' => $item->profile->username,
-				'caption'   => $item->rendered ?? $item->caption,
-				'thumb'  => $url,
-				'timestamp' => $item->created_at->diffForHumans()
-			]];
-		}
-
-		$remote = Helpers::fetchFromUrl($tag);
-
-		if(isset($remote['type']) && $remote['type'] == 'Note') {
-			$item = Helpers::statusFetch($tag);
-			$media = $item->firstMedia();
-			$url = null;
-			if($media) {
-				$url = $media->remote_url;
-			}
-			$this->tokens['posts'] = [[
-				'count'  => 0,
-				'url'    => "/i/web/post/_/$item->profile_id/$item->id",
-				'type'   => 'status',
-				'username' => $item->profile->username,
-				'caption'   => $item->rendered ?? $item->caption,
-				'thumb'  => $url,
-				'timestamp' => $item->created_at->diffForHumans()
-			]];
-		}
-	}
-
-	protected function remoteLookupSearch()
-	{
-		if(!Helpers::validateUrl($this->term)) {
-			return;
-		}
-		$this->getProfiles();
-		$this->remotePostLookup();
-	}
+    public $tokens = [];
+
+    public $term = '';
+
+    public $hash = '';
+
+    public $cacheKey = 'api:search:tag:';
+
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+
+    public function searchAPI(Request $request)
+    {
+        $this->validate($request, [
+            'q' => 'required|string|min:3|max:120',
+            'src' => 'required|string|in:metro',
+            'v' => 'required|integer|in:2',
+            'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
+        ]);
+
+        $scope = $request->input('scope') ?? 'all';
+        $this->term = e(urldecode($request->input('q')));
+        $this->hash = hash('sha256', $this->term);
+
+        switch ($scope) {
+            case 'all':
+                $this->getHashtags();
+                $this->getPosts();
+                $this->getProfiles();
+                // $this->getPlaces();
+                break;
+
+            case 'hashtag':
+                $this->getHashtags();
+                break;
+
+            case 'profile':
+                $this->getProfiles();
+                break;
+
+            case 'webfinger':
+                $this->webfingerSearch();
+                break;
+
+            case 'remote':
+                $this->remoteLookupSearch();
+                break;
+
+            case 'place':
+                $this->getPlaces();
+                break;
+
+            default:
+                break;
+        }
+
+        return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
+    }
+
+    protected function getPosts()
+    {
+        $tag = $this->term;
+        $hash = hash('sha256', $tag);
+        if (Helpers::validateUrl($tag) != false &&
+            Helpers::validateLocalUrl($tag) != true &&
+            (bool) config_cache('federation.activitypub.enabled') == true &&
+            config('federation.activitypub.remoteFollow') == true
+        ) {
+            $remote = Helpers::fetchFromUrl($tag);
+            if (isset($remote['type']) &&
+                in_array($remote['type'], ['Note', 'Question'])
+            ) {
+                $item = Helpers::statusFetch($tag);
+                $this->tokens['posts'] = [[
+                    'count' => 0,
+                    'url' => $item->url(),
+                    'type' => 'status',
+                    'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+                    'tokens' => [$item->caption],
+                    'name' => $item->caption,
+                    'thumb' => $item->thumb(),
+                ]];
+            }
+        } else {
+            $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
+                ->whereHas('media')
+                ->whereNull('in_reply_to_id')
+                ->whereNull('reblog_of_id')
+                ->whereProfileId(Auth::user()->profile_id)
+                ->where('caption', 'like', '%'.$tag.'%')
+                ->latest()
+                ->limit(10)
+                ->get();
+
+            if ($posts->count() > 0) {
+                $posts = $posts->map(function ($item, $key) {
+                    return [
+                        'count' => 0,
+                        'url' => $item->url(),
+                        'type' => 'status',
+                        'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
+                        'tokens' => [$item->caption],
+                        'name' => $item->caption,
+                        'thumb' => $item->thumb(),
+                        'filter' => $item->firstMedia()->filter_class,
+                    ];
+                });
+                $this->tokens['posts'] = $posts;
+            }
+        }
+    }
+
+    protected function getHashtags()
+    {
+        $tag = $this->term;
+        $key = $this->cacheKey.'hashtags:'.$this->hash;
+        $ttl = now()->addMinutes(1);
+        $tokens = Cache::remember($key, $ttl, function () use ($tag) {
+            $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
+            $hashtags = Hashtag::select('id', 'name', 'slug')
+                ->where('slug', 'like', '%'.$htag.'%')
+                ->whereHas('posts')
+                ->limit(20)
+                ->get();
+            if ($hashtags->count() > 0) {
+                $tags = $hashtags->map(function ($item, $key) {
+                    return [
+                        'count' => $item->posts()->count(),
+                        'url' => $item->url(),
+                        'type' => 'hashtag',
+                        'value' => $item->name,
+                        'tokens' => '',
+                        'name' => null,
+                    ];
+                });
+
+                return $tags;
+            }
+        });
+        $this->tokens['hashtags'] = $tokens;
+    }
+
+    protected function getPlaces()
+    {
+        $tag = $this->term;
+        // $key = $this->cacheKey . 'places:' . $this->hash;
+        // $ttl = now()->addHours(12);
+        // $tokens = Cache::remember($key, $ttl, function() use($tag) {
+        $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
+        $hashtags = Place::select('id', 'name', 'slug', 'country')
+            ->where('name', 'like', '%'.$htag[0].'%')
+            ->paginate(20);
+        $tags = [];
+        if ($hashtags->count() > 0) {
+            $tags = $hashtags->map(function ($item, $key) {
+                return [
+                    'count' => null,
+                    'url' => $item->url(),
+                    'type' => 'place',
+                    'value' => $item->name.', '.$item->country,
+                    'tokens' => '',
+                    'name' => null,
+                    'city' => $item->name,
+                    'country' => $item->country,
+                ];
+            });
+            // return $tags;
+        }
+        // });
+        $this->tokens['places'] = $tags;
+        $this->tokens['placesPagination'] = [
+            'total' => $hashtags->total(),
+            'current_page' => $hashtags->currentPage(),
+            'last_page' => $hashtags->lastPage(),
+        ];
+    }
+
+    protected function getProfiles()
+    {
+        $tag = $this->term;
+        $remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash;
+        $key = $this->cacheKey.'profiles:'.$this->hash;
+        $remoteTtl = now()->addMinutes(15);
+        $ttl = now()->addHours(2);
+        if (Helpers::validateUrl($tag) != false &&
+            Helpers::validateLocalUrl($tag) != true &&
+            (bool) config_cache('federation.activitypub.enabled') == true &&
+            config('federation.activitypub.remoteFollow') == true
+        ) {
+            $remote = Helpers::fetchFromUrl($tag);
+            if (isset($remote['type']) &&
+                $remote['type'] == 'Person'
+            ) {
+                $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) {
+                    $item = Helpers::profileFirstOrNew($tag);
+                    $tokens = [[
+                        'count' => 1,
+                        'url' => $item->url(),
+                        'type' => 'profile',
+                        'value' => $item->username,
+                        'tokens' => [$item->username],
+                        'name' => $item->name,
+                        'entity' => [
+                            'id' => (string) $item->id,
+                            'following' => $item->followedBy(Auth::user()->profile),
+                            'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+                            'thumb' => $item->avatarUrl(),
+                            'local' => (bool) ! $item->domain,
+                            'post_count' => $item->statuses()->count(),
+                        ],
+                    ]];
+
+                    return $tokens;
+                });
+            }
+        } else {
+            $this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) {
+                if (Str::startsWith($tag, '@')) {
+                    $tag = substr($tag, 1);
+                }
+                $users = Profile::select('status', 'domain', 'username', 'name', 'id')
+                    ->whereNull('status')
+                    ->where('username', 'like', '%'.$tag.'%')
+                    ->limit(20)
+                    ->orderBy('domain')
+                    ->get();
+
+                if ($users->count() > 0) {
+                    return $users->map(function ($item, $key) {
+                        return [
+                            'count' => 0,
+                            'url' => $item->url(),
+                            'type' => 'profile',
+                            'value' => $item->username,
+                            'tokens' => [$item->username],
+                            'name' => $item->name,
+                            'avatar' => $item->avatarUrl(),
+                            'id' => (string) $item->id,
+                            'entity' => [
+                                'id' => (string) $item->id,
+                                'following' => $item->followedBy(Auth::user()->profile),
+                                'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
+                                'thumb' => $item->avatarUrl(),
+                                'local' => (bool) ! $item->domain,
+                                'post_count' => $item->statuses()->count(),
+                            ],
+                        ];
+                    });
+                }
+            });
+        }
+    }
+
+    public function results(Request $request)
+    {
+        $this->validate($request, [
+            'q' => 'required|string|min:1',
+        ]);
+
+        return view('search.results');
+    }
+
+    protected function webfingerSearch()
+    {
+        $wfs = WebfingerService::lookup($this->term);
+
+        if (empty($wfs)) {
+            return;
+        }
+
+        $this->tokens['profiles'] = [
+            [
+                'count' => 1,
+                'url' => $wfs['url'],
+                'type' => 'profile',
+                'value' => $wfs['username'],
+                'tokens' => [$wfs['username']],
+                'name' => $wfs['display_name'],
+                'entity' => [
+                    'id' => (string) $wfs['id'],
+                    'following' => null,
+                    'follow_request' => null,
+                    'thumb' => $wfs['avatar'],
+                    'local' => (bool) $wfs['local'],
+                ],
+            ],
+        ];
+
+    }
+
+    protected function remotePostLookup()
+    {
+        $tag = $this->term;
+        $hash = hash('sha256', $tag);
+        $local = Helpers::validateLocalUrl($tag);
+        $valid = Helpers::validateUrl($tag);
+
+        if ($valid == false || $local == true) {
+            return;
+        }
+
+        if (Status::whereUri($tag)->whereLocal(false)->exists()) {
+            $item = Status::whereUri($tag)->first();
+            $media = $item->firstMedia();
+            $url = null;
+            if ($media) {
+                $url = $media->remote_url;
+            }
+            $this->tokens['posts'] = [[
+                'count' => 0,
+                'url' => "/i/web/post/_/$item->profile_id/$item->id",
+                'type' => 'status',
+                'username' => $item->profile->username,
+                'caption' => $item->rendered ?? $item->caption,
+                'thumb' => $url,
+                'timestamp' => $item->created_at->diffForHumans(),
+            ]];
+        }
+
+        $remote = Helpers::fetchFromUrl($tag);
+
+        if (isset($remote['type']) && $remote['type'] == 'Note') {
+            $item = Helpers::statusFetch($tag);
+            $media = $item->firstMedia();
+            $url = null;
+            if ($media) {
+                $url = $media->remote_url;
+            }
+            $this->tokens['posts'] = [[
+                'count' => 0,
+                'url' => "/i/web/post/_/$item->profile_id/$item->id",
+                'type' => 'status',
+                'username' => $item->profile->username,
+                'caption' => $item->rendered ?? $item->caption,
+                'thumb' => $url,
+                'timestamp' => $item->created_at->diffForHumans(),
+            ]];
+        }
+    }
+
+    protected function remoteLookupSearch()
+    {
+        if (! Helpers::validateUrl($this->term)) {
+            return;
+        }
+        $this->getProfiles();
+        $this->remotePostLookup();
+    }
 }

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

@@ -78,7 +78,7 @@ class StatusController extends Controller
             ]);
         }
 
-        if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
+        if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
             return $this->showActivityPub($request, $status);
         }
 

+ 136 - 135
app/Jobs/SharePipeline/SharePipeline.php

@@ -2,9 +2,15 @@
 
 namespace App\Jobs\SharePipeline;
 
-use Cache, Log;
-use Illuminate\Support\Facades\Redis;
-use App\{Status, Notification};
+use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
+use App\Notification;
+use App\Services\ReblogService;
+use App\Services\StatusService;
+use App\Status;
+use App\Transformer\ActivityPub\Verb\Announce;
+use App\Util\ActivityPub\HttpSignature;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -12,141 +18,136 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\Announce;
-use GuzzleHttp\{Pool, Client, Promise};
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\ReblogService;
-use App\Services\StatusService;
-use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
 
 class SharePipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	protected $status;
-
-	/**
-	 * Delete the job if its models no longer exist.
-	 *
-	 * @var bool
-	 */
-	public $deleteWhenMissingModels = true;
-
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$status = $this->status;
-		$parent = Status::find($this->status->reblog_of_id);
-        if(!$parent) {
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $status;
+
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $parent = Status::find($this->status->reblog_of_id);
+        if (! $parent) {
             return;
         }
-		$actor = $status->profile;
-		$target = $parent->profile;
-
-		if ($status->uri !== null) {
-			// Ignore notifications to remote statuses
-			return;
-		}
-
-		if($target->id === $status->profile_id) {
-			$this->remoteAnnounceDeliver();
-			return true;
-		}
-
-		ReblogService::addPostReblog($parent->profile_id, $status->id);
-
-		$parent->reblogs_count = $parent->reblogs_count + 1;
-		$parent->save();
-		StatusService::del($parent->id);
-
-		Notification::firstOrCreate(
-			[
-				'profile_id' => $target->id,
-				'actor_id' => $actor->id,
-				'action' => 'share',
-				'item_type' => 'App\Status',
-				'item_id' => $status->reblog_of_id ?? $status->id,
-			]
-		);
-
-		FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
-
-		return $this->remoteAnnounceDeliver();
-	}
-
-	public function remoteAnnounceDeliver()
-	{
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
-			return true;
-		}
-		$status = $this->status;
-		$profile = $status->profile;
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new Announce());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$audience = $status->profile->getAudienceInbox();
-
-		if(empty($audience) || $status->scope != 'public') {
-			// Return on profiles with no remote followers
-			return;
-		}
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
-
-	}
+        $actor = $status->profile;
+        $target = $parent->profile;
+
+        if ($status->uri !== null) {
+            // Ignore notifications to remote statuses
+            return;
+        }
+
+        if ($target->id === $status->profile_id) {
+            $this->remoteAnnounceDeliver();
+
+            return true;
+        }
+
+        ReblogService::addPostReblog($parent->profile_id, $status->id);
+
+        $parent->reblogs_count = $parent->reblogs_count + 1;
+        $parent->save();
+        StatusService::del($parent->id);
+
+        Notification::firstOrCreate(
+            [
+                'profile_id' => $target->id,
+                'actor_id' => $actor->id,
+                'action' => 'share',
+                'item_type' => 'App\Status',
+                'item_id' => $status->reblog_of_id ?? $status->id,
+            ]
+        );
+
+        FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
+
+        return $this->remoteAnnounceDeliver();
+    }
+
+    public function remoteAnnounceDeliver()
+    {
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
+            return true;
+        }
+        $status = $this->status;
+        $profile = $status->profile;
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new Announce());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $audience = $status->profile->getAudienceInbox();
+
+        if (empty($audience) || $status->scope != 'public') {
+            // Return on profiles with no remote followers
+            return;
+        }
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
+
+    }
 }

+ 126 - 123
app/Jobs/SharePipeline/UndoSharePipeline.php

@@ -2,9 +2,15 @@
 
 namespace App\Jobs\SharePipeline;
 
-use Cache, Log;
-use Illuminate\Support\Facades\Redis;
-use App\{Status, Notification};
+use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
+use App\Notification;
+use App\Services\ReblogService;
+use App\Services\StatusService;
+use App\Status;
+use App\Transformer\ActivityPub\Verb\UndoAnnounce;
+use App\Util\ActivityPub\HttpSignature;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -12,128 +18,125 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\UndoAnnounce;
-use GuzzleHttp\{Pool, Client, Promise};
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\ReblogService;
-use App\Services\StatusService;
-use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
 
 class UndoSharePipeline implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-	protected $status;
-	public $deleteWhenMissingModels = true;
-
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	public function handle()
-	{
-		$status = $this->status;
-		$actor = $status->profile;
-		$parent = Status::find($status->reblog_of_id);
-
-		FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
-
-		if($parent) {
-			$target = $parent->profile_id;
-			ReblogService::removePostReblog($parent->profile_id, $status->id);
-
-			if($parent->reblogs_count > 0) {
-				$parent->reblogs_count = $parent->reblogs_count - 1;
-				$parent->save();
-				StatusService::del($parent->id);
-			}
-
-			$notification = Notification::whereProfileId($target)
-				->whereActorId($status->profile_id)
-				->whereAction('share')
-				->whereItemId($status->reblog_of_id)
-				->whereItemType('App\Status')
-				->first();
-
-			if($notification) {
-				$notification->forceDelete();
-			}
-		}
-
-		if ($status->uri != null) {
-			return;
-		}
-
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
-			return $status->delete();
-		} else {
-			return $this->remoteAnnounceDeliver();
-		}
-	}
-
-	public function remoteAnnounceDeliver()
-	{
-		if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $status;
+
+    public $deleteWhenMissingModels = true;
+
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    public function handle()
+    {
+        $status = $this->status;
+        $actor = $status->profile;
+        $parent = Status::find($status->reblog_of_id);
+
+        FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
+
+        if ($parent) {
+            $target = $parent->profile_id;
+            ReblogService::removePostReblog($parent->profile_id, $status->id);
+
+            if ($parent->reblogs_count > 0) {
+                $parent->reblogs_count = $parent->reblogs_count - 1;
+                $parent->save();
+                StatusService::del($parent->id);
+            }
+
+            $notification = Notification::whereProfileId($target)
+                ->whereActorId($status->profile_id)
+                ->whereAction('share')
+                ->whereItemId($status->reblog_of_id)
+                ->whereItemType('App\Status')
+                ->first();
+
+            if ($notification) {
+                $notification->forceDelete();
+            }
+        }
+
+        if ($status->uri != null) {
+            return;
+        }
+
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
+            return $status->delete();
+        } else {
+            return $this->remoteAnnounceDeliver();
+        }
+    }
+
+    public function remoteAnnounceDeliver()
+    {
+        if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
             $status->delete();
-			return 1;
-		}
-
-		$status = $this->status;
-		$profile = $status->profile;
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$audience = $status->profile->getAudienceInbox();
-
-		if(empty($audience) || $status->scope != 'public') {
-			return 1;
-		}
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
-
-		$status->delete();
-
-		return 1;
-	}
+
+            return 1;
+        }
+
+        $status = $this->status;
+        $profile = $status->profile;
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new UndoAnnounce());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $audience = $status->profile->getAudienceInbox();
+
+        if (empty($audience) || $status->scope != 'public') {
+            return 1;
+        }
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
+
+        $status->delete();
+
+        return 1;
+    }
 }

+ 162 - 166
app/Jobs/StatusPipeline/StatusDelete.php

@@ -2,126 +2,122 @@
 
 namespace App\Jobs\StatusPipeline;
 
-use DB, Cache, Storage;
-use App\{
-	AccountInterstitial,
-    Bookmark,
-	CollectionItem,
-    DirectMessage,
-    Like,
-    Media,
-	MediaTag,
-    Mention,
-	Notification,
-	Report,
-	Status,
-    StatusArchived,
-	StatusHashtag,
-    StatusView
-};
+use App\AccountInterstitial;
+use App\Bookmark;
+use App\CollectionItem;
+use App\DirectMessage;
+use App\Jobs\MediaPipeline\MediaDeletePipeline;
+use App\Like;
+use App\Media;
+use App\MediaTag;
+use App\Mention;
+use App\Notification;
+use App\Report;
+use App\Services\CollectionService;
+use App\Services\NotificationService;
+use App\Services\StatusService;
+use App\Status;
+use App\StatusArchived;
+use App\StatusHashtag;
+use App\StatusView;
+use App\Transformer\ActivityPub\Verb\DeleteNote;
+use App\Util\ActivityPub\HttpSignature;
+use Cache;
+use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
-use Illuminate\Support\Str;
 use League\Fractal\Serializer\ArraySerializer;
-use App\Transformer\ActivityPub\Verb\DeleteNote;
-use App\Util\ActivityPub\Helpers;
-use GuzzleHttp\Pool;
-use GuzzleHttp\Client;
-use GuzzleHttp\Promise;
-use App\Util\ActivityPub\HttpSignature;
-use App\Services\CollectionService;
-use App\Services\StatusService;
-use App\Services\NotificationService;
-use App\Jobs\MediaPipeline\MediaDeletePipeline;
 
 class StatusDelete implements ShouldQueue
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
-	protected $status;
+    protected $status;
 
-	/**
-	 * Delete the job if its models no longer exist.
-	 *
-	 * @var bool
-	 */
-	public $deleteWhenMissingModels = true;
+    /**
+     * Delete the job if its models no longer exist.
+     *
+     * @var bool
+     */
+    public $deleteWhenMissingModels = true;
 
     public $timeout = 900;
+
     public $tries = 2;
 
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Status $status)
-	{
-		$this->status = $status;
-	}
-
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$status = $this->status;
-		$profile = $this->status->profile;
-
-		StatusService::del($status->id, true);
-		if($profile) {
-			if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
-				$profile->status_count = $profile->status_count - 1;
-				$profile->save();
-			}
-		}
-
-		Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id);
-
-		if(config_cache('federation.activitypub.enabled') == true) {
-			return $this->fanoutDelete($status);
-		} else {
-			return $this->unlinkRemoveMedia($status);
-		}
-	}
-
-	public function unlinkRemoveMedia($status)
-	{
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $profile = $this->status->profile;
+
+        StatusService::del($status->id, true);
+        if ($profile) {
+            if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
+                $profile->status_count = $profile->status_count - 1;
+                $profile->save();
+            }
+        }
+
+        Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
+
+        if ((bool) config_cache('federation.activitypub.enabled') == true) {
+            return $this->fanoutDelete($status);
+        } else {
+            return $this->unlinkRemoveMedia($status);
+        }
+    }
+
+    public function unlinkRemoveMedia($status)
+    {
         Media::whereStatusId($status->id)
-        ->get()
-        ->each(function($media) {
-            MediaDeletePipeline::dispatch($media);
-        });
-
-		if($status->in_reply_to_id) {
-			$parent = Status::findOrFail($status->in_reply_to_id);
-			--$parent->reply_count;
-			$parent->save();
-			StatusService::del($parent->id);
-		}
+            ->get()
+            ->each(function ($media) {
+                MediaDeletePipeline::dispatch($media);
+            });
+
+        if ($status->in_reply_to_id) {
+            $parent = Status::findOrFail($status->in_reply_to_id);
+            $parent->reply_count--;
+            $parent->save();
+            StatusService::del($parent->id);
+        }
 
         Bookmark::whereStatusId($status->id)->delete();
 
         CollectionItem::whereObjectType('App\Status')
             ->whereObjectId($status->id)
             ->get()
-            ->each(function($col) {
+            ->each(function ($col) {
                 CollectionService::removeItem($col->collection_id, $col->object_id);
                 $col->delete();
-        });
+            });
 
         $dms = DirectMessage::whereStatusId($status->id)->get();
-        foreach($dms as $dm) {
+        foreach ($dms as $dm) {
             $not = Notification::whereItemType('App\DirectMessage')
                 ->whereItemId($dm->id)
                 ->first();
-            if($not) {
+            if ($not) {
                 NotificationService::del($not->profile_id, $not->id);
                 $not->forceDeleteQuietly();
             }
@@ -130,11 +126,11 @@ class StatusDelete implements ShouldQueue
         Like::whereStatusId($status->id)->delete();
 
         $mediaTags = MediaTag::where('status_id', $status->id)->get();
-        foreach($mediaTags as $mtag) {
+        foreach ($mediaTags as $mtag) {
             $not = Notification::whereItemType('App\MediaTag')
                 ->whereItemId($mtag->id)
                 ->first();
-            if($not) {
+            if ($not) {
                 NotificationService::del($not->profile_id, $not->id);
                 $not->forceDeleteQuietly();
             }
@@ -142,85 +138,85 @@ class StatusDelete implements ShouldQueue
         }
         Mention::whereStatusId($status->id)->forceDelete();
 
-		Notification::whereItemType('App\Status')
-			->whereItemId($status->id)
-			->forceDelete();
+        Notification::whereItemType('App\Status')
+            ->whereItemId($status->id)
+            ->forceDelete();
 
-		Report::whereObjectType('App\Status')
-			->whereObjectId($status->id)
-			->delete();
+        Report::whereObjectType('App\Status')
+            ->whereObjectId($status->id)
+            ->delete();
 
         StatusArchived::whereStatusId($status->id)->delete();
         StatusHashtag::whereStatusId($status->id)->delete();
         StatusView::whereStatusId($status->id)->delete();
-		Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
-
-		AccountInterstitial::where('item_type', 'App\Status')
-			->where('item_id', $status->id)
-			->delete();
-
-		$status->delete();
-
-		return 1;
-	}
-
-	public function fanoutDelete($status)
-	{
-		$profile = $status->profile;
-
-		if(!$profile) {
-			return;
-		}
-
-		$audience = $status->profile->getAudienceInbox();
-
-		$fractal = new Fractal\Manager();
-		$fractal->setSerializer(new ArraySerializer());
-		$resource = new Fractal\Resource\Item($status, new DeleteNote());
-		$activity = $fractal->createData($resource)->toArray();
-
-		$this->unlinkRemoveMedia($status);
-
-		$payload = json_encode($activity);
-
-		$client = new Client([
-			'timeout'  => config('federation.activitypub.delivery.timeout')
-		]);
-
-		$version = config('pixelfed.version');
-		$appUrl = config('app.url');
-		$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
-
-		$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
-			foreach($audience as $url) {
-				$headers = HttpSignature::sign($profile, $url, $activity, [
-					'Content-Type'	=> 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-					'User-Agent'	=> $userAgent,
-				]);
-				yield function() use ($client, $url, $headers, $payload) {
-					return $client->postAsync($url, [
-						'curl' => [
-							CURLOPT_HTTPHEADER => $headers,
-							CURLOPT_POSTFIELDS => $payload,
-							CURLOPT_HEADER => true
-						]
-					]);
-				};
-			}
-		};
-
-		$pool = new Pool($client, $requests($audience), [
-			'concurrency' => config('federation.activitypub.delivery.concurrency'),
-			'fulfilled' => function ($response, $index) {
-			},
-			'rejected' => function ($reason, $index) {
-			}
-		]);
-
-		$promise = $pool->promise();
-
-		$promise->wait();
+        Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
+
+        AccountInterstitial::where('item_type', 'App\Status')
+            ->where('item_id', $status->id)
+            ->delete();
+
+        $status->delete();
+
+        return 1;
+    }
+
+    public function fanoutDelete($status)
+    {
+        $profile = $status->profile;
+
+        if (! $profile) {
+            return;
+        }
+
+        $audience = $status->profile->getAudienceInbox();
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new DeleteNote());
+        $activity = $fractal->createData($resource)->toArray();
+
+        $this->unlinkRemoveMedia($status);
+
+        $payload = json_encode($activity);
+
+        $client = new Client([
+            'timeout' => config('federation.activitypub.delivery.timeout'),
+        ]);
+
+        $version = config('pixelfed.version');
+        $appUrl = config('app.url');
+        $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+        $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+            foreach ($audience as $url) {
+                $headers = HttpSignature::sign($profile, $url, $activity, [
+                    'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+                    'User-Agent' => $userAgent,
+                ]);
+                yield function () use ($client, $url, $headers, $payload) {
+                    return $client->postAsync($url, [
+                        'curl' => [
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_POSTFIELDS => $payload,
+                            CURLOPT_HEADER => true,
+                        ],
+                    ]);
+                };
+            }
+        };
+
+        $pool = new Pool($client, $requests($audience), [
+            'concurrency' => config('federation.activitypub.delivery.concurrency'),
+            'fulfilled' => function ($response, $index) {
+            },
+            'rejected' => function ($reason, $index) {
+            },
+        ]);
+
+        $promise = $pool->promise();
+
+        $promise->wait();
 
         return 1;
-	}
+    }
 }

+ 22 - 21
app/Jobs/StatusPipeline/StatusEntityLexer.php

@@ -3,12 +3,16 @@
 namespace App\Jobs\StatusPipeline;
 
 use App\Hashtag;
+use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
 use App\Jobs\MentionPipeline\MentionPipeline;
 use App\Mention;
 use App\Profile;
+use App\Services\AdminShadowFilterService;
+use App\Services\PublicTimelineService;
+use App\Services\StatusService;
+use App\Services\UserFilterService;
 use App\Status;
 use App\StatusHashtag;
-use App\Services\PublicTimelineService;
 use App\Util\Lexer\Autolink;
 use App\Util\Lexer\Extractor;
 use App\Util\Sentiment\Bouncer;
@@ -19,18 +23,15 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use App\Services\StatusService;
-use App\Services\UserFilterService;
-use App\Services\AdminShadowFilterService;
-use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
-use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline;
 
 class StatusEntityLexer implements ShouldQueue
 {
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
     protected $status;
+
     protected $entities;
+
     protected $autolink;
 
     /**
@@ -60,12 +61,12 @@ class StatusEntityLexer implements ShouldQueue
         $profile = $this->status->profile;
         $status = $this->status;
 
-        if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
+        if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
             $profile->status_count = $profile->status_count + 1;
             $profile->save();
         }
 
-        if($profile->no_autolink == false) {
+        if ($profile->no_autolink == false) {
             $this->parseEntities();
         }
     }
@@ -103,16 +104,16 @@ class StatusEntityLexer implements ShouldQueue
         $status = $this->status;
 
         foreach ($tags as $tag) {
-            if(mb_strlen($tag) > 124) {
+            if (mb_strlen($tag) > 124) {
                 continue;
             }
             DB::transaction(function () use ($status, $tag) {
                 $slug = str_slug($tag, '-', false);
 
                 $hashtag = Hashtag::firstOrCreate([
-                    'slug' => $slug
+                    'slug' => $slug,
                 ], [
-                    'name' => $tag
+                    'name' => $tag,
                 ]);
 
                 StatusHashtag::firstOrCreate(
@@ -136,11 +137,11 @@ class StatusEntityLexer implements ShouldQueue
         foreach ($mentions as $mention) {
             $mentioned = Profile::whereUsername($mention)->first();
 
-            if (empty($mentioned) || !isset($mentioned->id)) {
+            if (empty($mentioned) || ! isset($mentioned->id)) {
                 continue;
             }
             $blocks = UserFilterService::blocks($mentioned->id);
-            if($blocks && in_array($status->profile_id, $blocks)) {
+            if ($blocks && in_array($status->profile_id, $blocks)) {
                 continue;
             }
 
@@ -161,8 +162,8 @@ class StatusEntityLexer implements ShouldQueue
         $status = $this->status;
         StatusService::refresh($status->id);
 
-        if(config('exp.cached_home_timeline')) {
-            if( $status->in_reply_to_id === null &&
+        if (config('exp.cached_home_timeline')) {
+            if ($status->in_reply_to_id === null &&
                 in_array($status->scope, ['public', 'unlisted', 'private'])
             ) {
                 FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
@@ -179,28 +180,28 @@ class StatusEntityLexer implements ShouldQueue
             'photo:album',
             'video',
             'video:album',
-            'photo:video:album'
+            'photo:video:album',
         ];
 
-        if(config_cache('pixelfed.bouncer.enabled')) {
+        if (config_cache('pixelfed.bouncer.enabled')) {
             Bouncer::get($status);
         }
 
-        Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id);
+        Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
         $hideNsfw = config('instance.hide_nsfw_on_public_feeds');
-        if( $status->uri == null &&
+        if ($status->uri == null &&
             $status->scope == 'public' &&
             in_array($status->type, $types) &&
             $status->in_reply_to_id === null &&
             $status->reblog_of_id === null &&
             ($hideNsfw ? $status->is_nsfw == false : true)
         ) {
-            if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
+            if (AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
                 PublicTimelineService::add($status->id);
             }
         }
 
-        if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
+        if ((bool) config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
             StatusActivityPubDeliver::dispatch($status);
         }
     }

+ 1 - 1
app/Services/LandingService.php

@@ -85,7 +85,7 @@ class LandingService
                 'media_types' => config_cache('pixelfed.media_types'),
             ],
             'features' => [
-                'federation' => config_cache('federation.activitypub.enabled'),
+                'federation' => (bool) config_cache('federation.activitypub.enabled'),
                 'timelines' => [
                     'local' => true,
                     'network' => (bool) config_cache('federation.network_timeline'),

+ 24 - 26
app/Util/ActivityPub/Outbox.php

@@ -2,34 +2,32 @@
 
 namespace App\Util\ActivityPub;
 
-use App\Profile;
-use App\Status;
-use League\Fractal;
 use App\Http\Controllers\ProfileController;
-use App\Transformer\ActivityPub\ProfileOutbox;
+use App\Status;
 use App\Transformer\ActivityPub\Verb\CreateNote;
+use League\Fractal;
 
-class Outbox {
-
-	public static function get($profile)
-	{
-        abort_if(!config_cache('federation.activitypub.enabled'), 404);
-        abort_if(!config('federation.activitypub.outbox'), 404);
+class Outbox
+{
+    public static function get($profile)
+    {
+        abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
+        abort_if(! config('federation.activitypub.outbox'), 404);
 
-        if($profile->status != null) {
+        if ($profile->status != null) {
             return ProfileController::accountCheck($profile);
         }
 
-        if($profile->is_private) {
-            return ['error'=>'403', 'msg' => 'private profile'];
+        if ($profile->is_private) {
+            return ['error' => '403', 'msg' => 'private profile'];
         }
 
         $timeline = $profile
-                    ->statuses()
-                    ->whereScope('public')
-                    ->orderBy('created_at', 'desc')
-                    ->take(10)
-                    ->get();
+            ->statuses()
+            ->whereScope('public')
+            ->orderBy('created_at', 'desc')
+            ->take(10)
+            ->get();
 
         $count = Status::whereProfileId($profile->id)->count();
 
@@ -38,14 +36,14 @@ class Outbox {
         $res = $fractal->createData($resource)->toArray();
 
         $outbox = [
-            '@context'     => 'https://www.w3.org/ns/activitystreams',
-            '_debug'       => 'Outbox only supports latest 10 objects, pagination is not supported',
-            'id'           => $profile->permalink('/outbox'),
-            'type'         => 'OrderedCollection',
-            'totalItems'   => $count,
-            'orderedItems' => $res['data']
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            '_debug' => 'Outbox only supports latest 10 objects, pagination is not supported',
+            'id' => $profile->permalink('/outbox'),
+            'type' => 'OrderedCollection',
+            'totalItems' => $count,
+            'orderedItems' => $res['data'],
         ];
-        return $outbox;
-	}
 
+        return $outbox;
+    }
 }

+ 1 - 1
resources/views/admin/diagnostics/home.blade.php

@@ -298,7 +298,7 @@
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>
 		<td><strong>ACTIVITY_PUB</strong></td>
-		<td><span>{{config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td>
+		<td><span>{{(bool) config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td>
 	</tr>
 	<tr>
 		<td><span class="badge badge-primary">FEDERATION</span></td>