Merge pull request #1461 from pixelfed/frontend-ui-refactor

ActivityPub improvements (Announce + Like)
pull/1479/head
daniel 6 years ago committed by GitHub
commit d316a647c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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(),

@ -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 <a href="#">here</a> 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;

@ -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);
}
}

@ -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();
}
}

@ -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());

@ -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(),
];
}
}

@ -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()

@ -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

@ -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);

@ -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;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -14,10 +14,10 @@
"/js/discover.js": "/js/discover.js?id=772fbc6c176aaa9039ec",
"/js/loops.js": "/js/loops.js?id=a2291e15b8b246621b07",
"/js/mode-dot.js": "/js/mode-dot.js?id=3c67c8f827dc52536a4b",
"/js/profile.js": "/js/profile.js?id=fe87d1e6d75323c9f7d9",
"/js/profile.js": "/js/profile.js?id=c45f4c00d9e3c9aac7c9",
"/js/quill.js": "/js/quill.js?id=d2e82cff48024d45384a",
"/js/search.js": "/js/search.js?id=38a3a6a07e3b37a1e444",
"/js/status.js": "/js/status.js?id=8333e903aaf01fc6436d",
"/js/status.js": "/js/status.js?id=28de331daaf3447a3d3f",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=1f7b44b76af99c0b4e38",
"/js/timeline.js": "/js/timeline.js?id=3b2c69ca0262332e6542"
"/js/timeline.js": "/js/timeline.js?id=52af49a558f589d1b569"
}

@ -179,14 +179,14 @@
<div class="reactions my-1">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
</div>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>
@ -268,13 +268,13 @@
<div class="reactions py-2">
<h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
<h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary float-right cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn float-right cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
</div>
<div class="reaction-counts font-weight-bold mb-0">
<span style="cursor:pointer;" v-on:click="likesModal">
<span class="like-count">{{status.favourites_count || 0}}</span> likes
</span>
<span class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span v-if="status.visibility == 'public'" class="float-right" style="cursor:pointer;" v-on:click="sharesModal">
<span class="share-count pl-4">{{status.reblogs_count || 0}}</span> shares
</span>
</div>

@ -242,7 +242,7 @@
<div class="reactions my-1" v-if="user.hasOwnProperty('id')">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div>
<div class="likes font-weight-bold">

@ -117,7 +117,7 @@
<div v-if="!modes.distractionFree" class="reactions my-1">
<h3 v-bind:class="[status.favourited ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus(status, $event)"></h3>
<h3 v-if="!status.comments_disabled" class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus(status, $event)"></h3>
<h3 v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
<h3 v-if="status.visibility == 'public'" v-bind:class="[status.reblogged ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus(status, $event)"></h3>
</div>
<div class="likes font-weight-bold" v-if="expLc(status) == true && !modes.distractionFree">

Loading…
Cancel
Save