diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa8801233..49b3abfaa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,9 @@
- Updated NotificationCard, fix typo in mention, share and comments. Fixes #2848. ([b37bb426](https://github.com/pixelfed/pixelfed/commit/b37bb426))
- Updated StatusCard.vue, add togglecw events to other presenters. ([9607243f](https://github.com/pixelfed/pixelfed/commit/9607243f))
- Updated presenters, fix content warning layout. ([fc56acb8](https://github.com/pixelfed/pixelfed/commit/fc56acb8))
+- Updated reply blade view, fix missing avatar and media images. ([5fb33772](https://github.com/pixelfed/pixelfed/commit/5fb33772))
+- Updated components, add fallback default avatar. ([726553f5](https://github.com/pixelfed/pixelfed/commit/726553f5))
+- Updated job queue, separate deletes into their own queue. ([7f421392](https://github.com/pixelfed/pixelfed/commit/7f421392))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php
index c17019f1b..8c121a898 100644
--- a/app/Http/Controllers/FederationController.php
+++ b/app/Http/Controllers/FederationController.php
@@ -3,16 +3,17 @@
namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\{
- InboxWorker,
- InboxValidator
+ DeleteWorker,
+ InboxWorker,
+ InboxValidator
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\{
- AccountLog,
- Like,
- Profile,
- Status,
- User
+ AccountLog,
+ Like,
+ Profile,
+ Status,
+ User
};
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
@@ -23,146 +24,158 @@ use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Site\Nodeinfo;
use App\Util\ActivityPub\{
- Helpers,
- HttpSignature,
- Outbox
+ Helpers,
+ HttpSignature,
+ Outbox
};
use Zttp\Zttp;
class FederationController extends Controller
{
- public function nodeinfoWellKnown()
- {
- abort_if(!config('federation.nodeinfo.enabled'), 404);
- return response()->json(Nodeinfo::wellKnown())
- ->header('Access-Control-Allow-Origin','*');
- }
+ public function nodeinfoWellKnown()
+ {
+ abort_if(!config('federation.nodeinfo.enabled'), 404);
+ return response()->json(Nodeinfo::wellKnown())
+ ->header('Access-Control-Allow-Origin','*');
+ }
- public function nodeinfo()
- {
- abort_if(!config('federation.nodeinfo.enabled'), 404);
- return response()->json(Nodeinfo::get())
- ->header('Access-Control-Allow-Origin','*');
- }
+ public function nodeinfo()
+ {
+ abort_if(!config('federation.nodeinfo.enabled'), 404);
+ return response()->json(Nodeinfo::get())
+ ->header('Access-Control-Allow-Origin','*');
+ }
- public function webfinger(Request $request)
- {
- abort_if(!config('federation.webfinger.enabled'), 400);
+ public function webfinger(Request $request)
+ {
+ abort_if(!config('federation.webfinger.enabled'), 400);
- abort_if(!$request->filled('resource'), 400);
+ abort_if(!$request->filled('resource'), 400);
- $resource = $request->input('resource');
- $parsed = Nickname::normalizeProfileUrl($resource);
- if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
- abort(404);
- }
- $username = $parsed['username'];
- $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
- if($profile->status != null) {
- return ProfileController::accountCheck($profile);
- }
- $webfinger = (new Webfinger($profile))->generate();
+ $resource = $request->input('resource');
+ $parsed = Nickname::normalizeProfileUrl($resource);
+ if(empty($parsed) || $parsed['domain'] !== config('pixelfed.domain.app')) {
+ abort(404);
+ }
+ $username = $parsed['username'];
+ $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
+ if($profile->status != null) {
+ return ProfileController::accountCheck($profile);
+ }
+ $webfinger = (new Webfinger($profile))->generate();
- return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
- ->header('Access-Control-Allow-Origin','*');
- }
+ return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
+ ->header('Access-Control-Allow-Origin','*');
+ }
- public function hostMeta(Request $request)
- {
- abort_if(!config('federation.webfinger.enabled'), 404);
+ public function hostMeta(Request $request)
+ {
+ abort_if(!config('federation.webfinger.enabled'), 404);
- $path = route('well-known.webfinger');
- $xml = '';
+ $path = route('well-known.webfinger');
+ $xml = '';
- return response($xml)->header('Content-Type', 'application/xrd+xml');
- }
+ return response($xml)->header('Content-Type', 'application/xrd+xml');
+ }
- public function userOutbox(Request $request, $username)
- {
- abort_if(!config_cache('federation.activitypub.enabled'), 404);
- abort_if(!config('federation.activitypub.outbox'), 404);
+ public function userOutbox(Request $request, $username)
+ {
+ abort_if(!config_cache('federation.activitypub.enabled'), 404);
+ abort_if(!config('federation.activitypub.outbox'), 404);
- $profile = Profile::whereNull('domain')
- ->whereNull('status')
- ->whereIsPrivate(false)
- ->whereUsername($username)
- ->firstOrFail();
+ $profile = Profile::whereNull('domain')
+ ->whereNull('status')
+ ->whereIsPrivate(false)
+ ->whereUsername($username)
+ ->firstOrFail();
- $key = 'ap:outbox:latest_10:pid:' . $profile->id;
- $ttl = now()->addMinutes(15);
- $res = Cache::remember($key, $ttl, function() use($profile) {
- return Outbox::get($profile);
- });
+ $key = 'ap:outbox:latest_10:pid:' . $profile->id;
+ $ttl = now()->addMinutes(15);
+ $res = Cache::remember($key, $ttl, function() use($profile) {
+ return Outbox::get($profile);
+ });
- return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
- }
+ return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
+ }
- public function userInbox(Request $request, $username)
- {
- abort_if(!config_cache('federation.activitypub.enabled'), 404);
- abort_if(!config('federation.activitypub.inbox'), 404);
+ public function userInbox(Request $request, $username)
+ {
+ abort_if(!config_cache('federation.activitypub.enabled'), 404);
+ abort_if(!config('federation.activitypub.inbox'), 404);
- $headers = $request->headers->all();
- $payload = $request->getContent();
- dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
- return;
- }
+ $headers = $request->headers->all();
+ $payload = $request->getContent();
+ $obj = json_decode($payload, true, 8);
- public function sharedInbox(Request $request)
- {
- abort_if(!config_cache('federation.activitypub.enabled'), 404);
- abort_if(!config('federation.activitypub.sharedInbox'), 404);
+ if(isset($obj['type']) && $obj['type'] === 'Delete') {
+ dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+ } else {
+ dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
+ }
+ return;
+ }
- $headers = $request->headers->all();
- $payload = $request->getContent();
- dispatch(new InboxWorker($headers, $payload))->onQueue('high');
- return;
- }
+ public function sharedInbox(Request $request)
+ {
+ abort_if(!config_cache('federation.activitypub.enabled'), 404);
+ abort_if(!config('federation.activitypub.sharedInbox'), 404);
- public function userFollowing(Request $request, $username)
- {
- abort_if(!config_cache('federation.activitypub.enabled'), 404);
+ $headers = $request->headers->all();
+ $payload = $request->getContent();
+ $obj = json_decode($payload, true, 8);
- $profile = Profile::whereNull('remote_url')
- ->whereUsername($username)
- ->whereIsPrivate(false)
- ->firstOrFail();
+ if(isset($obj['type']) && $obj['type'] === 'Delete') {
+ dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
+ } else {
+ dispatch(new InboxWorker($headers, $payload))->onQueue('high');
+ }
+ return;
+ }
- if($profile->status != null) {
- abort(404);
- }
+ public function userFollowing(Request $request, $username)
+ {
+ abort_if(!config_cache('federation.activitypub.enabled'), 404);
- $obj = [
- '@context' => 'https://www.w3.org/ns/activitystreams',
- 'id' => $request->getUri(),
- 'type' => 'OrderedCollectionPage',
- 'totalItems' => 0,
- 'orderedItems' => []
- ];
- return response()->json($obj);
- }
+ $profile = Profile::whereNull('remote_url')
+ ->whereUsername($username)
+ ->whereIsPrivate(false)
+ ->firstOrFail();
- public function userFollowers(Request $request, $username)
- {
- abort_if(!config_cache('federation.activitypub.enabled'), 404);
+ if($profile->status != null) {
+ abort(404);
+ }
- $profile = Profile::whereNull('remote_url')
- ->whereUsername($username)
- ->whereIsPrivate(false)
- ->firstOrFail();
+ $obj = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'id' => $request->getUri(),
+ 'type' => 'OrderedCollectionPage',
+ 'totalItems' => 0,
+ 'orderedItems' => []
+ ];
+ return response()->json($obj);
+ }
- if($profile->status != null) {
- abort(404);
- }
+ public function userFollowers(Request $request, $username)
+ {
+ abort_if(!config_cache('federation.activitypub.enabled'), 404);
- $obj = [
- '@context' => 'https://www.w3.org/ns/activitystreams',
- 'id' => $request->getUri(),
- 'type' => 'OrderedCollectionPage',
- 'totalItems' => 0,
- 'orderedItems' => []
- ];
+ $profile = Profile::whereNull('remote_url')
+ ->whereUsername($username)
+ ->whereIsPrivate(false)
+ ->firstOrFail();
- return response()->json($obj);
- }
+ if($profile->status != null) {
+ abort(404);
+ }
+
+ $obj = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'id' => $request->getUri(),
+ 'type' => 'OrderedCollectionPage',
+ 'totalItems' => 0,
+ 'orderedItems' => []
+ ];
+
+ return response()->json($obj);
+ }
}
diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php
index e017a9808..a3ce37ee2 100644
--- a/app/Http/Controllers/PublicApiController.php
+++ b/app/Http/Controllers/PublicApiController.php
@@ -573,9 +573,13 @@ class PublicApiController extends Controller
{
abort_unless(Auth::check(), 403);
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
+ $owner = Auth::id() == $profile->user_id;
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
return response()->json([]);
}
+ if(!$owner && $request->page > 5) {
+ return [];
+ }
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
$res = $this->fractal->createData($resource)->toArray();
@@ -600,6 +604,10 @@ class PublicApiController extends Controller
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
+ if(!$owner && $request->page > 5) {
+ return [];
+ }
+
if($search) {
abort_if(!$owner, 404);
$following = $profile->following()
diff --git a/app/Jobs/DeletePipeline/FanoutDeletePipeline.php b/app/Jobs/DeletePipeline/FanoutDeletePipeline.php
new file mode 100644
index 000000000..e1a9a6d11
--- /dev/null
+++ b/app/Jobs/DeletePipeline/FanoutDeletePipeline.php
@@ -0,0 +1,93 @@
+profile = $profile;
+ }
+
+ public function handle()
+ {
+ $profile = $this->profile;
+
+ $client = new Client([
+ 'timeout' => config('federation.activitypub.delivery.timeout')
+ ]);
+
+ $audience = Cache::remember('pf:ap:known_instances', now()->addHours(6), function() {
+ return Profile::whereNotNull('sharedInbox')->groupBy('sharedInbox')->pluck('sharedInbox')->toArray();
+ });
+
+ $activity = [
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => $profile->permalink('#delete'),
+ "type" => "Delete",
+ "actor" => $profile->permalink(),
+ "to" => [
+ "https://www.w3.org/ns/activitystreams#Public",
+ ],
+ "object" => $profile->permalink(),
+ ];
+
+ $payload = json_encode($activity);
+
+ $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();
+
+ return 1;
+ }
+}
diff --git a/app/Jobs/InboxPipeline/DeleteWorker.php b/app/Jobs/InboxPipeline/DeleteWorker.php
new file mode 100644
index 000000000..bed75f1df
--- /dev/null
+++ b/app/Jobs/InboxPipeline/DeleteWorker.php
@@ -0,0 +1,223 @@
+headers = $headers;
+ $this->payload = $payload;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $profile = null;
+ $headers = $this->headers;
+ $payload = json_decode($this->payload, true, 8);
+
+ if(isset($payload['id'])) {
+ $lockKey = 'pf:ap:del-lock:' . hash('sha256', $payload['id']);
+ if(Cache::get($lockKey) !== null) {
+ // Job processed already
+ return 1;
+ }
+ Cache::put($lockKey, 1, 300);
+ }
+
+ if(!isset($headers['signature']) || !isset($headers['date'])) {
+ return;
+ }
+
+ if(empty($headers) || empty($payload)) {
+ return;
+ }
+
+ if( $payload['type'] === 'Delete' &&
+ ( ( is_string($payload['object']) &&
+ $payload['object'] === $payload['actor'] ) ||
+ ( is_array($payload['object']) &&
+ isset($payload['object']['id'], $payload['object']['type']) &&
+ $payload['object']['type'] === 'Person' &&
+ $payload['actor'] === $payload['object']['id']
+ ))
+ ) {
+ $actor = $payload['actor'];
+ $hash = strlen($actor) <= 48 ?
+ 'b:' . base64_encode($actor) :
+ 'h:' . hash('sha256', $actor);
+
+ $lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
+ Cache::lock($lockKey, 10)->block(5, function () use(
+ $headers,
+ $payload,
+ $actor,
+ $hash
+ ) {
+ $key = 'ap:inbox:actor-delete-exists:' . $hash;
+ $actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
+ return Profile::whereRemoteUrl($actor)
+ ->whereNotNull('domain')
+ ->exists();
+ });
+ if($actorDelete) {
+ if($this->verifySignature($headers, $payload) == true) {
+ Cache::set($key, false);
+ $profile = Profile::whereNotNull('domain')
+ ->whereNull('status')
+ ->whereRemoteUrl($actor)
+ ->first();
+ if($profile) {
+ DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
+ }
+ return;
+ } else {
+ // Signature verification failed, exit.
+ return;
+ }
+ } else {
+ // Remote user doesn't exist, exit early.
+ return;
+ }
+ });
+
+ return;
+ }
+
+ $profile = null;
+
+ if($this->verifySignature($headers, $payload) == true) {
+ (new Inbox($headers, $profile, $payload))->handle();
+ return;
+ } else if($this->blindKeyRotation($headers, $payload) == true) {
+ (new Inbox($headers, $profile, $payload))->handle();
+ return;
+ } else {
+ return;
+ }
+ }
+
+ protected function verifySignature($headers, $payload)
+ {
+ $body = $this->payload;
+ $bodyDecoded = $payload;
+ $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
+ $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
+ if(!$signature) {
+ return;
+ }
+ if(!$date) {
+ return;
+ }
+ if(!now()->parse($date)->gt(now()->subDays(1)) ||
+ !now()->parse($date)->lt(now()->addDays(1))
+ ) {
+ return;
+ }
+ $signatureData = HttpSignature::parseSignatureHeader($signature);
+ $keyId = Helpers::validateUrl($signatureData['keyId']);
+ $id = Helpers::validateUrl($bodyDecoded['id']);
+ $keyDomain = parse_url($keyId, PHP_URL_HOST);
+ $idDomain = parse_url($id, PHP_URL_HOST);
+ if(isset($bodyDecoded['object'])
+ && is_array($bodyDecoded['object'])
+ && isset($bodyDecoded['object']['attributedTo'])
+ ) {
+ if(parse_url($bodyDecoded['object']['attributedTo'], PHP_URL_HOST) !== $keyDomain) {
+ return;
+ }
+ }
+ if(!$keyDomain || !$idDomain || $keyDomain !== $idDomain) {
+ return;
+ }
+ $actor = Profile::whereKeyId($keyId)->first();
+ if(!$actor) {
+ $actorUrl = is_array($bodyDecoded['actor']) ? $bodyDecoded['actor'][0] : $bodyDecoded['actor'];
+ $actor = Helpers::profileFirstOrNew($actorUrl);
+ }
+ if(!$actor) {
+ return;
+ }
+ $pkey = openssl_pkey_get_public($actor->public_key);
+ if(!$pkey) {
+ return 0;
+ }
+ $inboxPath = "/f/inbox";
+ list($verified, $headers) = HttpSignature::verify($pkey, $signatureData, $headers, $inboxPath, $body);
+ if($verified == 1) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function blindKeyRotation($headers, $payload)
+ {
+ $signature = is_array($headers['signature']) ? $headers['signature'][0] : $headers['signature'];
+ $date = is_array($headers['date']) ? $headers['date'][0] : $headers['date'];
+ if(!$signature) {
+ return;
+ }
+ if(!$date) {
+ return;
+ }
+ if(!now()->parse($date)->gt(now()->subDays(1)) ||
+ !now()->parse($date)->lt(now()->addDays(1))
+ ) {
+ return;
+ }
+ $signatureData = HttpSignature::parseSignatureHeader($signature);
+ $keyId = Helpers::validateUrl($signatureData['keyId']);
+ $actor = Profile::whereKeyId($keyId)->whereNotNull('remote_url')->first();
+ if(!$actor) {
+ return;
+ }
+ if(Helpers::validateUrl($actor->remote_url) == false) {
+ return;
+ }
+ $res = Zttp::timeout(5)->withHeaders([
+ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+ 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
+ ])->get($actor->remote_url);
+ $res = json_decode($res->body(), true, 8);
+ if($res['publicKey']['id'] !== $actor->key_id) {
+ return;
+ }
+ $actor->public_key = $res['publicKey']['publicKeyPem'];
+ $actor->save();
+ return $this->verifySignature($headers, $payload);
+ }
+}
diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php
index 4f938c747..68ecb118e 100644
--- a/app/Services/FollowerService.php
+++ b/app/Services/FollowerService.php
@@ -6,66 +6,95 @@ use Illuminate\Support\Facades\Redis;
use App\{
Follower,
- Profile
+ Profile,
+ User
};
-class FollowerService {
+class FollowerService
+{
+ const FOLLOWING_KEY = 'pf:services:follow:following:id:';
+ const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
- protected $profile;
- public static $follower_prefix = 'px:profile:followers-v1.3:';
- public static $following_prefix = 'px:profile:following-v1.3:';
-
- public static function build()
+ public static function add($actor, $target)
{
- return new self();
+ Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
+ Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
}
- public function profile(Profile $profile)
+ public static function remove($actor, $target)
{
- $this->profile = $profile;
- self::$follower_prefix .= $profile->id;
- self::$following_prefix .= $profile->id;
- return $this;
+ Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
+ Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
}
- public function followers($limit = 100, $offset = 1)
+ public static function followers($id, $start = 0, $stop = 10)
{
- if(Redis::zcard(self::$follower_prefix) == 0) {
- $followers = $this->profile->followers()->pluck('profile_id');
- $followers->map(function($i) {
- Redis::zadd(self::$follower_prefix, $i, $i);
- });
- return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
- } else {
- return Redis::zrevrange(self::$follower_prefix, $offset, $limit);
- }
+ return Redis::zrange(self::FOLLOWERS_KEY . $id, $start, $stop);
}
-
- public function following($limit = 100, $offset = 1)
+ public static function following($id, $start = 0, $stop = 10)
{
- if(Redis::zcard(self::$following_prefix) == 0) {
- $following = $this->profile->following()->pluck('following_id');
- $following->map(function($i) {
- Redis::zadd(self::$following_prefix, $i, $i);
- });
- return Redis::zrevrange(self::$following_prefix, $offset, $limit);
- } else {
- return Redis::zrevrange(self::$following_prefix, $offset, $limit);
- }
+ return Redis::zrange(self::FOLLOWING_KEY . $id, $start, $stop);
}
public static function follows(string $actor, string $target)
{
- $key = self::$follower_prefix . $target;
- if(Redis::zcard($key) == 0) {
- $p = Profile::findOrFail($target);
- self::build()->profile($p)->followers(1);
- self::build()->profile($p)->following(1);
- return (bool) Redis::zrank($key, $actor);
- } else {
- return (bool) Redis::zrank($key, $actor);
+ return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
+ }
+
+ public static function audience($profile)
+ {
+ return (new self)->getAudienceInboxes($profile);
+ }
+
+ protected function getAudienceInboxes($profile)
+ {
+ if($profile instanceOf User) {
+ return $profile
+ ->profile
+ ->followers()
+ ->whereLocalProfile(false)
+ ->get()
+ ->map(function($follow) {
+ return $follow->sharedInbox ?? $follow->inbox_url;
+ })
+ ->unique()
+ ->values()
+ ->toArray();
}
+
+ if($profile instanceOf Profile) {
+ return $profile
+ ->followers()
+ ->whereLocalProfile(false)
+ ->get()
+ ->map(function($follow) {
+ return $follow->sharedInbox ?? $follow->inbox_url;
+ })
+ ->unique()
+ ->values()
+ ->toArray();
+ }
+
+ if(is_string($profile) || is_integer($profile)) {
+ $profile = Profile::whereNull('domain')->find($profile);
+ if(!$profile) {
+ return [];
+ }
+
+ return $profile
+ ->followers()
+ ->whereLocalProfile(false)
+ ->get()
+ ->map(function($follow) {
+ return $follow->sharedInbox ?? $follow->inbox_url;
+ })
+ ->unique()
+ ->values()
+ ->toArray();
+ }
+
+ return [];
}
}
diff --git a/config/horizon.php b/config/horizon.php
index 62320ee8b..786eb6741 100644
--- a/config/horizon.php
+++ b/config/horizon.php
@@ -2,190 +2,191 @@
return [
- /*
- |--------------------------------------------------------------------------
- | Horizon Domain
- |--------------------------------------------------------------------------
- |
- | This is the subdomain where Horizon will be accessible from. If this
- | setting is null, Horizon will reside under the same domain as the
- | application. Otherwise, this value will serve as the subdomain.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Horizon Domain
+ |--------------------------------------------------------------------------
+ |
+ | This is the subdomain where Horizon will be accessible from. If this
+ | setting is null, Horizon will reside under the same domain as the
+ | application. Otherwise, this value will serve as the subdomain.
+ |
+ */
- 'domain' => null,
+ 'domain' => null,
- /*
- |--------------------------------------------------------------------------
- | Horizon Path
- |--------------------------------------------------------------------------
- |
- | This is the URI path where Horizon will be accessible from. Feel free
- | to change this path to anything you like. Note that the URI will not
- | affect the paths of its internal API that aren't exposed to users.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Horizon Path
+ |--------------------------------------------------------------------------
+ |
+ | This is the URI path where Horizon will be accessible from. Feel free
+ | to change this path to anything you like. Note that the URI will not
+ | affect the paths of its internal API that aren't exposed to users.
+ |
+ */
- 'path' => 'horizon',
+ 'path' => 'horizon',
- /*
- |--------------------------------------------------------------------------
- | Horizon Redis Connection
- |--------------------------------------------------------------------------
- |
- | This is the name of the Redis connection where Horizon will store the
- | meta information required for it to function. It includes the list
- | of supervisors, failed jobs, job metrics, and other information.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Horizon Redis Connection
+ |--------------------------------------------------------------------------
+ |
+ | This is the name of the Redis connection where Horizon will store the
+ | meta information required for it to function. It includes the list
+ | of supervisors, failed jobs, job metrics, and other information.
+ |
+ */
- 'use' => 'default',
+ 'use' => 'default',
- /*
- |--------------------------------------------------------------------------
- | Horizon Redis Prefix
- |--------------------------------------------------------------------------
- |
- | This prefix will be used when storing all Horizon data in Redis. You
- | may modify the prefix when you are running multiple installations
- | of Horizon on the same server so that they don't have problems.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Horizon Redis Prefix
+ |--------------------------------------------------------------------------
+ |
+ | This prefix will be used when storing all Horizon data in Redis. You
+ | may modify the prefix when you are running multiple installations
+ | of Horizon on the same server so that they don't have problems.
+ |
+ */
- 'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
+ 'prefix' => env('HORIZON_PREFIX', 'horizon-'.str_random(8).':'),
- /*
- |--------------------------------------------------------------------------
- | Horizon Route Middleware
- |--------------------------------------------------------------------------
- |
- | These middleware will get attached onto each Horizon route, giving you
- | the chance to add your own middleware to this list or change any of
- | the existing middleware. Or, you can simply stick with this list.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Horizon Route Middleware
+ |--------------------------------------------------------------------------
+ |
+ | These middleware will get attached onto each Horizon route, giving you
+ | the chance to add your own middleware to this list or change any of
+ | the existing middleware. Or, you can simply stick with this list.
+ |
+ */
- 'middleware' => ['web'],
+ 'middleware' => ['web'],
- /*
- |--------------------------------------------------------------------------
- | Queue Wait Time Thresholds
- |--------------------------------------------------------------------------
- |
- | This option allows you to configure when the LongWaitDetected event
- | will be fired. Every connection / queue combination may have its
- | own, unique threshold (in seconds) before this event is fired.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Queue Wait Time Thresholds
+ |--------------------------------------------------------------------------
+ |
+ | This option allows you to configure when the LongWaitDetected event
+ | will be fired. Every connection / queue combination may have its
+ | own, unique threshold (in seconds) before this event is fired.
+ |
+ */
- 'waits' => [
- 'redis:feed' => 30,
- 'redis:default' => 30,
- 'redis:high' => 30,
- ],
+ 'waits' => [
+ 'redis:feed' => 30,
+ 'redis:default' => 30,
+ 'redis:high' => 30,
+ 'redis:delete' => 30
+ ],
- /*
- |--------------------------------------------------------------------------
- | Job Trimming Times
- |--------------------------------------------------------------------------
- |
- | Here you can configure for how long (in minutes) you desire Horizon to
- | persist the recent and failed jobs. Typically, recent jobs are kept
- | for one hour while all failed jobs are stored for an entire week.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Job Trimming Times
+ |--------------------------------------------------------------------------
+ |
+ | Here you can configure for how long (in minutes) you desire Horizon to
+ | persist the recent and failed jobs. Typically, recent jobs are kept
+ | for one hour while all failed jobs are stored for an entire week.
+ |
+ */
- 'trim' => [
- 'recent' => 60,
- 'pending' => 60,
- 'completed' => 60,
- 'recent_failed' => 10080,
- 'failed' => 10080,
- 'monitored' => 10080,
- ],
+ 'trim' => [
+ 'recent' => 60,
+ 'pending' => 60,
+ 'completed' => 60,
+ 'recent_failed' => 10080,
+ 'failed' => 10080,
+ 'monitored' => 10080,
+ ],
- /*
- |--------------------------------------------------------------------------
- | Metrics
- |--------------------------------------------------------------------------
- |
- | Here you can configure how many snapshots should be kept to display in
- | the metrics graph. This will get used in combination with Horizon's
- | `horizon:snapshot` schedule to define how long to retain metrics.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Metrics
+ |--------------------------------------------------------------------------
+ |
+ | Here you can configure how many snapshots should be kept to display in
+ | the metrics graph. This will get used in combination with Horizon's
+ | `horizon:snapshot` schedule to define how long to retain metrics.
+ |
+ */
- 'metrics' => [
- 'trim_snapshots' => [
- 'job' => 24,
- 'queue' => 24,
- ],
- ],
+ 'metrics' => [
+ 'trim_snapshots' => [
+ 'job' => 24,
+ 'queue' => 24,
+ ],
+ ],
- /*
- |--------------------------------------------------------------------------
- | Fast Termination
- |--------------------------------------------------------------------------
- |
- | When this option is enabled, Horizon's "terminate" command will not
- | wait on all of the workers to terminate unless the --wait option
- | is provided. Fast termination can shorten deployment delay by
- | allowing a new instance of Horizon to start while the last
- | instance will continue to terminate each of its workers.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Fast Termination
+ |--------------------------------------------------------------------------
+ |
+ | When this option is enabled, Horizon's "terminate" command will not
+ | wait on all of the workers to terminate unless the --wait option
+ | is provided. Fast termination can shorten deployment delay by
+ | allowing a new instance of Horizon to start while the last
+ | instance will continue to terminate each of its workers.
+ |
+ */
- 'fast_termination' => false,
+ 'fast_termination' => false,
- /*
- |--------------------------------------------------------------------------
- | Memory Limit (MB)
- |--------------------------------------------------------------------------
- |
- | This value describes the maximum amount of memory the Horizon worker
- | may consume before it is terminated and restarted. You should set
- | this value according to the resources available to your server.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Memory Limit (MB)
+ |--------------------------------------------------------------------------
+ |
+ | This value describes the maximum amount of memory the Horizon worker
+ | may consume before it is terminated and restarted. You should set
+ | this value according to the resources available to your server.
+ |
+ */
- 'memory_limit' => 64,
+ 'memory_limit' => 64,
- /*
- |--------------------------------------------------------------------------
- | Queue Worker Configuration
- |--------------------------------------------------------------------------
- |
- | Here you may define the queue worker settings used by your application
- | in all environments. These supervisors and settings handle all your
- | queued jobs and will be provisioned by Horizon during deployment.
- |
- */
+ /*
+ |--------------------------------------------------------------------------
+ | Queue Worker Configuration
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define the queue worker settings used by your application
+ | in all environments. These supervisors and settings handle all your
+ | queued jobs and will be provisioned by Horizon during deployment.
+ |
+ */
- 'environments' => [
- 'production' => [
- 'supervisor-1' => [
- 'connection' => 'redis',
- 'queue' => ['high', 'default', 'feed'],
- 'balance' => 'auto',
- 'maxProcesses' => 20,
- 'memory' => 128,
- 'tries' => 3,
- 'nice' => 0,
- ],
- ],
+ 'environments' => [
+ 'production' => [
+ 'supervisor-1' => [
+ 'connection' => 'redis',
+ 'queue' => ['high', 'default', 'feed', 'delete'],
+ 'balance' => 'auto',
+ 'maxProcesses' => 20,
+ 'memory' => 128,
+ 'tries' => 3,
+ 'nice' => 0,
+ ],
+ ],
- 'local' => [
- 'supervisor-1' => [
- 'connection' => 'redis',
- 'queue' => ['high', 'default', 'feed'],
- 'balance' => 'auto',
- 'maxProcesses' => 20,
- 'memory' => 128,
- 'tries' => 3,
- 'nice' => 0,
- ],
- ],
- ],
+ 'local' => [
+ 'supervisor-1' => [
+ 'connection' => 'redis',
+ 'queue' => ['high', 'default', 'feed', 'delete'],
+ 'balance' => 'auto',
+ 'maxProcesses' => 20,
+ 'memory' => 128,
+ 'tries' => 3,
+ 'nice' => 0,
+ ],
+ ],
+ ],
- 'darkmode' => env('HORIZON_DARKMODE', false),
+ 'darkmode' => env('HORIZON_DARKMODE', false),
];
diff --git a/public/js/direct.js b/public/js/direct.js
index 221abfc24..c6499ef86 100644
Binary files a/public/js/direct.js and b/public/js/direct.js differ
diff --git a/public/js/status.js b/public/js/status.js
index 6aac08e77..1749b22a6 100644
Binary files a/public/js/status.js and b/public/js/status.js differ
diff --git a/public/js/timeline.js b/public/js/timeline.js
index ba472a81b..1ab6c883d 100644
Binary files a/public/js/timeline.js and b/public/js/timeline.js differ
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index ecf20d48c..c6ae53c3d 100644
Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ
diff --git a/resources/assets/js/components/Direct.vue b/resources/assets/js/components/Direct.vue
index b9093dabd..f0901ed03 100644
--- a/resources/assets/js/components/Direct.vue
+++ b/resources/assets/js/components/Direct.vue
@@ -26,10 +26,10 @@
-