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

No messages found :(

-
+
- Generic placeholder image +

@@ -62,10 +62,10 @@

No messages found :(

-
+
- Generic placeholder image +

@@ -98,10 +98,10 @@

No messages found :(

-
+
- Generic placeholder image +

@@ -373,4 +373,4 @@ export default { } } } - \ No newline at end of file + diff --git a/resources/assets/js/components/DirectMessage.vue b/resources/assets/js/components/DirectMessage.vue index 1d4dbe873..3f2346870 100644 --- a/resources/assets/js/components/DirectMessage.vue +++ b/resources/assets/js/components/DirectMessage.vue @@ -11,7 +11,7 @@

- Generic placeholder image +

{{thread.name}} @@ -40,10 +40,10 @@

  • - avatar + avatar

    - +

    @@ -90,7 +90,7 @@

    - +

    @@ -134,7 +134,7 @@

     

    - avatar + avatar
  • @@ -682,4 +682,4 @@ } } } - \ No newline at end of file + diff --git a/resources/assets/js/components/PostComponent.vue b/resources/assets/js/components/PostComponent.vue index 6e7a657ed..38b966ffb 100644 --- a/resources/assets/js/components/PostComponent.vue +++ b/resources/assets/js/components/PostComponent.vue @@ -466,7 +466,7 @@
    - +

    diff --git a/resources/assets/js/components/Profile.vue b/resources/assets/js/components/Profile.vue index eb9bb6cef..6545a8eae 100644 --- a/resources/assets/js/components/Profile.vue +++ b/resources/assets/js/components/Profile.vue @@ -43,10 +43,10 @@

    - +
    - +
    @@ -85,10 +85,10 @@
    - +
    - +

    -

    By using this embed, you agree to our Terms of Use

    -
    - + id="ctx-embed-modal" + hide-header + hide-footer + centered + rounded + size="md" + body-class="p-2 rounded"> +
    + +
    + +

    By using this embed, you agree to our Terms of Use

    +
    +