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

ActivityPub improvements (Announce + Like)
This commit is contained in:
daniel 2019-06-25 00:35:51 -06:00 committed by GitHub
commit d316a647c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 150 additions and 131 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

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