diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index d8d13d477..6c9d4b83c 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -56,7 +56,7 @@ class SearchController extends Controller ] ]]; } else if ($type == 'Note') { - $item = Helpers::statusFirstOrFetch($tag, false); + $item = Helpers::statusFetch($tag); $tokens['posts'] = [[ 'count' => 0, 'url' => $item->url(), diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 65d6a2da5..c79954250 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -30,11 +30,12 @@ class StatusController extends Controller } $status = Status::whereProfileId($user->id) + ->whereNull('reblog_of_id') ->whereNotIn('visibility',['draft','direct']) ->findOrFail($id); - if($status->uri) { - $url = $status->uri; + if($status->uri || $status->url) { + $url = $status->uri ?? $status->url; if(ends_with($url, '/activity')) { $url = str_replace('/activity', '', $url); } @@ -102,109 +103,6 @@ class StatusController extends Controller public function store(Request $request) { return; - - $this->authCheck(); - $user = Auth::user(); - - $size = Media::whereUserId($user->id)->sum('size') / 1000; - $limit = (int) config('pixelfed.max_account_size'); - if ($size >= $limit) { - return redirect()->back()->with('error', 'You have exceeded your storage limit. Please click here for more info.'); - } - - $this->validate($request, [ - 'photo.*' => 'required|mimetypes:' . config('pixelfed.media_types').'|max:' . config('pixelfed.max_photo_size'), - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length'), - 'cw' => 'nullable|string', - 'filter_class' => 'nullable|alpha_dash|max:30', - 'filter_name' => 'nullable|string', - 'visibility' => 'required|string|min:5|max:10', - ]); - - if (count($request->file('photo')) > config('pixelfed.max_album_length')) { - return redirect()->back()->with('error', 'Too many files, max limit per post: '.config('pixelfed.max_album_length')); - } - $cw = $request->filled('cw') && $request->cw == 'on' ? true : false; - $monthHash = hash('sha1', date('Y').date('m')); - $userHash = hash('sha1', $user->id.(string) $user->created_at); - $profile = $user->profile; - $visibility = $this->validateVisibility($request->visibility); - - $cw = $profile->cw == true ? true : $cw; - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } - - $status = new Status(); - $status->profile_id = $profile->id; - $status->caption = strip_tags($request->caption); - $status->is_nsfw = $cw; - - // TODO: remove deprecated visibility in favor of scope - $status->visibility = $visibility; - $status->scope = $visibility; - - $status->save(); - - $photos = $request->file('photo'); - $order = 1; - $mimes = []; - $medias = 0; - - foreach ($photos as $k => $v) { - - $allowedMimes = explode(',', config('pixelfed.media_types')); - if(in_array($v->getMimeType(), $allowedMimes) == false) { - continue; - } - $filter_class = $request->input('filter_class'); - $filter_name = $request->input('filter_name'); - - $storagePath = "public/m/{$monthHash}/{$userHash}"; - $path = $v->store($storagePath); - $hash = \hash_file('sha256', $v); - $media = new Media(); - $media->status_id = $status->id; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $v->getSize(); - $media->mime = $v->getMimeType(); - - $media->filter_class = in_array($filter_class, Filter::classes()) ? $filter_class : null; - $media->filter_name = in_array($filter_name, Filter::names()) ? $filter_name : null; - $media->order = $order; - $media->save(); - array_push($mimes, $media->mime); - ImageOptimize::dispatch($media); - $order++; - $medias++; - } - - if($medias == 0) { - $status->delete(); - return; - } - $status->type = (new self)::mimeTypeCheck($mimes); - $status->save(); - - Cache::forget('profile:status_count:'.$profile->id); - NewStatusPipeline::dispatch($status); - - // TODO: Send to subscribers - - return redirect($status->url()); } public function delete(Request $request) @@ -238,7 +136,9 @@ class StatusController extends Controller $user = Auth::user(); $profile = $user->profile; - $status = Status::withCount('shares')->findOrFail($request->input('item')); + $status = Status::withCount('shares') + ->whereIn('scope', ['public', 'unlisted']) + ->findOrFail($request->input('item')); $count = $status->shares_count; diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index f8aeb5c0a..896827917 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -2,16 +2,17 @@ namespace App\Jobs\LikePipeline; -use App\Like; -use App\Notification; -use Cache; +use Cache, Log, Redis; +use App\{Like, Notification}; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Log; -use Redis; +use App\Util\ActivityPub\Helpers; +use League\Fractal; +use League\Fractal\Serializer\ArraySerializer; +use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; class LikePipeline implements ShouldQueue { @@ -48,11 +49,15 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - if (!$status || $status->url !== null) { - // Ignore notifications to remote statuses, or deleted statuses + if (!$status) { + // Ignore notifications to deleted statuses return; } + if($status->url && $actor->domain == null) { + return $this->remoteLikeDeliver(); + } + $exists = Notification::whereProfileId($status->profile_id) ->whereActorId($actor->id) ->whereAction('like') @@ -78,4 +83,20 @@ class LikePipeline implements ShouldQueue } catch (Exception $e) { } } + + public function remoteLikeDeliver() + { + $like = $this->like; + $status = $this->like->status; + $actor = $this->like->actor; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $activity = $fractal->createData($resource)->toArray(); + + $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; + + Helpers::sendSignedObject($actor, $url, $activity); + } } diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php index 186188ea0..b62c51268 100644 --- a/app/Jobs/SharePipeline/SharePipeline.php +++ b/app/Jobs/SharePipeline/SharePipeline.php @@ -9,6 +9,11 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; 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; class SharePipeline implements ShouldQueue { @@ -60,6 +65,8 @@ class SharePipeline implements ShouldQueue return true; } + $this->remoteAnnounceDeliver(); + try { $notification = new Notification; $notification->profile_id = $target->id; @@ -78,4 +85,56 @@ class SharePipeline implements ShouldQueue Log::error($e); } } + + public function remoteAnnounceDeliver() + { + $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') + ]); + + $requests = function($audience) use ($client, $activity, $profile, $payload) { + foreach($audience as $url) { + $headers = HttpSignature::sign($profile, $url, $activity); + 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(); + + } } diff --git a/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php b/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php index a2b6d6f93..ec4243107 100644 --- a/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php +++ b/app/Jobs/StatusPipeline/StatusActivityPubDeliver.php @@ -49,6 +49,7 @@ class StatusActivityPubDeliver implements ShouldQueue public function handle() { $status = $this->status; + $profile = $status->profile; if($status->local == false || $status->url || $status->uri) { return; @@ -56,12 +57,11 @@ class StatusActivityPubDeliver implements ShouldQueue $audience = $status->profile->getAudienceInbox(); - if(empty($audience) || $status->visibility != 'public') { + if(empty($audience) || $status->scope != 'public') { // Return on profiles with no remote followers return; } - $profile = $status->profile; $fractal = new Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); diff --git a/app/Transformer/ActivityPub/Verb/Announce.php b/app/Transformer/ActivityPub/Verb/Announce.php index b6acb31d1..682885b77 100644 --- a/app/Transformer/ActivityPub/Verb/Announce.php +++ b/app/Transformer/ActivityPub/Verb/Announce.php @@ -11,9 +11,16 @@ class Announce extends Fractal\TransformerAbstract { return [ '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $status->permalink(), 'type' => 'Announce', 'actor' => $status->profile->permalink(), - 'object' => $status->parent()->url() + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], + 'cc' => [ + $status->profile->permalink(), + $status->profile->follower_url ?? $status->profile->permalink('/followers') + ], + 'published' => $status->created_at->format(DATE_ISO8601), + 'object' => $status->parent()->url(), ]; } } \ No newline at end of file diff --git a/app/Transformer/ActivityPub/Verb/Like.php b/app/Transformer/ActivityPub/Verb/Like.php index 5662ab758..b6f699158 100644 --- a/app/Transformer/ActivityPub/Verb/Like.php +++ b/app/Transformer/ActivityPub/Verb/Like.php @@ -11,6 +11,7 @@ class Like extends Fractal\TransformerAbstract { return [ '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $like->actor->permalink('#likes/'.$like->id), 'type' => 'Like', 'actor' => $like->actor->permalink(), 'object' => $like->status->url() diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index 1c033c1f1..8a16e4dfd 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -34,7 +34,7 @@ class StatusTransformer extends Fractal\TransformerAbstract 'muted' => null, 'sensitive' => (bool) $status->is_nsfw, 'spoiler_text' => $status->cw_summary, - 'visibility' => $status->visibility, + 'visibility' => $status->visibility ?? $status->scope, 'application' => [ 'name' => 'web', 'website' => null diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index a6e182624..88a30d585 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -206,7 +206,7 @@ class Helpers { return self::fetchFromUrl($url); } - public static function statusFirstOrFetch($url, $replyTo = true) + public static function statusFirstOrFetch($url, $replyTo = false) { $url = self::validateUrl($url); if($url == false) { @@ -337,6 +337,11 @@ class Helpers { } } + public static function statusFetch($url) + { + return self::statusFirstOrFetch($url); + } + public static function importNoteAttachment($data, Status $status) { if(self::verifyAttachments($data) == false) { @@ -435,6 +440,11 @@ class Helpers { return $profile; } + public static function profileFetch($url) + { + return self::profileFirstOrNew($url); + } + public static function sendSignedObject($senderProfile, $url, $body) { abort_if(!self::validateUrl($url), 400); diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 4731811f9..2bfa1c785 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -151,7 +151,7 @@ class Inbox if(Status::whereUrl($url)->exists()) { return; } - Helpers::statusFirstOrFetch($url, false); + Helpers::statusFetch($url); return; } @@ -205,21 +205,27 @@ class Inbox { $actor = $this->actorFirstOrCreate($this->payload['actor']); $activity = $this->payload['object']; + if(!$actor || $actor->domain == null) { return; } + if(Helpers::validateLocalUrl($activity) == false) { return; } - $parent = Helpers::statusFirstOrFetch($activity, true); - if(!$parent) { + + $parent = Helpers::statusFetch($activity); + + if(empty($parent)) { return; } + $status = Status::firstOrCreate([ 'profile_id' => $actor->id, 'reblog_of_id' => $parent->id, - 'type' => 'reply' + 'type' => 'share' ]); + Notification::firstOrCreate([ 'profile_id' => $parent->profile->id, 'actor_id' => $actor->id, @@ -229,6 +235,7 @@ class Inbox 'item_id' => $parent->id, 'item_type' => 'App\Status' ]); + $parent->reblogs_count = $parent->shares()->count(); $parent->save(); } @@ -316,6 +323,20 @@ class Inbox break; case 'Announce': + abort_if(!Helpers::validateLocalUrl($obj), 400); + $status = Helpers::statusFetch($obj); + if(!$status) { + return; + } + Status::whereProfileId($profile->id) + ->whereReblogOfId($status->id) + ->forceDelete(); + Notification::whereProfileId($status->profile->id) + ->whereActorId($profile->id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->forceDelete(); break; case 'Block': @@ -347,6 +368,6 @@ class Inbox ->forceDelete(); break; } - + return; } } diff --git a/public/js/profile.js b/public/js/profile.js index 338f6bd7d..0ddcd0afe 100644 Binary files a/public/js/profile.js and b/public/js/profile.js differ diff --git a/public/js/status.js b/public/js/status.js index b2c231a7c..ba4af018e 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index 4ec4f9432..f4c3175df 100644 Binary files a/public/js/timeline.js and b/public/js/timeline.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 4512a2b1b..cbe51aa43 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/js/components/PostComponent.vue b/resources/assets/js/components/PostComponent.vue index 7902fd6a7..f0be53b1c 100644 --- a/resources/assets/js/components/PostComponent.vue +++ b/resources/assets/js/components/PostComponent.vue @@ -179,14 +179,14 @@