Update activitpub setting, use config_cache()

This commit is contained in:
Daniel Supernault 2024-03-12 02:20:37 -06:00
parent 40478f258a
commit 5071aaf408
No known key found for this signature in database
GPG key ID: 23740873EE6F76A1
10 changed files with 833 additions and 839 deletions

View file

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

View file

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

View file

@ -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); return $this->showActivityPub($request, $status);
} }

View file

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

View file

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

View file

@ -2,126 +2,122 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use DB, Cache, Storage; use App\AccountInterstitial;
use App\{ use App\Bookmark;
AccountInterstitial, use App\CollectionItem;
Bookmark, use App\DirectMessage;
CollectionItem, use App\Jobs\MediaPipeline\MediaDeletePipeline;
DirectMessage, use App\Like;
Like, use App\Media;
Media, use App\MediaTag;
MediaTag, use App\Mention;
Mention, use App\Notification;
Notification, use App\Report;
Report, use App\Services\CollectionService;
Status, use App\Services\NotificationService;
StatusArchived, use App\Services\StatusService;
StatusHashtag, use App\Status;
StatusView 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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use League\Fractal; use League\Fractal;
use Illuminate\Support\Str;
use League\Fractal\Serializer\ArraySerializer; 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 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. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
public $timeout = 900; public $timeout = 900;
public $tries = 2; public $tries = 2;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Status $status) public function __construct(Status $status)
{ {
$this->status = $status; $this->status = $status;
} }
/** /**
* Execute the job. * Execute the job.
* *
* @return void * @return void
*/ */
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$profile = $this->status->profile; $profile = $this->status->profile;
StatusService::del($status->id, true); StatusService::del($status->id, true);
if($profile) { if ($profile) {
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->status_count = $profile->status_count - 1;
$profile->save(); $profile->save();
} }
} }
Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
if(config_cache('federation.activitypub.enabled') == true) { if ((bool) config_cache('federation.activitypub.enabled') == true) {
return $this->fanoutDelete($status); return $this->fanoutDelete($status);
} else { } else {
return $this->unlinkRemoveMedia($status); return $this->unlinkRemoveMedia($status);
} }
} }
public function unlinkRemoveMedia($status) public function unlinkRemoveMedia($status)
{ {
Media::whereStatusId($status->id) Media::whereStatusId($status->id)
->get() ->get()
->each(function($media) { ->each(function ($media) {
MediaDeletePipeline::dispatch($media); MediaDeletePipeline::dispatch($media);
}); });
if($status->in_reply_to_id) { if ($status->in_reply_to_id) {
$parent = Status::findOrFail($status->in_reply_to_id); $parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count; $parent->reply_count--;
$parent->save(); $parent->save();
StatusService::del($parent->id); StatusService::del($parent->id);
} }
Bookmark::whereStatusId($status->id)->delete(); Bookmark::whereStatusId($status->id)->delete();
CollectionItem::whereObjectType('App\Status') CollectionItem::whereObjectType('App\Status')
->whereObjectId($status->id) ->whereObjectId($status->id)
->get() ->get()
->each(function($col) { ->each(function ($col) {
CollectionService::removeItem($col->collection_id, $col->object_id); CollectionService::removeItem($col->collection_id, $col->object_id);
$col->delete(); $col->delete();
}); });
$dms = DirectMessage::whereStatusId($status->id)->get(); $dms = DirectMessage::whereStatusId($status->id)->get();
foreach($dms as $dm) { foreach ($dms as $dm) {
$not = Notification::whereItemType('App\DirectMessage') $not = Notification::whereItemType('App\DirectMessage')
->whereItemId($dm->id) ->whereItemId($dm->id)
->first(); ->first();
if($not) { if ($not) {
NotificationService::del($not->profile_id, $not->id); NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly(); $not->forceDeleteQuietly();
} }
@ -130,11 +126,11 @@ class StatusDelete implements ShouldQueue
Like::whereStatusId($status->id)->delete(); Like::whereStatusId($status->id)->delete();
$mediaTags = MediaTag::where('status_id', $status->id)->get(); $mediaTags = MediaTag::where('status_id', $status->id)->get();
foreach($mediaTags as $mtag) { foreach ($mediaTags as $mtag) {
$not = Notification::whereItemType('App\MediaTag') $not = Notification::whereItemType('App\MediaTag')
->whereItemId($mtag->id) ->whereItemId($mtag->id)
->first(); ->first();
if($not) { if ($not) {
NotificationService::del($not->profile_id, $not->id); NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly(); $not->forceDeleteQuietly();
} }
@ -142,85 +138,85 @@ class StatusDelete implements ShouldQueue
} }
Mention::whereStatusId($status->id)->forceDelete(); Mention::whereStatusId($status->id)->forceDelete();
Notification::whereItemType('App\Status') Notification::whereItemType('App\Status')
->whereItemId($status->id) ->whereItemId($status->id)
->forceDelete(); ->forceDelete();
Report::whereObjectType('App\Status') Report::whereObjectType('App\Status')
->whereObjectId($status->id) ->whereObjectId($status->id)
->delete(); ->delete();
StatusArchived::whereStatusId($status->id)->delete(); StatusArchived::whereStatusId($status->id)->delete();
StatusHashtag::whereStatusId($status->id)->delete(); StatusHashtag::whereStatusId($status->id)->delete();
StatusView::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete();
Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
AccountInterstitial::where('item_type', 'App\Status') AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id) ->where('item_id', $status->id)
->delete(); ->delete();
$status->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; 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;
}
} }

View file

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

View file

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

View file

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

View file

@ -298,7 +298,7 @@
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
<td><strong>ACTIVITY_PUB</strong></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>
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>