From af28aecf216bfd1a95b998f2a7180311a3d1a495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20van=20L=C3=BCck?= Date: Sun, 4 Dec 2022 22:54:27 +0100 Subject: [PATCH 001/977] Add a command to import emoji archives --- app/Console/Commands/ImportEmojis.php | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 app/Console/Commands/ImportEmojis.php diff --git a/app/Console/Commands/ImportEmojis.php b/app/Console/Commands/ImportEmojis.php new file mode 100644 index 000000000..09f5480a5 --- /dev/null +++ b/app/Console/Commands/ImportEmojis.php @@ -0,0 +1,110 @@ +argument('path'); + + if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') { + $this->error('Path does not exist or is not a tarfile'); + return Command::FAILURE; + } + + $imported = 0; + $skipped = 0; + $failed = 0; + + $tar = new \PharData($path); + $tar->decompress(); + + foreach (new \RecursiveIteratorIterator($tar) as $entry) { + $this->line("Processing {$entry->getFilename()}"); + if (!$entry->isFile() || !$this->isImage($entry)) { + $failed++; + continue; + } + + $filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME); + $extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION); + + // Skip macOS shadow files + if (str_starts_with($filename, '._')) { + continue; + } + + $shortcode = implode('', [ + $this->option('prefix'), + $filename, + $this->option('suffix'), + ]); + + $customEmoji = CustomEmoji::whereShortcode($shortcode)->first(); + + if ($customEmoji && !$this->option('overwrite')) { + $skipped++; + continue; + } + + $emoji = $customEmoji ?? new CustomEmoji(); + $emoji->shortcode = $shortcode; + $emoji->domain = config('pixelfed.domain.app'); + $emoji->disabled = $this->option('disabled'); + $emoji->save(); + + $fileName = $emoji->id . '.' . $extension; + Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName); + $emoji->media_path = 'emoji/' . $fileName; + $emoji->save(); + $imported++; + Cache::forget('pf:custom_emoji'); + } + + $this->line("Imported: {$imported}"); + $this->line("Skipped: {$skipped}"); + $this->line("Failed: {$failed}"); + + //delete file + unlink(str_replace('.tar.gz', '.tar', $path)); + + return Command::SUCCESS; + } + + private function isImage($file) + { + $image = getimagesize($file->getPathname()); + return $image !== false; + } +} From c96bcd559df3ba8b6fae65cfee2e71d3b7184fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20van=20L=C3=BCck?= Date: Thu, 8 Dec 2022 21:10:10 +0100 Subject: [PATCH 002/977] Check imported emojis for mimetype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nils van Lück --- app/Console/Commands/ImportEmojis.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/ImportEmojis.php b/app/Console/Commands/ImportEmojis.php index 09f5480a5..77a0c29a4 100644 --- a/app/Console/Commands/ImportEmojis.php +++ b/app/Console/Commands/ImportEmojis.php @@ -52,7 +52,7 @@ class ImportEmojis extends Command foreach (new \RecursiveIteratorIterator($tar) as $entry) { $this->line("Processing {$entry->getFilename()}"); - if (!$entry->isFile() || !$this->isImage($entry)) { + if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) { $failed++; continue; } @@ -107,4 +107,12 @@ class ImportEmojis extends Command $image = getimagesize($file->getPathname()); return $image !== false; } + + private function isEmoji($filename) + { + $allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp']; + $mimeType = mime_content_type($filename); + + return in_array($mimeType, $allowedMimeTypes); + } } From 312bc0668518a4e653a24009c58ee3f68f93f091 Mon Sep 17 00:00:00 2001 From: Shlee Date: Sun, 11 Dec 2022 15:09:29 +1030 Subject: [PATCH 003/977] Update NotificationCard.vue --- resources/assets/js/components/NotificationCard.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/components/NotificationCard.vue b/resources/assets/js/components/NotificationCard.vue index b2230cbca..d0452035d 100644 --- a/resources/assets/js/components/NotificationCard.vue +++ b/resources/assets/js/components/NotificationCard.vue @@ -34,7 +34,16 @@

- {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} commented on your post. + {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} commented on your + + post. + + + + + + post. +

From 031290d9873f51084387d176f1444d2b5f105d60 Mon Sep 17 00:00:00 2001 From: Shlee Date: Mon, 12 Dec 2022 00:31:34 +1030 Subject: [PATCH 004/977] Update NotificationCard.vue --- resources/assets/js/components/NotificationCard.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/components/NotificationCard.vue b/resources/assets/js/components/NotificationCard.vue index d0452035d..5c6eb6da5 100644 --- a/resources/assets/js/components/NotificationCard.vue +++ b/resources/assets/js/components/NotificationCard.vue @@ -73,7 +73,16 @@

- {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} shared your post. + {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} shared your + + post. + + + + + + post. +

From a6a0333170491db85349062a8eb68f58b41e9542 Mon Sep 17 00:00:00 2001 From: Happyfeet01 <3295104+Happyfeet01@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:05:11 +0200 Subject: [PATCH 005/977] Update Dockerfile.apache Update libwp6 to libwp7 --- contrib/docker/Dockerfile.apache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/docker/Dockerfile.apache b/contrib/docker/Dockerfile.apache index 9c33aee17..a400f8797 100644 --- a/contrib/docker/Dockerfile.apache +++ b/contrib/docker/Dockerfile.apache @@ -33,7 +33,7 @@ RUN apt-get update \ # Required for GD libxpm4 \ libxpm-dev \ - libwebp6 \ + libwebp7 \ libwebp-dev \ ## Video Processing ffmpeg \ From 1ea65db70dc9fbc3392f6fc6ce48a4fbe2c3e4e1 Mon Sep 17 00:00:00 2001 From: Happyfeet01 <3295104+Happyfeet01@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:05:55 +0200 Subject: [PATCH 006/977] Update Dockerfile.fpm update libwp6 to libwp7 --- contrib/docker/Dockerfile.fpm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/docker/Dockerfile.fpm b/contrib/docker/Dockerfile.fpm index 0b8e5c113..1bb0a15f7 100644 --- a/contrib/docker/Dockerfile.fpm +++ b/contrib/docker/Dockerfile.fpm @@ -33,7 +33,7 @@ RUN apt-get update \ # Required for GD libxpm4 \ libxpm-dev \ - libwebp6 \ + libwebp7 \ libwebp-dev \ ## Video Processing ffmpeg \ From fdb51d1f5a66caf13b368b83f3c6650e5644b0c8 Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Fri, 20 Oct 2023 21:09:29 +0000 Subject: [PATCH 007/977] Add check if collection is empty before publishing --- .../js/components/CollectionComponent.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/components/CollectionComponent.vue b/resources/assets/js/components/CollectionComponent.vue index dd7ebf433..3f77cfc13 100644 --- a/resources/assets/js/components/CollectionComponent.vue +++ b/resources/assets/js/components/CollectionComponent.vue @@ -205,12 +205,20 @@
+ +
-
+

Portfolio URL

{{ settings.url }}

diff --git a/resources/assets/js/components/partials/CommentFeed.vue b/resources/assets/js/components/partials/CommentFeed.vue index ee29e7c69..f471228e8 100644 --- a/resources/assets/js/components/partials/CommentFeed.vue +++ b/resources/assets/js/components/partials/CommentFeed.vue @@ -18,8 +18,8 @@
-
- diff --git a/resources/assets/js/components/partials/StatusCard.vue b/resources/assets/js/components/partials/StatusCard.vue index 7bb2f6697..1efa6e2eb 100644 --- a/resources/assets/js/components/partials/StatusCard.vue +++ b/resources/assets/js/components/partials/StatusCard.vue @@ -8,7 +8,6 @@
- Loading... · @@ -61,7 +60,6 @@
- Loading... From c7b304ef20880c7808b1748c19907e830d1e0b0d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 6 Nov 2023 02:08:51 -0700 Subject: [PATCH 014/977] Update http client --- app/Jobs/InboxPipeline/InboxValidator.php | 2 +- app/Jobs/InboxPipeline/InboxWorker.php | 2 +- app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php | 2 +- app/Services/ActivityPubFetchService.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Jobs/InboxPipeline/InboxValidator.php b/app/Jobs/InboxPipeline/InboxValidator.php index 4017d3acd..8d0f414c5 100644 --- a/app/Jobs/InboxPipeline/InboxValidator.php +++ b/app/Jobs/InboxPipeline/InboxValidator.php @@ -193,7 +193,7 @@ class InboxValidator implements ShouldQueue } try { - $res = Http::timeout(20)->withHeaders([ + $res = Http::withOptions(['allow_redirects' => false])->timeout(20)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/InboxPipeline/InboxWorker.php b/app/Jobs/InboxPipeline/InboxWorker.php index c8508c0fc..1bc88507d 100644 --- a/app/Jobs/InboxPipeline/InboxWorker.php +++ b/app/Jobs/InboxPipeline/InboxWorker.php @@ -173,7 +173,7 @@ class InboxWorker implements ShouldQueue } try { - $res = Http::timeout(20)->withHeaders([ + $res = Http::withOptions(['allow_redirects' => false])->timeout(20)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php b/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php index 6cb11ddc6..23b8716c1 100644 --- a/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php +++ b/app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php @@ -90,7 +90,7 @@ class StatusRemoteUpdatePipeline implements ShouldQueue ]); $nm->each(function($n, $key) use($status) { - $res = Http::retry(3, 100, throw: false)->head($n['url']); + $res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']); if(!$res->successful()) { return; diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php index 3d1980a11..cbf153ecb 100644 --- a/app/Services/ActivityPubFetchService.php +++ b/app/Services/ActivityPubFetchService.php @@ -28,7 +28,7 @@ class ActivityPubFetchService $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; try { - $res = Http::withHeaders($headers) + $res = Http::withOptions(['allow_redirects' => false])->withHeaders($headers) ->timeout(30) ->connectTimeout(5) ->retry(3, 500) From 5b3a56102f853c5ebcfd321302776c0e775b9aaf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 7 Nov 2023 00:49:36 -0700 Subject: [PATCH 015/977] Add S3 command to rewrite media urls --- app/Console/Commands/MediaCloudUrlRewrite.php | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 app/Console/Commands/MediaCloudUrlRewrite.php diff --git a/app/Console/Commands/MediaCloudUrlRewrite.php b/app/Console/Commands/MediaCloudUrlRewrite.php new file mode 100644 index 000000000..54329f7c7 --- /dev/null +++ b/app/Console/Commands/MediaCloudUrlRewrite.php @@ -0,0 +1,140 @@ + 'The old S3 domain', + 'newDomain' => 'The new S3 domain' + ]; + } + /** + * The console command description. + * + * @var string + */ + protected $description = 'Rewrite S3 media urls from local users'; + + /** + * Execute the console command. + */ + public function handle() + { + $this->preflightCheck(); + $this->bootMessage(); + $this->confirmCloudUrl(); + } + + protected function preflightCheck() + { + if(config_cache('pixelfed.cloud_storage') != true) { + $this->info('Error: Cloud storage is not enabled!'); + $this->error('Aborting...'); + exit; + } + } + + protected function bootMessage() + { + $this->info(' ____ _ ______ __ '); + $this->info(' / __ \(_) _____ / / __/__ ____/ / '); + $this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / '); + $this->info(' / ____/ /> info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ '); + $this->info(' '); + $this->info(' Media Cloud Url Rewrite Tool'); + $this->info(' ==='); + $this->info(' Old S3: ' . trim($this->argument('oldDomain'))); + $this->info(' New S3: ' . trim($this->argument('newDomain'))); + $this->info(' '); + } + + protected function confirmCloudUrl() + { + $disk = Storage::disk(config('filesystems.cloud'))->url('test'); + $domain = parse_url($disk, PHP_URL_HOST); + if(trim($this->argument('newDomain')) !== $domain) { + $this->error('Error: The new S3 domain you entered is not currently configured'); + exit; + } + + if(!$this->confirm('Confirm this is correct')) { + $this->error('Aborting...'); + exit; + } + + $this->updateUrls(); + } + + protected function updateUrls() + { + $this->info('Updating urls...'); + $oldDomain = trim($this->argument('oldDomain')); + $newDomain = trim($this->argument('newDomain')); + $disk = Storage::disk(config('filesystems.cloud')); + $count = Media::whereNotNull('cdn_url')->count(); + $bar = $this->output->createProgressBar($count); + $counter = 0; + $bar->start(); + foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) { + if(strncmp($media->media_path, 'http', 4) === 0) { + $bar->advance(); + continue; + } + $cdnHost = parse_url($media->cdn_url, PHP_URL_HOST); + if($oldDomain != $cdnHost || $newDomain == $cdnHost) { + $bar->advance(); + continue; + } + + $media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url); + + if($media->thumbnail_url != null) { + $thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST); + if($thumbHost == $oldDomain) { + $thumbUrl = $disk->url($media->thumbnail_path); + $media->thumbnail_url = $thumbUrl; + } + } + + if($media->optimized_url != null) { + $optiHost = parse_url($media->optimized_url, PHP_URL_HOST); + if($optiHost == $oldDomain) { + $optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url); + $media->optimized_url = $optiUrl; + } + } + + $media->save(); + $counter++; + $bar->advance(); + } + + $bar->finish(); + + $this->line(' '); + $this->info('Finished! Updated ' . $counter . ' total records!'); + $this->line(' '); + $this->info('Tip: Run `php artisan cache:clear` to purge cached urls'); + } +} From d84c84c1e2abcbc7c42d117840a950fdc2270a34 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 7 Nov 2023 00:54:01 -0700 Subject: [PATCH 016/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02ae77b6b..48ed36a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12)) - Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7)) - Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde)) +- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) From ddc217147c8b4ef8c0c852369dc080b36aa66efe Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 7 Nov 2023 02:24:52 -0700 Subject: [PATCH 017/977] Update ApiV1Controller, fix mutes in home feed --- app/Http/Controllers/Api/ApiV1Controller.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 9e8f7ef23..8397faa66 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2155,6 +2155,12 @@ class ApiV1Controller extends Controller return $following->push($pid)->toArray(); }); + $muted = UserFilterService::mutes($pid); + + if($muted && count($muted)) { + $following = array_diff($following, $muted); + } + if($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; From ff272292ef55af81095a1536998f65288a51b3c5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 7 Nov 2023 02:27:16 -0700 Subject: [PATCH 018/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ed36a44..47c05e1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089)) - Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e)) - Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a)) +- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 21218c794bf3633be114053edb837da0ef8a6ebb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 9 Nov 2023 02:47:20 -0700 Subject: [PATCH 019/977] Update AP helpers, improve preferredUsername validation --- app/Util/ActivityPub/Helpers.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 1304f0811..989334926 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -760,6 +760,13 @@ class Helpers { if(!isset($res['preferredUsername']) && !isset($res['nickname'])) { return; } + // skip invalid usernames + if(!ctype_alnum($res['preferredUsername'])) { + $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']); + if(!ctype_alnum($tmpUsername)) { + return; + } + } $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']); if(empty($username)) { return; From d24c60576f740137646c4aba8a6b0f88fc16d2a2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 9 Nov 2023 02:50:09 -0700 Subject: [PATCH 020/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47c05e1d4..1ded48d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e)) - Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a)) - Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714)) +- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 2c6edf37a7363e863756b6139d413ae5ab560f4c Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Thu, 9 Nov 2023 18:27:24 +0000 Subject: [PATCH 021/977] oFix #3698, make unlisted photos visible in collections --- app/Http/Controllers/CollectionController.php | 10 +++++----- resources/assets/js/components/CollectionComponent.vue | 2 +- resources/assets/js/components/CollectionCompose.vue | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index 6cd4bda57..46dcf4838 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -153,7 +153,7 @@ class CollectionController extends Controller abort(400, 'You can only add '.$max.' posts per collection'); } - $status = Status::whereScope('public') + $status = Status::whereIn('scope', ['public', 'unlisted']) ->whereProfileId($profileId) ->whereIn('type', ['photo', 'photo:album', 'video']) ->findOrFail($postId); @@ -176,7 +176,7 @@ class CollectionController extends Controller $collection->save(); CollectionService::setCollection($collection->id, $collection); - return StatusService::get($status->id); + return StatusService::get($status->id, false); } public function getCollection(Request $request, $id) @@ -226,10 +226,10 @@ class CollectionController extends Controller return collect($items) ->map(function($id) { - return StatusService::get($id); + return StatusService::get($id, false); }) ->filter(function($item) { - return $item && isset($item['account'], $item['media_attachments']); + return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']); }) ->values(); } @@ -298,7 +298,7 @@ class CollectionController extends Controller abort(400, 'You cannot delete the only post of a collection!'); } - $status = Status::whereScope('public') + $status = Status::whereIn('scope', ['public', 'unlisted']) ->whereIn('type', ['photo', 'photo:album', 'video']) ->findOrFail($postId); diff --git a/resources/assets/js/components/CollectionComponent.vue b/resources/assets/js/components/CollectionComponent.vue index 3f77cfc13..60510f559 100644 --- a/resources/assets/js/components/CollectionComponent.vue +++ b/resources/assets/js/components/CollectionComponent.vue @@ -460,7 +460,7 @@ export default { }) .then(res => { self.postsList = res.data.filter(l => { - return self.ids.indexOf(l.id) == -1; + return (l.visibility == 'public' || l.visibility == 'unlisted') && l.sensitive == false && self.ids.indexOf(l.id) == -1; }); self.loadingPostList = false; self.$refs.addPhotoModal.show(); diff --git a/resources/assets/js/components/CollectionCompose.vue b/resources/assets/js/components/CollectionCompose.vue index 84444cf1d..2920e1f76 100644 --- a/resources/assets/js/components/CollectionCompose.vue +++ b/resources/assets/js/components/CollectionCompose.vue @@ -194,7 +194,6 @@ export default { swal('Invalid URL', 'You can only add posts from this instance', 'error'); this.id = ''; } - if(url.includes('/i/web/post/') || url.includes('/p/')) { let id = split[split.length - 1]; console.log('adding ' + id); @@ -228,7 +227,7 @@ export default { let ids = this.posts.map(s => { return s.id; }); - return s.visibility == 'public' && s.sensitive == false && ids.indexOf(s.id) == -1; + return (s.visibility == 'public' || s.visibility == 'unlisted') && s.sensitive == false && ids.indexOf(s.id) == -1; }); }); }, From ce54d29c695ce3f783817af194be86af6e3f493e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 03:40:59 -0700 Subject: [PATCH 022/977] Update delete pipelines, properly invoke StatusHashtag delete events --- app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php | 5 ++++- app/Jobs/StatusPipeline/RemoteStatusDelete.php | 5 ++++- app/Jobs/StatusPipeline/StatusDelete.php | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php index 4969fca2f..353509c6c 100644 --- a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php +++ b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php @@ -76,7 +76,10 @@ class DeleteRemoteStatusPipeline implements ShouldQueue }); Mention::whereStatusId($status->id)->forceDelete(); Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->delete(); + $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); + foreach($statusHashtags as $stag) { + $stag->delete(); + } StatusView::whereStatusId($status->id)->delete(); Status::whereReblogOfId($status->id)->forceDelete(); $status->forceDelete(); diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index aabb81755..cb14288a1 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -153,7 +153,10 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing ->whereObjectId($status->id) ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->delete(); + $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); + foreach($statusHashtags as $stag) { + $stag->delete(); + } StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index 19c0ea68d..5b200fdf0 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -130,7 +130,10 @@ class StatusDelete implements ShouldQueue ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->delete(); + $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); + foreach($statusHashtags as $stag) { + $stag->delete(); + } StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); From 1f35da0d4bfa78fd0d6287e20ebe06a391d468c7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:25:23 -0700 Subject: [PATCH 023/977] Update HashtagServices --- app/Services/HashtagFollowService.php | 21 +++++ app/Services/HashtagService.php | 116 +++++++++++++++----------- 2 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 app/Services/HashtagFollowService.php diff --git a/app/Services/HashtagFollowService.php b/app/Services/HashtagFollowService.php new file mode 100644 index 000000000..8ddc9e6e2 --- /dev/null +++ b/app/Services/HashtagFollowService.php @@ -0,0 +1,21 @@ +pluck('profile_id')->toArray(); + }); + } +} diff --git a/app/Services/HashtagService.php b/app/Services/HashtagService.php index 87f895a65..81a7ae4ed 100644 --- a/app/Services/HashtagService.php +++ b/app/Services/HashtagService.php @@ -8,65 +8,79 @@ use App\Hashtag; use App\StatusHashtag; use App\HashtagFollow; -class HashtagService { +class HashtagService +{ + const FOLLOW_KEY = 'pf:services:hashtag:following:v1:'; + const FOLLOW_PIDS_KEY = 'pf:services:hashtag-follows:v1:'; - const FOLLOW_KEY = 'pf:services:hashtag:following:'; + public static function get($id) + { + return Cache::remember('services:hashtag:by_id:' . $id, 3600, function() use($id) { + $tag = Hashtag::find($id); + if(!$tag) { + return []; + } + return [ + 'name' => $tag->name, + 'slug' => $tag->slug, + ]; + }); + } - public static function get($id) - { - return Cache::remember('services:hashtag:by_id:' . $id, 3600, function() use($id) { - $tag = Hashtag::find($id); - if(!$tag) { - return []; - } - return [ - 'name' => $tag->name, - 'slug' => $tag->slug, - ]; - }); - } + public static function count($id) + { + return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) { + return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count(); + }); + } - public static function count($id) - { - return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) { - return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count(); - }); - } + public static function isFollowing($pid, $hid) + { + $res = Redis::zscore(self::FOLLOW_KEY . $hid, $pid); + if($res) { + return true; + } - public static function isFollowing($pid, $hid) - { - $res = Redis::zscore(self::FOLLOW_KEY . $pid, $hid); - if($res) { - return true; - } + $synced = Cache::get(self::FOLLOW_KEY . 'acct:' . $pid . ':synced'); + if(!$synced) { + $tags = HashtagFollow::whereProfileId($pid) + ->get() + ->each(function($tag) use($pid) { + self::follow($pid, $tag->hashtag_id); + }); + Cache::set(self::FOLLOW_KEY . 'acct:' . $pid . ':synced', true, 1209600); - $synced = Cache::get(self::FOLLOW_KEY . $pid . ':synced'); - if(!$synced) { - $tags = HashtagFollow::whereProfileId($pid) - ->get() - ->each(function($tag) use($pid) { - self::follow($pid, $tag->hashtag_id); - }); - Cache::set(self::FOLLOW_KEY . $pid . ':synced', true, 1209600); + return (bool) Redis::zscore(self::FOLLOW_KEY . $hid, $pid) >= 1; + } - return (bool) Redis::zscore(self::FOLLOW_KEY . $pid, $hid) > 1; - } + return false; + } - return false; - } + public static function follow($pid, $hid) + { + Cache::forget(self::FOLLOW_PIDS_KEY . $hid); + return Redis::zadd(self::FOLLOW_KEY . $hid, $pid, $pid); + } - public static function follow($pid, $hid) - { - return Redis::zadd(self::FOLLOW_KEY . $pid, $hid, $hid); - } + public static function unfollow($pid, $hid) + { + Cache::forget(self::FOLLOW_PIDS_KEY . $hid); + return Redis::zrem(self::FOLLOW_KEY . $hid, $pid); + } - public static function unfollow($pid, $hid) - { - return Redis::zrem(self::FOLLOW_KEY . $pid, $hid); - } + public static function following($hid, $start = 0, $limit = 10) + { + $synced = Cache::get(self::FOLLOW_KEY . 'acct-following:' . $hid . ':synced'); + if(!$synced) { + $tags = HashtagFollow::whereHashtagId($hid) + ->get() + ->each(function($tag) use($hid) { + self::follow($tag->profile_id, $hid); + }); + Cache::set(self::FOLLOW_KEY . 'acct-following:' . $hid . ':synced', true, 1209600); - public static function following($pid, $start = 0, $limit = 10) - { - return Redis::zrevrange(self::FOLLOW_KEY . $pid, $start, $limit); - } + return Redis::zrevrange(self::FOLLOW_KEY . $hid, $start, $limit); + } + return Redis::zrevrange(self::FOLLOW_KEY . $hid, $start, $limit); + } } From 448c0610707cad1bcffa2a79a28bf8bb3ded9feb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:43:33 -0700 Subject: [PATCH 024/977] Update HomeFeedPipeline, add hashtag jobs --- .../HashtagInsertFanoutPipeline.php | 86 +++++++++++++++++++ .../HashtagRemoveFanoutPipeline.php | 82 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php create mode 100644 app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php new file mode 100644 index 000000000..64adebadc --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -0,0 +1,86 @@ +hashtag->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:fanout:insert:{$this->hashtag->id}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct(StatusHashtag $hashtag) + { + $this->hashtag = $hashtag; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $hashtag = $this->hashtag; + + $ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id); + + if(!$ids || !count($ids)) { + return; + } + + foreach($ids as $id) { + HomeTimelineService::add($id, $hashtag->status_id); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php new file mode 100644 index 000000000..92e3b8e42 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php @@ -0,0 +1,82 @@ +hid . ':' . $this->sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:fanout:remove:{$this->hid}:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $hid) + { + $this->sid = $sid; + $this->hid = $hid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $sid = $this->sid; + $hid = $this->hid; + + $ids = HashtagFollowService::getPidByHid($hid); + + if(!$ids || !count($ids)) { + return; + } + + foreach($ids as $id) { + HomeTimelineService::rem($id, $sid); + } + } +} From 9dfc377322a1acf3ef45f5c5d5b08013dd9b8c01 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:46:37 -0700 Subject: [PATCH 025/977] Add HomeTimelineService --- app/Services/HomeTimelineService.php | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/Services/HomeTimelineService.php diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php new file mode 100644 index 000000000..937f3202b --- /dev/null +++ b/app/Services/HomeTimelineService.php @@ -0,0 +1,91 @@ + 100) { + $stop = 100; + } + + return Redis::zrevrange(self::CACHE_KEY . $id, $start, $stop); + } + + public static function getRankedMaxId($id, $start = null, $limit = 10) + { + if(!$start) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $id, $start, '-inf', [ + 'withscores' => true, + 'limit' => [1, $limit - 1] + ])); + } + + public static function getRankedMinId($id, $end = null, $limit = 10) + { + if(!$end) { + return []; + } + + return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $id, '+inf', $end, [ + 'withscores' => true, + 'limit' => [0, $limit] + ])); + } + + public static function add($id, $val) + { + if(self::count($id) > 400) { + Redis::zpopmin(self::CACHE_KEY . $id); + } + + return Redis::zadd(self::CACHE_KEY .$id, $val, $val); + } + + public static function rem($id, $val) + { + return Redis::zrem(self::CACHE_KEY . $id, $val); + } + + public static function count($id) + { + return Redis::zcard(self::CACHE_KEY . $id); + } + + public static function warmCache($id, $force = false, $limit = 100, $returnIds = false) + { + if(self::count($id) == 0 || $force == true) { + Redis::del(self::CACHE_KEY . $id); + $following = Cache::remember('profile:following:'.$id, 1209600, function() use($id) { + $following = Follower::whereProfileId($id)->pluck('following_id'); + return $following->push($id)->toArray(); + }); + + $ids = Status::whereIn('profile_id', $following) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->limit($limit) + ->pluck('id'); + + foreach($ids as $pid) { + self::add($id, $pid); + } + + return $returnIds ? $ids : 1; + } + return 0; + } +} From 1cd96ced2a8d81110973d31df209a9c958bf57eb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:47:52 -0700 Subject: [PATCH 026/977] Update StatusHashtagObserver --- app/Observers/StatusHashtagObserver.php | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app/Observers/StatusHashtagObserver.php b/app/Observers/StatusHashtagObserver.php index fa38ea3c3..569120a20 100644 --- a/app/Observers/StatusHashtagObserver.php +++ b/app/Observers/StatusHashtagObserver.php @@ -5,32 +5,31 @@ namespace App\Observers; use DB; use App\StatusHashtag; use App\Services\StatusHashtagService; +use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline; +use App\Jobs\HomeFeedPipeline\HashtagRemoveFanoutPipeline; +use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit; -class StatusHashtagObserver +class StatusHashtagObserver implements ShouldHandleEventsAfterCommit { - /** - * Handle events after all transactions are committed. - * - * @var bool - */ - public $afterCommit = true; - /** * Handle the notification "created" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function created(StatusHashtag $hashtag) { StatusHashtagService::set($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->increment('cached_count'); + if($hashtag->status_visibility && $hashtag->status_visibility === 'public') { + HashtagInsertFanoutPipeline::dispatch($hashtag)->onQueue('feed'); + } } /** * Handle the notification "updated" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function updated(StatusHashtag $hashtag) @@ -39,21 +38,24 @@ class StatusHashtagObserver } /** - * Handle the notification "deleted" event. + * Handle the notification "deleting" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ - public function deleted(StatusHashtag $hashtag) + public function deleting(StatusHashtag $hashtag) { StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->decrement('cached_count'); + if($hashtag->status_visibility && $hashtag->status_visibility === 'public') { + HashtagRemoveFanoutPipeline::dispatch($hashtag->status_id, $hashtag->hashtag_id)->onQueue('feed'); + } } /** * Handle the notification "restored" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function restored(StatusHashtag $hashtag) @@ -64,7 +66,7 @@ class StatusHashtagObserver /** * Handle the notification "force deleted" event. * - * @param \App\Notification $notification + * @param \App\StatusHashtag $hashtag * @return void */ public function forceDeleted(StatusHashtag $hashtag) From 6aa65b9a21eedc7fe38a17ddbc0d1000e2b395a8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:49:41 -0700 Subject: [PATCH 027/977] Add FeedWarmCachePipeline --- .../FeedWarmCachePipeline.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php diff --git a/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php b/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php new file mode 100644 index 000000000..00cdbda65 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedWarmCachePipeline.php @@ -0,0 +1,67 @@ +pid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:warm-cache:pid:{$this->pid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid) + { + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pid = $this->pid; + HomeTimelineService::warmCache($pid, true, 400, true); + } +} From c806bbce3fd3f15c3ca2bba0de2976e708880b91 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:51:10 -0700 Subject: [PATCH 028/977] Update composer deps --- composer.lock | 1457 ++++++++++++++++++++++++++----------------------- 1 file changed, 779 insertions(+), 678 deletions(-) diff --git a/composer.lock b/composer.lock index 74c453c67..1c1126539 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.2.1", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5" + "reference": "5545a4fa310aec39f54279fdacebcce33b3ff382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5", - "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/5545a4fa310aec39f54279fdacebcce33b3ff382", + "reference": "5545a4fa310aec39f54279fdacebcce33b3ff382", "shasum": "" }, "require": { @@ -56,35 +56,35 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.3" }, - "time": "2023-03-24T20:22:19+00:00" + "time": "2023-10-16T20:10:06+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.275.7", + "version": "3.285.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903" + "reference": "c462af819d81cba49939949032b20799f5ef0fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", - "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c462af819d81cba49939949032b20799f5ef0fff", + "reference": "c462af819d81cba49939949032b20799f5ef0fff", "shasum": "" }, "require": { - "aws/aws-crt-php": "^1.0.4", + "aws/aws-crt-php": "^1.2.3", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0", + "guzzlehttp/promises": "^1.4.0 || ^2.0", "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "mtdowling/jmespath.php": "^2.6", - "php": ">=5.5", - "psr/http-message": "^1.0" + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -99,7 +99,7 @@ "ext-sockets": "*", "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", "sebastian/comparator": "^1.2.3 || ^4.0", @@ -151,9 +151,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.275.7" + "source": "https://github.com/aws/aws-sdk-php/tree/3.285.4" }, - "time": "2023-07-13T18:21:04+00:00" + "time": "2023-11-10T19:25:49+00:00" }, { "name": "bacon/bacon-qr-code", @@ -211,16 +211,16 @@ }, { "name": "beyondcode/laravel-websockets", - "version": "1.14.0", + "version": "1.14.1", "source": { "type": "git", "url": "https://github.com/beyondcode/laravel-websockets.git", - "reference": "9ab87be1d96340979e67b462ea5fd6a8b06e6a02" + "reference": "fee9a81e42a096d2aaca216ce91acf6e25d8c06d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/9ab87be1d96340979e67b462ea5fd6a8b06e6a02", - "reference": "9ab87be1d96340979e67b462ea5fd6a8b06e6a02", + "url": "https://api.github.com/repos/beyondcode/laravel-websockets/zipball/fee9a81e42a096d2aaca216ce91acf6e25d8c06d", + "reference": "fee9a81e42a096d2aaca216ce91acf6e25d8c06d", "shasum": "" }, "require": { @@ -287,9 +287,9 @@ ], "support": { "issues": "https://github.com/beyondcode/laravel-websockets/issues", - "source": "https://github.com/beyondcode/laravel-websockets/tree/1.14.0" + "source": "https://github.com/beyondcode/laravel-websockets/tree/1.14.1" }, - "time": "2023-02-15T10:40:49+00:00" + "time": "2023-08-30T07:23:12+00:00" }, { "name": "brick/math", @@ -489,16 +489,16 @@ }, { "name": "dasprid/enum", - "version": "1.0.4", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f" + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", "shasum": "" }, "require": { @@ -533,9 +533,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.4" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" }, - "time": "2023-03-01T18:44:03+00:00" + "time": "2023-08-25T16:18:39+00:00" }, { "name": "defuse/php-encryption", @@ -774,16 +774,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.4", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f" + "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", - "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/5b7bd66c9ff58c04c5474ab85edce442f8081cb2", + "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2", "shasum": "" }, "require": { @@ -798,11 +798,12 @@ "require-dev": { "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.10.14", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.10.35", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.7", + "phpunit/phpunit": "9.6.13", "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", @@ -866,7 +867,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.4" + "source": "https://github.com/doctrine/dbal/tree/3.7.1" }, "funding": [ { @@ -882,20 +883,20 @@ "type": "tidelift" } ], - "time": "2023-06-15T07:40:12+00:00" + "time": "2023-10-06T05:06:20+00:00" }, { "name": "doctrine/deprecations", - "version": "v1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3" + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", - "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", "shasum": "" }, "require": { @@ -927,9 +928,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.1.1" + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" }, - "time": "2023-06-03T09:27:29+00:00" + "time": "2023-09-27T20:04:15+00:00" }, { "name": "doctrine/event-manager", @@ -1192,16 +1193,16 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.2", + "version": "v3.3.3", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8" + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8", - "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", "shasum": "" }, "require": { @@ -1241,7 +1242,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" }, "funding": [ { @@ -1249,20 +1250,20 @@ "type": "github" } ], - "time": "2022-09-10T18:51:20+00:00" + "time": "2023-08-10T19:36:49+00:00" }, { "name": "egulias/email-validator", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff" + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff", - "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", "shasum": "" }, "require": { @@ -1271,8 +1272,8 @@ "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^4.30" + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -1308,7 +1309,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.1" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" }, "funding": [ { @@ -1316,32 +1317,32 @@ "type": "github" } ], - "time": "2023-01-14T14:17:03+00:00" + "time": "2023-10-06T06:47:41+00:00" }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1361,9 +1362,9 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { "name": "ezyang/htmlpurifier", @@ -1537,16 +1538,16 @@ }, { "name": "firebase/php-jwt", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "48b0210c51718d682e53210c24d25c5a10a2299b" + "reference": "f03270e63eaccf3019ef0f32849c497385774e11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/48b0210c51718d682e53210c24d25c5a10a2299b", - "reference": "48b0210c51718d682e53210c24d25c5a10a2299b", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/f03270e63eaccf3019ef0f32849c497385774e11", + "reference": "f03270e63eaccf3019ef0f32849c497385774e11", "shasum": "" }, "require": { @@ -1594,27 +1595,27 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.8.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.9.0" }, - "time": "2023-06-20T16:45:35+00:00" + "time": "2023-10-05T00:24:42+00:00" }, { "name": "fruitcake/php-cors", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e" + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/58571acbaa5f9f462c9c77e911700ac66f446d4e", - "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6" + "symfony/http-foundation": "^4.4|^5.4|^6|^7" }, "require-dev": { "phpstan/phpstan": "^1.4", @@ -1624,7 +1625,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -1655,7 +1656,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.2.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" }, "funding": [ { @@ -1667,7 +1668,7 @@ "type": "github" } ], - "time": "2022-02-20T15:07:15+00:00" + "time": "2023-10-12T05:21:21+00:00" }, { "name": "graham-campbell/result-type", @@ -1733,22 +1734,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1839,7 +1840,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" }, "funding": [ { @@ -1855,33 +1856,37 @@ "type": "tidelift" } ], - "time": "2023-05-21T14:04:53+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -1918,7 +1923,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.3" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -1934,20 +1939,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T12:31:43+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", "shasum": "" }, "require": { @@ -2034,7 +2039,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.1" }, "funding": [ { @@ -2050,20 +2055,20 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2023-08-27T10:13:57+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2" + "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/b945d74a55a25a949158444f09ec0d3c120d69e2", - "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/61bf437fc2197f587f6857d3ff903a24f1731b5d", + "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d", "shasum": "" }, "require": { @@ -2071,15 +2076,11 @@ "symfony/polyfill-php80": "^1.17" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", "phpunit/phpunit": "^8.5.19 || ^9.5.8", "uri-template/tests": "1.0.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "GuzzleHttp\\UriTemplate\\": "src" @@ -2118,7 +2119,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.1" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.2" }, "funding": [ { @@ -2134,7 +2135,7 @@ "type": "tidelift" } ], - "time": "2021-10-07T12:57:01+00:00" + "time": "2023-08-27T10:19:19+00:00" }, { "name": "intervention/image", @@ -2222,16 +2223,16 @@ }, { "name": "jaybizzle/crawler-detect", - "version": "v1.2.115", + "version": "v1.2.116", "source": { "type": "git", "url": "https://github.com/JayBizzle/Crawler-Detect.git", - "reference": "4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e" + "reference": "97e9fe30219e60092e107651abb379a38b342921" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e", - "reference": "4531e4a70d55d10cbe7d41ac1ff0d75a5fe2ef1e", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/97e9fe30219e60092e107651abb379a38b342921", + "reference": "97e9fe30219e60092e107651abb379a38b342921", "shasum": "" }, "require": { @@ -2268,9 +2269,9 @@ ], "support": { "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", - "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.115" + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.116" }, - "time": "2023-06-05T21:32:18+00:00" + "time": "2023-07-21T15:49:49+00:00" }, { "name": "jenssegers/agent", @@ -2357,16 +2358,16 @@ }, { "name": "laravel/framework", - "version": "v10.15.0", + "version": "v10.31.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8" + "reference": "507ce9b28bce4b5e4140c28943092ca38e9a52e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c7599dc92e04532824bafbd226c2936ce6a905b8", - "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8", + "url": "https://api.github.com/repos/laravel/framework/zipball/507ce9b28bce4b5e4140c28943092ca38e9a52e4", + "reference": "507ce9b28bce4b5e4140c28943092ca38e9a52e4", "shasum": "" }, "require": { @@ -2384,11 +2385,12 @@ "ext-tokenizer": "*", "fruitcake/php-cors": "^1.2", "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.1.9", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.62.1", + "nesbot/carbon": "^2.67", "nunomaduro/termwind": "^1.13", "php": "^8.1", "psr/container": "^1.1.1|^2.0.1", @@ -2398,7 +2400,7 @@ "symfony/console": "^6.2", "symfony/error-handler": "^6.2", "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.3", "symfony/http-kernel": "^6.2", "symfony/mailer": "^6.2", "symfony/mime": "^6.2", @@ -2465,14 +2467,15 @@ "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", - "orchestra/testbench-core": "^8.4", + "nyholm/psr7": "^1.2", + "orchestra/testbench-core": "^8.12", "pda/pheanstalk": "^4.0", - "phpstan/phpdoc-parser": "^1.15", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", "predis/predis": "^2.0.2", "symfony/cache": "^6.2", - "symfony/http-client": "^6.2.4" + "symfony/http-client": "^6.2.4", + "symfony/psr-http-message-bridge": "^2.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", @@ -2553,7 +2556,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-07-11T13:43:52+00:00" + "time": "2023-11-07T13:48:30+00:00" }, { "name": "laravel/helpers", @@ -2613,16 +2616,16 @@ }, { "name": "laravel/horizon", - "version": "v5.18.0", + "version": "v5.21.3", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7" + "reference": "6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/b14498a09af826035e46ae8d6b013d0ec849bdb7", - "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7", + "url": "https://api.github.com/repos/laravel/horizon/zipball/6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31", + "reference": "6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31", "shasum": "" }, "require": { @@ -2685,22 +2688,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.18.0" + "source": "https://github.com/laravel/horizon/tree/v5.21.3" }, - "time": "2023-06-30T15:11:51+00:00" + "time": "2023-10-27T13:58:13+00:00" }, { "name": "laravel/passport", - "version": "v11.8.8", + "version": "v11.10.0", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "401836130d46c94138a637ada29f9e5b2bf053b6" + "reference": "966bc8e477d08c86a11dc4c5a86f85fa0abdb89b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/401836130d46c94138a637ada29f9e5b2bf053b6", - "reference": "401836130d46c94138a637ada29f9e5b2bf053b6", + "url": "https://api.github.com/repos/laravel/passport/zipball/966bc8e477d08c86a11dc4c5a86f85fa0abdb89b", + "reference": "966bc8e477d08c86a11dc4c5a86f85fa0abdb89b", "shasum": "" }, "require": { @@ -2724,7 +2727,7 @@ }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^7.0|^8.0", + "orchestra/testbench": "^7.31|^8.11", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3" }, @@ -2765,20 +2768,77 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2023-07-07T06:37:11+00:00" + "time": "2023-11-02T17:16:12+00:00" }, { - "name": "laravel/serializable-closure", - "version": "v1.3.0", + "name": "laravel/prompts", + "version": "v0.1.13", "source": { "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37" + "url": "https://github.com/laravel/prompts.git", + "reference": "e1379d8ead15edd6cc4369c22274345982edc95a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f23fe9d4e95255dacee1bf3525e0810d1a1b0f37", - "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37", + "url": "https://api.github.com/repos/laravel/prompts/zipball/e1379d8ead15edd6cc4369c22274345982edc95a", + "reference": "e1379d8ead15edd6cc4369c22274345982edc95a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/collections": "^10.0|^11.0", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.1.13" + }, + "time": "2023-10-27T13:53:59+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "076fe2cf128bd54b4341cdc6d49b95b34e101e4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/076fe2cf128bd54b4341cdc6d49b95b34e101e4c", + "reference": "076fe2cf128bd54b4341cdc6d49b95b34e101e4c", "shasum": "" }, "require": { @@ -2825,20 +2885,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2023-01-30T18:31:20+00:00" + "time": "2023-10-17T13:38:16+00:00" }, { "name": "laravel/tinker", - "version": "v2.8.1", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10" + "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/04a2d3bd0d650c0764f70bf49d1ee39393e4eb10", - "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10", + "url": "https://api.github.com/repos/laravel/tinker/zipball/b936d415b252b499e8c3b1f795cd4fc20f57e1f3", + "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3", "shasum": "" }, "require": { @@ -2851,6 +2911,7 @@ }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^8.5.8|^9.3.3" }, "suggest": { @@ -2891,9 +2952,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.8.1" + "source": "https://github.com/laravel/tinker/tree/v2.8.2" }, - "time": "2023-02-15T16:40:09+00:00" + "time": "2023-08-15T14:27:00+00:00" }, { "name": "laravel/ui", @@ -2959,20 +3020,20 @@ }, { "name": "lcobucci/clock", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/30a854ceb22bd87d83a7a4563b3f6312453945fc", + "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0", + "php": "~8.2.0", "psr/clock": "^1.0" }, "provide": { @@ -2980,13 +3041,13 @@ }, "require-dev": { "infection/infection": "^0.26", - "lcobucci/coding-standard": "^9.0", + "lcobucci/coding-standard": "^10.0.0", "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-deprecation-rules": "^1.1.1", - "phpstan/phpstan-phpunit": "^1.3.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.27" + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.0.17" }, "type": "library", "autoload": { @@ -3007,7 +3068,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.0.0" + "source": "https://github.com/lcobucci/clock/tree/3.1.0" }, "funding": [ { @@ -3019,20 +3080,20 @@ "type": "patreon" } ], - "time": "2022-12-19T15:00:24+00:00" + "time": "2023-03-20T19:12:25+00:00" }, { "name": "lcobucci/jwt", - "version": "5.0.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34" + "reference": "f0031c07b96db6a0ca649206e7eacddb7e9d5908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", - "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/f0031c07b96db6a0ca649206e7eacddb7e9d5908", + "reference": "f0031c07b96db6a0ca649206e7eacddb7e9d5908", "shasum": "" }, "require": { @@ -3040,20 +3101,20 @@ "ext-json": "*", "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.26.19", + "infection/infection": "^0.27.0", "lcobucci/clock": "^3.0", - "lcobucci/coding-standard": "^9.0", - "phpbench/phpbench": "^1.2.8", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.3", - "phpstan/phpstan-deprecation-rules": "^1.1.2", - "phpstan/phpstan-phpunit": "^1.3.8", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.0.12" + "phpunit/phpunit": "^10.2.6" }, "suggest": { "lcobucci/clock": ">= 3.0" @@ -3082,7 +3143,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.0.0" + "source": "https://github.com/lcobucci/jwt/tree/5.1.0" }, "funding": [ { @@ -3094,20 +3155,20 @@ "type": "patreon" } ], - "time": "2023-02-25T21:35:16+00:00" + "time": "2023-10-31T06:41:47+00:00" }, { "name": "league/commonmark", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048" + "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5", + "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5", "shasum": "" }, "require": { @@ -3200,7 +3261,7 @@ "type": "tidelift" } ], - "time": "2023-03-24T15:16:10+00:00" + "time": "2023-08-30T16:55:00+00:00" }, { "name": "league/config", @@ -3340,16 +3401,16 @@ }, { "name": "league/flysystem", - "version": "3.15.1", + "version": "3.19.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "a141d430414fcb8bf797a18716b09f759a385bed" + "reference": "1b2aa10f2326e0351399b8ce68e287d8e9209a83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed", - "reference": "a141d430414fcb8bf797a18716b09f759a385bed", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1b2aa10f2326e0351399b8ce68e287d8e9209a83", + "reference": "1b2aa10f2326e0351399b8ce68e287d8e9209a83", "shasum": "" }, "require": { @@ -3358,6 +3419,8 @@ "php": "^8.0.2" }, "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", "aws/aws-sdk-php": "3.209.31 || 3.210.0", "guzzlehttp/guzzle": "<7.0", "guzzlehttp/ringphp": "<1.1.1", @@ -3365,8 +3428,8 @@ "symfony/http-client": "<5.2" }, "require-dev": { - "async-aws/s3": "^1.5", - "async-aws/simple-s3": "^1.1", + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", "aws/aws-sdk-php": "^3.220.0", "composer/semver": "^3.0", "ext-fileinfo": "*", @@ -3376,8 +3439,8 @@ "google/cloud-storage": "^1.23", "microsoft/azure-storage-blob": "^1.1", "phpseclib/phpseclib": "^3.0.14", - "phpstan/phpstan": "^0.12.26", - "phpunit/phpunit": "^9.5.11", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", "sabre/dav": "^4.3.1" }, "type": "library", @@ -3412,7 +3475,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.15.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.19.0" }, "funding": [ { @@ -3424,20 +3487,20 @@ "type": "github" } ], - "time": "2023-05-04T09:04:26+00:00" + "time": "2023-11-07T09:04:28+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.15.0", + "version": "3.19.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a" + "reference": "03be643c8ed4dea811d946101be3bc875b5cf214" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a", - "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/03be643c8ed4dea811d946101be3bc875b5cf214", + "reference": "03be643c8ed4dea811d946101be3bc875b5cf214", "shasum": "" }, "require": { @@ -3478,7 +3541,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.19.0" }, "funding": [ { @@ -3490,20 +3553,20 @@ "type": "github" } ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2023-11-06T20:35:28+00:00" }, { "name": "league/flysystem-local", - "version": "3.15.0", + "version": "3.19.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3" + "reference": "8d868217f9eeb4e9a7320db5ccad825e9a7a4076" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3", - "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/8d868217f9eeb4e9a7320db5ccad825e9a7a4076", + "reference": "8d868217f9eeb4e9a7320db5ccad825e9a7a4076", "shasum": "" }, "require": { @@ -3538,7 +3601,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.19.0" }, "funding": [ { @@ -3550,20 +3613,20 @@ "type": "github" } ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2023-11-06T20:35:28+00:00" }, { "name": "league/iso3166", - "version": "4.3.0", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/thephpleague/iso3166.git", - "reference": "628f1b4992169917f3f59c14020ea4513c63f6db" + "reference": "11703e0313f34920add11c0228f0dd43ebd10f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/iso3166/zipball/628f1b4992169917f3f59c14020ea4513c63f6db", - "reference": "628f1b4992169917f3f59c14020ea4513c63f6db", + "url": "https://api.github.com/repos/thephpleague/iso3166/zipball/11703e0313f34920add11c0228f0dd43ebd10f9a", + "reference": "11703e0313f34920add11c0228f0dd43ebd10f9a", "shasum": "" }, "require": { @@ -3608,30 +3671,30 @@ "issues": "https://github.com/thephpleague/iso3166/issues", "source": "https://github.com/thephpleague/iso3166" }, - "time": "2023-06-05T15:02:58+00:00" + "time": "2023-09-11T07:59:36+00:00" }, { "name": "league/mime-type-detection", - "version": "1.11.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd" + "reference": "b6a5854368533df0295c5761a0253656a2e52d9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/b6a5854368533df0295c5761a0253656a2e52d9e", + "reference": "b6a5854368533df0295c5761a0253656a2e52d9e", "shasum": "" }, "require": { "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" }, "type": "library", "autoload": { @@ -3652,7 +3715,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.14.0" }, "funding": [ { @@ -3664,20 +3727,20 @@ "type": "tidelift" } ], - "time": "2022-04-17T13:12:02+00:00" + "time": "2023-10-17T14:13:20+00:00" }, { "name": "league/oauth2-server", - "version": "8.5.3", + "version": "8.5.4", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "eb91b4190e7f6169053ebf8ffa352d47e756b2ce" + "reference": "ab7714d073844497fd222d5d0a217629089936bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/eb91b4190e7f6169053ebf8ffa352d47e756b2ce", - "reference": "eb91b4190e7f6169053ebf8ffa352d47e756b2ce", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/ab7714d073844497fd222d5d0a217629089936bc", + "reference": "ab7714d073844497fd222d5d0a217629089936bc", "shasum": "" }, "require": { @@ -3686,7 +3749,7 @@ "lcobucci/clock": "^2.2 || ^3.0", "lcobucci/jwt": "^4.3 || ^5.0", "league/event": "^2.2", - "league/uri": "^6.7", + "league/uri": "^6.7 || ^7.0", "php": "^8.0", "psr/http-message": "^1.0.1 || ^2.0" }, @@ -3744,7 +3807,7 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.3" + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.4" }, "funding": [ { @@ -3752,58 +3815,48 @@ "type": "github" } ], - "time": "2023-07-05T23:01:32+00:00" + "time": "2023-08-25T22:35:12+00:00" }, { "name": "league/uri", - "version": "6.8.0", + "version": "7.3.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39" + "reference": "36743c3961bb82bf93da91917b6bced0358a8d45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/a700b4656e4c54371b799ac61e300ab25a2d1d39", - "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/36743c3961bb82bf93da91917b6bced0358a8d45", + "reference": "36743c3961bb82bf93da91917b6bced0358a8d45", "shasum": "" }, "require": { - "ext-json": "*", - "league/uri-interfaces": "^2.3", - "php": "^8.1", - "psr/http-message": "^1.0.1" + "league/uri-interfaces": "^7.3", + "php": "^8.1" }, "conflict": { "league/uri-schemes": "^1.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.9.5", - "nyholm/psr7": "^1.5.1", - "php-http/psr7-integration-tests": "^1.1.1", - "phpbench/phpbench": "^1.2.6", - "phpstan/phpstan": "^1.8.5", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.1.1", - "phpstan/phpstan-strict-rules": "^1.4.3", - "phpunit/phpunit": "^9.5.24", - "psr/http-factory": "^1.0.1" - }, "suggest": { - "ext-fileinfo": "Needed to create Data URI from a filepath", - "ext-intl": "Needed to improve host validation", - "league/uri-components": "Needed to easily manipulate URI objects", - "psr/http-factory": "Needed to use the URI factory" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3843,8 +3896,8 @@ "support": { "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri/issues", - "source": "https://github.com/thephpleague/uri/tree/6.8.0" + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.3.0" }, "funding": [ { @@ -3852,46 +3905,44 @@ "type": "github" } ], - "time": "2022-09-13T19:58:47+00:00" + "time": "2023-09-09T17:21:43+00:00" }, { "name": "league/uri-interfaces", - "version": "2.3.0", + "version": "7.3.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" + "reference": "c409b60ed2245ff94c965a8c798a60166db53361" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c409b60ed2245ff94c965a8c798a60166db53361", + "reference": "c409b60ed2245ff94c965a8c798a60166db53361", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "phpstan/phpstan": "^0.12.90", - "phpstan/phpstan-phpunit": "^0.12.19", - "phpstan/phpstan-strict-rules": "^0.12.9", - "phpunit/phpunit": "^8.5.15 || ^9.5" + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" }, "suggest": { - "ext-intl": "to use the IDNA feature", - "symfony/intl": "to use the IDNA feature via Symfony Polyfill" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3905,17 +3956,32 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interface for URI representation", - "homepage": "http://github.com/thephpleague/uri-interfaces", + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", "rfc3986", "rfc3987", + "rfc6570", "uri", - "url" + "url", + "ws" ], "support": { - "issues": "https://github.com/thephpleague/uri-interfaces/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.3.0" }, "funding": [ { @@ -3923,27 +3989,27 @@ "type": "github" } ], - "time": "2021-06-28T04:27:21+00:00" + "time": "2023-09-09T17:21:43+00:00" }, { "name": "mobiledetect/mobiledetectlib", - "version": "2.8.41", + "version": "2.8.45", "source": { "type": "git", "url": "https://github.com/serbanghita/Mobile-Detect.git", - "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1" + "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", - "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266", + "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266", "shasum": "" }, "require": { "php": ">=5.0.0" }, "require-dev": { - "phpunit/phpunit": "~4.8.35||~5.7" + "phpunit/phpunit": "~4.8.36" }, "type": "library", "autoload": { @@ -3977,22 +4043,28 @@ ], "support": { "issues": "https://github.com/serbanghita/Mobile-Detect/issues", - "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.41" + "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45" }, - "time": "2022-11-08T18:31:26+00:00" + "funding": [ + { + "url": "https://github.com/serbanghita", + "type": "github" + } + ], + "time": "2023-11-07T21:57:25+00:00" }, { "name": "monolog/monolog", - "version": "3.4.0", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "e2392369686d420ca32df3803de28b5d6f76867d" + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/e2392369686d420ca32df3803de28b5d6f76867d", - "reference": "e2392369686d420ca32df3803de28b5d6f76867d", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", "shasum": "" }, "require": { @@ -4068,7 +4140,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.4.0" + "source": "https://github.com/Seldaek/monolog/tree/3.5.0" }, "funding": [ { @@ -4080,29 +4152,29 @@ "type": "tidelift" } ], - "time": "2023-06-21T08:46:11+00:00" + "time": "2023-10-27T15:32:31+00:00" }, { "name": "mtdowling/jmespath.php", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb" + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb", - "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", "shasum": "" }, "require": { - "php": "^5.4 || ^7.0 || ^8.0", + "php": "^7.2.5 || ^8.0", "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "composer/xdebug-handler": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^7.5.15" + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" }, "bin": [ "bin/jp.php" @@ -4110,7 +4182,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.7-dev" } }, "autoload": { @@ -4126,6 +4198,11 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", @@ -4139,31 +4216,35 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1" + "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" }, - "time": "2021-06-14T00:11:39+00:00" + "time": "2023-08-25T10:54:48+00:00" }, { "name": "nesbot/carbon", - "version": "2.68.1", + "version": "2.71.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da" + "reference": "98276233188583f2ff845a0f992a235472d9466a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/98276233188583f2ff845a0f992a235472d9466a", + "reference": "98276233188583f2ff845a0f992a235472d9466a", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", "symfony/polyfill-mbstring": "^1.0", "symfony/polyfill-php80": "^1.16", "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" }, + "provide": { + "psr/clock-implementation": "1.0" + }, "require-dev": { "doctrine/dbal": "^2.0 || ^3.1.4", "doctrine/orm": "^2.7", @@ -4243,25 +4324,25 @@ "type": "tidelift" } ], - "time": "2023-06-20T18:29:04+00:00" + "time": "2023-09-25T11:31:05+00:00" }, { "name": "nette/schema", - "version": "v1.2.3", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", "shasum": "" }, "require": { "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.3" + "php": "7.1 - 8.3" }, "require-dev": { "nette/tester": "^2.3 || ^2.4", @@ -4303,26 +4384,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.3" + "source": "https://github.com/nette/schema/tree/v1.2.5" }, - "time": "2022-10-13T01:24:26+00:00" + "time": "2023-10-05T20:37:59+00:00" }, { "name": "nette/utils", - "version": "v4.0.0", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e" + "reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/cacdbf5a91a657ede665c541eda28941d4b09c1e", - "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e", + "url": "https://api.github.com/repos/nette/utils/zipball/a9d127dd6a203ce6d255b2e2db49759f7506e015", + "reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015", "shasum": "" }, "require": { - "php": ">=8.0 <8.3" + "php": ">=8.0 <8.4" }, "conflict": { "nette/finder": "<3", @@ -4330,7 +4411,7 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "dev-master", - "nette/tester": "^2.4", + "nette/tester": "^2.5", "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.9" }, @@ -4340,8 +4421,7 @@ "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", - "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", - "ext-xml": "to use Strings::length() etc. when mbstring is not available" + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "type": "library", "extra": { @@ -4390,22 +4470,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.0" + "source": "https://github.com/nette/utils/tree/v4.0.3" }, - "time": "2023-02-02T10:41:53+00:00" + "time": "2023-10-29T21:02:13+00:00" }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -4446,9 +4526,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "nunomaduro/termwind", @@ -5061,16 +5141,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.44", + "version": "2.0.45", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "149f608243f8133c61926aae26ce67d2b22b37e5" + "reference": "28d8f438a0064c9de80857e3270d071495544640" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/149f608243f8133c61926aae26ce67d2b22b37e5", - "reference": "149f608243f8133c61926aae26ce67d2b22b37e5", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/28d8f438a0064c9de80857e3270d071495544640", + "reference": "28d8f438a0064c9de80857e3270d071495544640", "shasum": "" }, "require": { @@ -5151,7 +5231,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/2.0.44" + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.45" }, "funding": [ { @@ -5167,7 +5247,7 @@ "type": "tidelift" } ], - "time": "2023-06-13T08:41:47+00:00" + "time": "2023-09-15T20:55:47+00:00" }, { "name": "pixelfed/fractal", @@ -5398,16 +5478,16 @@ }, { "name": "predis/predis", - "version": "v2.2.0", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d" + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/33b70b971a32b0d28b4f748b0547593dce316e0d", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d", + "url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", "shasum": "" }, "require": { @@ -5447,7 +5527,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.2.0" + "source": "https://github.com/predis/predis/tree/v2.2.2" }, "funding": [ { @@ -5455,7 +5535,7 @@ "type": "github" } ], - "time": "2023-06-14T10:37:31+00:00" + "time": "2023-09-13T16:42:03+00:00" }, { "name": "psr/cache", @@ -5659,16 +5739,16 @@ }, { "name": "psr/http-client", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { @@ -5705,9 +5785,9 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/1.0.2" + "source": "https://github.com/php-fig/http-client" }, - "time": "2023-04-10T20:12:12+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", @@ -5920,16 +6000,16 @@ }, { "name": "psy/psysh", - "version": "v0.11.18", + "version": "v0.11.22", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec" + "reference": "128fa1b608be651999ed9789c95e6e2a31b5802b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", - "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/128fa1b608be651999ed9789c95e6e2a31b5802b", + "reference": "128fa1b608be651999ed9789c95e6e2a31b5802b", "shasum": "" }, "require": { @@ -5958,7 +6038,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "0.11.x-dev" + "dev-0.11": "0.11.x-dev" + }, + "bamarni-bin": { + "bin-links": false, + "forward-command": false } }, "autoload": { @@ -5990,9 +6074,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.11.18" + "source": "https://github.com/bobthecow/psysh/tree/v0.11.22" }, - "time": "2023-05-23T02:31:11+00:00" + "time": "2023-10-14T21:56:36+00:00" }, { "name": "pusher/pusher-php-server", @@ -6190,16 +6274,16 @@ }, { "name": "ramsey/uuid", - "version": "4.7.4", + "version": "4.7.5", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "60a4c63ab724854332900504274f6150ff26d286" + "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", - "reference": "60a4c63ab724854332900504274f6150ff26d286", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", + "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", "shasum": "" }, "require": { @@ -6266,7 +6350,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.4" + "source": "https://github.com/ramsey/uuid/tree/4.7.5" }, "funding": [ { @@ -6278,7 +6362,7 @@ "type": "tidelift" } ], - "time": "2023-04-15T23:01:58+00:00" + "time": "2023-11-08T05:53:05+00:00" }, { "name": "ratchet/rfc6455", @@ -6724,16 +6808,16 @@ }, { "name": "react/socket", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "cff482bbad5848ecbe8b57da57e4e213b03619aa" + "reference": "21591111d3ea62e31f2254280ca0656bc2b1bda6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/cff482bbad5848ecbe8b57da57e4e213b03619aa", - "reference": "cff482bbad5848ecbe8b57da57e4e213b03619aa", + "url": "https://api.github.com/repos/reactphp/socket/zipball/21591111d3ea62e31f2254280ca0656bc2b1bda6", + "reference": "21591111d3ea62e31f2254280ca0656bc2b1bda6", "shasum": "" }, "require": { @@ -6748,7 +6832,7 @@ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", "react/promise-stream": "^1.4", - "react/promise-timer": "^1.9" + "react/promise-timer": "^1.10" }, "type": "library", "autoload": { @@ -6792,7 +6876,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.13.0" + "source": "https://github.com/reactphp/socket/tree/v1.14.0" }, "funding": [ { @@ -6800,7 +6884,7 @@ "type": "open_collective" } ], - "time": "2023-06-07T10:28:34+00:00" + "time": "2023-08-25T13:48:09+00:00" }, { "name": "react/stream", @@ -7006,28 +7090,28 @@ }, { "name": "spatie/image-optimizer", - "version": "1.6.4", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/spatie/image-optimizer.git", - "reference": "d997e01ba980b2769ddca2f00badd3b80c2a2512" + "reference": "62f7463483d1bd975f6f06025d89d42a29608fe1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/d997e01ba980b2769ddca2f00badd3b80c2a2512", - "reference": "d997e01ba980b2769ddca2f00badd3b80c2a2512", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/62f7463483d1bd975f6f06025d89d42a29608fe1", + "reference": "62f7463483d1bd975f6f06025d89d42a29608fe1", "shasum": "" }, "require": { "ext-fileinfo": "*", "php": "^7.3|^8.0", "psr/log": "^1.0 | ^2.0 | ^3.0", - "symfony/process": "^4.2|^5.0|^6.0" + "symfony/process": "^4.2|^5.0|^6.0|^7.0" }, "require-dev": { "pestphp/pest": "^1.21", "phpunit/phpunit": "^8.5.21|^9.4.4", - "symfony/var-dumper": "^4.2|^5.0|^6.0" + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7055,34 +7139,34 @@ ], "support": { "issues": "https://github.com/spatie/image-optimizer/issues", - "source": "https://github.com/spatie/image-optimizer/tree/1.6.4" + "source": "https://github.com/spatie/image-optimizer/tree/1.7.2" }, - "time": "2023-03-10T08:43:19+00:00" + "time": "2023-11-03T10:08:02+00:00" }, { "name": "spatie/laravel-backup", - "version": "8.1.11", + "version": "8.4.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c" + "reference": "64f4b816c61f802e9f4c831a589c9d2e21573ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c", - "reference": "e4f5c3f6783d40a219a02bc99dc4171ecdd6d20c", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/64f4b816c61f802e9f4c831a589c9d2e21573ddd", + "reference": "64f4b816c61f802e9f4c831a589c9d2e21573ddd", "shasum": "" }, "require": { "ext-zip": "^1.14.0", - "illuminate/console": "^9.0|^10.0", - "illuminate/contracts": "^9.0|^10.0", - "illuminate/events": "^9.0|^10.0", - "illuminate/filesystem": "^9.0|^10.0", - "illuminate/notifications": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", + "illuminate/console": "^10.10.0", + "illuminate/contracts": "^10.10.0", + "illuminate/events": "^10.10.0", + "illuminate/filesystem": "^10.10.0", + "illuminate/notifications": "^10.10.0", + "illuminate/support": "^10.10.0", "league/flysystem": "^3.0", - "php": "^8.0", + "php": "^8.1", "spatie/db-dumper": "^3.0", "spatie/laravel-package-tools": "^1.6.2", "spatie/laravel-signal-aware-command": "^1.2", @@ -7097,7 +7181,7 @@ "league/flysystem-aws-s3-v3": "^2.0|^3.0", "mockery/mockery": "^1.4", "nunomaduro/larastan": "^2.1", - "orchestra/testbench": "^7.0|^8.0", + "orchestra/testbench": "^8.0", "pestphp/pest": "^1.20", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", @@ -7144,7 +7228,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/8.1.11" + "source": "https://github.com/spatie/laravel-backup/tree/8.4.0" }, "funding": [ { @@ -7156,7 +7240,7 @@ "type": "other" } ], - "time": "2023-06-02T08:56:10+00:00" + "time": "2023-10-17T15:51:49+00:00" }, { "name": "spatie/laravel-image-optimizer", @@ -7228,16 +7312,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.15.0", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "efab1844b8826443135201c4443690f032c3d533" + "reference": "cc7c991555a37f9fa6b814aa03af73f88026a83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/efab1844b8826443135201c4443690f032c3d533", - "reference": "efab1844b8826443135201c4443690f032c3d533", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/cc7c991555a37f9fa6b814aa03af73f88026a83d", + "reference": "cc7c991555a37f9fa6b814aa03af73f88026a83d", "shasum": "" }, "require": { @@ -7276,7 +7360,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.15.0" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.1" }, "funding": [ { @@ -7284,7 +7368,7 @@ "type": "github" } ], - "time": "2023-04-27T08:09:01+00:00" + "time": "2023-08-23T09:04:39+00:00" }, { "name": "spatie/laravel-signal-aware-command", @@ -7362,16 +7446,16 @@ }, { "name": "spatie/temporary-directory", - "version": "2.1.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/spatie/temporary-directory.git", - "reference": "0c804873f6b4042aa8836839dca683c7d0f71831" + "reference": "efc258c9f4da28f0c7661765b8393e4ccee3d19c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/0c804873f6b4042aa8836839dca683c7d0f71831", - "reference": "0c804873f6b4042aa8836839dca683c7d0f71831", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/efc258c9f4da28f0c7661765b8393e4ccee3d19c", + "reference": "efc258c9f4da28f0c7661765b8393e4ccee3d19c", "shasum": "" }, "require": { @@ -7407,7 +7491,7 @@ ], "support": { "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.1.2" + "source": "https://github.com/spatie/temporary-directory/tree/2.2.0" }, "funding": [ { @@ -7419,20 +7503,20 @@ "type": "github" } ], - "time": "2023-04-28T07:47:42+00:00" + "time": "2023-09-25T07:13:36+00:00" }, { "name": "stevebauman/purify", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/stevebauman/purify.git", - "reference": "7b63762b05db9eadc36d7e8b74cf58fa64bfa527" + "reference": "ce8d10c0dfe804d90470ff819b84d891037cd6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stevebauman/purify/zipball/7b63762b05db9eadc36d7e8b74cf58fa64bfa527", - "reference": "7b63762b05db9eadc36d7e8b74cf58fa64bfa527", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/ce8d10c0dfe804d90470ff819b84d891037cd6bc", + "reference": "ce8d10c0dfe804d90470ff819b84d891037cd6bc", "shasum": "" }, "require": { @@ -7483,22 +7567,22 @@ ], "support": { "issues": "https://github.com/stevebauman/purify/issues", - "source": "https://github.com/stevebauman/purify/tree/v6.0.1" + "source": "https://github.com/stevebauman/purify/tree/v6.0.2" }, - "time": "2023-04-06T21:16:20+00:00" + "time": "2023-08-24T18:53:12+00:00" }, { "name": "symfony/cache", - "version": "v6.3.1", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "52cff7608ef6e38376ac11bd1fbb0a220107f066" + "reference": "ba33517043c22c94c7ab04b056476f6f86816cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/52cff7608ef6e38376ac11bd1fbb0a220107f066", - "reference": "52cff7608ef6e38376ac11bd1fbb0a220107f066", + "url": "https://api.github.com/repos/symfony/cache/zipball/ba33517043c22c94c7ab04b056476f6f86816cf8", + "reference": "ba33517043c22c94c7ab04b056476f6f86816cf8", "shasum": "" }, "require": { @@ -7507,7 +7591,7 @@ "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.2.10" + "symfony/var-exporter": "^6.3.6" }, "conflict": { "doctrine/dbal": "<2.13.1", @@ -7522,7 +7606,7 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "symfony/config": "^5.4|^6.0", @@ -7565,7 +7649,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.3.1" + "source": "https://github.com/symfony/cache/tree/v6.3.8" }, "funding": [ { @@ -7581,7 +7665,7 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2023-11-07T10:17:15+00:00" }, { "name": "symfony/cache-contracts", @@ -7661,16 +7745,16 @@ }, { "name": "symfony/console", - "version": "v6.3.0", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7" + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", "shasum": "" }, "require": { @@ -7731,7 +7815,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.0" + "source": "https://github.com/symfony/console/tree/v6.3.8" }, "funding": [ { @@ -7747,20 +7831,20 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2023-10-31T08:09:35+00:00" }, { "name": "symfony/css-selector", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf" + "reference": "883d961421ab1709877c10ac99451632a3d6fa57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/883d961421ab1709877c10ac99451632a3d6fa57", + "reference": "883d961421ab1709877c10ac99451632a3d6fa57", "shasum": "" }, "require": { @@ -7796,7 +7880,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.3.0" + "source": "https://github.com/symfony/css-selector/tree/v6.3.2" }, "funding": [ { @@ -7812,7 +7896,7 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:43:42+00:00" + "time": "2023-07-12T16:00:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -7883,16 +7967,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.3.0", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99d2d814a6351461af350ead4d963bd67451236f" + "reference": "1f69476b64fb47105c06beef757766c376b548c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99d2d814a6351461af350ead4d963bd67451236f", - "reference": "99d2d814a6351461af350ead4d963bd67451236f", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/1f69476b64fb47105c06beef757766c376b548c4", + "reference": "1f69476b64fb47105c06beef757766c376b548c4", "shasum": "" }, "require": { @@ -7937,7 +8021,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.3.0" + "source": "https://github.com/symfony/error-handler/tree/v6.3.5" }, "funding": [ { @@ -7953,20 +8037,20 @@ "type": "tidelift" } ], - "time": "2023-05-10T12:03:13+00:00" + "time": "2023-09-12T06:57:20+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa" + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", - "reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e", "shasum": "" }, "require": { @@ -8017,7 +8101,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2" }, "funding": [ { @@ -8033,7 +8117,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T14:41:17+00:00" + "time": "2023-07-06T06:56:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -8113,16 +8197,16 @@ }, { "name": "symfony/finder", - "version": "v6.3.0", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2" + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d9b01ba073c44cef617c7907ce2419f8d00d75e2", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", "shasum": "" }, "require": { @@ -8157,7 +8241,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.0" + "source": "https://github.com/symfony/finder/tree/v6.3.5" }, "funding": [ { @@ -8173,20 +8257,20 @@ "type": "tidelift" } ], - "time": "2023-04-02T01:25:41+00:00" + "time": "2023-09-26T12:56:25+00:00" }, { "name": "symfony/http-client", - "version": "v6.3.1", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123" + "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1c828a06aef2f5eeba42026dfc532d4fc5406123", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123", + "url": "https://api.github.com/repos/symfony/http-client/zipball/0314e2d49939a9831929d6fc81c01c6df137fd0a", + "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a", "shasum": "" }, "require": { @@ -8249,7 +8333,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.3.1" + "source": "https://github.com/symfony/http-client/tree/v6.3.8" }, "funding": [ { @@ -8265,7 +8349,7 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2023-11-06T18:31:59+00:00" }, { "name": "symfony/http-client-contracts", @@ -8347,16 +8431,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.3.1", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e0ad0d153e1c20069250986cd9e9dd1ccebb0d66" + "reference": "ce332676de1912c4389222987193c3ef38033df6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e0ad0d153e1c20069250986cd9e9dd1ccebb0d66", - "reference": "e0ad0d153e1c20069250986cd9e9dd1ccebb0d66", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce332676de1912c4389222987193c3ef38033df6", + "reference": "ce332676de1912c4389222987193c3ef38033df6", "shasum": "" }, "require": { @@ -8366,12 +8450,12 @@ "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.2" + "symfony/cache": "<6.3" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^5.4|^6.0", + "symfony/cache": "^6.3", "symfony/dependency-injection": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", @@ -8404,7 +8488,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.3.1" + "source": "https://github.com/symfony/http-foundation/tree/v6.3.8" }, "funding": [ { @@ -8420,20 +8504,20 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2023-11-07T10:17:15+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.3.1", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "161e16fd2e35fb4881a43bc8b383dfd5be4ac374" + "reference": "929202375ccf44a309c34aeca8305408442ebcc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/161e16fd2e35fb4881a43bc8b383dfd5be4ac374", - "reference": "161e16fd2e35fb4881a43bc8b383dfd5be4ac374", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/929202375ccf44a309c34aeca8305408442ebcc1", + "reference": "929202375ccf44a309c34aeca8305408442ebcc1", "shasum": "" }, "require": { @@ -8442,7 +8526,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.3", "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.2.7", + "symfony/http-foundation": "^6.3.4", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -8450,7 +8534,7 @@ "symfony/cache": "<5.4", "symfony/config": "<6.1", "symfony/console": "<5.4", - "symfony/dependency-injection": "<6.3", + "symfony/dependency-injection": "<6.3.4", "symfony/doctrine-bridge": "<5.4", "symfony/form": "<5.4", "symfony/http-client": "<5.4", @@ -8474,7 +8558,7 @@ "symfony/config": "^6.1", "symfony/console": "^5.4|^6.0", "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^6.3", + "symfony/dependency-injection": "^6.3.4", "symfony/dom-crawler": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", @@ -8517,7 +8601,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.3.1" + "source": "https://github.com/symfony/http-kernel/tree/v6.3.8" }, "funding": [ { @@ -8533,20 +8617,20 @@ "type": "tidelift" } ], - "time": "2023-06-26T06:07:32+00:00" + "time": "2023-11-10T13:47:32+00:00" }, { "name": "symfony/mailer", - "version": "v6.3.0", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435" + "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/7b03d9be1dea29bfec0a6c7b603f5072a4c97435", - "reference": "7b03d9be1dea29bfec0a6c7b603f5072a4c97435", + "url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06", + "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06", "shasum": "" }, "require": { @@ -8597,7 +8681,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.3.0" + "source": "https://github.com/symfony/mailer/tree/v6.3.5" }, "funding": [ { @@ -8613,20 +8697,20 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2023-09-06T09:47:15+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.3.0", + "version": "v6.3.6", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "2fafefe8683a93155aceb6cca622c7cee2e27174" + "reference": "8d9741467c53750dc8ccda23a1cdb91cda732571" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/2fafefe8683a93155aceb6cca622c7cee2e27174", - "reference": "2fafefe8683a93155aceb6cca622c7cee2e27174", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/8d9741467c53750dc8ccda23a1cdb91cda732571", + "reference": "8d9741467c53750dc8ccda23a1cdb91cda732571", "shasum": "" }, "require": { @@ -8666,7 +8750,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.3.0" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.3.6" }, "funding": [ { @@ -8682,24 +8766,25 @@ "type": "tidelift" } ], - "time": "2023-05-02T16:15:19+00:00" + "time": "2023-10-12T13:32:47+00:00" }, { "name": "symfony/mime", - "version": "v6.3.0", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad" + "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", + "url": "https://api.github.com/repos/symfony/mime/zipball/d5179eedf1cb2946dbd760475ebf05c251ef6a6e", + "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -8708,7 +8793,7 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<5.4", - "symfony/serializer": "<6.2" + "symfony/serializer": "<6.2.13|>=6.3,<6.3.2" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", @@ -8717,7 +8802,7 @@ "symfony/dependency-injection": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0", "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "^6.2" + "symfony/serializer": "~6.2.13|^6.3.2" }, "type": "library", "autoload": { @@ -8749,7 +8834,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.3.0" + "source": "https://github.com/symfony/mime/tree/v6.3.5" }, "funding": [ { @@ -8765,20 +8850,20 @@ "type": "tidelift" } ], - "time": "2023-04-28T15:57:00+00:00" + "time": "2023-09-29T06:59:36+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -8793,7 +8878,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8831,7 +8916,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -8847,20 +8932,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -8872,7 +8957,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8912,7 +8997,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -8928,20 +9013,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", "shasum": "" }, "require": { @@ -8955,7 +9040,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8999,7 +9084,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" }, "funding": [ { @@ -9015,20 +9100,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:30:37+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -9040,7 +9125,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9083,7 +9168,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -9099,20 +9184,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -9127,7 +9212,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9166,7 +9251,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -9182,20 +9267,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", "shasum": "" }, "require": { @@ -9204,7 +9289,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9242,7 +9327,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" }, "funding": [ { @@ -9258,20 +9343,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -9280,7 +9365,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9325,7 +9410,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -9341,20 +9426,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57" + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/508c652ba3ccf69f8c97f251534f229791b52a57", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", "shasum": "" }, "require": { @@ -9364,7 +9449,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9377,7 +9462,10 @@ ], "psr-4": { "Symfony\\Polyfill\\Php83\\": "" - } + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9402,7 +9490,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" }, "funding": [ { @@ -9418,20 +9506,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-08-16T06:22:46+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166" + "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e", + "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e", "shasum": "" }, "require": { @@ -9446,7 +9534,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -9484,7 +9572,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0" }, "funding": [ { @@ -9500,20 +9588,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v6.3.0", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628" + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", "shasum": "" }, "require": { @@ -9545,7 +9633,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.0" + "source": "https://github.com/symfony/process/tree/v6.3.4" }, "funding": [ { @@ -9561,25 +9649,26 @@ "type": "tidelift" } ], - "time": "2023-05-19T08:06:44+00:00" + "time": "2023-08-07T10:39:22+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.2.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "28a732c05bbad801304ad5a5c674cf2970508993" + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/28a732c05bbad801304ad5a5c674cf2970508993", - "reference": "28a732c05bbad801304ad5a5c674cf2970508993", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", "symfony/http-foundation": "^5.4 || ^6.0" }, "require-dev": { @@ -9598,7 +9687,7 @@ "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-main": "2.2-dev" + "dev-main": "2.3-dev" } }, "autoload": { @@ -9633,7 +9722,7 @@ ], "support": { "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.2.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" }, "funding": [ { @@ -9649,24 +9738,25 @@ "type": "tidelift" } ], - "time": "2023-04-21T08:40:19+00:00" + "time": "2023-07-26T11:53:26+00:00" }, { "name": "symfony/routing", - "version": "v6.3.1", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "d37ad1779c38b8eb71996d17dc13030dcb7f9cf5" + "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/d37ad1779c38b8eb71996d17dc13030dcb7f9cf5", - "reference": "d37ad1779c38b8eb71996d17dc13030dcb7f9cf5", + "url": "https://api.github.com/repos/symfony/routing/zipball/82616e59acd3e3d9c916bba798326cb7796d7d31", + "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.12", @@ -9715,7 +9805,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.3.1" + "source": "https://github.com/symfony/routing/tree/v6.3.5" }, "funding": [ { @@ -9731,7 +9821,7 @@ "type": "tidelift" } ], - "time": "2023-06-05T15:30:22+00:00" + "time": "2023-09-20T16:05:51+00:00" }, { "name": "symfony/service-contracts", @@ -9817,16 +9907,16 @@ }, { "name": "symfony/string", - "version": "v6.3.0", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f" + "reference": "13880a87790c76ef994c91e87efb96134522577a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", "shasum": "" }, "require": { @@ -9883,7 +9973,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.0" + "source": "https://github.com/symfony/string/tree/v6.3.8" }, "funding": [ { @@ -9899,24 +9989,25 @@ "type": "tidelift" } ], - "time": "2023-03-21T21:06:29+00:00" + "time": "2023-11-09T08:28:21+00:00" }, { "name": "symfony/translation", - "version": "v6.3.0", + "version": "v6.3.7", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f" + "reference": "30212e7c87dcb79c83f6362b00bde0e0b1213499" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f72b2cba8f79dd9d536f534f76874b58ad37876f", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f", + "url": "https://api.github.com/repos/symfony/translation/zipball/30212e7c87dcb79c83f6362b00bde0e0b1213499", + "reference": "30212e7c87dcb79c83f6362b00bde0e0b1213499", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, @@ -9977,7 +10068,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.3.0" + "source": "https://github.com/symfony/translation/tree/v6.3.7" }, "funding": [ { @@ -9993,7 +10084,7 @@ "type": "tidelift" } ], - "time": "2023-05-19T12:46:45+00:00" + "time": "2023-10-28T23:11:45+00:00" }, { "name": "symfony/translation-contracts", @@ -10075,16 +10166,16 @@ }, { "name": "symfony/uid", - "version": "v6.3.0", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384" + "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/01b0f20b1351d997711c56f1638f7a8c3061e384", - "reference": "01b0f20b1351d997711c56f1638f7a8c3061e384", + "url": "https://api.github.com/repos/symfony/uid/zipball/819fa5ac210fb7ddda4752b91a82f50be7493dd9", + "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9", "shasum": "" }, "require": { @@ -10129,7 +10220,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.3.0" + "source": "https://github.com/symfony/uid/tree/v6.3.8" }, "funding": [ { @@ -10145,24 +10236,25 @@ "type": "tidelift" } ], - "time": "2023-04-08T07:25:02+00:00" + "time": "2023-10-31T08:07:48+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.1", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c81268d6960ddb47af17391a27d222bd58cf0515" + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c81268d6960ddb47af17391a27d222bd58cf0515", - "reference": "c81268d6960ddb47af17391a27d222bd58cf0515", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/81acabba9046550e89634876ca64bfcd3c06aa0a", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -10171,6 +10263,7 @@ "require-dev": { "ext-iconv": "*", "symfony/console": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", "symfony/process": "^5.4|^6.0", "symfony/uid": "^5.4|^6.0", "twig/twig": "^2.13|^3.0.4" @@ -10211,7 +10304,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.1" + "source": "https://github.com/symfony/var-dumper/tree/v6.3.8" }, "funding": [ { @@ -10227,20 +10320,20 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-11-08T10:42:36+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.3.0", + "version": "v6.3.6", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "db5416d04269f2827d8c54331ba4cfa42620d350" + "reference": "374d289c13cb989027274c86206ddc63b16a2441" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/db5416d04269f2827d8c54331ba4cfa42620d350", - "reference": "db5416d04269f2827d8c54331ba4cfa42620d350", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/374d289c13cb989027274c86206ddc63b16a2441", + "reference": "374d289c13cb989027274c86206ddc63b16a2441", "shasum": "" }, "require": { @@ -10285,7 +10378,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.3.0" + "source": "https://github.com/symfony/var-exporter/tree/v6.3.6" }, "funding": [ { @@ -10301,7 +10394,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T08:48:44+00:00" + "time": "2023-10-13T09:16:49+00:00" }, { "name": "tightenco/collect", @@ -10630,16 +10723,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.10.0", + "version": "v6.11.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa" + "reference": "8083a421cee7dad847ee7c464529043ba30de380" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/c2243b20bcd99c3f651018d1447144372f39b4fa", - "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/8083a421cee7dad847ee7c464529043ba30de380", + "reference": "8083a421cee7dad847ee7c464529043ba30de380", "shasum": "" }, "require": { @@ -10647,7 +10740,7 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "jean85/pretty-package-versions": "^2.0.5", "php": "^7.3 || ^8.0", "phpunit/php-code-coverage": "^9.2.25", @@ -10655,16 +10748,16 @@ "phpunit/php-timer": "^5.0.3", "phpunit/phpunit": "^9.6.4", "sebastian/environment": "^5.1.5", - "symfony/console": "^5.4.21 || ^6.2.7", - "symfony/process": "^5.4.21 || ^6.2.7" + "symfony/console": "^5.4.28 || ^6.3.4 || ^7.0.0", + "symfony/process": "^5.4.28 || ^6.3.4 || ^7.0.0" }, "require-dev": { - "doctrine/coding-standard": "^10.0.0", + "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "infection/infection": "^0.26.19", + "infection/infection": "^0.27.6", "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^5.4.21 || ^6.2.7", + "symfony/filesystem": "^5.4.25 || ^6.3.1 || ^7.0.0", "vimeo/psalm": "^5.7.7" }, "bin": [ @@ -10706,7 +10799,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.10.0" + "source": "https://github.com/paratestphp/paratest/tree/v6.11.0" }, "funding": [ { @@ -10718,7 +10811,7 @@ "type": "paypal" } ], - "time": "2023-05-25T13:47:58+00:00" + "time": "2023-10-31T09:13:57+00:00" }, { "name": "doctrine/instantiator", @@ -10860,16 +10953,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "0.5.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" + "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/85193c0b0cb5c47894b5eaec906e946f054e7077", + "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077", "shasum": "" }, "require": { @@ -10877,13 +10970,13 @@ }, "require-dev": { "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", "phpstan/phpstan": "^1.9.2", "phpstan/phpstan-deprecation-rules": "^1.0.0", "phpstan/phpstan-phpunit": "^1.2.2", "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, "type": "library", @@ -10909,7 +11002,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.0.0" }, "funding": [ { @@ -10917,20 +11010,20 @@ "type": "github" } ], - "time": "2022-12-24T12:35:10+00:00" + "time": "2023-09-17T21:38:23+00:00" }, { "name": "filp/whoops", - "version": "2.15.3", + "version": "2.15.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "c83e88a30524f9360b11f585f71e6b17313b7187" + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/c83e88a30524f9360b11f585f71e6b17313b7187", - "reference": "c83e88a30524f9360b11f585f71e6b17313b7187", + "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", "shasum": "" }, "require": { @@ -10980,7 +11073,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.3" + "source": "https://github.com/filp/whoops/tree/2.15.4" }, "funding": [ { @@ -10988,7 +11081,7 @@ "type": "github" } ], - "time": "2023-07-13T12:00:00+00:00" + "time": "2023-11-03T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -11102,16 +11195,16 @@ }, { "name": "laravel/telescope", - "version": "v4.15.2", + "version": "v4.17.2", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14" + "reference": "64da53ee46b99ef328458eaed32202b51e325a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/5d74ae4c9f269b756d7877ad1527770c59846e14", - "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14", + "url": "https://api.github.com/repos/laravel/telescope/zipball/64da53ee46b99ef328458eaed32202b51e325a11", + "reference": "64da53ee46b99ef328458eaed32202b51e325a11", "shasum": "" }, "require": { @@ -11167,43 +11260,39 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v4.15.2" + "source": "https://github.com/laravel/telescope/tree/v4.17.2" }, - "time": "2023-07-13T20:06:27+00:00" + "time": "2023-11-01T14:01:06+00:00" }, { "name": "mockery/mockery", - "version": "1.6.2", + "version": "1.6.6", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191" + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/13a7fa2642c76c58fa2806ef7f565344c817a191", - "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191", + "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e", + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e", "shasum": "" }, "require": { "hamcrest/hamcrest-php": "^2.0.1", "lib-pcre": ">=7.0", - "php": "^7.4 || ^8.0" + "php": ">=7.3" }, "conflict": { "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3", - "psalm/plugin-phpunit": "^0.18", - "vimeo/psalm": "^5.9" + "phpunit/phpunit": "^8.5 || ^9.6.10", + "psalm/plugin-phpunit": "^0.18.4", + "symplify/easy-coding-standard": "^11.5.0", + "vimeo/psalm": "^4.30" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.6.x-dev" - } - }, "autoload": { "files": [ "library/helpers.php", @@ -11221,12 +11310,20 @@ { "name": "Pádraic Brady", "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" + "homepage": "https://github.com/padraic", + "role": "Author" }, { "name": "Dave Marshall", "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], "description": "Mockery is a simple yet flexible PHP mock object framework", @@ -11244,10 +11341,13 @@ "testing" ], "support": { + "docs": "https://docs.mockery.io/", "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.6.2" + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" }, - "time": "2023-06-07T09:07:52+00:00" + "time": "2023-08-09T00:03:52+00:00" }, { "name": "myclabs/deep-copy", @@ -11509,16 +11609,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "version": "9.2.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", "shasum": "" }, "require": { @@ -11574,7 +11674,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" }, "funding": [ { @@ -11582,7 +11683,7 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2023-09-19T04:57:46+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11827,16 +11928,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.10", + "version": "9.6.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", - "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", "shasum": "" }, "require": { @@ -11851,7 +11952,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -11910,7 +12011,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" }, "funding": [ { @@ -11926,7 +12027,7 @@ "type": "tidelift" } ], - "time": "2023-07-10T04:04:23+00:00" + "time": "2023-09-19T05:39:22+00:00" }, { "name": "sebastian/cli-parser", @@ -12434,16 +12535,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -12486,7 +12587,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -12494,7 +12595,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -12959,5 +13060,5 @@ "ext-openssl": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 0fce5de6cd0165d62cc9fa77bc19cf47cfccb98e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 05:53:16 -0700 Subject: [PATCH 029/977] Update composer deps --- composer.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index 1c1126539..ccf0148d6 100644 --- a/composer.lock +++ b/composer.lock @@ -3020,20 +3020,20 @@ }, { "name": "lcobucci/clock", - "version": "3.1.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc" + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/30a854ceb22bd87d83a7a4563b3f6312453945fc", - "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", "shasum": "" }, "require": { - "php": "~8.2.0", + "php": "~8.1.0 || ~8.2.0", "psr/clock": "^1.0" }, "provide": { @@ -3041,13 +3041,13 @@ }, "require-dev": { "infection/infection": "^0.26", - "lcobucci/coding-standard": "^10.0.0", + "lcobucci/coding-standard": "^9.0", "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.7", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.10", - "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.0.17" + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-deprecation-rules": "^1.1.1", + "phpstan/phpstan-phpunit": "^1.3.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.27" }, "type": "library", "autoload": { @@ -3068,7 +3068,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.1.0" + "source": "https://github.com/lcobucci/clock/tree/3.0.0" }, "funding": [ { @@ -3080,7 +3080,7 @@ "type": "patreon" } ], - "time": "2023-03-20T19:12:25+00:00" + "time": "2022-12-19T15:00:24+00:00" }, { "name": "lcobucci/jwt", From 20a560bfd140f1f23e68ce57bb9d7e9e0073fb68 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 07:23:11 -0700 Subject: [PATCH 030/977] Update FollowerService, add localFollowerIds method --- app/Services/FollowerService.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 1c00a6f49..677507b03 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -19,6 +19,7 @@ class FollowerService const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; const FOLLOWING_KEY = 'pf:services:follow:following:id:'; const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; + const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; public static function add($actor, $target, $refresh = true) { @@ -212,4 +213,15 @@ class FollowerService Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); Cache::forget(self::FOLLOWING_SYNC_KEY . $id); } + + public static function localFollowerIds($pid, $limit = 0) + { + $key = self::FOLLOWERS_LOCAL_KEY . $pid; + $res = Cache::remember($key, 86400, function() use($pid) { + return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); + }); + return $limit ? + $res->take($limit)->values()->toArray() : + $res->values()->toArray(); + } } From df1f98d5f7cabe2c39c0ae05c9951a4093a54ef5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 11 Nov 2023 07:23:42 -0700 Subject: [PATCH 031/977] Add FeedInsertPipeline job --- .../HomeFeedPipeline/FeedInsertPipeline.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php new file mode 100644 index 000000000..6ccdc18ef --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -0,0 +1,76 @@ +status->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:f-insert:sid:{$this->status->id}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct(Status $status) + { + $this->status = $status; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $status = $this->status; + $sid = $status->id; + $pid = $status->profile_id; + $ids = FollowerService::localFollowerIds($pid); + + foreach($ids as $id) { + HomeTimelineService::add($id, $sid); + } + } +} From de2b5ba4e97c7a94bc4bde1f24f55fd545160d78 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 16:36:02 -0700 Subject: [PATCH 032/977] Update FollowerService, reduce localFollowerIds ttl --- app/Services/FollowerService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 677507b03..5525a5da2 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -217,7 +217,7 @@ class FollowerService public static function localFollowerIds($pid, $limit = 0) { $key = self::FOLLOWERS_LOCAL_KEY . $pid; - $res = Cache::remember($key, 86400, function() use($pid) { + $res = Cache::remember($key, 7200, function() use($pid) { return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); }); return $limit ? From ce63c4997b5eb4406696953cdfa84ed1e8419aaa Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 20:54:32 -0700 Subject: [PATCH 033/977] Add Feed fanout --- .../HomeFeedPipeline/FeedInsertPipeline.php | 21 +++--- .../HomeFeedPipeline/FeedRemovePipeline.php | 73 +++++++++++++++++++ app/Jobs/StatusPipeline/StatusEntityLexer.php | 26 +++++-- app/Observers/StatusObserver.php | 5 ++ 4 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index 6ccdc18ef..f0455c638 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -10,8 +10,6 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; -use App\Status; -use App\Follower; use App\Services\FollowerService; use App\Services\HomeTimelineService; @@ -19,7 +17,8 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; + protected $sid; + protected $pid; public $timeout = 900; public $tries = 3; @@ -38,7 +37,7 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function uniqueId(): string { - return 'hfp:f-insert:sid:' . $this->status->id; + return 'hts:feed:insert:sid:' . $this->sid; } /** @@ -48,15 +47,16 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function middleware(): array { - return [(new WithoutOverlapping("hfp:f-insert:sid:{$this->status->id}"))->shared()->dontRelease()]; + return [(new WithoutOverlapping("hts:feed:insert:sid:{$this->sid}"))->shared()->dontRelease()]; } /** * Create a new job instance. */ - public function __construct(Status $status) + public function __construct($sid, $pid) { - $this->status = $status; + $this->sid = $sid; + $this->pid = $pid; } /** @@ -64,13 +64,10 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function handle(): void { - $status = $this->status; - $sid = $status->id; - $pid = $status->profile_id; - $ids = FollowerService::localFollowerIds($pid); + $ids = FollowerService::localFollowerIds($this->pid); foreach($ids as $id) { - HomeTimelineService::add($id, $sid); + HomeTimelineService::add($id, $this->sid); } } } diff --git a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php new file mode 100644 index 000000000..745907084 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php @@ -0,0 +1,73 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ids = FollowerService::localFollowerIds($this->pid); + + foreach($ids as $id) { + HomeTimelineService::rem($id, $this->sid); + } + } +} diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 2bbc92102..0a913d793 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -21,6 +21,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use App\Services\UserFilterService; use App\Services\AdminShadowFilterService; +use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; +use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline; class StatusEntityLexer implements ShouldQueue { @@ -105,12 +107,12 @@ class StatusEntityLexer implements ShouldQueue } DB::transaction(function () use ($status, $tag) { $slug = str_slug($tag, '-', false); - $hashtag = Hashtag::where('slug', $slug)->first(); - if (!$hashtag) { - $hashtag = Hashtag::create( - ['name' => $tag, 'slug' => $slug] - ); - } + + $hashtag = Hashtag::firstOrCreate([ + 'slug' => $slug + ], [ + 'name' => $tag + ]); StatusHashtag::firstOrCreate( [ @@ -150,6 +152,18 @@ class StatusEntityLexer implements ShouldQueue MentionPipeline::dispatch($status, $m); }); } + $this->fanout(); + } + + public function fanout() + { + $status = $this->status; + + if(config('exp.cached_home_timeline')) { + if($status->in_reply_to_id == null && in_array($status->scope, ['public', 'unlisted', 'private'])) { + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } + } $this->deliver(); } diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php index e58997165..9b9f8e4f1 100644 --- a/app/Observers/StatusObserver.php +++ b/app/Observers/StatusObserver.php @@ -7,6 +7,7 @@ use App\Services\ProfileStatusService; use Cache; use App\Models\ImportPost; use App\Services\ImportService; +use App\Jobs\HomeFeedPipeline\FeedRemovePipeline; class StatusObserver { @@ -63,6 +64,10 @@ class StatusObserver ImportPost::whereProfileId($status->profile_id)->whereStatusId($status->id)->delete(); ImportService::clearImportedFiles($status->profile_id); } + + if(config('exp.cached_home_timeline')) { + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } } /** From 2a8a29905829a996e913c5fe49c79bb1eeb5c6d6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 21:09:06 -0700 Subject: [PATCH 034/977] Update HomeTimelineService --- app/Services/HomeTimelineService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php index 937f3202b..c42c528ab 100644 --- a/app/Services/HomeTimelineService.php +++ b/app/Services/HomeTimelineService.php @@ -46,7 +46,7 @@ class HomeTimelineService public static function add($id, $val) { - if(self::count($id) > 400) { + if(self::count($id) >= 400) { Redis::zpopmin(self::CACHE_KEY . $id); } From 24c370ee220f412f73b28a2278f67fff3bea1a18 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 21:13:08 -0700 Subject: [PATCH 035/977] Update ApiV1Controller, add experimental home timeline support to v1/timelines/home --- app/Http/Controllers/Api/ApiV1Controller.php | 136 ++++++++++++++----- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 8397faa66..95180dfb2 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -71,6 +71,7 @@ use App\Services\{ CollectionService, FollowerService, HashtagService, + HomeTimelineService, InstanceService, LikeService, NetworkTimelineService, @@ -102,6 +103,7 @@ use Illuminate\Support\Facades\RateLimiter; use Purify; use Carbon\Carbon; use App\Http\Resources\MastoApi\FollowedTagResource; +use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline; class ApiV1Controller extends Controller { @@ -2129,11 +2131,11 @@ class ApiV1Controller extends Controller public function timelineHome(Request $request) { $this->validate($request,[ - 'page' => 'sometimes|integer|max:40', - 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'sometimes|integer|min:1|max:100', - 'include_reblogs' => 'sometimes', + 'page' => 'sometimes|integer|max:40', + 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, + 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, + 'limit' => 'sometimes|integer|min:1|max:40', + 'include_reblogs' => 'sometimes', ]); $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -2142,13 +2144,77 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 20; $pid = $request->user()->profile_id; - $includeReblogs = $request->filled('include_reblogs'); - $nullFields = $includeReblogs ? - ['in_reply_to_id'] : - ['in_reply_to_id', 'reblog_of_id']; - $inTypes = $includeReblogs ? - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + $includeReblogs = $request->filled('include_reblogs'); + $nullFields = $includeReblogs ? + ['in_reply_to_id'] : + ['in_reply_to_id', 'reblog_of_id']; + $inTypes = $includeReblogs ? + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + + if(config('exp.cached_home_timeline')) { + if($min || $max) { + if($request->has('min_id')) { + $res = HomeTimelineService::getRankedMinId($pid, $min ?? 0, $limit + 10); + } else { + $res = HomeTimelineService::getRankedMaxId($pid, $max ?? 0, $limit + 10); + } + } else { + $res = HomeTimelineService::get($pid, 0, $limit + 10); + } + + if(!$res) { + $res = Cache::has('pf:services:apiv1:home:cached:coldbootcheck:' . $pid); + if(!$res) { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); + FeedWarmCachePipeline::dispatchSync($pid); + return response()->json([], 206); + } else { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); + return response()->json([], 206); + } + } + + $res = collect($res)->take($limit)->map(function($id) use($napi) { + return $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + })->filter(function($res) { + return $res && isset($res['account']); + })->map(function($status) use($pid) { + if($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $status['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + return $status; + }); + + $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; + $minId = $res->map(function($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function($s) { + return ['id' => $s['id']]; + })->max('id'); + + if($minId == $maxId) { + $minId = null; + } + + if($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } + + if($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + if($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } + + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res->toArray(), 200, $headers); + } $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); @@ -2161,6 +2227,7 @@ class ApiV1Controller extends Controller $following = array_diff($following, $muted); } + if($min || $max) { $dir = $min ? '>' : '<'; $id = $min ?? $max; @@ -2199,22 +2266,22 @@ class ApiV1Controller extends Controller if($pid) { $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); } return $status; }) ->filter(function($status) { return $status && isset($status['account']); }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } + ->map(function($status) use($pid) { + if(!empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } - return $status; - }) + return $status; + }) ->take($limit) ->values(); } else { @@ -2252,22 +2319,22 @@ class ApiV1Controller extends Controller if($pid) { $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); } return $status; }) ->filter(function($status) { return $status && isset($status['account']); }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } + ->map(function($status) use($pid) { + if(!empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } - return $status; - }) + return $status; + }) ->take($limit) ->values(); } @@ -2321,10 +2388,11 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 20; $user = $request->user(); - $remote = ($request->has('remote') && $request->input('remote') == true) || ($request->filled('local') && $request->input('local') != true); + $remote = $request->has('remote'); + $local = $request->has('local'); $filtered = $user ? UserFilterService::filters($user->profile_id) : []; - if((!$request->has('local') || $remote) && config('instance.timeline.network.cached')) { + if($remote && config('instance.timeline.network.cached')) { Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { if(NetworkTimelineService::count() == 0) { NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); @@ -2338,7 +2406,9 @@ class ApiV1Controller extends Controller } else { $feed = NetworkTimelineService::get(0, $limit + 5); } - } else { + } + + if($local || !$remote && !$local) { Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { if(PublicTimelineService::count() == 0) { PublicTimelineService::warmCache(true, 400); From 73cb8b43b3b82a4c766a151197d20b6288452c90 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 22:44:15 -0700 Subject: [PATCH 036/977] Update HomeFeedPipeline, add follow/unfollow --- app/Jobs/FollowPipeline/UnfollowPipeline.php | 3 + .../HomeFeedPipeline/FeedFollowPipeline.php | 83 +++++++++++++++++++ .../HomeFeedPipeline/FeedUnfollowPipeline.php | 81 ++++++++++++++++++ app/Observers/FollowerObserver.php | 3 + 4 files changed, 170 insertions(+) create mode 100644 app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php create mode 100644 app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php diff --git a/app/Jobs/FollowPipeline/UnfollowPipeline.php b/app/Jobs/FollowPipeline/UnfollowPipeline.php index c00246e2f..99f763f5c 100644 --- a/app/Jobs/FollowPipeline/UnfollowPipeline.php +++ b/app/Jobs/FollowPipeline/UnfollowPipeline.php @@ -17,6 +17,7 @@ use Illuminate\Support\Facades\Redis; use App\Services\AccountService; use App\Services\FollowerService; use App\Services\NotificationService; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; class UnfollowPipeline implements ShouldQueue { @@ -55,6 +56,8 @@ class UnfollowPipeline implements ShouldQueue return; } + FeedUnfollowPipeline::dispatch($actor, $target)->onQueue('follow'); + FollowerService::remove($actor, $target); $actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY . $actor); diff --git a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php new file mode 100644 index 000000000..841409b6f --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php @@ -0,0 +1,83 @@ +actorId . ':fid:' . $this->followingId; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:insert:follows:aid:{$this->actorId}:fid:{$this->followingId}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($actorId, $followingId) + { + $this->actorId = $actorId; + $this->followingId = $followingId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $actorId = $this->actorId; + $followingId = $this->followingId; + + $ids = Status::where('profile_id', $followingId) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->limit(HomeTimelineService::FOLLOWER_FEED_POST_LIMIT) + ->pluck('id'); + + foreach($ids as $id) { + HomeTimelineService::add($actorId, $id); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php new file mode 100644 index 000000000..996e74c10 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedUnfollowPipeline.php @@ -0,0 +1,81 @@ +actorId . ':fid:' . $this->followingId; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:follows:aid:{$this->actorId}:fid:{$this->followingId}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($actorId, $followingId) + { + $this->actorId = $actorId; + $this->followingId = $followingId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $actorId = $this->actorId; + $followingId = $this->followingId; + + $ids = HomeTimelineService::get($actorId, 0, -1); + foreach($ids as $id) { + $status = StatusService::get($id, false); + if($status && isset($status['account'], $status['account']['id'])) { + if($status['account']['id'] == $followingId) { + HomeTimelineService::rem($actorId, $id); + } + } + } + } +} diff --git a/app/Observers/FollowerObserver.php b/app/Observers/FollowerObserver.php index f230bb79f..bc85e48a0 100644 --- a/app/Observers/FollowerObserver.php +++ b/app/Observers/FollowerObserver.php @@ -5,6 +5,8 @@ namespace App\Observers; use App\Follower; use App\Services\FollowerService; use Cache; +use App\Jobs\HomeFeedPipeline\FeedFollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; class FollowerObserver { @@ -21,6 +23,7 @@ class FollowerObserver } FollowerService::add($follower->profile_id, $follower->following_id); + FeedFollowPipeline::dispatch($follower->profile_id, $follower->following_id)->onQueue('follow'); } /** From 115a9d2dec6ed536d714cbbb75e86866080bb2d2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 22:45:09 -0700 Subject: [PATCH 037/977] Update HomeTimelineService --- app/Services/HomeTimelineService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php index c42c528ab..35d9f405f 100644 --- a/app/Services/HomeTimelineService.php +++ b/app/Services/HomeTimelineService.php @@ -10,6 +10,7 @@ use App\Status; class HomeTimelineService { const CACHE_KEY = 'pf:services:timeline:home:'; + const FOLLOWER_FEED_POST_LIMIT = 10; public static function get($id, $start = 0, $stop = 10) { From 43443503a1e0e37419a6143284a3ae57fd3d7a4a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:00:33 -0700 Subject: [PATCH 038/977] Update FeedFollowPipeline, use more efficient query --- app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php | 6 +++++- app/Services/HomeTimelineService.php | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php index 841409b6f..64d646354 100644 --- a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php @@ -12,6 +12,7 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\Services\AccountService; use App\Services\HomeTimelineService; +use App\Services\SnowflakeService; use App\Status; class FeedFollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing @@ -68,7 +69,10 @@ class FeedFollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing $actorId = $this->actorId; $followingId = $this->followingId; - $ids = Status::where('profile_id', $followingId) + $minId = SnowflakeService::byDate(now()->subMonths(6)); + + $ids = Status::where('id', '>', $minId) + ->where('profile_id', $followingId) ->whereNull(['in_reply_to_id', 'reblog_of_id']) ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) ->whereIn('visibility',['public', 'unlisted', 'private']) diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php index 35d9f405f..075791362 100644 --- a/app/Services/HomeTimelineService.php +++ b/app/Services/HomeTimelineService.php @@ -73,7 +73,10 @@ class HomeTimelineService return $following->push($id)->toArray(); }); - $ids = Status::whereIn('profile_id', $following) + $minId = SnowflakeService::byDate(now()->subMonths(6)); + + $ids = Status::where('id', '>', $minId) + ->whereIn('profile_id', $following) ->whereNull(['in_reply_to_id', 'reblog_of_id']) ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) ->whereIn('visibility',['public', 'unlisted', 'private']) From 7deaaed4ddbaaaf040d83c8e488b17c2661a49b4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:32:14 -0700 Subject: [PATCH 039/977] Add migration --- ...ollowers_count_index_to_profiles_table.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php diff --git a/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php b/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php new file mode 100644 index 000000000..bcc97577c --- /dev/null +++ b/database/migrations/2023_11_13_062429_add_followers_count_index_to_profiles_table.php @@ -0,0 +1,34 @@ +index('followers_count', 'profiles_followers_count_index'); + $table->index('following_count', 'profiles_following_count_index'); + $table->index('status_count', 'profiles_status_count_index'); + $table->index('is_private', 'profiles_is_private_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('profiles', function (Blueprint $table) { + $table->dropIndex('profiles_followers_count_index'); + $table->dropIndex('profiles_following_count_index'); + $table->dropIndex('profiles_status_count_index'); + $table->dropIndex('profiles_is_private_index'); + }); + } +}; From e917341651fe85835885721b3c116f6e4aaa7e11 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:32:45 -0700 Subject: [PATCH 040/977] Update ApiV1Controller --- app/Http/Controllers/Api/ApiV1Controller.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 95180dfb2..51243361b 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3603,19 +3603,6 @@ class ApiV1Controller extends Controller ->filter(function($profile) use($pid) { return $profile['id'] != $pid; }) - ->map(function($profile) { - $ids = collect(ProfileStatusService::get($profile['id'], 0, 9)) - ->map(function($id) { - return StatusService::get($id, true); - }) - ->filter(function($post) { - return $post && isset($post['id']); - }) - ->take(3) - ->values(); - $profile['recent_posts'] = $ids; - return $profile; - }) ->take(6) ->values(); From 125208fb9e5956bac2c32b4f68ac08ba640eb88a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:52:10 -0700 Subject: [PATCH 041/977] Update UserFilterObserver, dispatch FeedFollowPipeline jobs --- app/Observers/UserFilterObserver.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Observers/UserFilterObserver.php b/app/Observers/UserFilterObserver.php index 8e149e7d9..75867e64b 100644 --- a/app/Observers/UserFilterObserver.php +++ b/app/Observers/UserFilterObserver.php @@ -4,6 +4,8 @@ namespace App\Observers; use App\UserFilter; use App\Services\UserFilterService; +use App\Jobs\HomeFeedPipeline\FeedFollowPipeline; +use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; class UserFilterObserver { @@ -78,10 +80,12 @@ class UserFilterObserver switch ($userFilter->filter_type) { case 'mute': UserFilterService::mute($userFilter->user_id, $userFilter->filterable_id); + FeedUnfollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; case 'block': UserFilterService::block($userFilter->user_id, $userFilter->filterable_id); + FeedUnfollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; } } @@ -96,10 +100,12 @@ class UserFilterObserver switch ($userFilter->filter_type) { case 'mute': UserFilterService::unmute($userFilter->user_id, $userFilter->filterable_id); + FeedFollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; case 'block': UserFilterService::unblock($userFilter->user_id, $userFilter->filterable_id); + FeedFollowPipeline::dispatch($userFilter->user_id, $userFilter->filterable_id)->onQueue('feed'); break; } } From 386e64d5e86e00d85a6a057ae7915d32ac65d250 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:52:44 -0700 Subject: [PATCH 042/977] Update StatusEntityLexer, skip reblogs on FeedInsertPipeline --- app/Jobs/StatusPipeline/StatusEntityLexer.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 0a913d793..76d2d98d0 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -160,7 +160,10 @@ class StatusEntityLexer implements ShouldQueue $status = $this->status; if(config('exp.cached_home_timeline')) { - if($status->in_reply_to_id == null && in_array($status->scope, ['public', 'unlisted', 'private'])) { + if( $status->in_reply_to_id === null && + $status->reblog_of_id === null && + in_array($status->scope, ['public', 'unlisted', 'private']) + ) { FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); } } From c39b9afbfd2328a598fdc2e8bb45d88609040911 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 12 Nov 2023 23:53:22 -0700 Subject: [PATCH 043/977] Update HomeTimelineService, apply filters to feed warm logic --- app/Services/HomeTimelineService.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php index 075791362..6a2db0482 100644 --- a/app/Services/HomeTimelineService.php +++ b/app/Services/HomeTimelineService.php @@ -75,6 +75,12 @@ class HomeTimelineService $minId = SnowflakeService::byDate(now()->subMonths(6)); + $filters = UserFilterService::filters($id); + + if($filters && count($filters)) { + $following = array_diff($following, $filters); + } + $ids = Status::where('id', '>', $minId) ->whereIn('profile_id', $following) ->whereNull(['in_reply_to_id', 'reblog_of_id']) From 05d646c034dd274e6ff8f6a09a9dcad8b579c90d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 00:00:53 -0700 Subject: [PATCH 044/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ded48d3e..60c9e187a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7)) - Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde)) - Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610)) +- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) @@ -46,6 +47,7 @@ - Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a)) - Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714)) - Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) +- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 0e431271978427b9cad01a7238357dfd0a4bd193 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 01:00:53 -0700 Subject: [PATCH 045/977] Update mail config --- config/mail.php | 148 ++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 79 deletions(-) diff --git a/config/mail.php b/config/mail.php index 4baa90ec8..1cfb51767 100644 --- a/config/mail.php +++ b/config/mail.php @@ -4,45 +4,89 @@ return [ /* |-------------------------------------------------------------------------- - | Mail Driver + | Default Mailer |-------------------------------------------------------------------------- | - | Laravel supports both SMTP and PHP's "mail" function as drivers for the - | sending of e-mail. You may specify which one you're using throughout - | your application here. By default, Laravel is setup for SMTP mail. - | - | Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses", - | "sparkpost", "log", "array" + | This option controls the default mailer that is used to send any email + | messages sent by your application. Alternative mailers may be setup + | and used as needed; however, this mailer will be used by default. | */ - 'driver' => env('MAIL_DRIVER', 'smtp'), + 'default' => env('MAIL_DRIVER', 'smtp'), /* |-------------------------------------------------------------------------- - | SMTP Host Address + | Mailer Configurations |-------------------------------------------------------------------------- | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Mailgun mail service which will provide reliable deliveries. + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "log", "array", "failover" | */ - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN'), + 'verify_peer' => env('MAIL_SMTP_VERIFY_PEER', true), + ], - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to deliver e-mails to - | users of the application. Like the host we have set this value to - | stay compatible with the Mailgun e-mail application by default. - | - */ + 'ses' => [ + 'transport' => 'ses', + ], - 'port' => env('MAIL_PORT', 587), + 'mailgun' => [ + 'transport' => 'mailgun', + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + ], /* |-------------------------------------------------------------------------- @@ -57,63 +101,9 @@ return [ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), + 'name' => env('MAIL_FROM_NAME', 'Example'), ], - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => env('MAIL_USERNAME'), - 'password' => env('MAIL_PASSWORD'), - - - /* - |-------------------------------------------------------------------------- - | SMTP EHLO Domain - |-------------------------------------------------------------------------- - | - | Some SMTP servers require to present a known domain in order to - | allow sending through its relay. (ie: Google Workspace) - | This will use the MAIL_SMTP_EHLO env variable to avoid the 421 error - | if not present by authenticating the sender domain instead the host. - | - */ - 'local_domain' => env('MAIL_EHLO_DOMAIN'), - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - /* |-------------------------------------------------------------------------- | Markdown Mail Settings From 06bf0c14bf02e53836c61d9f17f3e49f7bdf95f3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 01:01:55 -0700 Subject: [PATCH 046/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c9e187a..7394c780b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714)) - Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) - Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) +- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 446ca3a87877f3ead443812c7dc7b1eeae225d04 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 01:59:38 -0700 Subject: [PATCH 047/977] Update notification epoch generation --- .../Commands/NotificationEpochUpdate.php | 31 ++++++++ app/Console/Kernel.php | 1 + .../NotificationEpochUpdatePipeline.php | 71 +++++++++++++++++++ app/Services/NotificationService.php | 13 ++-- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 app/Console/Commands/NotificationEpochUpdate.php create mode 100644 app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php diff --git a/app/Console/Commands/NotificationEpochUpdate.php b/app/Console/Commands/NotificationEpochUpdate.php new file mode 100644 index 000000000..e606b47ad --- /dev/null +++ b/app/Console/Commands/NotificationEpochUpdate.php @@ -0,0 +1,31 @@ +command('app:import-remove-deleted-accounts')->hourlyAt(37); $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); } + $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); } /** diff --git a/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php new file mode 100644 index 000000000..477b1f9b3 --- /dev/null +++ b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php @@ -0,0 +1,71 @@ + + */ + public function middleware(): array + { + return [(new WithoutOverlapping('ip:notification-epoch-update'))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct() + { + // + } + + /** + * Execute the job. + */ + public function handle(): void + { + $rec = Notification::where('created_at', '>', now()->subMonths(6))->first(); + $id = 1; + if($rec) { + $id = $rec->id; + } + Cache::put(NotificationService::EPOCH_CACHE_KEY . '6', $id, 1209600); + } +} diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index d088c2015..634038baf 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -12,6 +12,7 @@ use App\Transformer\Api\NotificationTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline; class NotificationService { @@ -48,12 +49,12 @@ class NotificationService { public static function getEpochId($months = 6) { - return Cache::remember(self::EPOCH_CACHE_KEY . $months, 1209600, function() use($months) { - if(Notification::count() === 0) { - return 0; - } - return Notification::where('created_at', '>', now()->subMonths($months))->first()->id; - }); + $epoch = Cache::get(self::EPOCH_CACHE_KEY . $months); + if(!$epoch) { + NotificationEpochUpdatePipeline::dispatch(); + return 1; + } + return $epoch; } public static function coldGet($id, $start = 0, $stop = 400) From 015b1b80b4630681adc04f5ddd124c6d0e6e0925 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 05:29:38 -0700 Subject: [PATCH 048/977] Update hashtag following --- app/Http/Controllers/Api/ApiV1Controller.php | 3 + .../HomeFeedPipeline/FeedRemovePipeline.php | 3 + .../HashtagUnfollowPipeline.php | 97 +++++++++++++++++++ app/Observers/HashtagFollowObserver.php | 53 ++++++++++ app/Observers/StatusHashtagObserver.php | 4 +- app/Services/HashtagFollowService.php | 62 +++++++++++- 6 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php create mode 100644 app/Observers/HashtagFollowObserver.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 51243361b..dee5fa4c6 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -71,6 +71,7 @@ use App\Services\{ CollectionService, FollowerService, HashtagService, + HashtagFollowService, HomeTimelineService, InstanceService, LikeService, @@ -3780,6 +3781,7 @@ class ApiV1Controller extends Controller ); HashtagService::follow($pid, $tag->id); + HashtagFollowService::add($tag->id, $pid); return response()->json(FollowedTagResource::make($follows)->toArray($request)); } @@ -3819,6 +3821,7 @@ class ApiV1Controller extends Controller if($follows) { HashtagService::unfollow($pid, $tag->id); + HashtagFollowService::unfollow($tag->id, $pid); $follows->delete(); } diff --git a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php index 745907084..5c09d749a 100644 --- a/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedRemovePipeline.php @@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\Services\FollowerService; +use App\Services\StatusService; use App\Services\HomeTimelineService; class FeedRemovePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing @@ -66,6 +67,8 @@ class FeedRemovePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { $ids = FollowerService::localFollowerIds($this->pid); + HomeTimelineService::rem($this->pid, $this->sid); + foreach($ids as $id) { HomeTimelineService::rem($id, $this->sid); } diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php new file mode 100644 index 000000000..354f19798 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -0,0 +1,97 @@ +hid . ':' . $this->pid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hfp:hashtag:unfollow:{$this->hid}:{$this->pid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($hid, $pid) + { + $this->hid = $hid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $hid = $this->hid; + $pid = $this->pid; + + $statusIds = HomeTimelineService::get($pid, 0, -1); + + if(!$statusIds || !count($statusIds)) { + return; + } + + $followingIds = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); + }); + + foreach($statusIds as $id) { + $status = StatusService::get($id, false); + if(!$status) { + HomeTimelineService::rem($pid, $id); + continue; + } + if(!in_array($status['account']['id'], $followingIds)) { + HomeTimelineService::rem($pid, $id); + } + } + } +} diff --git a/app/Observers/HashtagFollowObserver.php b/app/Observers/HashtagFollowObserver.php new file mode 100644 index 000000000..822ee0805 --- /dev/null +++ b/app/Observers/HashtagFollowObserver.php @@ -0,0 +1,53 @@ +hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "updated" event. + */ + public function updated(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "deleting" event. + */ + public function deleting(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } + + /** + * Handle the HashtagFollow "restored" event. + */ + public function restored(HashtagFollow $hashtagFollow): void + { + // + } + + /** + * Handle the HashtagFollow "force deleted" event. + */ + public function forceDeleted(HashtagFollow $hashtagFollow): void + { + HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); + } +} diff --git a/app/Observers/StatusHashtagObserver.php b/app/Observers/StatusHashtagObserver.php index 569120a20..cac223d51 100644 --- a/app/Observers/StatusHashtagObserver.php +++ b/app/Observers/StatusHashtagObserver.php @@ -38,12 +38,12 @@ class StatusHashtagObserver implements ShouldHandleEventsAfterCommit } /** - * Handle the notification "deleting" event. + * Handle the notification "deleted" event. * * @param \App\StatusHashtag $hashtag * @return void */ - public function deleting(StatusHashtag $hashtag) + public function deleted(StatusHashtag $hashtag) { StatusHashtagService::del($hashtag->hashtag_id, $hashtag->status_id); DB::table('hashtags')->where('id', $hashtag->hashtag_id)->decrement('cached_count'); diff --git a/app/Services/HashtagFollowService.php b/app/Services/HashtagFollowService.php index 8ddc9e6e2..012c8c00a 100644 --- a/app/Services/HashtagFollowService.php +++ b/app/Services/HashtagFollowService.php @@ -11,11 +11,67 @@ use App\HashtagFollow; class HashtagFollowService { const FOLLOW_KEY = 'pf:services:hashtag-follows:v1:'; + const CACHE_KEY = 'pf:services:hfs:byHid:'; + const CACHE_WARMED = 'pf:services:hfs:wc:byHid'; public static function getPidByHid($hid) { - return Cache::remember(self::FOLLOW_KEY . $hid, 86400, function() use($hid) { - return HashtagFollow::whereHashtagId($hid)->pluck('profile_id')->toArray(); - }); + if(!self::isWarm($hid)) { + return self::warmCache($hid); + } + return self::get($hid); + } + + public static function unfollow($hid, $pid) + { + $list = self::getPidByHid($hid); + if($list && count($list)) { + $list = array_values(array_diff($list, [$pid])); + Cache::put(self::FOLLOW_KEY . $hid, $list, 86400); + } + return; + } + + public static function add($hid, $pid) + { + return Redis::zadd(self::CACHE_KEY . $hid, $pid, $pid); + } + + public static function rem($hid, $pid) + { + return Redis::zrem(self::CACHE_KEY . $hid, $pid); + } + + public static function get($hid) + { + return Redis::zrange(self::CACHE_KEY . $hid, 0, -1); + } + + public static function count($hid) + { + return Redis::zcard(self::CACHE_KEY . $hid); + } + + public static function warmCache($hid) + { + foreach(HashtagFollow::whereHashtagId($hid)->lazyById(20, 'id') as $h) { + if($h) { + self::add($h->hashtag_id, $h->profile_id); + } + } + + self::setWarm($hid); + + return self::get($hid); + } + + public static function isWarm($hid) + { + return Redis::zcount($hid, 0, -1) ?? Redis::zscore(self::CACHE_WARMED, $hid) != null; + } + + public static function setWarm($hid) + { + return Redis::zadd(self::CACHE_WARMED, $hid, $hid); } } From dde858bd5f2b8d88fdbb1ed37079cf3bdaefb504 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 05:32:36 -0700 Subject: [PATCH 049/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7394c780b..f434955ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79)) - Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) - Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) +- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From b2c9cc2318ddc58f05624859f0cd1081966d3816 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 06:11:39 -0700 Subject: [PATCH 050/977] Update IncrementPostCount job --- .../ProfilePipeline/IncrementPostCount.php | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ProfilePipeline/IncrementPostCount.php b/app/Jobs/ProfilePipeline/IncrementPostCount.php index fe8d90648..1a94f1e6c 100644 --- a/app/Jobs/ProfilePipeline/IncrementPostCount.php +++ b/app/Jobs/ProfilePipeline/IncrementPostCount.php @@ -8,16 +8,48 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\Middleware\WithoutOverlapping; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\Profile; use App\Status; use App\Services\AccountService; -class IncrementPostCount implements ShouldQueue +class IncrementPostCount implements ShouldQueue, ShouldBeUniqueUntilProcessing { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $id; + public $timeout = 900; + public $tries = 3; + public $maxExceptions = 1; + public $failOnTimeout = true; + + /** + * The number of seconds after which the job's unique lock will be released. + * + * @var int + */ + public $uniqueFor = 3600; + + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return 'propipe:ipc:' . $this->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("propipe:ipc:{$this->id}"))->shared()->dontRelease()]; + } + /** * Create a new job instance. * @@ -47,6 +79,7 @@ class IncrementPostCount implements ShouldQueue $profile->last_status_at = now(); $profile->save(); AccountService::del($id); + AccountService::get($id); return 1; } From a5204f3e6798b429760d4158bdcee2827dd42a74 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 06:12:30 -0700 Subject: [PATCH 051/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f434955ca..2bc197ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c)) - Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) - Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80)) +- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From cf50618696c4fe9a1b34a4d40fb465a66d191b82 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 13 Nov 2023 06:33:52 -0700 Subject: [PATCH 052/977] Update FeedInsertPipeline, self fanout, oof --- app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index f0455c638..bf9c849a5 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -66,6 +66,7 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { $ids = FollowerService::localFollowerIds($this->pid); + HomeTimelineService::add($this->pid, $this->sid); foreach($ids as $id) { HomeTimelineService::add($id, $this->sid); } From d8fbb4ff32bdde439a180314c3cbc8e6b0db8a6e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 14 Nov 2023 23:05:17 -0700 Subject: [PATCH 053/977] Update HashtagUnfollowPipeline --- app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php index 354f19798..61e9529c0 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -74,10 +74,6 @@ class HashtagUnfollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcess $statusIds = HomeTimelineService::get($pid, 0, -1); - if(!$statusIds || !count($statusIds)) { - return; - } - $followingIds = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { $following = Follower::whereProfileId($pid)->pluck('following_id'); return $following->push($pid)->toArray(); From 84f4e88573099a0548636235866be1bdd63918fb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 14 Nov 2023 23:55:53 -0700 Subject: [PATCH 054/977] Update HashtagFollowService, fix cache invalidation bug --- app/Services/HashtagFollowService.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/Services/HashtagFollowService.php b/app/Services/HashtagFollowService.php index 012c8c00a..d4f93f404 100644 --- a/app/Services/HashtagFollowService.php +++ b/app/Services/HashtagFollowService.php @@ -24,12 +24,7 @@ class HashtagFollowService public static function unfollow($hid, $pid) { - $list = self::getPidByHid($hid); - if($list && count($list)) { - $list = array_values(array_diff($list, [$pid])); - Cache::put(self::FOLLOW_KEY . $hid, $list, 86400); - } - return; + return Redis::zrem(self::CACHE_KEY . $hid, $pid); } public static function add($hid, $pid) @@ -67,7 +62,7 @@ class HashtagFollowService public static function isWarm($hid) { - return Redis::zcount($hid, 0, -1) ?? Redis::zscore(self::CACHE_WARMED, $hid) != null; + return Redis::zcount(self::CACHE_KEY . $hid, 0, -1) ?? Redis::zscore(self::CACHE_WARMED, $hid) != null; } public static function setWarm($hid) From c8092116e53fa6d0542e668dcc5916074ff550c0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 00:02:17 -0700 Subject: [PATCH 055/977] Update HashtagUnfollowPipeline --- .../HashtagUnfollowPipeline.php | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php index 61e9529c0..ece5dbf8f 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -18,7 +18,7 @@ use App\Services\HashtagFollowService; use App\Services\StatusService; use App\Services\HomeTimelineService; -class HashtagUnfollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing +class HashtagUnfollowPipeline implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -30,31 +30,6 @@ class HashtagUnfollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcess public $maxExceptions = 1; public $failOnTimeout = true; - /** - * The number of seconds after which the job's unique lock will be released. - * - * @var int - */ - public $uniqueFor = 3600; - - /** - * Get the unique ID for the job. - */ - public function uniqueId(): string - { - return 'hfp:hashtag:unfollow:' . $this->hid . ':' . $this->pid; - } - - /** - * Get the middleware the job should pass through. - * - * @return array - */ - public function middleware(): array - { - return [(new WithoutOverlapping("hfp:hashtag:unfollow:{$this->hid}:{$this->pid}"))->shared()->dontRelease()]; - } - /** * Create a new job instance. */ From b365aa7e064a71e60dac3bce48fa6a47606000dd Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 00:17:31 -0700 Subject: [PATCH 056/977] Update ApiV1Controller --- app/Http/Controllers/Api/ApiV1Controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index dee5fa4c6..b08e29452 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -105,6 +105,7 @@ use Purify; use Carbon\Carbon; use App\Http\Resources\MastoApi\FollowedTagResource; use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline; +use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline; class ApiV1Controller extends Controller { @@ -3822,6 +3823,7 @@ class ApiV1Controller extends Controller if($follows) { HashtagService::unfollow($pid, $tag->id); HashtagFollowService::unfollow($tag->id, $pid); + HashtagUnfollowPipeline::dispatch($tag->id, $pid)->onQueue('feed'); $follows->delete(); } From c6a6b3ae3002965963ba923511be0b0e825db527 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 21:57:13 -0700 Subject: [PATCH 057/977] Update Experimental Home Feed, fix remote posts, shares and reblogs --- app/Http/Controllers/Api/ApiV1Controller.php | 16 +++- .../FeedInsertRemotePipeline.php | 73 ++++++++++++++++++ .../FeedRemoveRemotePipeline.php | 74 +++++++++++++++++++ app/Jobs/SharePipeline/SharePipeline.php | 3 + app/Jobs/SharePipeline/UndoSharePipeline.php | 3 + app/Jobs/StatusPipeline/StatusEntityLexer.php | 1 - app/Observers/StatusObserver.php | 7 +- app/Util/ActivityPub/Helpers.php | 3 + app/Util/ActivityPub/Inbox.php | 3 + 9 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php create mode 100644 app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index b08e29452..f750503aa 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2177,18 +2177,26 @@ class ApiV1Controller extends Controller } } - $res = collect($res)->take($limit)->map(function($id) use($napi) { + $res = collect($res) + ->map(function($id) use($napi) { return $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); - })->filter(function($res) { + }) + ->filter(function($res) { return $res && isset($res['account']); - })->map(function($status) use($pid) { + }) + ->filter(function($s) use($includeReblogs) { + return $includeReblogs ? true : $s['reblog'] == null; + }) + ->take($limit) + ->map(function($status) use($pid) { if($pid) { $status['favourited'] = (bool) LikeService::liked($pid, $status['id']); $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); } return $status; - }); + }) + ->values(); $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; $minId = $res->map(function($s) { diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php new file mode 100644 index 000000000..579c290a7 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php @@ -0,0 +1,73 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:insert:remote:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ids = FollowerService::localFollowerIds($this->pid); + + foreach($ids as $id) { + HomeTimelineService::add($id, $this->sid); + } + } +} diff --git a/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php new file mode 100644 index 000000000..d9ee716ba --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemoveRemotePipeline.php @@ -0,0 +1,74 @@ +sid; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:remote:sid:{$this->sid}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($sid, $pid) + { + $this->sid = $sid; + $this->pid = $pid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ids = FollowerService::localFollowerIds($this->pid); + + foreach($ids as $id) { + HomeTimelineService::rem($id, $this->sid); + } + } +} diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php index ae184957e..4eca4e1ab 100644 --- a/app/Jobs/SharePipeline/SharePipeline.php +++ b/app/Jobs/SharePipeline/SharePipeline.php @@ -17,6 +17,7 @@ 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 { @@ -82,6 +83,8 @@ class SharePipeline implements ShouldQueue ] ); + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + return $this->remoteAnnounceDeliver(); } diff --git a/app/Jobs/SharePipeline/UndoSharePipeline.php b/app/Jobs/SharePipeline/UndoSharePipeline.php index 3850a4752..1435688d9 100644 --- a/app/Jobs/SharePipeline/UndoSharePipeline.php +++ b/app/Jobs/SharePipeline/UndoSharePipeline.php @@ -17,6 +17,7 @@ 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 { @@ -35,6 +36,8 @@ class UndoSharePipeline implements ShouldQueue $actor = $status->profile; $parent = Status::find($status->reblog_of_id); + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + if($parent) { $target = $parent->profile_id; ReblogService::removePostReblog($parent->profile_id, $status->id); diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index 76d2d98d0..a6266044c 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -161,7 +161,6 @@ class StatusEntityLexer implements ShouldQueue if(config('exp.cached_home_timeline')) { if( $status->in_reply_to_id === null && - $status->reblog_of_id === null && in_array($status->scope, ['public', 'unlisted', 'private']) ) { FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php index 9b9f8e4f1..d78585175 100644 --- a/app/Observers/StatusObserver.php +++ b/app/Observers/StatusObserver.php @@ -8,6 +8,7 @@ use Cache; use App\Models\ImportPost; use App\Services\ImportService; use App\Jobs\HomeFeedPipeline\FeedRemovePipeline; +use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; class StatusObserver { @@ -66,7 +67,11 @@ class StatusObserver } if(config('exp.cached_home_timeline')) { - FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + if($status->uri) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } else { + FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } } } diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 989334926..459bda7c3 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -35,6 +35,7 @@ use App\Services\MediaStorageService; use App\Services\NetworkTimelineService; use App\Jobs\MediaPipeline\MediaStoragePipeline; use App\Jobs\AvatarPipeline\RemoteAvatarFetch; +use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline; use App\Util\Media\License; use App\Models\Poll; use Illuminate\Contracts\Cache\LockTimeoutException; @@ -537,6 +538,8 @@ class Helpers { IncrementPostCount::dispatch($pid)->onQueue('low'); + FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed'); + return $status; } diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 4441becfb..e97eda34d 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -49,6 +49,7 @@ use App\Models\Conversation; use App\Models\RemoteReport; use App\Jobs\ProfilePipeline\IncrementPostCount; use App\Jobs\ProfilePipeline\DecrementPostCount; +use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; class Inbox { @@ -707,6 +708,7 @@ class Inbox if(!$status) { return; } + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); RemoteStatusDelete::dispatch($status)->onQueue('high'); return; break; @@ -803,6 +805,7 @@ class Inbox if(!$status) { return; } + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); Status::whereProfileId($profile->id) ->whereReblogOfId($status->id) ->delete(); From 19233cc9763ee5ec80a2ccc7a7d6065689c5ae63 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 22:16:23 -0700 Subject: [PATCH 058/977] Update HashtagFollowObserver --- app/Http/Controllers/Api/ApiV1Controller.php | 2 +- .../HomeFeedPipeline/HashtagUnfollowPipeline.php | 16 ++++++++++++++-- app/Observers/HashtagFollowObserver.php | 2 -- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index f750503aa..7711726ae 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3831,7 +3831,7 @@ class ApiV1Controller extends Controller if($follows) { HashtagService::unfollow($pid, $tag->id); HashtagFollowService::unfollow($tag->id, $pid); - HashtagUnfollowPipeline::dispatch($tag->id, $pid)->onQueue('feed'); + HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); $follows->delete(); } diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php index ece5dbf8f..69573f410 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -24,6 +24,7 @@ class HashtagUnfollowPipeline implements ShouldQueue protected $pid; protected $hid; + protected $slug; public $timeout = 900; public $tries = 3; @@ -33,10 +34,11 @@ class HashtagUnfollowPipeline implements ShouldQueue /** * Create a new job instance. */ - public function __construct($hid, $pid) + public function __construct($hid, $pid, $slug) { $this->hid = $hid; $this->pid = $pid; + $this->slug = $slug; } /** @@ -46,6 +48,7 @@ class HashtagUnfollowPipeline implements ShouldQueue { $hid = $this->hid; $pid = $this->pid; + $slug = $this->slug; $statusIds = HomeTimelineService::get($pid, 0, -1); @@ -60,7 +63,16 @@ class HashtagUnfollowPipeline implements ShouldQueue HomeTimelineService::rem($pid, $id); continue; } - if(!in_array($status['account']['id'], $followingIds)) { + $following = in_array($status['account']['id'], $followingIds); + if($following || !isset($status['tags'])) { + continue; + } + + $tags = collect($status['tags'])->filter(function($tag) { + return $tag['name']; + })->toArray(); + + if(in_array($slug, $tags)) { HomeTimelineService::rem($pid, $id); } } diff --git a/app/Observers/HashtagFollowObserver.php b/app/Observers/HashtagFollowObserver.php index 822ee0805..56158c21a 100644 --- a/app/Observers/HashtagFollowObserver.php +++ b/app/Observers/HashtagFollowObserver.php @@ -31,7 +31,6 @@ class HashtagFollowObserver implements ShouldHandleEventsAfterCommit public function deleting(HashtagFollow $hashtagFollow): void { HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); - HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); } /** @@ -48,6 +47,5 @@ class HashtagFollowObserver implements ShouldHandleEventsAfterCommit public function forceDeleted(HashtagFollow $hashtagFollow): void { HashtagFollowService::unfollow($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); - HashtagUnfollowPipeline::dispatch($hashtagFollow->hashtag_id, $hashtagFollow->profile_id); } } From 3e96fa8a565333583b18f07746eafb003e3820fb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 22:37:21 -0700 Subject: [PATCH 059/977] Updaet HashtagUnfollowPipeline, fix typo --- app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php index 69573f410..de2af9132 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -68,9 +68,9 @@ class HashtagUnfollowPipeline implements ShouldQueue continue; } - $tags = collect($status['tags'])->filter(function($tag) { + $tags = collect($status['tags'])->map(function($tag) { return $tag['name']; - })->toArray(); + })->filter()->values()->toArray(); if(in_array($slug, $tags)) { HomeTimelineService::rem($pid, $id); From 3327a008fa330f723d45b80f8972cd662e4fe4d2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 23:23:17 -0700 Subject: [PATCH 060/977] Update HashtagService, improve count perf --- app/Services/HashtagService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Services/HashtagService.php b/app/Services/HashtagService.php index 81a7ae4ed..d3cfb2743 100644 --- a/app/Services/HashtagService.php +++ b/app/Services/HashtagService.php @@ -29,8 +29,9 @@ class HashtagService public static function count($id) { - return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) { - return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count(); + return Cache::remember('services:hashtag:public-count:by_id:' . $id, 3600, function() use($id) { + $tag = Hashtag::find($id); + return $tag ? $tag->cached_count ?? 0 : 0; }); } From e1b39bcf6feaa193acb2cd7117991b665b348de3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 23:24:32 -0700 Subject: [PATCH 061/977] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc197ff5..f130ce819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ - Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271)) - Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80)) - Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23)) +- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885)) +- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae)) +- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 33a60e767d66ae25213a2a1dcb84379ae96831ec Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 15 Nov 2023 23:43:57 -0700 Subject: [PATCH 062/977] Update AP helpers, fix fanout scope --- app/Util/ActivityPub/Helpers.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 459bda7c3..612c38d75 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -538,7 +538,11 @@ class Helpers { IncrementPostCount::dispatch($pid)->onQueue('low'); - FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed'); + if( $status->in_reply_to_id === null && + in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ) { + FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed'); + } return $status; } From e5401f8558aefdc80caad75bc604a6b672cd9e83 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 00:29:10 -0700 Subject: [PATCH 063/977] Update StatusHashtagService, remove problemaatic cache layer --- app/Services/StatusHashtagService.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/Services/StatusHashtagService.php b/app/Services/StatusHashtagService.php index ececca633..6e740db42 100644 --- a/app/Services/StatusHashtagService.php +++ b/app/Services/StatusHashtagService.php @@ -84,18 +84,14 @@ class StatusHashtagService { public static function statusTags($statusId) { - $key = 'pf:services:sh:id:' . $statusId; + $status = Status::with('hashtags')->find($statusId); + if(!$status) { + return []; + } - return Cache::remember($key, 604800, function() use($statusId) { - $status = Status::find($statusId); - if(!$status) { - return []; - } - - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer()); - return $fractal->createData($resource)->toArray(); - }); + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer()); + return $fractal->createData($resource)->toArray(); } } From f105f4e8f6335126dbde84836bf8c9ddd9148bb7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 00:47:49 -0700 Subject: [PATCH 064/977] Update HomeFeedPipeline, fix tag filtering --- .editorconfig | 2 +- .../HashtagInsertFanoutPipeline.php | 13 +- .../HashtagRemoveFanoutPipeline.php | 10 + .../HashtagUnfollowPipeline.php | 10 +- app/Jobs/StatusPipeline/StatusEntityLexer.php | 290 +++++++++--------- .../StatusPipeline/StatusTagsPipeline.php | 184 +++++------ 6 files changed, 267 insertions(+), 242 deletions(-) diff --git a/.editorconfig b/.editorconfig index 1cd7d1077..3c44241cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,8 @@ root = true [*] +indent_style = space indent_size = 4 -indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php index 64adebadc..581b8784f 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -12,6 +12,7 @@ use App\Hashtag; use App\StatusHashtag; use App\Services\HashtagFollowService; use App\Services\HomeTimelineService; +use App\Services\StatusService; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -72,11 +73,21 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro public function handle(): void { $hashtag = $this->hashtag; + $sid = $hashtag->status_id; + $status = StatusService::get($sid, false); + + if(!$status) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } $ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id); if(!$ids || !count($ids)) { - return; + return; } foreach($ids as $id) { diff --git a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php index 92e3b8e42..ea9d87fbb 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php @@ -12,6 +12,7 @@ use App\Hashtag; use App\StatusHashtag; use App\Services\HashtagFollowService; use App\Services\HomeTimelineService; +use App\Services\StatusService; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -68,6 +69,15 @@ class HashtagRemoveFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro { $sid = $this->sid; $hid = $this->hid; + $status = StatusService::get($sid, false); + + if(!$status) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } $ids = HashtagFollowService::getPidByHid($hid); diff --git a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php index de2af9132..232179ec3 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagUnfollowPipeline.php @@ -48,7 +48,7 @@ class HashtagUnfollowPipeline implements ShouldQueue { $hid = $this->hid; $pid = $this->pid; - $slug = $this->slug; + $slug = strtolower($this->slug); $statusIds = HomeTimelineService::get($pid, 0, -1); @@ -59,17 +59,17 @@ class HashtagUnfollowPipeline implements ShouldQueue foreach($statusIds as $id) { $status = StatusService::get($id, false); - if(!$status) { + if(!$status || empty($status['tags'])) { HomeTimelineService::rem($pid, $id); continue; } - $following = in_array($status['account']['id'], $followingIds); - if($following || !isset($status['tags'])) { + $following = in_array((int) $status['account']['id'], $followingIds); + if($following === true) { continue; } $tags = collect($status['tags'])->map(function($tag) { - return $tag['name']; + return strtolower($tag['name']); })->filter()->values()->toArray(); if(in_array($slug, $tags)) { diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php index a6266044c..872594a96 100644 --- a/app/Jobs/StatusPipeline/StatusEntityLexer.php +++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php @@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use App\Services\StatusService; use App\Services\UserFilterService; use App\Services\AdminShadowFilterService; use App\Jobs\HomeFeedPipeline\FeedInsertPipeline; @@ -26,87 +27,87 @@ use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline; class StatusEntityLexer implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $status; - protected $entities; - protected $autolink; + protected $status; + protected $entities; + protected $autolink; - /** - * Delete the job if its models no longer exist. - * - * @var bool - */ - public $deleteWhenMissingModels = true; + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Status $status) - { - $this->status = $status; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Status $status) + { + $this->status = $status; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $profile = $this->status->profile; - $status = $this->status; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $profile = $this->status->profile; + $status = $this->status; - if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { - $profile->status_count = $profile->status_count + 1; - $profile->save(); - } + if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + $profile->status_count = $profile->status_count + 1; + $profile->save(); + } - if($profile->no_autolink == false) { - $this->parseEntities(); - } - } + if($profile->no_autolink == false) { + $this->parseEntities(); + } + } - public function parseEntities() - { - $this->extractEntities(); - } + public function parseEntities() + { + $this->extractEntities(); + } - public function extractEntities() - { - $this->entities = Extractor::create()->extract($this->status->caption); - $this->autolinkStatus(); - } + public function extractEntities() + { + $this->entities = Extractor::create()->extract($this->status->caption); + $this->autolinkStatus(); + } - public function autolinkStatus() - { - $this->autolink = Autolink::create()->autolink($this->status->caption); - $this->storeEntities(); - } + public function autolinkStatus() + { + $this->autolink = Autolink::create()->autolink($this->status->caption); + $this->storeEntities(); + } - public function storeEntities() - { - $this->storeHashtags(); - DB::transaction(function () { - $status = $this->status; - $status->rendered = nl2br($this->autolink); - $status->save(); - }); - } + public function storeEntities() + { + $this->storeHashtags(); + DB::transaction(function () { + $status = $this->status; + $status->rendered = nl2br($this->autolink); + $status->save(); + }); + } - public function storeHashtags() - { - $tags = array_unique($this->entities['hashtags']); - $status = $this->status; + public function storeHashtags() + { + $tags = array_unique($this->entities['hashtags']); + $status = $this->status; - foreach ($tags as $tag) { - if(mb_strlen($tag) > 124) { - continue; - } - DB::transaction(function () use ($status, $tag) { - $slug = str_slug($tag, '-', false); + foreach ($tags as $tag) { + if(mb_strlen($tag) > 124) { + continue; + } + DB::transaction(function () use ($status, $tag) { + $slug = str_slug($tag, '-', false); $hashtag = Hashtag::firstOrCreate([ 'slug' => $slug @@ -114,92 +115,93 @@ class StatusEntityLexer implements ShouldQueue 'name' => $tag ]); - StatusHashtag::firstOrCreate( - [ - 'status_id' => $status->id, - 'hashtag_id' => $hashtag->id, - 'profile_id' => $status->profile_id, - 'status_visibility' => $status->visibility, - ] - ); - }); - } - $this->storeMentions(); - } + StatusHashtag::firstOrCreate( + [ + 'status_id' => $status->id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->visibility, + ] + ); + }); + } + $this->storeMentions(); + } - public function storeMentions() - { - $mentions = array_unique($this->entities['mentions']); - $status = $this->status; + public function storeMentions() + { + $mentions = array_unique($this->entities['mentions']); + $status = $this->status; - foreach ($mentions as $mention) { - $mentioned = Profile::whereUsername($mention)->first(); + foreach ($mentions as $mention) { + $mentioned = Profile::whereUsername($mention)->first(); - if (empty($mentioned) || !isset($mentioned->id)) { - continue; - } + if (empty($mentioned) || !isset($mentioned->id)) { + continue; + } $blocks = UserFilterService::blocks($mentioned->id); if($blocks && in_array($status->profile_id, $blocks)) { continue; } - DB::transaction(function () use ($status, $mentioned) { - $m = new Mention(); - $m->status_id = $status->id; - $m->profile_id = $mentioned->id; - $m->save(); + DB::transaction(function () use ($status, $mentioned) { + $m = new Mention(); + $m->status_id = $status->id; + $m->profile_id = $mentioned->id; + $m->save(); - MentionPipeline::dispatch($status, $m); - }); - } - $this->fanout(); - } + MentionPipeline::dispatch($status, $m); + }); + } + $this->fanout(); + } - public function fanout() - { - $status = $this->status; + public function fanout() + { + $status = $this->status; + StatusService::refresh($status->id); - if(config('exp.cached_home_timeline')) { - if( $status->in_reply_to_id === null && - in_array($status->scope, ['public', 'unlisted', 'private']) - ) { - FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - } - } - $this->deliver(); - } - - public function deliver() - { - $status = $this->status; - $types = [ - 'photo', - 'photo:album', - 'video', - 'video:album', - 'photo:video:album' - ]; - - if(config_cache('pixelfed.bouncer.enabled')) { - Bouncer::get($status); - } - - Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); - $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); - if( $status->uri == null && - $status->scope == 'public' && - in_array($status->type, $types) && - $status->in_reply_to_id === null && - $status->reblog_of_id === null && - ($hideNsfw ? $status->is_nsfw == false : true) - ) { - if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { - PublicTimelineService::add($status->id); + if(config('exp.cached_home_timeline')) { + if( $status->in_reply_to_id === null && + in_array($status->scope, ['public', 'unlisted', 'private']) + ) { + FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); } - } + } + $this->deliver(); + } - if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { - StatusActivityPubDeliver::dispatch($status); - } - } + public function deliver() + { + $status = $this->status; + $types = [ + 'photo', + 'photo:album', + 'video', + 'video:album', + 'photo:video:album' + ]; + + if(config_cache('pixelfed.bouncer.enabled')) { + Bouncer::get($status); + } + + Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); + $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); + if( $status->uri == null && + $status->scope == 'public' && + in_array($status->type, $types) && + $status->in_reply_to_id === null && + $status->reblog_of_id === null && + ($hideNsfw ? $status->is_nsfw == false : true) + ) { + if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { + PublicTimelineService::add($status->id); + } + } + + if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { + StatusActivityPubDeliver::dispatch($status); + } + } } diff --git a/app/Jobs/StatusPipeline/StatusTagsPipeline.php b/app/Jobs/StatusPipeline/StatusTagsPipeline.php index 893fa6a83..003196e0d 100644 --- a/app/Jobs/StatusPipeline/StatusTagsPipeline.php +++ b/app/Jobs/StatusPipeline/StatusTagsPipeline.php @@ -20,117 +20,119 @@ use App\Util\ActivityPub\Helpers; class StatusTagsPipeline implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $activity; - protected $status; + protected $activity; + protected $status; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct($activity, $status) - { - $this->activity = $activity; - $this->status = $status; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct($activity, $status) + { + $this->activity = $activity; + $this->status = $status; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $res = $this->activity; - $status = $this->status; + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $res = $this->activity; + $status = $this->status; if(isset($res['tag']['type'], $res['tag']['name'])) { $res['tag'] = [$res['tag']]; } - $tags = collect($res['tag']); + $tags = collect($res['tag']); - // Emoji - $tags->filter(function($tag) { - return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji'; - }) - ->map(function($tag) { - CustomEmojiService::import($tag['id'], $this->status->id); - }); + // Emoji + $tags->filter(function($tag) { + return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji'; + }) + ->map(function($tag) { + CustomEmojiService::import($tag['id'], $this->status->id); + }); - // Hashtags - $tags->filter(function($tag) { - return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']); - }) - ->map(function($tag) use($status) { - $name = substr($tag['name'], 0, 1) == '#' ? - substr($tag['name'], 1) : $tag['name']; + // Hashtags + $tags->filter(function($tag) { + return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']); + }) + ->map(function($tag) use($status) { + $name = substr($tag['name'], 0, 1) == '#' ? + substr($tag['name'], 1) : $tag['name']; - $banned = TrendingHashtagService::getBannedHashtagNames(); + $banned = TrendingHashtagService::getBannedHashtagNames(); - if(count($banned)) { + if(count($banned)) { if(in_array(strtolower($name), array_map('strtolower', $banned))) { - return; + return; } } if(config('database.default') === 'pgsql') { - $hashtag = Hashtag::where('name', 'ilike', $name) - ->orWhere('slug', 'ilike', str_slug($name, '-', false)) - ->first(); + $hashtag = Hashtag::where('name', 'ilike', $name) + ->orWhere('slug', 'ilike', str_slug($name, '-', false)) + ->first(); - if(!$hashtag) { - $hashtag = Hashtag::updateOrCreate([ - 'slug' => str_slug($name, '-', false), - 'name' => $name - ]); - } + if(!$hashtag) { + $hashtag = Hashtag::updateOrCreate([ + 'slug' => str_slug($name, '-', false), + 'name' => $name + ]); + } } else { - $hashtag = Hashtag::updateOrCreate([ - 'slug' => str_slug($name, '-', false), - 'name' => $name - ]); + $hashtag = Hashtag::updateOrCreate([ + 'slug' => str_slug($name, '-', false), + 'name' => $name + ]); } - StatusHashtag::firstOrCreate([ - 'status_id' => $status->id, - 'hashtag_id' => $hashtag->id, - 'profile_id' => $status->profile_id, - 'status_visibility' => $status->scope - ]); - }); + StatusHashtag::firstOrCreate([ + 'status_id' => $status->id, + 'hashtag_id' => $hashtag->id, + 'profile_id' => $status->profile_id, + 'status_visibility' => $status->scope + ]); + }); - // Mentions - $tags->filter(function($tag) { - return $tag && - $tag['type'] == 'Mention' && - isset($tag['href']) && - substr($tag['href'], 0, 8) === 'https://'; - }) - ->map(function($tag) use($status) { - if(Helpers::validateLocalUrl($tag['href'])) { - $parts = explode('/', $tag['href']); - if(!$parts) { - return; - } - $pid = AccountService::usernameToId(end($parts)); - if(!$pid) { - return; - } - } else { - $acct = Helpers::profileFetch($tag['href']); - if(!$acct) { - return; - } - $pid = $acct->id; - } - $mention = new Mention; - $mention->status_id = $status->id; - $mention->profile_id = $pid; - $mention->save(); - MentionPipeline::dispatch($status, $mention); - }); - } + // Mentions + $tags->filter(function($tag) { + return $tag && + $tag['type'] == 'Mention' && + isset($tag['href']) && + substr($tag['href'], 0, 8) === 'https://'; + }) + ->map(function($tag) use($status) { + if(Helpers::validateLocalUrl($tag['href'])) { + $parts = explode('/', $tag['href']); + if(!$parts) { + return; + } + $pid = AccountService::usernameToId(end($parts)); + if(!$pid) { + return; + } + } else { + $acct = Helpers::profileFetch($tag['href']); + if(!$acct) { + return; + } + $pid = $acct->id; + } + $mention = new Mention; + $mention->status_id = $status->id; + $mention->profile_id = $pid; + $mention->save(); + MentionPipeline::dispatch($status, $mention); + }); + + StatusService::refresh($status->id); + } } From e6d3c7f4d7838cc7afb26ec82210497eb76b9f4c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 00:49:51 -0700 Subject: [PATCH 065/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f130ce819..505df79bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ - Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885)) - Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae)) - Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008)) +- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85)) +- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 1e31fee6a65209ad18682772d6b522ec855a150d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 02:43:11 -0700 Subject: [PATCH 066/977] Add app:hashtag-cached-count-update command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour --- .../Commands/HashtagCachedCountUpdate.php | 57 +++++++++++++++++++ app/Console/Kernel.php | 1 + 2 files changed, 58 insertions(+) create mode 100644 app/Console/Commands/HashtagCachedCountUpdate.php diff --git a/app/Console/Commands/HashtagCachedCountUpdate.php b/app/Console/Commands/HashtagCachedCountUpdate.php new file mode 100644 index 000000000..49f354e2b --- /dev/null +++ b/app/Console/Commands/HashtagCachedCountUpdate.php @@ -0,0 +1,57 @@ +option('limit'); + $tags = Hashtag::whereNull('cached_count')->limit($limit)->get(); + $count = count($tags); + if(!$count) { + return; + } + + $bar = $this->output->createProgressBar($count); + $bar->start(); + + foreach($tags as $tag) { + $count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count(); + if(!$count) { + $tag->cached_count = 0; + $tag->saveQuietly(); + $bar->advance(); + continue; + } + $tag->cached_count = $count; + $tag->saveQuietly(); + $bar->advance(); + } + $bar->finish(); + $this->line(' '); + return; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7de830c35..4148e38ab 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -44,6 +44,7 @@ class Kernel extends ConsoleKernel $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); } $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); + $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25); } /** From 4aca04729b6c0564ed5814bbcfd0deb06a7aad6a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 02:44:21 -0700 Subject: [PATCH 067/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 505df79bd..c55aaa8a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde)) - Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610)) - Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb)) +- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) From 15f29f7d79a6cb0d2f7164e672f0ee318287a72a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 02:53:22 -0700 Subject: [PATCH 068/977] Update HashtagService, reduce cached_count cache ttl --- app/Services/HashtagService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/HashtagService.php b/app/Services/HashtagService.php index d3cfb2743..84fd1986b 100644 --- a/app/Services/HashtagService.php +++ b/app/Services/HashtagService.php @@ -29,7 +29,7 @@ class HashtagService public static function count($id) { - return Cache::remember('services:hashtag:public-count:by_id:' . $id, 3600, function() use($id) { + return Cache::remember('services:hashtag:total-count:by_id:' . $id, 300, function() use($id) { $tag = Hashtag::find($id); return $tag ? $tag->cached_count ?? 0 : 0; }); From 051eb962e1471e24d2fb0086cef909ad58edaf3b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 02:54:30 -0700 Subject: [PATCH 069/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c55aaa8a3..77555a5bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008)) - Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85)) - Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8)) +- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 175203089b681fe3c6e818dbb039de193947c3f5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 16 Nov 2023 06:06:22 -0700 Subject: [PATCH 070/977] Add Related Hashtags --- app/Models/HashtagRelated.php | 22 ++++++++++++ app/Services/HashtagRelatedService.php | 36 +++++++++++++++++++ ...16_124107_create_hashtag_related_table.php | 33 +++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 app/Models/HashtagRelated.php create mode 100644 app/Services/HashtagRelatedService.php create mode 100644 database/migrations/2023_11_16_124107_create_hashtag_related_table.php diff --git a/app/Models/HashtagRelated.php b/app/Models/HashtagRelated.php new file mode 100644 index 000000000..42722a205 --- /dev/null +++ b/app/Models/HashtagRelated.php @@ -0,0 +1,22 @@ + 'array', + 'last_calculated_at' => 'datetime', + 'last_moderated_at' => 'datetime', + ]; +} diff --git a/app/Services/HashtagRelatedService.php b/app/Services/HashtagRelatedService.php new file mode 100644 index 000000000..53a387b45 --- /dev/null +++ b/app/Services/HashtagRelatedService.php @@ -0,0 +1,36 @@ +first(); + } + + public static function fetchRelatedTags($tag) + { + $res = StatusHashtag::query() + ->select('h2.name', DB::raw('COUNT(*) as related_count')) + ->join('status_hashtags as hs2', function ($join) { + $join->on('status_hashtags.status_id', '=', 'hs2.status_id') + ->whereRaw('status_hashtags.hashtag_id != hs2.hashtag_id') + ->where('status_hashtags.created_at', '>', now()->subMonths(3)); + }) + ->join('hashtags as h1', 'status_hashtags.hashtag_id', '=', 'h1.id') + ->join('hashtags as h2', 'hs2.hashtag_id', '=', 'h2.id') + ->where('h1.name', '=', $tag) + ->groupBy('h2.name') + ->orderBy('related_count', 'desc') + ->limit(10) + ->get(); + + return $res; + } +} diff --git a/database/migrations/2023_11_16_124107_create_hashtag_related_table.php b/database/migrations/2023_11_16_124107_create_hashtag_related_table.php new file mode 100644 index 000000000..33d7494d8 --- /dev/null +++ b/database/migrations/2023_11_16_124107_create_hashtag_related_table.php @@ -0,0 +1,33 @@ +bigIncrements('id'); + $table->bigInteger('hashtag_id')->unsigned()->unique()->index(); + $table->json('related_tags')->nullable(); + $table->bigInteger('agg_score')->unsigned()->nullable()->index(); + $table->timestamp('last_calculated_at')->nullable()->index(); + $table->timestamp('last_moderated_at')->nullable()->index(); + $table->boolean('skip_refresh')->default(false)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('hashtag_related'); + } +}; From 287f903bf3ceb9a0d0766b9a0c3b1a7ecbd6fc0b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 17 Nov 2023 20:50:07 -0700 Subject: [PATCH 071/977] Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic --- app/Http/Controllers/Api/ApiV1Controller.php | 7069 +++++++++--------- 1 file changed, 3535 insertions(+), 3534 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 7711726ae..d43a867bd 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -11,35 +11,35 @@ use Laravel\Passport\Passport; use Auth, Cache, DB, Storage, URL; use Illuminate\Support\Facades\Redis; use App\{ - Avatar, - Bookmark, - Collection, - CollectionItem, - DirectMessage, - Follower, - FollowRequest, - Hashtag, - HashtagFollow, - Instance, - Like, - Media, - Notification, - Profile, - Status, - StatusHashtag, - User, - UserSetting, - UserFilter, + Avatar, + Bookmark, + Collection, + CollectionItem, + DirectMessage, + Follower, + FollowRequest, + Hashtag, + HashtagFollow, + Instance, + Like, + Media, + Notification, + Profile, + Status, + StatusHashtag, + User, + UserSetting, + UserFilter, }; use League\Fractal; use App\Transformer\Api\Mastodon\v1\{ - AccountTransformer, - MediaTransformer, - NotificationTransformer, - StatusTransformer, + AccountTransformer, + MediaTransformer, + NotificationTransformer, + StatusTransformer, }; use App\Transformer\Api\{ - RelationshipTransformer, + RelationshipTransformer, }; use App\Http\Controllers\FollowerController; use League\Fractal\Serializer\ArraySerializer; @@ -59,35 +59,35 @@ use App\Jobs\FollowPipeline\FollowPipeline; use App\Jobs\FollowPipeline\UnfollowPipeline; use App\Jobs\ImageOptimizePipeline\ImageOptimize; use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail + VideoOptimize, + VideoPostProcess, + VideoThumbnail }; use App\Services\{ - AccountService, - BookmarkService, - BouncerService, - CollectionService, - FollowerService, - HashtagService, - HashtagFollowService, - HomeTimelineService, - InstanceService, - LikeService, - NetworkTimelineService, - NotificationService, - MediaService, - MediaPathService, + AccountService, + BookmarkService, + BouncerService, + CollectionService, + FollowerService, + HashtagService, + HashtagFollowService, + HomeTimelineService, + InstanceService, + LikeService, + NetworkTimelineService, + NotificationService, + MediaService, + MediaPathService, ProfileStatusService, - PublicTimelineService, - ReblogService, - RelationshipService, - SearchApiV2Service, - StatusService, - MediaBlocklistService, - SnowflakeService, - UserFilterService + PublicTimelineService, + ReblogService, + RelationshipService, + SearchApiV2Service, + StatusService, + MediaBlocklistService, + SnowflakeService, + UserFilterService }; use App\Util\Lexer\Autolink; use App\Util\Lexer\PrettyNumber; @@ -109,344 +109,344 @@ use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline; class ApiV1Controller extends Controller { - protected $fractal; - const PF_API_ENTITY_KEY = "_pe"; + protected $fractal; + const PF_API_ENTITY_KEY = "_pe"; - public function __construct() - { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } + public function __construct() + { + $this->fractal = new Fractal\Manager(); + $this->fractal->setSerializer(new ArraySerializer()); + } - public function json($res, $code = 200, $headers = []) - { - return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); - } + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } - public function getApp(Request $request) - { - if(!$request->user()) { - return response('', 403); - } + public function getApp(Request $request) + { + if(!$request->user()) { + return response('', 403); + } - $client = $request->user()->token()->client; - $res = [ - 'name' => $client->name, - 'website' => null, - 'vapid_key' => null - ]; + $client = $request->user()->token()->client; + $res = [ + 'name' => $client->name, + 'website' => null, + 'vapid_key' => null + ]; - return $this->json($res); - } + return $this->json($res); + } - public function apps(Request $request) - { - abort_if(!config_cache('pixelfed.oauth_enabled'), 404); + public function apps(Request $request) + { + abort_if(!config_cache('pixelfed.oauth_enabled'), 404); - $this->validate($request, [ - 'client_name' => 'required', - 'redirect_uris' => 'required' - ]); + $this->validate($request, [ + 'client_name' => 'required', + 'redirect_uris' => 'required' + ]); - $uris = implode(',', explode('\n', $request->redirect_uris)); + $uris = implode(',', explode('\n', $request->redirect_uris)); - $client = Passport::client()->forceFill([ - 'user_id' => null, - 'name' => e($request->client_name), - 'secret' => Str::random(40), - 'redirect' => $uris, - 'personal_access_client' => false, - 'password_client' => false, - 'revoked' => false, - ]); + $client = Passport::client()->forceFill([ + 'user_id' => null, + 'name' => e($request->client_name), + 'secret' => Str::random(40), + 'redirect' => $uris, + 'personal_access_client' => false, + 'password_client' => false, + 'revoked' => false, + ]); - $client->save(); + $client->save(); - $res = [ - 'id' => (string) $client->id, - 'name' => $client->name, - 'website' => null, - 'redirect_uri' => $client->redirect, - 'client_id' => (string) $client->id, - 'client_secret' => $client->secret, - 'vapid_key' => null - ]; + $res = [ + 'id' => (string) $client->id, + 'name' => $client->name, + 'website' => null, + 'redirect_uri' => $client->redirect, + 'client_id' => (string) $client->id, + 'client_secret' => $client->secret, + 'vapid_key' => null + ]; - return $this->json($res, 200, [ - 'Access-Control-Allow-Origin' => '*' - ]); - } + return $this->json($res, 200, [ + 'Access-Control-Allow-Origin' => '*' + ]); + } - /** - * GET /api/v1/accounts/verify_credentials - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function verifyCredentials(Request $request) - { - $user = $request->user(); + /** + * GET /api/v1/accounts/verify_credentials + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function verifyCredentials(Request $request) + { + $user = $request->user(); - abort_if(!$user, 403); - abort_if($user->status != null, 403); + abort_if(!$user, 403); + abort_if($user->status != null, 403); - $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id); + $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id); - $res['source'] = [ - 'privacy' => $res['locked'] ? 'private' : 'public', - 'sensitive' => false, - 'language' => $user->language ?? 'en', - 'note' => strip_tags($res['note']), - 'fields' => [] - ]; + $res['source'] = [ + 'privacy' => $res['locked'] ? 'private' : 'public', + 'sensitive' => false, + 'language' => $user->language ?? 'en', + 'note' => strip_tags($res['note']), + 'fields' => [] + ]; - return $this->json($res); - } + return $this->json($res); + } - /** - * GET /api/v1/accounts/{id} - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountById(Request $request, $id) - { - $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true); - if(!$res) { - return response()->json(['error' => 'Record not found'], 404); - } - return $this->json($res); - } + /** + * GET /api/v1/accounts/{id} + * + * @param integer $id + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountById(Request $request, $id) + { + $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true); + if(!$res) { + return response()->json(['error' => 'Record not found'], 404); + } + return $this->json($res); + } - /** - * PATCH /api/v1/accounts/update_credentials - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountUpdateCredentials(Request $request) - { - abort_if(!$request->user(), 403); + /** + * PATCH /api/v1/accounts/update_credentials + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountUpdateCredentials(Request $request) + { + abort_if(!$request->user(), 403); - if(config('pixelfed.bouncer.cloud_ips.ban_api')) { - abort_if(BouncerService::checkIp($request->ip()), 404); - } + if(config('pixelfed.bouncer.cloud_ips.ban_api')) { + abort_if(BouncerService::checkIp($request->ip()), 404); + } - $this->validate($request, [ - 'avatar' => 'sometimes|mimetypes:image/jpeg,image/png|max:' . config('pixelfed.max_avatar_size'), - 'display_name' => 'nullable|string|max:30', - 'note' => 'nullable|string|max:200', - 'locked' => 'nullable', - 'website' => 'nullable|string|max:120', - // 'source.privacy' => 'nullable|in:unlisted,public,private', - // 'source.sensitive' => 'nullable|boolean' - ], [ - 'required' => 'The :attribute field is required.', - 'avatar.mimetypes' => 'The file must be in jpeg or png format', - 'avatar.max' => 'The :attribute exceeds the file size limit of ' . PrettyNumber::size(config('pixelfed.max_avatar_size'), true, false), - ]); + $this->validate($request, [ + 'avatar' => 'sometimes|mimetypes:image/jpeg,image/png|max:' . config('pixelfed.max_avatar_size'), + 'display_name' => 'nullable|string|max:30', + 'note' => 'nullable|string|max:200', + 'locked' => 'nullable', + 'website' => 'nullable|string|max:120', + // 'source.privacy' => 'nullable|in:unlisted,public,private', + // 'source.sensitive' => 'nullable|boolean' + ], [ + 'required' => 'The :attribute field is required.', + 'avatar.mimetypes' => 'The file must be in jpeg or png format', + 'avatar.max' => 'The :attribute exceeds the file size limit of ' . PrettyNumber::size(config('pixelfed.max_avatar_size'), true, false), + ]); - $user = $request->user(); - $profile = $user->profile; - $settings = $user->settings; + $user = $request->user(); + $profile = $user->profile; + $settings = $user->settings; - $changes = false; - $other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []); - $syncLicenses = false; - $licenseChanged = false; - $composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []); + $changes = false; + $other = array_merge(AccountService::defaultSettings()['other'], $settings->other ?? []); + $syncLicenses = false; + $licenseChanged = false; + $composeSettings = array_merge(AccountService::defaultSettings()['compose_settings'], $settings->compose_settings ?? []); - if($request->has('avatar')) { - $av = Avatar::whereProfileId($profile->id)->first(); - if($av) { - $currentAvatar = storage_path('app/'.$av->media_path); - $file = $request->file('avatar'); - $path = "public/avatars/{$profile->id}"; - $name = strtolower(str_random(6)). '.' . $file->guessExtension(); - $request->file('avatar')->storePubliclyAs($path, $name); - $av->media_path = "{$path}/{$name}"; - $av->save(); - Cache::forget("avatar:{$profile->id}"); - Cache::forget('user:account:id:'.$user->id); - AvatarOptimize::dispatch($user->profile, $currentAvatar); - } - $changes = true; - } + if($request->has('avatar')) { + $av = Avatar::whereProfileId($profile->id)->first(); + if($av) { + $currentAvatar = storage_path('app/'.$av->media_path); + $file = $request->file('avatar'); + $path = "public/avatars/{$profile->id}"; + $name = strtolower(str_random(6)). '.' . $file->guessExtension(); + $request->file('avatar')->storePubliclyAs($path, $name); + $av->media_path = "{$path}/{$name}"; + $av->save(); + Cache::forget("avatar:{$profile->id}"); + Cache::forget('user:account:id:'.$user->id); + AvatarOptimize::dispatch($user->profile, $currentAvatar); + } + $changes = true; + } - if($request->has('source[language]')) { - $lang = $request->input('source[language]'); - if(in_array($lang, Localization::languages())) { - $user->language = $lang; - $changes = true; - $other['language'] = $lang; - } - } + if($request->has('source[language]')) { + $lang = $request->input('source[language]'); + if(in_array($lang, Localization::languages())) { + $user->language = $lang; + $changes = true; + $other['language'] = $lang; + } + } - if($request->has('website')) { - $website = $request->input('website'); - if($website != $profile->website) { - if($website) { - if(!strpos($website, '.')) { - $website = null; - } + if($request->has('website')) { + $website = $request->input('website'); + if($website != $profile->website) { + if($website) { + if(!strpos($website, '.')) { + $website = null; + } - if($website && !strpos($website, '://')) { - $website = 'https://' . $website; - } + if($website && !strpos($website, '://')) { + $website = 'https://' . $website; + } - $host = parse_url($website, PHP_URL_HOST); + $host = parse_url($website, PHP_URL_HOST); - $bannedInstances = InstanceService::getBannedDomains(); - if(in_array($host, $bannedInstances)) { - $website = null; - } - } - $profile->website = $website ? $website : null; - $changes = true; - } - } + $bannedInstances = InstanceService::getBannedDomains(); + if(in_array($host, $bannedInstances)) { + $website = null; + } + } + $profile->website = $website ? $website : null; + $changes = true; + } + } - if($request->has('display_name')) { - $displayName = $request->input('display_name'); - if($displayName !== $user->name) { - $user->name = $displayName; - $profile->name = $displayName; - $changes = true; - } - } + if($request->has('display_name')) { + $displayName = $request->input('display_name'); + if($displayName !== $user->name) { + $user->name = $displayName; + $profile->name = $displayName; + $changes = true; + } + } - if($request->has('note')) { - $note = $request->input('note'); - if($note !== strip_tags($profile->bio)) { - $profile->bio = Autolink::create()->autolink(strip_tags($note)); - $changes = true; - } - } + if($request->has('note')) { + $note = $request->input('note'); + if($note !== strip_tags($profile->bio)) { + $profile->bio = Autolink::create()->autolink(strip_tags($note)); + $changes = true; + } + } - if($request->has('locked')) { - $locked = $request->input('locked') == 'true'; - if($profile->is_private != $locked) { - $profile->is_private = $locked; - $changes = true; - } - } + if($request->has('locked')) { + $locked = $request->input('locked') == 'true'; + if($profile->is_private != $locked) { + $profile->is_private = $locked; + $changes = true; + } + } - if($request->has('reduce_motion')) { - $reduced = $request->input('reduce_motion'); - if($settings->reduce_motion != $reduced) { - $settings->reduce_motion = $reduced; - $changes = true; - } - } + if($request->has('reduce_motion')) { + $reduced = $request->input('reduce_motion'); + if($settings->reduce_motion != $reduced) { + $settings->reduce_motion = $reduced; + $changes = true; + } + } - if($request->has('high_contrast_mode')) { - $contrast = $request->input('high_contrast_mode'); - if($settings->high_contrast_mode != $contrast) { - $settings->high_contrast_mode = $contrast; - $changes = true; - } - } + if($request->has('high_contrast_mode')) { + $contrast = $request->input('high_contrast_mode'); + if($settings->high_contrast_mode != $contrast) { + $settings->high_contrast_mode = $contrast; + $changes = true; + } + } - if($request->has('video_autoplay')) { - $autoplay = $request->input('video_autoplay'); - if($settings->video_autoplay != $autoplay) { - $settings->video_autoplay = $autoplay; - $changes = true; - } - } + if($request->has('video_autoplay')) { + $autoplay = $request->input('video_autoplay'); + if($settings->video_autoplay != $autoplay) { + $settings->video_autoplay = $autoplay; + $changes = true; + } + } - if($request->has('license')) { - $license = $request->input('license'); - abort_if(!in_array($license, License::keys()), 422, 'Invalid media license id'); - $syncLicenses = $request->input('sync_licenses') == true; - abort_if($syncLicenses && Cache::get('pf:settings:mls_recently:'.$user->id) == 2, 422, 'You can only sync licenses twice per 24 hours'); - if($composeSettings['default_license'] != $license) { - $composeSettings['default_license'] = $license; - $licenseChanged = true; - $changes = true; - } - } + if($request->has('license')) { + $license = $request->input('license'); + abort_if(!in_array($license, License::keys()), 422, 'Invalid media license id'); + $syncLicenses = $request->input('sync_licenses') == true; + abort_if($syncLicenses && Cache::get('pf:settings:mls_recently:'.$user->id) == 2, 422, 'You can only sync licenses twice per 24 hours'); + if($composeSettings['default_license'] != $license) { + $composeSettings['default_license'] = $license; + $licenseChanged = true; + $changes = true; + } + } - if($request->has('media_descriptions')) { - $md = $request->input('media_descriptions') == true; - if($composeSettings['media_descriptions'] != $md) { - $composeSettings['media_descriptions'] = $md; - $changes = true; - } - } + if($request->has('media_descriptions')) { + $md = $request->input('media_descriptions') == true; + if($composeSettings['media_descriptions'] != $md) { + $composeSettings['media_descriptions'] = $md; + $changes = true; + } + } - if($request->has('crawlable')) { - $crawlable = $request->input('crawlable'); - if($settings->crawlable != $crawlable) { - $settings->crawlable = $crawlable; - $changes = true; - } - } + if($request->has('crawlable')) { + $crawlable = $request->input('crawlable'); + if($settings->crawlable != $crawlable) { + $settings->crawlable = $crawlable; + $changes = true; + } + } - if($request->has('show_profile_follower_count')) { - $show_profile_follower_count = $request->input('show_profile_follower_count'); - if($settings->show_profile_follower_count != $show_profile_follower_count) { - $settings->show_profile_follower_count = $show_profile_follower_count; - $changes = true; - } - } + if($request->has('show_profile_follower_count')) { + $show_profile_follower_count = $request->input('show_profile_follower_count'); + if($settings->show_profile_follower_count != $show_profile_follower_count) { + $settings->show_profile_follower_count = $show_profile_follower_count; + $changes = true; + } + } - if($request->has('show_profile_following_count')) { - $show_profile_following_count = $request->input('show_profile_following_count'); - if($settings->show_profile_following_count != $show_profile_following_count) { - $settings->show_profile_following_count = $show_profile_following_count; - $changes = true; - } - } + if($request->has('show_profile_following_count')) { + $show_profile_following_count = $request->input('show_profile_following_count'); + if($settings->show_profile_following_count != $show_profile_following_count) { + $settings->show_profile_following_count = $show_profile_following_count; + $changes = true; + } + } - if($request->has('public_dm')) { - $public_dm = $request->input('public_dm'); - if($settings->public_dm != $public_dm) { - $settings->public_dm = $public_dm; - $changes = true; - } - } + if($request->has('public_dm')) { + $public_dm = $request->input('public_dm'); + if($settings->public_dm != $public_dm) { + $settings->public_dm = $public_dm; + $changes = true; + } + } - if($request->has('source[privacy]')) { - $scope = $request->input('source[privacy]'); - if(in_array($scope, ['public', 'private', 'unlisted'])) { - if($composeSettings['default_scope'] != $scope) { - $composeSettings['default_scope'] = $profile->is_private ? 'private' : $scope; - $changes = true; - } - } - } + if($request->has('source[privacy]')) { + $scope = $request->input('source[privacy]'); + if(in_array($scope, ['public', 'private', 'unlisted'])) { + if($composeSettings['default_scope'] != $scope) { + $composeSettings['default_scope'] = $profile->is_private ? 'private' : $scope; + $changes = true; + } + } + } - if($request->has('disable_embeds')) { - $disabledEmbeds = $request->input('disable_embeds'); - if($other['disable_embeds'] != $disabledEmbeds) { - $other['disable_embeds'] = $disabledEmbeds; - $changes = true; - } - } + if($request->has('disable_embeds')) { + $disabledEmbeds = $request->input('disable_embeds'); + if($other['disable_embeds'] != $disabledEmbeds) { + $other['disable_embeds'] = $disabledEmbeds; + $changes = true; + } + } - if($changes) { - $settings->other = $other; - $settings->compose_settings = $composeSettings; - $settings->save(); - $user->save(); - $profile->save(); - Cache::forget('profile:settings:' . $profile->id); - Cache::forget('user:account:id:' . $profile->user_id); - Cache::forget('profile:follower_count:' . $profile->id); - Cache::forget('profile:following_count:' . $profile->id); - Cache::forget('profile:embed:' . $profile->id); - Cache::forget('profile:compose:settings:' . $user->id); - Cache::forget('profile:view:'.$user->username); - AccountService::del($user->profile_id); - } + if($changes) { + $settings->other = $other; + $settings->compose_settings = $composeSettings; + $settings->save(); + $user->save(); + $profile->save(); + Cache::forget('profile:settings:' . $profile->id); + Cache::forget('user:account:id:' . $profile->user_id); + Cache::forget('profile:follower_count:' . $profile->id); + Cache::forget('profile:following_count:' . $profile->id); + Cache::forget('profile:embed:' . $profile->id); + Cache::forget('profile:compose:settings:' . $user->id); + Cache::forget('profile:view:'.$user->username); + AccountService::del($user->profile_id); + } - if($syncLicenses && $licenseChanged) { - $key = 'pf:settings:mls_recently:'.$user->id; - $val = Cache::has($key) ? 2 : 1; - Cache::put($key, $val, 86400); - MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); - } + if($syncLicenses && $licenseChanged) { + $key = 'pf:settings:mls_recently:'.$user->id; + $val = Cache::has($key) ? 2 : 1; + Cache::put($key, $val, 86400); + MediaSyncLicensePipeline::dispatch($user->id, $request->input('license')); + } if($request->has(self::PF_API_ENTITY_KEY)) { $res = AccountService::get($user->profile_id, true); @@ -456,502 +456,502 @@ class ApiV1Controller extends Controller $res = array_merge($res, $other); } - return $this->json($res); - } + return $this->json($res); + } - /** - * GET /api/v1/accounts/{id}/followers - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowersById(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/accounts/{id}/followers + * + * @param integer $id + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowersById(Request $request, $id) + { + abort_if(!$request->user(), 403); - $account = AccountService::get($id); - abort_if(!$account, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - $limit = $request->input('limit', 10); - $napi = $request->has(self::PF_API_ENTITY_KEY); + $account = AccountService::get($id); + abort_if(!$account, 404); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:80' + ]); + $limit = $request->input('limit', 10); + $napi = $request->has(self::PF_API_ENTITY_KEY); - if(intval($pid) !== intval($account['id'])) { - if($account['locked']) { - if(!FollowerService::follows($pid, $account['id'])) { - return []; - } - } + if(intval($pid) !== intval($account['id'])) { + if($account['locked']) { + if(!FollowerService::follows($pid, $account['id'])) { + return []; + } + } - if(AccountService::hiddenFollowers($id)) { - return []; - } + if(AccountService::hiddenFollowers($id)) { + return []; + } - if($request->has('page') && $request->user()->is_admin == false) { - $page = (int) $request->input('page'); - if(($page * $limit) >= 100) { - return []; - } - } - } - if($request->has('page')) { - $res = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereFollowingId($account['id']) - ->orderByDesc('id') - ->simplePaginate($limit) - ->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); + if($request->has('page') && $request->user()->is_admin == false) { + $page = (int) $request->input('page'); + if(($page * $limit) >= 100) { + return []; + } + } + } + if($request->has('page')) { + $res = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereFollowingId($account['id']) + ->orderByDesc('id') + ->simplePaginate($limit) + ->map(function($follower) use($napi) { + return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); - return $this->json($res); - } - - $paginator = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereFollowingId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - $link = null; - - if($paginator->onFirstPage()) { - if($paginator->hasMorePages()) { - $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } else { - if($paginator->previousPageUrl()) { - $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; - } - - if($paginator->hasMorePages()) { - $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } - - $res = $paginator->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/accounts/{id}/following - * - * @param integer $id - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowingById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $account = AccountService::get($id); - abort_if(!$account, 404); - $pid = $request->user()->profile_id; - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - $limit = $request->input('limit', 10); - $napi = $request->has(self::PF_API_ENTITY_KEY); - - if(intval($pid) !== intval($account['id'])) { - if($account['locked']) { - if(!FollowerService::follows($pid, $account['id'])) { - return []; - } - } - - if(AccountService::hiddenFollowing($id)) { - return []; - } - - if($request->has('page') && $request->user()->is_admin == false) { - $page = (int) $request->input('page'); - if(($page * $limit) >= 100) { - return []; - } - } - } - - if($request->has('page')) { - $res = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereProfileId($account['id']) - ->orderByDesc('id') - ->simplePaginate($limit) - ->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - return $this->json($res); - } - - $paginator = DB::table('followers') - ->select('id', 'profile_id', 'following_id') - ->whereProfileId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - $link = null; - - if($paginator->onFirstPage()) { - if($paginator->hasMorePages()) { - $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } else { - if($paginator->previousPageUrl()) { - $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; - } - - if($paginator->hasMorePages()) { - $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; - } - } - - $res = $paginator->map(function($follower) use($napi) { - return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->toArray(); - - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/accounts/{id}/statuses - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function accountStatusesById(Request $request, $id) - { - $user = $request->user(); - - $this->validate($request, [ - 'only_media' => 'nullable', - 'media_type' => 'sometimes|string|in:photo,video', - 'pinned' => 'nullable', - 'exclude_replies' => 'nullable', - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|min:1|max:100' - ]); - - $napi = $request->has(self::PF_API_ENTITY_KEY); - $profile = $napi ? AccountService::get($id, true) : AccountService::getMastodon($id, true); - - if(!$profile || !isset($profile['id']) || !$user) { - return $this->json(['error' => 'Account not found'], 404); + return $this->json($res); } - $limit = $request->limit ?? 20; - $max_id = $request->max_id; - $min_id = $request->min_id; + $paginator = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereFollowingId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); - if(!$max_id && !$min_id) { - $min_id = 1; - } + $link = null; - $pid = $request->user()->profile_id; - $scope = $request->only_media == true ? - ['photo', 'photo:album', 'video', 'video:album'] : - ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; + if($paginator->onFirstPage()) { + if($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } - if($request->only_media && $request->has('media_type')) { - $mt = $request->input('media_type'); - if($mt == 'video') { - $scope = ['video', 'video:album']; - } - } + if($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } - if(intval($pid) === intval($profile['id'])) { - $visibility = ['public', 'unlisted', 'private']; - } else if($profile['locked']) { - $following = FollowerService::follows($pid, $profile['id']); - if(!$following) { - return response('', 403); - } - $visibility = ['public', 'unlisted', 'private']; - } else { - $following = FollowerService::follows($pid, $profile['id']); - $visibility = $following ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; - } + $res = $paginator->map(function($follower) use($napi) { + return $napi ? AccountService::get($follower->profile_id, true) : AccountService::getMastodon($follower->profile_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; - $res = Status::whereProfileId($profile['id']) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('type', $scope) - ->where('id', $dir, $id) - ->whereIn('scope', $visibility) - ->limit($limit) - ->orderByDesc('id') - ->get() - ->map(function($s) use($user, $napi, $profile) { + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/accounts/{id}/following + * + * @param integer $id + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowingById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $account = AccountService::get($id); + abort_if(!$account, 404); + $pid = $request->user()->profile_id; + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:80' + ]); + $limit = $request->input('limit', 10); + $napi = $request->has(self::PF_API_ENTITY_KEY); + + if(intval($pid) !== intval($account['id'])) { + if($account['locked']) { + if(!FollowerService::follows($pid, $account['id'])) { + return []; + } + } + + if(AccountService::hiddenFollowing($id)) { + return []; + } + + if($request->has('page') && $request->user()->is_admin == false) { + $page = (int) $request->input('page'); + if(($page * $limit) >= 100) { + return []; + } + } + } + + if($request->has('page')) { + $res = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereProfileId($account['id']) + ->orderByDesc('id') + ->simplePaginate($limit) + ->map(function($follower) use($napi) { + return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); + return $this->json($res); + } + + $paginator = DB::table('followers') + ->select('id', 'profile_id', 'following_id') + ->whereProfileId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + $link = null; + + if($paginator->onFirstPage()) { + if($paginator->hasMorePages()) { + $link = '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } else { + if($paginator->previousPageUrl()) { + $link = '<'.$paginator->previousPageUrl().'>; rel="next"'; + } + + if($paginator->hasMorePages()) { + $link .= ($link ? ', ' : '') . '<'.$paginator->nextPageUrl().'>; rel="prev"'; + } + } + + $res = $paginator->map(function($follower) use($napi) { + return $napi ? AccountService::get($follower->following_id, true) : AccountService::getMastodon($follower->following_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->toArray(); + + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/accounts/{id}/statuses + * + * @param integer $id + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function accountStatusesById(Request $request, $id) + { + $user = $request->user(); + + $this->validate($request, [ + 'only_media' => 'nullable', + 'media_type' => 'sometimes|string|in:photo,video', + 'pinned' => 'nullable', + 'exclude_replies' => 'nullable', + 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'limit' => 'nullable|integer|min:1|max:100' + ]); + + $napi = $request->has(self::PF_API_ENTITY_KEY); + $profile = $napi ? AccountService::get($id, true) : AccountService::getMastodon($id, true); + + if(!$profile || !isset($profile['id']) || !$user) { + return $this->json(['error' => 'Account not found'], 404); + } + + $limit = $request->limit ?? 20; + $max_id = $request->max_id; + $min_id = $request->min_id; + + if(!$max_id && !$min_id) { + $min_id = 1; + } + + $pid = $request->user()->profile_id; + $scope = $request->only_media == true ? + ['photo', 'photo:album', 'video', 'video:album'] : + ['photo', 'photo:album', 'video', 'video:album', 'share', 'reply']; + + if($request->only_media && $request->has('media_type')) { + $mt = $request->input('media_type'); + if($mt == 'video') { + $scope = ['video', 'video:album']; + } + } + + if(intval($pid) === intval($profile['id'])) { + $visibility = ['public', 'unlisted', 'private']; + } else if($profile['locked']) { + $following = FollowerService::follows($pid, $profile['id']); + if(!$following) { + return response('', 403); + } + $visibility = ['public', 'unlisted', 'private']; + } else { + $following = FollowerService::follows($pid, $profile['id']); + $visibility = $following ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; + } + + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; + $res = Status::whereProfileId($profile['id']) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereIn('type', $scope) + ->where('id', $dir, $id) + ->whereIn('scope', $visibility) + ->limit($limit) + ->orderByDesc('id') + ->get() + ->map(function($s) use($user, $napi, $profile) { try { $status = $napi ? StatusService::get($s->id, false) : StatusService::getMastodon($s->id, false); } catch (\Exception $e) { return false; } - if($profile) { - $status['account'] = $profile; - } + if($profile) { + $status['account'] = $profile; + } - if($user && $status) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + if($user && $status) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id); $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id); - } - return $status; - }) - ->filter(function($s) { - return $s; - }) - ->values(); + } + return $status; + }) + ->filter(function($s) { + return $s; + }) + ->values(); - return $this->json($res); - } + return $this->json($res); + } - /** - * POST /api/v1/accounts/{id}/follow - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountFollowById(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/accounts/{id}/follow + * + * @param integer $id + * + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountFollowById(Request $request, $id) + { + abort_if(!$request->user(), 403); - $user = $request->user(); + $user = $request->user(); - $target = Profile::where('id', '!=', $user->profile_id) - ->whereNull('status') - ->findOrFail($id); + $target = Profile::where('id', '!=', $user->profile_id) + ->whereNull('status') + ->findOrFail($id); - $private = (bool) $target->is_private; - $remote = (bool) $target->domain; - $blocked = UserFilter::whereUserId($target->id) - ->whereFilterType('block') - ->whereFilterableId($user->profile_id) - ->whereFilterableType('App\Profile') - ->exists(); + $private = (bool) $target->is_private; + $remote = (bool) $target->domain; + $blocked = UserFilter::whereUserId($target->id) + ->whereFilterType('block') + ->whereFilterableId($user->profile_id) + ->whereFilterableType('App\Profile') + ->exists(); - if($blocked == true) { - abort(400, 'You cannot follow this user.'); - } + if($blocked == true) { + abort(400, 'You cannot follow this user.'); + } - $isFollowing = Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->exists(); + $isFollowing = Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->exists(); - // Following already, return empty relationship - if($isFollowing == true) { - $res = RelationshipService::get($user->profile_id, $target->id) ?? []; - return $this->json($res); - } + // Following already, return empty relationship + if($isFollowing == true) { + $res = RelationshipService::get($user->profile_id, $target->id) ?? []; + return $this->json($res); + } - // Rate limits, max 7500 followers per account - if($user->profile->following_count && $user->profile->following_count >= Follower::MAX_FOLLOWING) { - abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts'); - } + // Rate limits, max 7500 followers per account + if($user->profile->following_count && $user->profile->following_count >= Follower::MAX_FOLLOWING) { + abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts'); + } - if($private == true) { - $follow = FollowRequest::firstOrCreate([ - 'follower_id' => $user->profile_id, - 'following_id' => $target->id - ]); - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); - } - } else { - $follower = Follower::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'following_id' => $target->id - ]); + if($private == true) { + $follow = FollowRequest::firstOrCreate([ + 'follower_id' => $user->profile_id, + 'following_id' => $target->id + ]); + if($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController())->sendFollow($user->profile, $target); + } + } else { + $follower = Follower::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'following_id' => $target->id + ]); - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendFollow($user->profile, $target); - } - FollowPipeline::dispatch($follower)->onQueue('high'); - } + if($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController())->sendFollow($user->profile, $target); + } + FollowPipeline::dispatch($follower)->onQueue('high'); + } - RelationshipService::refresh($user->profile_id, $target->id); - Cache::forget('profile:following:'.$target->id); - Cache::forget('profile:followers:'.$target->id); - Cache::forget('profile:following:'.$user->profile_id); - Cache::forget('profile:followers:'.$user->profile_id); - Cache::forget('api:local:exp:rec:'.$user->profile_id); - Cache::forget('user:account:id:'.$target->user_id); - Cache::forget('user:account:id:'.$user->id); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$user->profile_id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$user->profile_id); - AccountService::del($user->profile_id); - AccountService::del($target->id); + RelationshipService::refresh($user->profile_id, $target->id); + Cache::forget('profile:following:'.$target->id); + Cache::forget('profile:followers:'.$target->id); + Cache::forget('profile:following:'.$user->profile_id); + Cache::forget('profile:followers:'.$user->profile_id); + Cache::forget('api:local:exp:rec:'.$user->profile_id); + Cache::forget('user:account:id:'.$target->user_id); + Cache::forget('user:account:id:'.$user->id); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$user->profile_id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$user->profile_id); + AccountService::del($user->profile_id); + AccountService::del($target->id); - $res = RelationshipService::get($user->profile_id, $target->id); + $res = RelationshipService::get($user->profile_id, $target->id); - return $this->json($res); - } + return $this->json($res); + } - /** - * POST /api/v1/accounts/{id}/unfollow - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountUnfollowById(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/accounts/{id}/unfollow + * + * @param integer $id + * + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountUnfollowById(Request $request, $id) + { + abort_if(!$request->user(), 403); - $user = $request->user(); + $user = $request->user(); - $target = Profile::where('id', '!=', $user->profile_id) - ->whereNull('status') - ->findOrFail($id); + $target = Profile::where('id', '!=', $user->profile_id) + ->whereNull('status') + ->findOrFail($id); - $private = (bool) $target->is_private; - $remote = (bool) $target->domain; + $private = (bool) $target->is_private; + $remote = (bool) $target->domain; - $isFollowing = Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->exists(); + $isFollowing = Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->exists(); - if($isFollowing == false) { - $followRequest = FollowRequest::whereFollowerId($user->profile_id) - ->whereFollowingId($target->id) - ->first(); - if($followRequest) { - $followRequest->delete(); - RelationshipService::refresh($target->id, $user->profile_id); - } - $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + if($isFollowing == false) { + $followRequest = FollowRequest::whereFollowerId($user->profile_id) + ->whereFollowingId($target->id) + ->first(); + if($followRequest) { + $followRequest->delete(); + RelationshipService::refresh($target->id, $user->profile_id); + } + $resource = new Fractal\Resource\Item($target, new RelationshipTransformer()); + $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } + return $this->json($res); + } - Follower::whereProfileId($user->profile_id) - ->whereFollowingId($target->id) - ->delete(); + Follower::whereProfileId($user->profile_id) + ->whereFollowingId($target->id) + ->delete(); - UnfollowPipeline::dispatch($user->profile_id, $target->id)->onQueue('high'); + UnfollowPipeline::dispatch($user->profile_id, $target->id)->onQueue('high'); - if($remote == true && config('federation.activitypub.remoteFollow') == true) { - (new FollowerController())->sendUndoFollow($user->profile, $target); - } + if($remote == true && config('federation.activitypub.remoteFollow') == true) { + (new FollowerController())->sendUndoFollow($user->profile, $target); + } - RelationshipService::refresh($user->profile_id, $target->id); - Cache::forget('profile:following:'.$target->id); - Cache::forget('profile:followers:'.$target->id); - Cache::forget('profile:following:'.$user->profile_id); - Cache::forget('profile:followers:'.$user->profile_id); - Cache::forget('api:local:exp:rec:'.$user->profile_id); - Cache::forget('user:account:id:'.$target->user_id); - Cache::forget('user:account:id:'.$user->id); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$user->profile_id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$user->profile_id); - AccountService::del($user->profile_id); - AccountService::del($target->id); + RelationshipService::refresh($user->profile_id, $target->id); + Cache::forget('profile:following:'.$target->id); + Cache::forget('profile:followers:'.$target->id); + Cache::forget('profile:following:'.$user->profile_id); + Cache::forget('profile:followers:'.$user->profile_id); + Cache::forget('api:local:exp:rec:'.$user->profile_id); + Cache::forget('user:account:id:'.$target->user_id); + Cache::forget('user:account:id:'.$user->id); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$user->profile_id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$user->profile_id); + AccountService::del($user->profile_id); + AccountService::del($target->id); - $res = RelationshipService::get($user->profile_id, $target->id); + $res = RelationshipService::get($user->profile_id, $target->id); - return $this->json($res); - } + return $this->json($res); + } - /** - * GET /api/v1/accounts/relationships - * - * @param array|integer $id - * - * @return \App\Services\RelationshipService - */ - public function accountRelationshipsById(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/accounts/relationships + * + * @param array|integer $id + * + * @return \App\Services\RelationshipService + */ + public function accountRelationshipsById(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'id' => 'required|array|min:1|max:20', - 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX - ]); - $napi = $request->has(self::PF_API_ENTITY_KEY); - $pid = $request->user()->profile_id ?? $request->user()->profile->id; - $res = collect($request->input('id')) - ->filter(function($id) use($pid) { - return intval($id) !== intval($pid); - }) - ->map(function($id) use($pid, $napi) { - return $napi ? - RelationshipService::getWithDate($pid, $id) : - RelationshipService::get($pid, $id); - }); - return $this->json($res); - } + $this->validate($request, [ + 'id' => 'required|array|min:1|max:20', + 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX + ]); + $napi = $request->has(self::PF_API_ENTITY_KEY); + $pid = $request->user()->profile_id ?? $request->user()->profile->id; + $res = collect($request->input('id')) + ->filter(function($id) use($pid) { + return intval($id) !== intval($pid); + }) + ->map(function($id) use($pid, $napi) { + return $napi ? + RelationshipService::getWithDate($pid, $id) : + RelationshipService::get($pid, $id); + }); + return $this->json($res); + } - /** - * GET /api/v1/accounts/search - * - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountSearch(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/accounts/search + * + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountSearch(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|min:1|max:255', - 'limit' => 'nullable|integer|min:1|max:40', - 'resolve' => 'nullable' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:1|max:255', + 'limit' => 'nullable|integer|min:1|max:40', + 'resolve' => 'nullable' + ]); - $user = $request->user(); - $query = $request->input('q'); - $limit = $request->input('limit') ?? 20; - $resolve = (bool) $request->input('resolve', false); - $q = '%' . $query . '%'; + $user = $request->user(); + $query = $request->input('q'); + $limit = $request->input('limit') ?? 20; + $resolve = (bool) $request->input('resolve', false); + $q = '%' . $query . '%'; - $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { + $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { return Profile::whereNull('status') - ->where('username', 'like', $q) - ->orWhere('name', 'like', $q) - ->limit($limit) - ->pluck('id') + ->where('username', 'like', $q) + ->orWhere('name', 'like', $q) + ->limit($limit) + ->pluck('id') ->map(function($id) { return AccountService::getMastodon($id); }) @@ -960,1656 +960,1657 @@ class ApiV1Controller extends Controller }); }); - return $this->json($profiles); - } - - /** - * GET /api/v1/blocks - * - * - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountBlocks(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'page' => 'nullable|integer|min:1|max:10' - ]); - - $user = $request->user(); - $limit = $request->input('limit') ?? 40; - - $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') - ->whereUserId($user->profile_id) - ->whereFilterableType('App\Profile') - ->whereFilterType('block') - ->orderByDesc('id') - ->simplePaginate($limit) - ->pluck('filterable_id') - ->map(function($id) { - return AccountService::get($id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($blocked); - } - - /** - * POST /api/v1/accounts/{id}/block - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountBlockById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id ?? $user->profile->id; - - if(intval($id) === intval($pid)) { - abort(400, 'You cannot block yourself'); - } - - $profile = Profile::findOrFail($id); - - if($profile->user && $profile->user->is_admin == true) { - abort(400, 'You cannot block an admin'); - } - - $count = UserFilterService::blockCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_blocks')); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid) - ->whereFilterType('block') - ->get() - ->map(function($rec) { - return AccountService::get($rec->filterable_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->count(); - abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - } else { - abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); - } - - $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); - if($followed) { - $followed->delete(); - $profile->following_count = Follower::whereProfileId($profile->id)->count(); - $profile->save(); - $selfProfile = $user->profile; - $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($profile->id, $pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); - if($following) { - $following->delete(); - $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); - $profile->save(); - $selfProfile = $user->profile; - $selfProfile->following_count = Follower::whereProfileId($pid)->count(); - $selfProfile->save(); - FollowerService::remove($pid, $profile->pid); - AccountService::del($pid); - AccountService::del($profile->id); - } - - Notification::whereProfileId($pid) - ->whereActorId($profile->id) - ->get() - ->map(function($n) use($pid) { - NotificationService::del($pid, $n['id']); - $n->forceDelete(); - }); - - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $profile->id, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'block', - ]); - - UserFilterService::block($pid, $id); - RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return $this->json($res); - } - - /** - * POST /api/v1/accounts/{id}/unblock - * - * @param integer $id - * - * @return \App\Transformer\Api\RelationshipTransformer - */ - public function accountUnblockById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id ?? $user->profile->id; - - if(intval($id) === intval($pid)) { - abort(400, 'You cannot unblock yourself'); - } - - $profile = Profile::findOrFail($id); - - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('block') - ->first(); - - if($filter) { - $filter->delete(); - UserFilterService::unblock($pid, $profile->id); - RelationshipService::refresh($pid, $id); - } - - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - - return $this->json($res); - } - - /** - * GET /api/v1/custom_emojis - * - * Return custom emoji - * - * @return array - */ - public function customEmojis() - { - return response(CustomEmojiService::all())->header('Content-Type', 'application/json'); - } - - /** - * GET /api/v1/domain_blocks - * - * Return empty array - * - * @return array - */ - public function accountDomainBlocks(Request $request) - { - abort_if(!$request->user(), 403); - return response()->json([]); - } - - /** - * GET /api/v1/endorsements - * - * Return empty array - * - * @return array - */ - public function accountEndorsements(Request $request) - { - abort_if(!$request->user(), 403); - return response()->json([]); - } - - /** - * GET /api/v1/favourites - * - * Returns collection of liked statuses - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function accountFavourites(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:20' - ]); - - $user = $request->user(); - $maxId = $request->input('max_id'); - $minId = $request->input('min_id'); - $limit = $request->input('limit') ?? 10; - - $res = Like::whereProfileId($user->profile_id) - ->when($maxId, function($q, $maxId) { - return $q->where('id', '<', $maxId); - }) - ->when($minId, function($q, $minId) { - return $q->where('id', '>', $minId); - }) - ->orderByDesc('id') - ->limit($limit) - ->get() - ->map(function($like) { - $status = StatusService::getMastodon($like['status_id'], false); - $status['favourited'] = true; - $status['like_id'] = $like->id; - $status['liked_at'] = str_replace('+00:00', 'Z', $like->created_at->format(DATE_RFC3339_EXTENDED)); - return $status; - }) - ->filter(function($status) { - return $status && isset($status['id'], $status['like_id']); - }) - ->values(); - - if($res->count()) { - $ids = $res->map(function($status) { - return $status['like_id']; - }); - $max = $ids->max(); - $min = $ids->min(); - - $baseUrl = config('app.url') . '/api/v1/favourites?limit=' . $limit . '&'; - $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; - return $this->json($res, 200, ['Link' => $link]); - } else { - return $this->json($res); - } - } - - /** - * POST /api/v1/statuses/{id}/favourite - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function statusFavouriteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $status = StatusService::getMastodon($id, false); - - abort_unless($status, 400); - - $spid = $status['account']['id']; - - if(intval($spid) !== intval($user->profile_id)) { - if($status['visibility'] == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $spid), 403); - } else { - abort_if(!in_array($status['visibility'], ['public','unlisted']), 403); - } - } - - abort_if( - Like::whereProfileId($user->profile_id) - ->where('created_at', '>', now()->subDay()) - ->count() >= Like::MAX_PER_DAY, - 429 - ); - - $blocks = UserFilterService::blocks($spid); - if($blocks && in_array($user->profile_id, $blocks)) { - abort(422); - } - - $like = Like::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'status_id' => $status['id'] - ]); - - if($like->wasRecentlyCreated == true) { - $like->status_profile_id = $spid; - $like->is_comment = !empty($status['in_reply_to_id']); - $like->save(); - Status::findOrFail($status['id'])->update([ - 'likes_count' => ($status['favourites_count'] ?? 0) + 1 - ]); - LikePipeline::dispatch($like)->onQueue('feed'); - } - - $status['favourited'] = true; - $status['favourites_count'] = $status['favourites_count'] + 1; - return $this->json($status); - } - - /** - * POST /api/v1/statuses/{id}/unfavourite - * - * @param integer $id - * - * @return \App\Transformer\Api\StatusTransformer - */ - public function statusUnfavouriteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $status = Status::findOrFail($id); - - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!$status->profile->followedBy($user->profile), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - } - - $like = Like::whereProfileId($user->profile_id) - ->whereStatusId($status->id) - ->first(); - - if($like) { - $like->forceDelete(); - $status->likes_count = $status->likes_count > 1 ? $status->likes_count - 1 : 0; - $status->save(); - } - - StatusService::del($status->id); - - $res = StatusService::getMastodon($status->id, false); - $res['favourited'] = false; - return $this->json($res); - } - - /** - * GET /api/v1/filters - * - * Return empty response since we filter server side - * - * @return array - */ - public function accountFilters(Request $request) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * GET /api/v1/follow_requests - * - * Return array of Accounts that have sent follow requests - * - * @return \App\Transformer\Api\AccountTransformer - */ - public function accountFollowRequests(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:100' - ]); - - $user = $request->user(); - - $res = FollowRequest::whereFollowingId($user->profile->id) - ->limit($request->input('limit', 40)) - ->pluck('follower_id') - ->map(function($id) { - return AccountService::getMastodon($id, true); - }) - ->filter(function($acct) { - return $acct && isset($acct['id']); - }) - ->values(); - - return $this->json($res); - } - - /** - * POST /api/v1/follow_requests/{id}/authorize - * - * @param integer $id - * - * @return null - */ - public function accountFollowRequestAccept(Request $request, $id) - { - abort_if(!$request->user(), 403); - $pid = $request->user()->profile_id; - $target = AccountService::getMastodon($id); - - if(!$target) { - return response()->json(['error' => 'Record not found'], 404); - } - - $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); - - if(!$followRequest) { - return response()->json(['error' => 'Record not found'], 404); - } - - $follower = $followRequest->follower; - $follow = new Follower(); - $follow->profile_id = $follower->id; - $follow->following_id = $pid; - $follow->save(); - - $profile = Profile::findOrFail($pid); - $profile->followers_count++; - $profile->save(); - AccountService::del($profile->id); - - $profile = Profile::findOrFail($follower->id); - $profile->following_count++; - $profile->save(); - AccountService::del($profile->id); - - if($follower->domain != null && $follower->private_key === null) { - FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - FollowPipeline::dispatch($follow); - $followRequest->delete(); - } - - RelationshipService::refresh($pid, $id); - $res = RelationshipService::get($pid, $id); - $res['followed_by'] = true; - return $this->json($res); - } - - /** - * POST /api/v1/follow_requests/{id}/reject - * - * @param integer $id - * - * @return null - */ - public function accountFollowRequestReject(Request $request, $id) - { - abort_if(!$request->user(), 403); - $pid = $request->user()->profile_id; - $target = AccountService::getMastodon($id); - - if(!$target) { - return response()->json(['error' => 'Record not found'], 404); - } - - $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); - - if(!$followRequest) { - return response()->json(['error' => 'Record not found'], 404); - } - - $follower = $followRequest->follower; - - if($follower->domain != null && $follower->private_key === null) { - FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); - } else { - $followRequest->delete(); - } - - RelationshipService::refresh($pid, $id); - $res = RelationshipService::get($pid, $id); - return $this->json($res); - } - - /** - * GET /api/v1/suggestions - * - * Return empty array as we don't support suggestions - * - * @return null - */ - public function accountSuggestions(Request $request) - { - abort_if(!$request->user(), 403); - - // todo - - return response()->json([]); - } - - /** - * GET /api/v1/instance - * - * Information about the server. - * - * @return Instance - */ - public function instance(Request $request) - { - $res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () { - $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); - - $stats = Cache::remember('api:v1:instance-data:stats', 43200, function () { - return [ - 'user_count' => User::count(), - 'status_count' => Status::whereNull('uri')->count(), - 'domain_count' => Instance::count(), - ]; - }); - - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); - - return [ - 'uri' => config('pixelfed.domain.app'), - 'title' => config('app.name'), - 'short_description' => config_cache('app.short_description'), - 'description' => config_cache('app.description'), - 'email' => config('instance.email'), - 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')', - 'urls' => [ - 'streaming_api' => 'wss://' . config('pixelfed.domain.app') - ], - 'stats' => $stats, - 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - 'languages' => [config('app.locale')], - 'registrations' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, - 'contact_account' => $contact, - 'rules' => $rules, - 'configuration' => [ - 'media_attachments' => [ - 'image_matrix_limit' => 16777216, - 'image_size_limit' => config('pixelfed.max_photo_size') * 1024, - 'supported_mime_types' => explode(',', config('pixelfed.media_types')), - 'video_frame_rate_limit' => 120, - 'video_matrix_limit' => 2304000, - 'video_size_limit' => config('pixelfed.max_photo_size') * 1024, - ], - 'polls' => [ - 'max_characters_per_option' => 50, - 'max_expiration' => 2629746, - 'max_options' => 4, - 'min_expiration' => 300 - ], - 'statuses' => [ - 'characters_reserved_per_url' => 23, - 'max_characters' => (int) config('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config('pixelfed.max_album_length') - ] - ] - ]; - }); - - return $this->json($res); - } - - /** - * GET /api/v1/lists - * - * Return empty array as we don't support lists - * - * @return null - */ - public function accountLists(Request $request) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * GET /api/v1/accounts/{id}/lists - * - * @param integer $id - * - * @return null - */ - public function accountListsById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - return response()->json([]); - } - - /** - * POST /api/v1/media - * - * - * @return MediaTransformer - */ - public function mediaUpload(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') - ]); - - $user = $request->user(); - - if($user->last_active_at == null) { - return []; - } - - if(empty($request->file('file'))) { - return response('', 422); - } - - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); - - $profile = $user->profile; - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); - - // if($photo->getMimeType() == 'image/heic') { - // abort_if(config('image.driver') !== 'imagick', 422, 'Invalid media type'); - // abort_if(!in_array('HEIC', \Imagick::queryformats()), 422, 'Unsupported media type'); - // $oldPath = $path; - // $path = str_replace('.heic', '.jpg', $path); - // $mime = 'image/jpeg'; - // \Image::make($photo)->save(storage_path("app/{$path}")); - // @unlink(storage_path("app/{$oldPath}")); - // } - - $settings = UserSetting::whereUserId($user->id)->first(); - - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; - - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); - - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; - - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } - - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = $media->url(). '?v=' . time(); - return $this->json($res); - } - - /** - * PUT /api/v1/media/{id} - * - * @param integer $id - * - * @return MediaTransformer - */ - public function mediaUpdate(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') - ]); - - $user = $request->user(); - - $media = Media::whereUserId($user->id) - ->whereProfileId($user->profile_id) - ->findOrFail($id); - - $executed = RateLimiter::attempt( - 'media:update:'.$user->id, - 10, - function() use($media, $request) { - $caption = Purify::clean($request->input('description')); - - if($caption != $media->caption) { - $media->caption = $caption; - $media->save(); - - if($media->status_id) { - MediaService::del($media->status_id); - StatusService::del($media->status_id); - } - } - }); - - if(!$executed) { - return response()->json([ - 'error' => 'Too many attempts. Try again in a few minutes.' - ], 429); - }; - - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - return $this->json($fractal->createData($resource)->toArray()); - } - - /** - * GET /api/v1/media/{id} - * - * @param integer $id - * - * @return MediaTransformer - */ - public function mediaGet(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - - $media = Media::whereUserId($user->id) - ->whereNull('status_id') - ->findOrFail($id); - - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } - - /** - * POST /api/v2/media - * - * - * @return MediaTransformer - */ - public function mediaUploadV2(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), - 'replace_id' => 'sometimes' - ]); - - $user = $request->user(); - - if($user->last_active_at == null) { - return []; - } - - if(empty($request->file('file'))) { - return response('', 422); - } - - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); - - $profile = $user->profile; - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); - - $settings = UserSetting::whereUserId($user->id)->first(); - - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; - - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - if($request->has('replace_id')) { - $rpid = $request->input('replace_id'); - $removeMedia = Media::whereNull('status_id') - ->whereUserId($user->id) - ->whereProfileId($profile->id) - ->where('created_at', '>', now()->subHours(2)) - ->find($rpid); - if($removeMedia) { - $dateTime = Carbon::now(); - MediaDeletePipeline::dispatch($removeMedia) - ->onQueue('mmo') - ->delay($dateTime->addMinutes(15)); - } - } - - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); - - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; - - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } - - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = null; - return $this->json($res, 202); - } - - /** - * GET /api/v1/mutes - * - * - * @return AccountTransformer - */ - public function accountMutes(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40' - ]); - - $user = $request->user(); - $limit = $request->input('limit', 40); - - $mutes = UserFilter::whereUserId($user->profile_id) - ->whereFilterableType('App\Profile') - ->whereFilterType('mute') - ->orderByDesc('id') - ->simplePaginate($limit) - ->pluck('filterable_id') - ->map(function($id) { - return AccountService::get($id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($mutes); - } - - /** - * POST /api/v1/accounts/{id}/mute - * - * @param integer $id - * - * @return RelationshipTransformer - */ - public function accountMuteById(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id; + return $this->json($profiles); + } + + /** + * GET /api/v1/blocks + * + * + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountBlocks(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'nullable|integer|min:1|max:40', + 'page' => 'nullable|integer|min:1|max:10' + ]); + + $user = $request->user(); + $limit = $request->input('limit') ?? 40; + + $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') + ->whereUserId($user->profile_id) + ->whereFilterableType('App\Profile') + ->whereFilterType('block') + ->orderByDesc('id') + ->simplePaginate($limit) + ->pluck('filterable_id') + ->map(function($id) { + return AccountService::get($id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($blocked); + } + + /** + * POST /api/v1/accounts/{id}/block + * + * @param integer $id + * + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountBlockById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + $pid = $user->profile_id ?? $user->profile->id; + + if(intval($id) === intval($pid)) { + abort(400, 'You cannot block yourself'); + } + + $profile = Profile::findOrFail($id); + + if($profile->user && $profile->user->is_admin == true) { + abort(400, 'You cannot block an admin'); + } + + $count = UserFilterService::blockCount($pid); + $maxLimit = intval(config('instance.user_filters.max_user_blocks')); + if($count == 0) { + $filterCount = UserFilter::whereUserId($pid) + ->whereFilterType('block') + ->get() + ->map(function($rec) { + return AccountService::get($rec->filterable_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->count(); + abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); + } else { + abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); + } + + $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first(); + if($followed) { + $followed->delete(); + $profile->following_count = Follower::whereProfileId($profile->id)->count(); + $profile->save(); + $selfProfile = $user->profile; + $selfProfile->followers_count = Follower::whereFollowingId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($profile->id, $pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + $following = Follower::whereProfileId($pid)->whereFollowingId($profile->id)->first(); + if($following) { + $following->delete(); + $profile->followers_count = Follower::whereFollowingId($profile->id)->count(); + $profile->save(); + $selfProfile = $user->profile; + $selfProfile->following_count = Follower::whereProfileId($pid)->count(); + $selfProfile->save(); + FollowerService::remove($pid, $profile->pid); + AccountService::del($pid); + AccountService::del($profile->id); + } + + Notification::whereProfileId($pid) + ->whereActorId($profile->id) + ->get() + ->map(function($n) use($pid) { + NotificationService::del($pid, $n['id']); + $n->forceDelete(); + }); + + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $profile->id, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'block', + ]); + + UserFilterService::block($pid, $id); + RelationshipService::refresh($pid, $id); + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + /** + * POST /api/v1/accounts/{id}/unblock + * + * @param integer $id + * + * @return \App\Transformer\Api\RelationshipTransformer + */ + public function accountUnblockById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + $pid = $user->profile_id ?? $user->profile->id; + + if(intval($id) === intval($pid)) { + abort(400, 'You cannot unblock yourself'); + } + + $profile = Profile::findOrFail($id); + + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('block') + ->first(); + + if($filter) { + $filter->delete(); + UserFilterService::unblock($pid, $profile->id); + RelationshipService::refresh($pid, $id); + } + + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + + return $this->json($res); + } + + /** + * GET /api/v1/custom_emojis + * + * Return custom emoji + * + * @return array + */ + public function customEmojis() + { + return response(CustomEmojiService::all())->header('Content-Type', 'application/json'); + } + + /** + * GET /api/v1/domain_blocks + * + * Return empty array + * + * @return array + */ + public function accountDomainBlocks(Request $request) + { + abort_if(!$request->user(), 403); + return response()->json([]); + } + + /** + * GET /api/v1/endorsements + * + * Return empty array + * + * @return array + */ + public function accountEndorsements(Request $request) + { + abort_if(!$request->user(), 403); + return response()->json([]); + } + + /** + * GET /api/v1/favourites + * + * Returns collection of liked statuses + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function accountFavourites(Request $request) + { + abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:20' + ]); + + $user = $request->user(); + $maxId = $request->input('max_id'); + $minId = $request->input('min_id'); + $limit = $request->input('limit') ?? 10; + + $res = Like::whereProfileId($user->profile_id) + ->when($maxId, function($q, $maxId) { + return $q->where('id', '<', $maxId); + }) + ->when($minId, function($q, $minId) { + return $q->where('id', '>', $minId); + }) + ->orderByDesc('id') + ->limit($limit) + ->get() + ->map(function($like) { + $status = StatusService::getMastodon($like['status_id'], false); + $status['favourited'] = true; + $status['like_id'] = $like->id; + $status['liked_at'] = str_replace('+00:00', 'Z', $like->created_at->format(DATE_RFC3339_EXTENDED)); + return $status; + }) + ->filter(function($status) { + return $status && isset($status['id'], $status['like_id']); + }) + ->values(); + + if($res->count()) { + $ids = $res->map(function($status) { + return $status['like_id']; + }); + $max = $ids->max(); + $min = $ids->min(); + + $baseUrl = config('app.url') . '/api/v1/favourites?limit=' . $limit . '&'; + $link = '<'.$baseUrl.'max_id='.$max.'>; rel="next",<'.$baseUrl.'min_id='.$min.'>; rel="prev"'; + return $this->json($res, 200, ['Link' => $link]); + } else { + return $this->json($res); + } + } + + /** + * POST /api/v1/statuses/{id}/favourite + * + * @param integer $id + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function statusFavouriteById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + + $status = StatusService::getMastodon($id, false); + + abort_unless($status, 400); + + $spid = $status['account']['id']; + + if(intval($spid) !== intval($user->profile_id)) { + if($status['visibility'] == 'private') { + abort_if(!FollowerService::follows($user->profile_id, $spid), 403); + } else { + abort_if(!in_array($status['visibility'], ['public','unlisted']), 403); + } + } + + abort_if( + Like::whereProfileId($user->profile_id) + ->where('created_at', '>', now()->subDay()) + ->count() >= Like::MAX_PER_DAY, + 429 + ); + + $blocks = UserFilterService::blocks($spid); + if($blocks && in_array($user->profile_id, $blocks)) { + abort(422); + } + + $like = Like::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'status_id' => $status['id'] + ]); + + if($like->wasRecentlyCreated == true) { + $like->status_profile_id = $spid; + $like->is_comment = !empty($status['in_reply_to_id']); + $like->save(); + Status::findOrFail($status['id'])->update([ + 'likes_count' => ($status['favourites_count'] ?? 0) + 1 + ]); + LikePipeline::dispatch($like)->onQueue('feed'); + } + + $status['favourited'] = true; + $status['favourites_count'] = $status['favourites_count'] + 1; + return $this->json($status); + } + + /** + * POST /api/v1/statuses/{id}/unfavourite + * + * @param integer $id + * + * @return \App\Transformer\Api\StatusTransformer + */ + public function statusUnfavouriteById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + + $status = Status::findOrFail($id); + + if(intval($status->profile_id) !== intval($user->profile_id)) { + if($status->scope == 'private') { + abort_if(!$status->profile->followedBy($user->profile), 403); + } else { + abort_if(!in_array($status->scope, ['public','unlisted']), 403); + } + } + + $like = Like::whereProfileId($user->profile_id) + ->whereStatusId($status->id) + ->first(); + + if($like) { + $like->forceDelete(); + $status->likes_count = $status->likes_count > 1 ? $status->likes_count - 1 : 0; + $status->save(); + } + + StatusService::del($status->id); + + $res = StatusService::getMastodon($status->id, false); + $res['favourited'] = false; + return $this->json($res); + } + + /** + * GET /api/v1/filters + * + * Return empty response since we filter server side + * + * @return array + */ + public function accountFilters(Request $request) + { + abort_if(!$request->user(), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/follow_requests + * + * Return array of Accounts that have sent follow requests + * + * @return \App\Transformer\Api\AccountTransformer + */ + public function accountFollowRequests(Request $request) + { + abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:100' + ]); + + $user = $request->user(); + + $res = FollowRequest::whereFollowingId($user->profile->id) + ->limit($request->input('limit', 40)) + ->pluck('follower_id') + ->map(function($id) { + return AccountService::getMastodon($id, true); + }) + ->filter(function($acct) { + return $acct && isset($acct['id']); + }) + ->values(); + + return $this->json($res); + } + + /** + * POST /api/v1/follow_requests/{id}/authorize + * + * @param integer $id + * + * @return null + */ + public function accountFollowRequestAccept(Request $request, $id) + { + abort_if(!$request->user(), 403); + $pid = $request->user()->profile_id; + $target = AccountService::getMastodon($id); + + if(!$target) { + return response()->json(['error' => 'Record not found'], 404); + } + + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); + + if(!$followRequest) { + return response()->json(['error' => 'Record not found'], 404); + } + + $follower = $followRequest->follower; + $follow = new Follower(); + $follow->profile_id = $follower->id; + $follow->following_id = $pid; + $follow->save(); + + $profile = Profile::findOrFail($pid); + $profile->followers_count++; + $profile->save(); + AccountService::del($profile->id); + + $profile = Profile::findOrFail($follower->id); + $profile->following_count++; + $profile->save(); + AccountService::del($profile->id); + + if($follower->domain != null && $follower->private_key === null) { + FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + FollowPipeline::dispatch($follow); + $followRequest->delete(); + } + + RelationshipService::refresh($pid, $id); + $res = RelationshipService::get($pid, $id); + $res['followed_by'] = true; + return $this->json($res); + } + + /** + * POST /api/v1/follow_requests/{id}/reject + * + * @param integer $id + * + * @return null + */ + public function accountFollowRequestReject(Request $request, $id) + { + abort_if(!$request->user(), 403); + $pid = $request->user()->profile_id; + $target = AccountService::getMastodon($id); + + if(!$target) { + return response()->json(['error' => 'Record not found'], 404); + } + + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); + + if(!$followRequest) { + return response()->json(['error' => 'Record not found'], 404); + } + + $follower = $followRequest->follower; + + if($follower->domain != null && $follower->private_key === null) { + FollowRejectPipeline::dispatch($followRequest)->onQueue('follow'); + } else { + $followRequest->delete(); + } + + RelationshipService::refresh($pid, $id); + $res = RelationshipService::get($pid, $id); + return $this->json($res); + } + + /** + * GET /api/v1/suggestions + * + * Return empty array as we don't support suggestions + * + * @return null + */ + public function accountSuggestions(Request $request) + { + abort_if(!$request->user(), 403); + + // todo + + return response()->json([]); + } + + /** + * GET /api/v1/instance + * + * Information about the server. + * + * @return Instance + */ + public function instance(Request $request) + { + $res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () { + $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if(config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); + + $stats = Cache::remember('api:v1:instance-data:stats', 43200, function () { + return [ + 'user_count' => User::count(), + 'status_count' => Status::whereNull('uri')->count(), + 'domain_count' => Instance::count(), + ]; + }); + + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function($rule, $key) { + $id = $key + 1; + return [ + 'id' => "{$id}", + 'text' => $rule + ]; + }) + ->toArray() : []; + }); + + return [ + 'uri' => config('pixelfed.domain.app'), + 'title' => config('app.name'), + 'short_description' => config_cache('app.short_description'), + 'description' => config_cache('app.description'), + 'email' => config('instance.email'), + 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')', + 'urls' => [ + 'streaming_api' => 'wss://' . config('pixelfed.domain.app') + ], + 'stats' => $stats, + 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + 'languages' => [config('app.locale')], + 'registrations' => (bool) config_cache('pixelfed.open_registration'), + 'approval_required' => false, + 'contact_account' => $contact, + 'rules' => $rules, + 'configuration' => [ + 'media_attachments' => [ + 'image_matrix_limit' => 16777216, + 'image_size_limit' => config('pixelfed.max_photo_size') * 1024, + 'supported_mime_types' => explode(',', config('pixelfed.media_types')), + 'video_frame_rate_limit' => 120, + 'video_matrix_limit' => 2304000, + 'video_size_limit' => config('pixelfed.max_photo_size') * 1024, + ], + 'polls' => [ + 'max_characters_per_option' => 50, + 'max_expiration' => 2629746, + 'max_options' => 4, + 'min_expiration' => 300 + ], + 'statuses' => [ + 'characters_reserved_per_url' => 23, + 'max_characters' => (int) config('pixelfed.max_caption_length'), + 'max_media_attachments' => (int) config('pixelfed.max_album_length') + ] + ] + ]; + }); + + return $this->json($res); + } + + /** + * GET /api/v1/lists + * + * Return empty array as we don't support lists + * + * @return null + */ + public function accountLists(Request $request) + { + abort_if(!$request->user(), 403); + + return response()->json([]); + } + + /** + * GET /api/v1/accounts/{id}/lists + * + * @param integer $id + * + * @return null + */ + public function accountListsById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + return response()->json([]); + } + + /** + * POST /api/v1/media + * + * + * @return MediaTransformer + */ + public function mediaUpload(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') + ]); + + $user = $request->user(); + + if($user->last_active_at == null) { + return []; + } + + if(empty($request->file('file'))) { + return response('', 422); + } + + $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); + + $profile = $user->profile; + + if(config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + return Media::whereUserId($user->id)->sum('size') / 1000; + }); + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($size >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + + $photo = $request->file('file'); + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); + + // if($photo->getMimeType() == 'image/heic') { + // abort_if(config('image.driver') !== 'imagick', 422, 'Invalid media type'); + // abort_if(!in_array('HEIC', \Imagick::queryformats()), 422, 'Unsupported media type'); + // $oldPath = $path; + // $path = str_replace('.heic', '.jpg', $path); + // $mime = 'image/jpeg'; + // \Image::make($photo)->save(storage_path("app/{$path}")); + // @unlink(storage_path("app/{$oldPath}")); + // } + + $settings = UserSetting::whereUserId($user->id)->first(); + + if($settings && !empty($settings->compose_settings)) { + $compose = $settings->compose_settings; + + if(isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + $media = new Media(); + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description'); + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if($license) { + $media->license = $license; + } + $media->save(); + + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url(). '?v=' . time(); + $res['url'] = $media->url(). '?v=' . time(); + return $this->json($res); + } + + /** + * PUT /api/v1/media/{id} + * + * @param integer $id + * + * @return MediaTransformer + */ + public function mediaUpdate(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') + ]); + + $user = $request->user(); + + $media = Media::whereUserId($user->id) + ->whereProfileId($user->profile_id) + ->findOrFail($id); + + $executed = RateLimiter::attempt( + 'media:update:'.$user->id, + 10, + function() use($media, $request) { + $caption = Purify::clean($request->input('description')); + + if($caption != $media->caption) { + $media->caption = $caption; + $media->save(); + + if($media->status_id) { + MediaService::del($media->status_id); + StatusService::del($media->status_id); + } + } + }); + + if(!$executed) { + return response()->json([ + 'error' => 'Too many attempts. Try again in a few minutes.' + ], 429); + }; + + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + return $this->json($fractal->createData($resource)->toArray()); + } + + /** + * GET /api/v1/media/{id} + * + * @param integer $id + * + * @return MediaTransformer + */ + public function mediaGet(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + + $media = Media::whereUserId($user->id) + ->whereNull('status_id') + ->findOrFail($id); + + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + return $this->json($res); + } + + /** + * POST /api/v2/media + * + * + * @return MediaTransformer + */ + public function mediaUploadV2(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), + 'replace_id' => 'sometimes' + ]); + + $user = $request->user(); + + if($user->last_active_at == null) { + return []; + } + + if(empty($request->file('file'))) { + return response('', 422); + } + + $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); + + $profile = $user->profile; + + if(config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + return Media::whereUserId($user->id)->sum('size') / 1000; + }); + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($size >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + + $photo = $request->file('file'); + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); + + $settings = UserSetting::whereUserId($user->id)->first(); + + if($settings && !empty($settings->compose_settings)) { + $compose = $settings->compose_settings; + + if(isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + if($request->has('replace_id')) { + $rpid = $request->input('replace_id'); + $removeMedia = Media::whereNull('status_id') + ->whereUserId($user->id) + ->whereProfileId($profile->id) + ->where('created_at', '>', now()->subHours(2)) + ->find($rpid); + if($removeMedia) { + $dateTime = Carbon::now(); + MediaDeletePipeline::dispatch($removeMedia) + ->onQueue('mmo') + ->delay($dateTime->addMinutes(15)); + } + } + + $media = new Media(); + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description'); + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if($license) { + $media->license = $license; + } + $media->save(); + + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; + + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url(). '?v=' . time(); + $res['url'] = null; + return $this->json($res, 202); + } + + /** + * GET /api/v1/mutes + * + * + * @return AccountTransformer + */ + public function accountMutes(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'nullable|integer|min:1|max:40' + ]); + + $user = $request->user(); + $limit = $request->input('limit', 40); + + $mutes = UserFilter::whereUserId($user->profile_id) + ->whereFilterableType('App\Profile') + ->whereFilterType('mute') + ->orderByDesc('id') + ->simplePaginate($limit) + ->pluck('filterable_id') + ->map(function($id) { + return AccountService::get($id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($mutes); + } + + /** + * POST /api/v1/accounts/{id}/mute + * + * @param integer $id + * + * @return RelationshipTransformer + */ + public function accountMuteById(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + $pid = $user->profile_id; if(intval($pid) === intval($id)) { return $this->json(['error' => 'You cannot mute yourself'], 500); } - $account = Profile::findOrFail($id); + $account = Profile::findOrFail($id); - $count = UserFilterService::muteCount($pid); - $maxLimit = intval(config('instance.user_filters.max_user_mutes')); - if($count == 0) { - $filterCount = UserFilter::whereUserId($pid) - ->whereFilterType('mute') - ->get() - ->map(function($rec) { - return AccountService::get($rec->filterable_id, true); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values() - ->count(); - abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - } else { - abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); - } + $count = UserFilterService::muteCount($pid); + $maxLimit = intval(config('instance.user_filters.max_user_mutes')); + if($count == 0) { + $filterCount = UserFilter::whereUserId($pid) + ->whereFilterType('mute') + ->get() + ->map(function($rec) { + return AccountService::get($rec->filterable_id, true); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values() + ->count(); + abort_if($filterCount >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); + } else { + abort_if($count >= $maxLimit, 422, AccountController::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); + } - $filter = UserFilter::firstOrCreate([ - 'user_id' => $pid, - 'filterable_id' => $account->id, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'mute', - ]); + $filter = UserFilter::firstOrCreate([ + 'user_id' => $pid, + 'filterable_id' => $account->id, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'mute', + ]); - RelationshipService::refresh($pid, $id); + RelationshipService::refresh($pid, $id); - $resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } + $resource = new Fractal\Resource\Item($account, new RelationshipTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + return $this->json($res); + } - /** - * POST /api/v1/accounts/{id}/unmute - * - * @param integer $id - * - * @return RelationshipTransformer - */ - public function accountUnmuteById(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/accounts/{id}/unmute + * + * @param integer $id + * + * @return RelationshipTransformer + */ + public function accountUnmuteById(Request $request, $id) + { + abort_if(!$request->user(), 403); - $user = $request->user(); - $pid = $user->profile_id; + $user = $request->user(); + $pid = $user->profile_id; if(intval($pid) === intval($id)) { return $this->json(['error' => 'You cannot unmute yourself'], 500); } - $profile = Profile::findOrFail($id); + $profile = Profile::findOrFail($id); - $filter = UserFilter::whereUserId($pid) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('mute') - ->first(); + $filter = UserFilter::whereUserId($pid) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('mute') + ->first(); - if($filter) { - $filter->delete(); - UserFilterService::unmute($pid, $profile->id); - RelationshipService::refresh($pid, $id); - } + if($filter) { + $filter->delete(); + UserFilterService::unmute($pid, $profile->id); + RelationshipService::refresh($pid, $id); + } - $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - return $this->json($res); - } + $resource = new Fractal\Resource\Item($profile, new RelationshipTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + return $this->json($res); + } - /** - * GET /api/v1/notifications - * - * - * @return NotificationTransformer - */ - public function accountNotifications(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/notifications + * + * + * @return NotificationTransformer + */ + public function accountNotifications(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:100', - 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, - ]); + $this->validate($request, [ + 'limit' => 'nullable|integer|min:1|max:100', + 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + ]); - $pid = $request->user()->profile_id; - $limit = $request->input('limit', 20); + $pid = $request->user()->profile_id; + $limit = $request->input('limit', 20); - $since = $request->input('since_id'); - $min = $request->input('min_id'); - $max = $request->input('max_id'); + $since = $request->input('since_id'); + $min = $request->input('min_id'); + $max = $request->input('max_id'); - if(!$since && !$min && !$max) { - $min = 1; - } + if(!$since && !$min && !$max) { + $min = 1; + } - $maxId = null; - $minId = null; + $maxId = null; + $minId = null; - if($max) { - $res = NotificationService::getMaxMastodon($pid, $max, $limit); - $ids = NotificationService::getRankedMaxId($pid, $max, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } else { - $res = NotificationService::getMinMastodon($pid, $min ?? $since, $limit); - $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } + if($max) { + $res = NotificationService::getMaxMastodon($pid, $max, $limit); + $ids = NotificationService::getRankedMaxId($pid, $max, $limit); + if(!empty($ids)) { + $maxId = max($ids); + $minId = min($ids); + } + } else { + $res = NotificationService::getMinMastodon($pid, $min ?? $since, $limit); + $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); + if(!empty($ids)) { + $maxId = max($ids); + $minId = min($ids); + } + } - if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) { - Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); - NotificationService::warmCache($pid, 400, true); - } + if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) { + Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); + NotificationService::warmCache($pid, 400, true); + } - $baseUrl = config('app.url') . '/api/v1/notifications?limit=' . $limit . '&'; + $baseUrl = config('app.url') . '/api/v1/notifications?limit=' . $limit . '&'; - if($minId == $maxId) { - $minId = null; - } + if($minId == $maxId) { + $minId = null; + } - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } + if($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res, 200, $headers); - } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res, 200, $headers); + } - /** - * GET /api/v1/timelines/home - * - * - * @return StatusTransformer - */ - public function timelineHome(Request $request) - { - $this->validate($request,[ - 'page' => 'sometimes|integer|max:40', - 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'sometimes|integer|min:1|max:40', - 'include_reblogs' => 'sometimes', - ]); + /** + * GET /api/v1/timelines/home + * + * + * @return StatusTransformer + */ + public function timelineHome(Request $request) + { + $this->validate($request,[ + 'page' => 'sometimes|integer|max:40', + 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, + 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, + 'limit' => 'sometimes|integer|min:1|max:40', + 'include_reblogs' => 'sometimes', + ]); - $napi = $request->has(self::PF_API_ENTITY_KEY); - $page = $request->input('page'); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit') ?? 20; - $pid = $request->user()->profile_id; - $includeReblogs = $request->filled('include_reblogs'); - $nullFields = $includeReblogs ? - ['in_reply_to_id'] : - ['in_reply_to_id', 'reblog_of_id']; - $inTypes = $includeReblogs ? - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : - ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + $napi = $request->has(self::PF_API_ENTITY_KEY); + $page = $request->input('page'); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $limit = $request->input('limit') ?? 20; + $pid = $request->user()->profile_id; + $includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false; + $nullFields = $includeReblogs ? + ['in_reply_to_id'] : + ['in_reply_to_id', 'reblog_of_id']; + $inTypes = $includeReblogs ? + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : + ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; - if(config('exp.cached_home_timeline')) { - if($min || $max) { - if($request->has('min_id')) { - $res = HomeTimelineService::getRankedMinId($pid, $min ?? 0, $limit + 10); - } else { - $res = HomeTimelineService::getRankedMaxId($pid, $max ?? 0, $limit + 10); - } - } else { - $res = HomeTimelineService::get($pid, 0, $limit + 10); - } + if(config('exp.cached_home_timeline')) { + $paddedLimit = $includeReblogs ? $limit + 10 : $limit + 50; + if($min || $max) { + if($request->has('min_id')) { + $res = HomeTimelineService::getRankedMinId($pid, $min ?? 0, $paddedLimit); + } else { + $res = HomeTimelineService::getRankedMaxId($pid, $max ?? 0, $paddedLimit); + } + } else { + $res = HomeTimelineService::get($pid, 0, $paddedLimit); + } - if(!$res) { - $res = Cache::has('pf:services:apiv1:home:cached:coldbootcheck:' . $pid); - if(!$res) { - Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); - FeedWarmCachePipeline::dispatchSync($pid); - return response()->json([], 206); - } else { - Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); - return response()->json([], 206); - } - } + if(!$res) { + $res = Cache::has('pf:services:apiv1:home:cached:coldbootcheck:' . $pid); + if(!$res) { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); + FeedWarmCachePipeline::dispatchSync($pid); + return response()->json([], 206); + } else { + Cache::set('pf:services:apiv1:home:cached:coldbootcheck:' . $pid, 1, 86400); + return response()->json([], 206); + } + } - $res = collect($res) - ->map(function($id) use($napi) { - return $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); - }) - ->filter(function($res) { - return $res && isset($res['account']); - }) - ->filter(function($s) use($includeReblogs) { - return $includeReblogs ? true : $s['reblog'] == null; - }) - ->take($limit) - ->map(function($status) use($pid) { - if($pid) { - $status['favourited'] = (bool) LikeService::liked($pid, $status['id']); - $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - return $status; - }) - ->values(); + $res = collect($res) + ->map(function($id) use($napi) { + return $napi ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + }) + ->filter(function($res) { + return $res && isset($res['account']); + }) + ->filter(function($s) use($includeReblogs) { + return $includeReblogs ? true : $s['reblog'] == null; + }) + ->take($limit) + ->map(function($status) use($pid) { + if($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $status['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + return $status; + }) + ->values(); - $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; - $minId = $res->map(function($s) { - return ['id' => $s['id']]; - })->min('id'); - $maxId = $res->map(function($s) { - return ['id' => $s['id']]; - })->max('id'); + $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; + $minId = $res->map(function($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function($s) { + return ['id' => $s['id']]; + })->max('id'); - if($minId == $maxId) { - $minId = null; - } + if($minId == $maxId) { + $minId = null; + } - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } + if($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res->toArray(), 200, $headers); - } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res->toArray(), 200, $headers); + } - $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { - $following = Follower::whereProfileId($pid)->pluck('following_id'); - return $following->push($pid)->toArray(); - }); + $following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) { + $following = Follower::whereProfileId($pid)->pluck('following_id'); + return $following->push($pid)->toArray(); + }); - $muted = UserFilterService::mutes($pid); + $muted = UserFilterService::mutes($pid); - if($muted && count($muted)) { - $following = array_diff($following, $muted); - } + if($muted && count($muted)) { + $following = array_diff($following, $muted); + } - if($min || $max) { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - $res = Status::select( - 'id', - 'profile_id', - 'type', - 'visibility', - 'in_reply_to_id', - 'reblog_of_id' - ) - ->where('id', $dir, $id) - ->whereNull($nullFields) - ->whereIntegerInRaw('profile_id', $following) - ->whereIn('type', $inTypes) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderByDesc('id') - ->take(($limit * 2)) - ->get() - ->map(function($s) use($pid, $napi) { - try { - $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); - if(!$account) { - return false; - } - $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } + if($min || $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + $res = Status::select( + 'id', + 'profile_id', + 'type', + 'visibility', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->where('id', $dir, $id) + ->whereNull($nullFields) + ->whereIntegerInRaw('profile_id', $following) + ->whereIn('type', $inTypes) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->take(($limit * 2)) + ->get() + ->map(function($s) use($pid, $napi) { + try { + $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); + if(!$account) { + return false; + } + $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + return false; + } + } catch(\Exception $e) { + return false; + } - $status['account'] = $account; + $status['account'] = $account; - if($pid) { - $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); - $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - return $status; - }) - ->filter(function($status) { - return $status && isset($status['account']); - }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } + if($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + return $status; + }) + ->filter(function($status) { + return $status && isset($status['account']); + }) + ->map(function($status) use($pid) { + if(!empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } - return $status; - }) - ->take($limit) - ->values(); - } else { - $res = Status::select( - 'id', - 'profile_id', - 'type', - 'visibility', - 'in_reply_to_id', - 'reblog_of_id', - ) - ->whereNull($nullFields) - ->whereIntegerInRaw('profile_id', $following) - ->whereIn('type', $inTypes) - ->whereIn('visibility',['public', 'unlisted', 'private']) - ->orderByDesc('id') - ->take(($limit * 2)) - ->get() - ->map(function($s) use($pid, $napi) { - try { - $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); - if(!$account) { - return false; - } - $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } + return $status; + }) + ->take($limit) + ->values(); + } else { + $res = Status::select( + 'id', + 'profile_id', + 'type', + 'visibility', + 'in_reply_to_id', + 'reblog_of_id', + ) + ->whereNull($nullFields) + ->whereIntegerInRaw('profile_id', $following) + ->whereIn('type', $inTypes) + ->whereIn('visibility',['public', 'unlisted', 'private']) + ->orderByDesc('id') + ->take(($limit * 2)) + ->get() + ->map(function($s) use($pid, $napi) { + try { + $account = $napi ? AccountService::get($s['profile_id'], true) : AccountService::getMastodon($s['profile_id'], true); + if(!$account) { + return false; + } + $status = $napi ? StatusService::get($s['id'], false) : StatusService::getMastodon($s['id'], false); + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + return false; + } + } catch(\Exception $e) { + return false; + } - $status['account'] = $account; + $status['account'] = $account; - if($pid) { - $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); - $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } - return $status; - }) - ->filter(function($status) { - return $status && isset($status['account']); - }) - ->map(function($status) use($pid) { - if(!empty($status['reblog'])) { - $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); - $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); - $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); - } + if($pid) { + $status['favourited'] = (bool) LikeService::liked($pid, $s['id']); + $status['reblogged'] = (bool) ReblogService::get($pid, $status['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } + return $status; + }) + ->filter(function($status) { + return $status && isset($status['account']); + }) + ->map(function($status) use($pid) { + if(!empty($status['reblog'])) { + $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']); + $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']); + $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']); + } - return $status; - }) - ->take($limit) - ->values(); - } + return $status; + }) + ->take($limit) + ->values(); + } - $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; - $minId = $res->map(function($s) { - return ['id' => $s['id']]; - })->min('id'); - $maxId = $res->map(function($s) { - return ['id' => $s['id']]; - })->max('id'); + $baseUrl = config('app.url') . '/api/v1/timelines/home?limit=' . $limit . '&'; + $minId = $res->map(function($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function($s) { + return ['id' => $s['id']]; + })->max('id'); - if($minId == $maxId) { - $minId = null; - } + if($minId == $maxId) { + $minId = null; + } - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } + if($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res->toArray(), 200, $headers); - } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res->toArray(), 200, $headers); + } - /** - * GET /api/v1/timelines/public - * - * - * @return StatusTransformer - */ - public function timelinePublic(Request $request) - { - $this->validate($request,[ - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', - 'remote' => 'sometimes', - 'local' => 'sometimes' - ]); + /** + * GET /api/v1/timelines/public + * + * + * @return StatusTransformer + */ + public function timelinePublic(Request $request) + { + $this->validate($request,[ + 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'limit' => 'nullable|integer|max:100', + 'remote' => 'sometimes', + 'local' => 'sometimes' + ]); - $napi = $request->has(self::PF_API_ENTITY_KEY); - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit') ?? 20; - $user = $request->user(); - $remote = $request->has('remote'); - $local = $request->has('local'); + $napi = $request->has(self::PF_API_ENTITY_KEY); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $limit = $request->input('limit') ?? 20; + $user = $request->user(); + $remote = $request->has('remote'); + $local = $request->has('local'); $filtered = $user ? UserFilterService::filters($user->profile_id) : []; if($remote && config('instance.timeline.network.cached')) { - Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { - if(NetworkTimelineService::count() == 0) { - NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); - } - }); + Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { + if(NetworkTimelineService::count() == 0) { + NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); + } + }); - if ($max) { - $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); - } else { - $feed = NetworkTimelineService::get(0, $limit + 5); - } + if ($max) { + $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); + } else if ($min) { + $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = NetworkTimelineService::get(0, $limit + 5); + } } if($local || !$remote && !$local) { - Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { - if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); - } - }); + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { + if(PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); - if ($max) { - $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); - } else { - $feed = PublicTimelineService::get(0, $limit + 5); - } + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); + } else if ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = PublicTimelineService::get(0, $limit + 5); + } } - $res = collect($feed) - ->filter(function($k) use($min, $max) { - if(!$min && !$max) { - return true; - } + $res = collect($feed) + ->filter(function($k) use($min, $max) { + if(!$min && !$max) { + return true; + } - if($min) { - return $min != $k; - } + if($min) { + return $min != $k; + } - if($max) { - return $max != $k; - } - }) - ->map(function($k) use($user, $napi) { - try { - $status = $napi ? StatusService::get($k) : StatusService::getMastodon($k); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { - return false; - } - } catch(\Exception $e) { - return false; - } + if($max) { + return $max != $k; + } + }) + ->map(function($k) use($user, $napi) { + try { + $status = $napi ? StatusService::get($k) : StatusService::getMastodon($k); + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + return false; + } + } catch(\Exception $e) { + return false; + } - $account = $napi ? AccountService::get($status['account']['id'], true) : AccountService::getMastodon($status['account']['id'], true); - if(!$account) { - return false; - } + $account = $napi ? AccountService::get($status['account']['id'], true) : AccountService::getMastodon($status['account']['id'], true); + if(!$account) { + return false; + } - $status['account'] = $account; + $status['account'] = $account; - if($user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $status['id']); + if($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $status['id']); $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status['id']); - } - return $status; - }) - ->filter(function($s) use($filtered) { - return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; - }) - ->take($limit) - ->values(); + } + return $status; + }) + ->filter(function($s) use($filtered) { + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; + }) + ->take($limit) + ->values(); - $baseUrl = config('app.url') . '/api/v1/timelines/public?limit=' . $limit . '&'; - if($remote) { - $baseUrl .= 'remote=1&'; - } - $minId = $res->map(function($s) { - return ['id' => $s['id']]; - })->min('id'); - $maxId = $res->map(function($s) { - return ['id' => $s['id']]; - })->max('id'); + $baseUrl = config('app.url') . '/api/v1/timelines/public?limit=' . $limit . '&'; + if($remote) { + $baseUrl .= 'remote=1&'; + } + $minId = $res->map(function($s) { + return ['id' => $s['id']]; + })->min('id'); + $maxId = $res->map(function($s) { + return ['id' => $s['id']]; + })->max('id'); - if($minId == $maxId) { - $minId = null; - } + if($minId == $maxId) { + $minId = null; + } - if($maxId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; - } + if($maxId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next"'; + } - if($minId) { - $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($minId) { + $link = '<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - if($maxId && $minId) { - $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; - } + if($maxId && $minId) { + $link = '<'.$baseUrl.'max_id='.$minId.'>; rel="next",<'.$baseUrl.'min_id='.$maxId.'>; rel="prev"'; + } - $headers = isset($link) ? ['Link' => $link] : []; - return $this->json($res->toArray(), 200, $headers); - } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res->toArray(), 200, $headers); + } - /** - * GET /api/v1/conversations - * - * Not implemented - * - * @return array - */ - public function conversations(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'min:1|max:40', - 'scope' => 'nullable|in:inbox,sent,requests' - ]); + /** + * GET /api/v1/conversations + * + * Not implemented + * + * @return array + */ + public function conversations(Request $request) + { + abort_if(!$request->user(), 403); + $this->validate($request, [ + 'limit' => 'min:1|max:40', + 'scope' => 'nullable|in:inbox,sent,requests' + ]); - $limit = $request->input('limit', 20); - $scope = $request->input('scope', 'inbox'); - $pid = $request->user()->profile_id; + $limit = $request->input('limit', 20); + $scope = $request->input('scope', 'inbox'); + $pid = $request->user()->profile_id; - if(config('database.default') == 'pgsql') { - $dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) { - return $q->whereIsHidden(false)->where('to_id', $pid)->orWhere('from_id', $pid); - }) - ->when($scope === 'sent', function($q, $scope) use($pid) { - return $q->whereFromId($pid)->groupBy(['to_id', 'id']); - }) - ->when($scope === 'requests', function($q, $scope) use($pid) { - return $q->whereToId($pid)->whereIsHidden(true); - }); - } else { - $dms = Conversation::when($scope === 'inbox', function($q, $scope) use($pid) { - return $q->whereIsHidden(false) - ->where('to_id', $pid) - ->orWhere('from_id', $pid) - ->orderByDesc('status_id') - ->groupBy(['to_id', 'from_id']); - }) - ->when($scope === 'sent', function($q, $scope) use($pid) { - return $q->whereFromId($pid)->groupBy('to_id'); - }) - ->when($scope === 'requests', function($q, $scope) use($pid) { - return $q->whereToId($pid)->whereIsHidden(true); - }); - } + if(config('database.default') == 'pgsql') { + $dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) { + return $q->whereIsHidden(false)->where('to_id', $pid)->orWhere('from_id', $pid); + }) + ->when($scope === 'sent', function($q, $scope) use($pid) { + return $q->whereFromId($pid)->groupBy(['to_id', 'id']); + }) + ->when($scope === 'requests', function($q, $scope) use($pid) { + return $q->whereToId($pid)->whereIsHidden(true); + }); + } else { + $dms = Conversation::when($scope === 'inbox', function($q, $scope) use($pid) { + return $q->whereIsHidden(false) + ->where('to_id', $pid) + ->orWhere('from_id', $pid) + ->orderByDesc('status_id') + ->groupBy(['to_id', 'from_id']); + }) + ->when($scope === 'sent', function($q, $scope) use($pid) { + return $q->whereFromId($pid)->groupBy('to_id'); + }) + ->when($scope === 'requests', function($q, $scope) use($pid) { + return $q->whereToId($pid)->whereIsHidden(true); + }); + } - $dms = $dms->orderByDesc('status_id') - ->simplePaginate($limit) - ->map(function($dm) use($pid) { - $from = $pid == $dm->to_id ? $dm->from_id : $dm->to_id; - $res = [ - 'id' => $dm->id, - 'unread' => false, - 'accounts' => [ - AccountService::getMastodon($from, true) - ], - 'last_status' => StatusService::getDirectMessage($dm->status_id) - ]; - return $res; - }) - ->filter(function($dm) { - if(!$dm || empty($dm['last_status']) || !isset($dm['accounts']) || !count($dm['accounts']) || !isset($dm['accounts'][0]) || !isset($dm['accounts'][0]['id'])) { - return false; - } - return true; - }) - ->unique(function($item, $key) { - return $item['accounts'][0]['id']; - }) - ->values(); + $dms = $dms->orderByDesc('status_id') + ->simplePaginate($limit) + ->map(function($dm) use($pid) { + $from = $pid == $dm->to_id ? $dm->from_id : $dm->to_id; + $res = [ + 'id' => $dm->id, + 'unread' => false, + 'accounts' => [ + AccountService::getMastodon($from, true) + ], + 'last_status' => StatusService::getDirectMessage($dm->status_id) + ]; + return $res; + }) + ->filter(function($dm) { + if(!$dm || empty($dm['last_status']) || !isset($dm['accounts']) || !count($dm['accounts']) || !isset($dm['accounts'][0]) || !isset($dm['accounts'][0]['id'])) { + return false; + } + return true; + }) + ->unique(function($item, $key) { + return $item['accounts'][0]['id']; + }) + ->values(); - return $this->json($dms); - } + return $this->json($dms); + } - /** - * GET /api/v1/statuses/{id} - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusById(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/statuses/{id} + * + * @param integer $id + * + * @return StatusTransformer + */ + public function statusById(Request $request, $id) + { + abort_if(!$request->user(), 403); - $pid = $request->user()->profile_id; + $pid = $request->user()->profile_id; - $res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false); - if(!$res || !isset($res['visibility'])) { - abort(404); - } + $res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false); + if(!$res || !isset($res['visibility'])) { + abort(404); + } - $scope = $res['visibility']; - if(!in_array($scope, ['public', 'unlisted'])) { - if($scope === 'private') { - if(intval($res['account']['id']) !== intval($pid)) { - abort_unless(FollowerService::follows($pid, $res['account']['id']), 403); - } - } else { - abort(400, 'Invalid request'); - } - } + $scope = $res['visibility']; + if(!in_array($scope, ['public', 'unlisted'])) { + if($scope === 'private') { + if(intval($res['account']['id']) !== intval($pid)) { + abort_unless(FollowerService::follows($pid, $res['account']['id']), 403); + } + } else { + abort(400, 'Invalid request'); + } + } if(!empty($res['reblog']) && isset($res['reblog']['id'])) { $res['reblog']['favourited'] = (bool) LikeService::liked($pid, $res['reblog']['id']); @@ -2617,734 +2618,734 @@ class ApiV1Controller extends Controller $res['reblog']['bookmarked'] = BookmarkService::get($pid, $res['reblog']['id']); } - $res['favourited'] = LikeService::liked($pid, $res['id']); - $res['reblogged'] = ReblogService::get($pid, $res['id']); - $res['bookmarked'] = BookmarkService::get($pid, $res['id']); - - return $this->json($res); - } - - /** - * GET /api/v1/statuses/{id}/context - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusContext(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $user = $request->user(); - $pid = $user->profile_id; - $status = StatusService::getMastodon($id, false); - - if(!$status || !isset($status['account'])) { - return response('', 404); - } - - if(intval($status['account']['id']) !== intval($user->profile_id)) { - if($status['visibility'] == 'private') { - if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { - return response('', 404); - } - } else { - if(!in_array($status['visibility'], ['public','unlisted'])) { - return response('', 404); - } - } - } - - $ancestors = []; - $descendants = []; - - if($status['in_reply_to_id']) { - $ancestors[] = StatusService::getMastodon($status['in_reply_to_id'], false); - } - - if($status['replies_count']) { - $filters = UserFilterService::filters($pid); - - $descendants = DB::table('statuses') - ->where('in_reply_to_id', $id) - ->limit(20) - ->pluck('id') - ->map(function($sid) { - return StatusService::getMastodon($sid, false); - }) - ->filter(function($post) use($filters) { - return $post && isset($post['account'], $post['account']['id']) && !in_array($post['account']['id'], $filters); - }) - ->map(function($status) use($pid) { - $status['favourited'] = LikeService::liked($pid, $status['id']); - $status['reblogged'] = ReblogService::get($pid, $status['id']); - return $status; - }) - ->values(); - } - - $res = [ - 'ancestors' => $ancestors, - 'descendants' => $descendants - ]; - - return $this->json($res); - } - - /** - * GET /api/v1/statuses/{id}/card - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusCard(Request $request, $id) - { - abort_if(!$request->user(), 403); - $res = []; - return response()->json($res); - } - - /** - * GET /api/v1/statuses/{id}/reblogged_by - * - * @param integer $id - * - * @return AccountTransformer - */ - public function statusRebloggedBy(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' - ]); - - $limit = $request->input('limit', 10); - $user = $request->user(); - $pid = $user->profile_id; - $status = Status::findOrFail($id); - $account = AccountService::get($status->profile_id, true); - abort_if(!$account, 404); - $author = intval($status->profile_id) === intval($pid) || $user->is_admin; - $napi = $request->has(self::PF_API_ENTITY_KEY); - - abort_if( - !$status->type || - !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), - 404, - ); - - if(!$author) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($pid, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - - if($request->has('cursor')) { - return $this->json([]); - } - } - - $res = Status::where('reblog_of_id', $status->id) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - if(!$res) { - return $this->json([]); - } - - $headers = []; - if($author && $res->hasPages()) { - $links = ''; - if($res->onFirstPage()) { - if($res->nextPageUrl()) { - $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } else { - if($res->previousPageUrl()) { - $links = '<' . $res->previousPageUrl() .'>; rel="next"'; - } - - if($res->nextPageUrl()) { - if(!empty($links)) { - $links .= ', '; - } - $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } - - $headers = ['Link' => $links]; - } - - $res = $res->map(function($status) use($pid, $napi) { - $account = $napi ? AccountService::get($status->profile_id, true) : AccountService::getMastodon($status->profile_id, true); - if(!$account) { - return false; - } - if($napi) { - $account['follows'] = $status->profile_id == $pid ? null : FollowerService::follows($pid, $status->profile_id); - } - return $account; - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($res, 200, $headers); - } - - /** - * GET /api/v1/statuses/{id}/favourited_by - * - * @param integer $id - * - * @return AccountTransformer - */ - public function statusFavouritedBy(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:80' - ]); - - $limit = $request->input('limit', 10); - $user = $request->user(); - $pid = $user->profile_id; - $status = Status::findOrFail($id); - $account = AccountService::get($status->profile_id, true); - abort_if(!$account, 404); - $author = intval($status->profile_id) === intval($pid) || $user->is_admin; - $napi = $request->has(self::PF_API_ENTITY_KEY); - - abort_if( - !$status->type || - !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), - 404, - ); - - if(!$author) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($pid, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - - if($request->has('cursor')) { - return $this->json([]); - } - } - - $res = Like::where('status_id', $status->id) - ->orderByDesc('id') - ->cursorPaginate($limit) - ->withQueryString(); - - if(!$res) { - return $this->json([]); - } - - $headers = []; - if($author && $res->hasPages()) { - $links = ''; - - if($res->onFirstPage()) { - if($res->nextPageUrl()) { - $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } else { - if($res->previousPageUrl()) { - $links = '<' . $res->previousPageUrl() .'>; rel="next"'; - } - - if($res->nextPageUrl()) { - if(!empty($links)) { - $links .= ', '; - } - $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; - } - } - - $headers = ['Link' => $links]; - } - - $res = $res->map(function($like) use($pid, $napi) { - $account = $napi ? AccountService::get($like->profile_id, true) : AccountService::getMastodon($like->profile_id, true); - if(!$account) { - return false; - } - - if($napi) { - $account['follows'] = $like->profile_id == $pid ? null : FollowerService::follows($pid, $like->profile_id); - } - return $account; - }) - ->filter(function($account) { - return $account && isset($account['id']); - }) - ->values(); - - return $this->json($res, 200, $headers); - } - - /** - * POST /api/v1/statuses - * - * - * @return StatusTransformer - */ - public function statusCreate(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'status' => 'nullable|string', - 'in_reply_to_id' => 'nullable', - 'media_ids' => 'sometimes|array|max:' . config_cache('pixelfed.max_album_length'), - 'sensitive' => 'nullable', - 'visibility' => 'string|in:private,unlisted,public', - 'spoiler_text' => 'sometimes|max:140', - 'place_id' => 'sometimes|integer|min:1|max:128769', - 'collection_ids' => 'sometimes|array|max:3', - 'comments_disabled' => 'sometimes|boolean', - ]); - - if($request->hasHeader('idempotency-key')) { - $key = 'pf:api:v1:status:idempotency-key:' . $request->user()->id . ':' . hash('sha1', $request->header('idempotency-key')); - $exists = Cache::has($key); - abort_if($exists, 400, 'Duplicate idempotency key.'); - Cache::put($key, 1, 3600); - } - - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->status) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->status, $kw) == true) { - abort(400, 'Invalid object. Contains banned keyword.'); - } - } - } - } - - if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) { - abort(403, 'Empty statuses are not allowed'); - } - - $ids = $request->input('media_ids'); - $in_reply_to_id = $request->input('in_reply_to_id'); - - $user = $request->user(); - $profile = $user->profile; - - $limitKey = 'compose:rate-limit:store:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Status::whereProfileId($user->profile_id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->where('created_at', '>', now()->subDays(1)) - ->count(); - - return $dailyLimit >= 1000; - }); - - abort_if($limitReached == true, 429); - - $visibility = $profile->is_private ? 'private' : ( - $profile->unlisted == true && - $request->input('visibility', 'public') == 'public' ? - 'unlisted' : - $request->input('visibility', 'public')); - - if($user->last_active_at == null) { - return []; - } - - $content = strip_tags($request->input('status')); - $rendered = Autolink::create()->autolink($content); - $cw = $user->profile->cw == true ? true : $request->input('sensitive', false); - $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; - - if($in_reply_to_id) { - $parent = Status::findOrFail($in_reply_to_id); - if($parent->comments_disabled) { - return $this->json("Comments have been disabled on this post", 422); - } - $blocks = UserFilterService::blocks($parent->profile_id); - abort_if(in_array($profile->id, $blocks), 422, 'Cannot reply to this post at this time.'); - - $status = new Status; - $status->caption = $content; - $status->rendered = $rendered; - $status->scope = $visibility; - $status->visibility = $visibility; - $status->profile_id = $user->profile_id; - $status->is_nsfw = $cw; - $status->cw_summary = $spoilerText; - $status->in_reply_to_id = $parent->id; - $status->in_reply_to_profile_id = $parent->profile_id; - $status->save(); - StatusService::del($parent->id); - Cache::forget('status:replies:all:' . $parent->id); - } - - if($ids) { - if(Media::whereUserId($user->id) - ->whereNull('status_id') - ->find($ids) - ->count() == 0 - ) { - abort(400, 'Invalid media_ids'); - } - - if(!$in_reply_to_id) { - $status = new Status; - $status->caption = $content; - $status->rendered = $rendered; - $status->profile_id = $user->profile_id; - $status->is_nsfw = $cw; - $status->cw_summary = $spoilerText; - $status->scope = 'draft'; - $status->visibility = 'draft'; - if($request->has('place_id')) { - $status->place_id = $request->input('place_id'); - } - $status->save(); - } - - $mimes = []; - - foreach($ids as $k => $v) { - if($k + 1 > config_cache('pixelfed.max_album_length')) { - continue; - } - $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); - if($m->profile_id !== $user->profile_id || $m->status_id) { - abort(403, 'Invalid media id'); - } - $m->order = $k + 1; - $m->status_id = $status->id; - $m->save(); - array_push($mimes, $m->mime); - } - - if(empty($mimes)) { - $status->delete(); - abort(400, 'Invalid media ids'); - } - - if($request->has('comments_disabled') && $request->input('comments_disabled')) { - $status->comments_disabled = true; - } - - $status->scope = $visibility; - $status->visibility = $visibility; - $status->type = StatusController::mimeTypeCheck($mimes); - $status->save(); - } - - if(!$status) { - abort(500, 'An error occured.'); - } - - NewStatusPipeline::dispatch($status); - if($status->in_reply_to_id) { - CommentPipeline::dispatch($parent, $status); - } - Cache::forget('user:account:id:'.$user->id); - Cache::forget('_api:statuses:recent_9:'.$user->profile_id); - Cache::forget('profile:status_count:'.$user->profile_id); - Cache::forget($user->storageUsedKey()); - Cache::forget('profile:embed:' . $status->profile_id); - Cache::forget($limitKey); - - if($request->has('collection_ids') && $ids) { - $collections = Collection::whereProfileId($user->profile_id) - ->find($request->input('collection_ids')) - ->each(function($collection) use($status) { - $count = $collection->items()->count(); - $item = CollectionItem::firstOrCreate([ - 'collection_id' => $collection->id, - 'object_type' => 'App\Status', - 'object_id' => $status->id - ],[ - 'order' => $count, - ]); - - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + $res['favourited'] = LikeService::liked($pid, $res['id']); + $res['reblogged'] = ReblogService::get($pid, $res['id']); + $res['bookmarked'] = BookmarkService::get($pid, $res['id']); + + return $this->json($res); + } + + /** + * GET /api/v1/statuses/{id}/context + * + * @param integer $id + * + * @return StatusTransformer + */ + public function statusContext(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $user = $request->user(); + $pid = $user->profile_id; + $status = StatusService::getMastodon($id, false); + + if(!$status || !isset($status['account'])) { + return response('', 404); + } + + if(intval($status['account']['id']) !== intval($user->profile_id)) { + if($status['visibility'] == 'private') { + if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { + return response('', 404); + } + } else { + if(!in_array($status['visibility'], ['public','unlisted'])) { + return response('', 404); + } + } + } + + $ancestors = []; + $descendants = []; + + if($status['in_reply_to_id']) { + $ancestors[] = StatusService::getMastodon($status['in_reply_to_id'], false); + } + + if($status['replies_count']) { + $filters = UserFilterService::filters($pid); + + $descendants = DB::table('statuses') + ->where('in_reply_to_id', $id) + ->limit(20) + ->pluck('id') + ->map(function($sid) { + return StatusService::getMastodon($sid, false); + }) + ->filter(function($post) use($filters) { + return $post && isset($post['account'], $post['account']['id']) && !in_array($post['account']['id'], $filters); + }) + ->map(function($status) use($pid) { + $status['favourited'] = LikeService::liked($pid, $status['id']); + $status['reblogged'] = ReblogService::get($pid, $status['id']); + return $status; + }) + ->values(); + } + + $res = [ + 'ancestors' => $ancestors, + 'descendants' => $descendants + ]; + + return $this->json($res); + } + + /** + * GET /api/v1/statuses/{id}/card + * + * @param integer $id + * + * @return StatusTransformer + */ + public function statusCard(Request $request, $id) + { + abort_if(!$request->user(), 403); + $res = []; + return response()->json($res); + } + + /** + * GET /api/v1/statuses/{id}/reblogged_by + * + * @param integer $id + * + * @return AccountTransformer + */ + public function statusRebloggedBy(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:80' + ]); + + $limit = $request->input('limit', 10); + $user = $request->user(); + $pid = $user->profile_id; + $status = Status::findOrFail($id); + $account = AccountService::get($status->profile_id, true); + abort_if(!$account, 404); + $author = intval($status->profile_id) === intval($pid) || $user->is_admin; + $napi = $request->has(self::PF_API_ENTITY_KEY); + + abort_if( + !$status->type || + !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), + 404, + ); + + if(!$author) { + if($status->scope == 'private') { + abort_if(!FollowerService::follows($pid, $status->profile_id), 403); + } else { + abort_if(!in_array($status->scope, ['public','unlisted']), 403); + } + + if($request->has('cursor')) { + return $this->json([]); + } + } + + $res = Status::where('reblog_of_id', $status->id) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + if(!$res) { + return $this->json([]); + } + + $headers = []; + if($author && $res->hasPages()) { + $links = ''; + if($res->onFirstPage()) { + if($res->nextPageUrl()) { + $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; + } + } else { + if($res->previousPageUrl()) { + $links = '<' . $res->previousPageUrl() .'>; rel="next"'; + } + + if($res->nextPageUrl()) { + if(!empty($links)) { + $links .= ', '; + } + $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; + } + } + + $headers = ['Link' => $links]; + } + + $res = $res->map(function($status) use($pid, $napi) { + $account = $napi ? AccountService::get($status->profile_id, true) : AccountService::getMastodon($status->profile_id, true); + if(!$account) { + return false; + } + if($napi) { + $account['follows'] = $status->profile_id == $pid ? null : FollowerService::follows($pid, $status->profile_id); + } + return $account; + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($res, 200, $headers); + } + + /** + * GET /api/v1/statuses/{id}/favourited_by + * + * @param integer $id + * + * @return AccountTransformer + */ + public function statusFavouritedBy(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'nullable|integer|min:1|max:80' + ]); + + $limit = $request->input('limit', 10); + $user = $request->user(); + $pid = $user->profile_id; + $status = Status::findOrFail($id); + $account = AccountService::get($status->profile_id, true); + abort_if(!$account, 404); + $author = intval($status->profile_id) === intval($pid) || $user->is_admin; + $napi = $request->has(self::PF_API_ENTITY_KEY); + + abort_if( + !$status->type || + !in_array($status->type, ['photo','photo:album', 'photo:video:album', 'reply', 'text', 'video', 'video:album']), + 404, + ); + + if(!$author) { + if($status->scope == 'private') { + abort_if(!FollowerService::follows($pid, $status->profile_id), 403); + } else { + abort_if(!in_array($status->scope, ['public','unlisted']), 403); + } + + if($request->has('cursor')) { + return $this->json([]); + } + } + + $res = Like::where('status_id', $status->id) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + if(!$res) { + return $this->json([]); + } + + $headers = []; + if($author && $res->hasPages()) { + $links = ''; + + if($res->onFirstPage()) { + if($res->nextPageUrl()) { + $links = '<' . $res->nextPageUrl() .'>; rel="prev"'; + } + } else { + if($res->previousPageUrl()) { + $links = '<' . $res->previousPageUrl() .'>; rel="next"'; + } + + if($res->nextPageUrl()) { + if(!empty($links)) { + $links .= ', '; + } + $links .= '<' . $res->nextPageUrl() .'>; rel="prev"'; + } + } + + $headers = ['Link' => $links]; + } + + $res = $res->map(function($like) use($pid, $napi) { + $account = $napi ? AccountService::get($like->profile_id, true) : AccountService::getMastodon($like->profile_id, true); + if(!$account) { + return false; + } + + if($napi) { + $account['follows'] = $like->profile_id == $pid ? null : FollowerService::follows($pid, $like->profile_id); + } + return $account; + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values(); + + return $this->json($res, 200, $headers); + } + + /** + * POST /api/v1/statuses + * + * + * @return StatusTransformer + */ + public function statusCreate(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'status' => 'nullable|string', + 'in_reply_to_id' => 'nullable', + 'media_ids' => 'sometimes|array|max:' . config_cache('pixelfed.max_album_length'), + 'sensitive' => 'nullable', + 'visibility' => 'string|in:private,unlisted,public', + 'spoiler_text' => 'sometimes|max:140', + 'place_id' => 'sometimes|integer|min:1|max:128769', + 'collection_ids' => 'sometimes|array|max:3', + 'comments_disabled' => 'sometimes|boolean', + ]); + + if($request->hasHeader('idempotency-key')) { + $key = 'pf:api:v1:status:idempotency-key:' . $request->user()->id . ':' . hash('sha1', $request->header('idempotency-key')); + $exists = Cache::has($key); + abort_if($exists, 400, 'Duplicate idempotency key.'); + Cache::put($key, 1, 3600); + } + + if(config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if($blockedKeywords !== null && $request->status) { + $keywords = config('costar.keyword.block'); + foreach($keywords as $kw) { + if(Str::contains($request->status, $kw) == true) { + abort(400, 'Invalid object. Contains banned keyword.'); + } + } + } + } + + if(!$request->filled('media_ids') && !$request->filled('in_reply_to_id')) { + abort(403, 'Empty statuses are not allowed'); + } + + $ids = $request->input('media_ids'); + $in_reply_to_id = $request->input('in_reply_to_id'); + + $user = $request->user(); + $profile = $user->profile; + + $limitKey = 'compose:rate-limit:store:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Status::whereProfileId($user->profile_id) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->where('created_at', '>', now()->subDays(1)) + ->count(); + + return $dailyLimit >= 1000; + }); + + abort_if($limitReached == true, 429); + + $visibility = $profile->is_private ? 'private' : ( + $profile->unlisted == true && + $request->input('visibility', 'public') == 'public' ? + 'unlisted' : + $request->input('visibility', 'public')); + + if($user->last_active_at == null) { + return []; + } + + $content = strip_tags($request->input('status')); + $rendered = Autolink::create()->autolink($content); + $cw = $user->profile->cw == true ? true : $request->input('sensitive', false); + $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; + + if($in_reply_to_id) { + $parent = Status::findOrFail($in_reply_to_id); + if($parent->comments_disabled) { + return $this->json("Comments have been disabled on this post", 422); + } + $blocks = UserFilterService::blocks($parent->profile_id); + abort_if(in_array($profile->id, $blocks), 422, 'Cannot reply to this post at this time.'); + + $status = new Status; + $status->caption = $content; + $status->rendered = $rendered; + $status->scope = $visibility; + $status->visibility = $visibility; + $status->profile_id = $user->profile_id; + $status->is_nsfw = $cw; + $status->cw_summary = $spoilerText; + $status->in_reply_to_id = $parent->id; + $status->in_reply_to_profile_id = $parent->profile_id; + $status->save(); + StatusService::del($parent->id); + Cache::forget('status:replies:all:' . $parent->id); + } + + if($ids) { + if(Media::whereUserId($user->id) + ->whereNull('status_id') + ->find($ids) + ->count() == 0 + ) { + abort(400, 'Invalid media_ids'); + } + + if(!$in_reply_to_id) { + $status = new Status; + $status->caption = $content; + $status->rendered = $rendered; + $status->profile_id = $user->profile_id; + $status->is_nsfw = $cw; + $status->cw_summary = $spoilerText; + $status->scope = 'draft'; + $status->visibility = 'draft'; + if($request->has('place_id')) { + $status->place_id = $request->input('place_id'); + } + $status->save(); + } + + $mimes = []; + + foreach($ids as $k => $v) { + if($k + 1 > config_cache('pixelfed.max_album_length')) { + continue; + } + $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); + if($m->profile_id !== $user->profile_id || $m->status_id) { + abort(403, 'Invalid media id'); + } + $m->order = $k + 1; + $m->status_id = $status->id; + $m->save(); + array_push($mimes, $m->mime); + } + + if(empty($mimes)) { + $status->delete(); + abort(400, 'Invalid media ids'); + } + + if($request->has('comments_disabled') && $request->input('comments_disabled')) { + $status->comments_disabled = true; + } + + $status->scope = $visibility; + $status->visibility = $visibility; + $status->type = StatusController::mimeTypeCheck($mimes); + $status->save(); + } + + if(!$status) { + abort(500, 'An error occured.'); + } + + NewStatusPipeline::dispatch($status); + if($status->in_reply_to_id) { + CommentPipeline::dispatch($parent, $status); + } + Cache::forget('user:account:id:'.$user->id); + Cache::forget('_api:statuses:recent_9:'.$user->profile_id); + Cache::forget('profile:status_count:'.$user->profile_id); + Cache::forget($user->storageUsedKey()); + Cache::forget('profile:embed:' . $status->profile_id); + Cache::forget($limitKey); + + if($request->has('collection_ids') && $ids) { + $collections = Collection::whereProfileId($user->profile_id) + ->find($request->input('collection_ids')) + ->each(function($collection) use($status) { + $count = $collection->items()->count(); + $item = CollectionItem::firstOrCreate([ + 'collection_id' => $collection->id, + 'object_type' => 'App\Status', + 'object_id' => $status->id + ],[ + 'order' => $count, + ]); + + CollectionService::addItem( + $collection->id, + $status->id, + $count + ); $collection->updated_at = now(); $collection->save(); CollectionService::setCollection($collection->id, $collection); - }); - } + }); + } - $res = StatusService::getMastodon($status->id, false); - $res['favourited'] = false; - $res['language'] = 'en'; - $res['bookmarked'] = false; - $res['card'] = null; - return $this->json($res); - } + $res = StatusService::getMastodon($status->id, false); + $res['favourited'] = false; + $res['language'] = 'en'; + $res['bookmarked'] = false; + $res['card'] = null; + return $this->json($res); + } - /** - * DELETE /api/v1/statuses - * - * @param integer $id - * - * @return null - */ - public function statusDelete(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * DELETE /api/v1/statuses + * + * @param integer $id + * + * @return null + */ + public function statusDelete(Request $request, $id) + { + abort_if(!$request->user(), 403); - $status = Status::whereProfileId($request->user()->profile->id) - ->findOrFail($id); + $status = Status::whereProfileId($request->user()->profile->id) + ->findOrFail($id); - $resource = new Fractal\Resource\Item($status, new StatusTransformer()); + $resource = new Fractal\Resource\Item($status, new StatusTransformer()); - Cache::forget('profile:status_count:'.$status->profile_id); - StatusDelete::dispatch($status); + Cache::forget('profile:status_count:'.$status->profile_id); + StatusDelete::dispatch($status); - $res = $this->fractal->createData($resource)->toArray(); - $res['text'] = $res['content']; - unset($res['content']); + $res = $this->fractal->createData($resource)->toArray(); + $res['text'] = $res['content']; + unset($res['content']); - return $this->json($res); - } + return $this->json($res); + } - /** - * POST /api/v1/statuses/{id}/reblog - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusShare(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/statuses/{id}/reblog + * + * @param integer $id + * + * @return StatusTransformer + */ + public function statusShare(Request $request, $id) + { + abort_if(!$request->user(), 403); - $user = $request->user(); - $status = Status::whereScope('public')->findOrFail($id); + $user = $request->user(); + $status = Status::whereScope('public')->findOrFail($id); - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } + if(intval($status->profile_id) !== intval($user->profile_id)) { + if($status->scope == 'private') { + abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); + } else { + abort_if(!in_array($status->scope, ['public','unlisted']), 403); + } - $blocks = UserFilterService::blocks($status->profile_id); - if($blocks && in_array($user->profile_id, $blocks)) { - abort(422); - } - } + $blocks = UserFilterService::blocks($status->profile_id); + if($blocks && in_array($user->profile_id, $blocks)) { + abort(422); + } + } - $share = Status::firstOrCreate([ - 'profile_id' => $user->profile_id, - 'reblog_of_id' => $status->id, - 'type' => 'share', - 'in_reply_to_profile_id' => $status->profile_id, - 'scope' => 'public', - 'visibility' => 'public' - ]); + $share = Status::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'reblog_of_id' => $status->id, + 'type' => 'share', + 'in_reply_to_profile_id' => $status->profile_id, + 'scope' => 'public', + 'visibility' => 'public' + ]); - SharePipeline::dispatch($share)->onQueue('low'); + SharePipeline::dispatch($share)->onQueue('low'); - StatusService::del($status->id); - ReblogService::add($user->profile_id, $status->id); - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = true; + StatusService::del($status->id); + ReblogService::add($user->profile_id, $status->id); + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = true; - return $this->json($res); - } + return $this->json($res); + } - /** - * POST /api/v1/statuses/{id}/unreblog - * - * @param integer $id - * - * @return StatusTransformer - */ - public function statusUnshare(Request $request, $id) - { - abort_if(!$request->user(), 403); + /** + * POST /api/v1/statuses/{id}/unreblog + * + * @param integer $id + * + * @return StatusTransformer + */ + public function statusUnshare(Request $request, $id) + { + abort_if(!$request->user(), 403); - $user = $request->user(); - $status = Status::whereScope('public')->findOrFail($id); + $user = $request->user(); + $status = Status::whereScope('public')->findOrFail($id); - if(intval($status->profile_id) !== intval($user->profile_id)) { - if($status->scope == 'private') { - abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); - } else { - abort_if(!in_array($status->scope, ['public','unlisted']), 403); - } - } + if(intval($status->profile_id) !== intval($user->profile_id)) { + if($status->scope == 'private') { + abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); + } else { + abort_if(!in_array($status->scope, ['public','unlisted']), 403); + } + } - $reblog = Status::whereProfileId($user->profile_id) - ->whereReblogOfId($status->id) - ->first(); + $reblog = Status::whereProfileId($user->profile_id) + ->whereReblogOfId($status->id) + ->first(); - if(!$reblog) { - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = false; - return $this->json($res); - } + if(!$reblog) { + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = false; + return $this->json($res); + } - UndoSharePipeline::dispatch($reblog)->onQueue('low'); - ReblogService::del($user->profile_id, $status->id); + UndoSharePipeline::dispatch($reblog)->onQueue('low'); + ReblogService::del($user->profile_id, $status->id); - $res = StatusService::getMastodon($status->id); - $res['reblogged'] = false; + $res = StatusService::getMastodon($status->id); + $res['reblogged'] = false; - return $this->json($res); - } + return $this->json($res); + } - /** - * GET /api/v1/timelines/tag/{hashtag} - * - * @param string $hashtag - * - * @return StatusTransformer - */ - public function timelineHashtag(Request $request, $hashtag) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/timelines/tag/{hashtag} + * + * @param string $hashtag + * + * @return StatusTransformer + */ + public function timelineHashtag(Request $request, $hashtag) + { + abort_if(!$request->user(), 403); - $this->validate($request,[ - 'page' => 'nullable|integer|max:40', - 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', - 'only_media' => 'sometimes|boolean', - '_pe' => 'sometimes' - ]); + $this->validate($request,[ + 'page' => 'nullable|integer|max:40', + 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, + 'limit' => 'nullable|integer|max:100', + 'only_media' => 'sometimes|boolean', + '_pe' => 'sometimes' + ]); - if(config('database.default') === 'pgsql') { - $tag = Hashtag::where('name', 'ilike', $hashtag) - ->orWhere('slug', 'ilike', $hashtag) - ->first(); - } else { - $tag = Hashtag::whereName($hashtag) - ->orWhere('slug', $hashtag) - ->first(); - } + if(config('database.default') === 'pgsql') { + $tag = Hashtag::where('name', 'ilike', $hashtag) + ->orWhere('slug', 'ilike', $hashtag) + ->first(); + } else { + $tag = Hashtag::whereName($hashtag) + ->orWhere('slug', $hashtag) + ->first(); + } - if(!$tag) { - return response()->json([]); - } + if(!$tag) { + return response()->json([]); + } - if($tag->is_banned == true) { - return $this->json([]); - } + if($tag->is_banned == true) { + return $this->json([]); + } - $min = $request->input('min_id'); - $max = $request->input('max_id'); - $limit = $request->input('limit', 20); - $onlyMedia = $request->input('only_media', true); - $pe = $request->has(self::PF_API_ENTITY_KEY); + $min = $request->input('min_id'); + $max = $request->input('max_id'); + $limit = $request->input('limit', 20); + $onlyMedia = $request->input('only_media', true); + $pe = $request->has(self::PF_API_ENTITY_KEY); - if($min || $max) { - $minMax = SnowflakeService::byDate(now()->subMonths(6)); - if($min && intval($min) < $minMax) { - return []; - } - if($max && intval($max) < $minMax) { - return []; - } - } + if($min || $max) { + $minMax = SnowflakeService::byDate(now()->subMonths(6)); + if($min && intval($min) < $minMax) { + return []; + } + if($max && intval($max) < $minMax) { + return []; + } + } - $filters = UserFilterService::filters($request->user()->profile_id); + $filters = UserFilterService::filters($request->user()->profile_id); - if(!$min && !$max) { - $id = 1; - $dir = '>'; - } else { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - } + if(!$min && !$max) { + $id = 1; + $dir = '>'; + } else { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + } - $res = StatusHashtag::whereHashtagId($tag->id) - ->whereStatusVisibility('public') - ->where('status_id', $dir, $id) - ->orderBy('status_id', 'desc') - ->limit($limit) - ->pluck('status_id') - ->map(function ($i) use($pe) { - return $pe ? StatusService::get($i) : StatusService::getMastodon($i); - }) - ->filter(function($i) use($onlyMedia) { - if(!$i) { - return false; - } - if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) { - return false; - } - return $i && isset($i['account']); - }) - ->filter(function($i) use($filters) { - return !in_array($i['account']['id'], $filters); - }) - ->values() - ->toArray(); + $res = StatusHashtag::whereHashtagId($tag->id) + ->whereStatusVisibility('public') + ->where('status_id', $dir, $id) + ->orderBy('status_id', 'desc') + ->limit($limit) + ->pluck('status_id') + ->map(function ($i) use($pe) { + return $pe ? StatusService::get($i) : StatusService::getMastodon($i); + }) + ->filter(function($i) use($onlyMedia) { + if(!$i) { + return false; + } + if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) { + return false; + } + return $i && isset($i['account']); + }) + ->filter(function($i) use($filters) { + return !in_array($i['account']['id'], $filters); + }) + ->values() + ->toArray(); - return $this->json($res); - } + return $this->json($res); + } - /** - * GET /api/v1/bookmarks - * - * - * - * @return StatusTransformer - */ - public function bookmarks(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v1/bookmarks + * + * + * + * @return StatusTransformer + */ + public function bookmarks(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'max_id' => 'nullable|integer|min:0', - 'since_id' => 'nullable|integer|min:0', - 'min_id' => 'nullable|integer|min:0' - ]); + $this->validate($request, [ + 'limit' => 'nullable|integer|min:1|max:40', + 'max_id' => 'nullable|integer|min:0', + 'since_id' => 'nullable|integer|min:0', + 'min_id' => 'nullable|integer|min:0' + ]); - $pe = $request->has('_pe'); - $pid = $request->user()->profile_id; - $limit = $request->input('limit') ?? 20; - $max_id = $request->input('max_id'); - $since_id = $request->input('since_id'); - $min_id = $request->input('min_id'); + $pe = $request->has('_pe'); + $pid = $request->user()->profile_id; + $limit = $request->input('limit') ?? 20; + $max_id = $request->input('max_id'); + $since_id = $request->input('since_id'); + $min_id = $request->input('min_id'); - $dir = $min_id ? '>' : '<'; - $id = $min_id ?? $max_id; + $dir = $min_id ? '>' : '<'; + $id = $min_id ?? $max_id; - $bookmarkQuery = Bookmark::whereProfileId($pid) + $bookmarkQuery = Bookmark::whereProfileId($pid) ->orderByDesc('id') ->cursorPaginate($limit); $bookmarks = $bookmarkQuery->map(function($bookmark) use($pid, $pe) { - $status = $pe ? StatusService::get($bookmark->status_id, false) : StatusService::getMastodon($bookmark->status_id, false); + $status = $pe ? StatusService::get($bookmark->status_id, false) : StatusService::getMastodon($bookmark->status_id, false); - if($status) { - $status['bookmarked'] = true; - $status['favourited'] = LikeService::liked($pid, $status['id']); - $status['reblogged'] = ReblogService::get($pid, $status['id']); - } - return $status; - }) - ->filter() - ->values() - ->toArray(); + if($status) { + $status['bookmarked'] = true; + $status['favourited'] = LikeService::liked($pid, $status['id']); + $status['reblogged'] = ReblogService::get($pid, $status['id']); + } + return $status; + }) + ->filter() + ->values() + ->toArray(); $links = null; $headers = []; @@ -3364,519 +3365,519 @@ class ApiV1Controller extends Controller $headers = ['Link' => $links]; } - return $this->json($bookmarks, 200, $headers); - } - - /** - * POST /api/v1/statuses/{id}/bookmark - * - * - * - * @return StatusTransformer - */ - public function bookmarkStatus(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; - - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); - - if($status->scope == 'private') { - abort_if( - $pid !== $status->profile_id && !FollowerService::follows($pid, $status->profile_id), - 404, - 'Error: You cannot bookmark private posts from accounts you do not follow.' - ); - } - - Bookmark::firstOrCreate([ - 'status_id' => $status->id, - 'profile_id' => $pid - ]); - - BookmarkService::add($pid, $status->id); - - $res = StatusService::getMastodon($status->id, false); - $res['bookmarked'] = true; - - return $this->json($res); - } - - /** - * POST /api/v1/statuses/{id}/unbookmark - * - * - * - * @return StatusTransformer - */ - public function unbookmarkStatus(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; - - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); - - $bookmark = Bookmark::whereStatusId($status->id) - ->whereProfileId($pid) - ->first(); - - if($bookmark) { - BookmarkService::del($pid, $status->id); - $bookmark->delete(); - } - $res = StatusService::getMastodon($status->id, false); - $res['bookmarked'] = false; - - return $this->json($res); - } - - /** - * GET /api/v1/discover/posts - * - * - * @return array - */ - public function discoverPosts(Request $request) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'integer|min:1|max:40' - ]); - - $limit = $request->input('limit', 40); - $pid = $request->user()->profile_id; - $filters = UserFilterService::filters($pid); - $forYou = DiscoverService::getForYou(); - $posts = $forYou->take(50)->map(function($post) { - return StatusService::getMastodon($post); - }) - ->filter(function($post) use($filters) { - return $post && - isset($post['account']) && - isset($post['account']['id']) && - !in_array($post['account']['id'], $filters); - }) - ->take(12) - ->values(); - return $this->json(compact('posts')); - } - - /** - * GET /api/v2/statuses/{id}/replies - * - * - * @return array - */ - public function statusReplies(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $this->validate($request, [ - 'limit' => 'int|min:1|max:10', - 'sort' => 'in:all,newest,popular' - ]); - - $limit = $request->input('limit', 3); - $pid = $request->user()->profile_id; - $status = StatusService::getMastodon($id, false); - - abort_if(!$status, 404); - - if($status['visibility'] == 'private') { - if($pid != $status['account']['id']) { - abort_unless(FollowerService::follows($pid, $status['account']['id']), 404); - } - } - - $sortBy = $request->input('sort', 'all'); - - if($sortBy == 'all' && isset($status['replies_count']) && $status['replies_count'] && $request->has('refresh_cache')) { - if(!Cache::has('status:replies:all-rc:' . $id)) { - Cache::forget('status:replies:all:' . $id); - Cache::put('status:replies:all-rc:' . $id, true, 300); - } - } - - if($sortBy == 'all' && !$request->has('cursor')) { - $ids = Cache::remember('status:replies:all:' . $id, 3600, function() use($id) { - return DB::table('statuses') - ->where('in_reply_to_id', $id) - ->orderBy('id') - ->cursorPaginate(3); - }); - } else { - $ids = DB::table('statuses') - ->where('in_reply_to_id', $id) - ->when($sortBy, function($q, $sortBy) { - if($sortBy === 'all') { - return $q->orderBy('id'); - } - - if($sortBy === 'newest') { - return $q->orderByDesc('created_at'); - } - - if($sortBy === 'popular') { - return $q->orderByDesc('likes_count'); - } - }) - ->cursorPaginate($limit); - } - - $filters = UserFilterService::filters($pid); - $data = $ids->filter(function($post) use($filters) { - return !in_array($post->profile_id, $filters); - }) - ->map(function($post) use($pid) { - $status = StatusService::get($post->id, false); - - if(!$status || !isset($status['id'])) { - return false; - } - - $status['favourited'] = LikeService::liked($pid, $post->id); - return $status; - }) - ->map(function($post) { - if(isset($post['account']) && isset($post['account']['id'])) { - $account = AccountService::get($post['account']['id'], true); - $post['account'] = $account; - } - return $post; - }) - ->filter(function($post) { - return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']); - }) - ->values(); - - $res = [ - 'data' => $data, - 'next' => $ids->nextPageUrl() - ]; - - return $this->json($res); - } - - /** - * GET /api/v2/statuses/{id}/state - * - * - * @return array - */ - public function statusState(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $status = Status::findOrFail($id); - $pid = $request->user()->profile_id; - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - - return $this->json(StatusService::getState($status->id, $pid)); - } - - /** - * GET /api/v1.1/discover/accounts/popular - * - * - * @return array - */ - public function discoverAccountsPopular(Request $request) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - - $ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() { - return DB::table('profiles') - ->where('is_private', false) - ->whereNull('status') - ->orderByDesc('profiles.followers_count') - ->limit(20) - ->get(); - }); - - $ids = $ids->map(function($profile) { - return AccountService::get($profile->id, true); - }) - ->filter(function($profile) use($pid) { - return $profile && isset($profile['id']); - }) - ->filter(function($profile) use($pid) { - return $profile['id'] != $pid; - }) - ->take(6) - ->values(); - - return $this->json($ids); - } - - /** - * GET /api/v1/preferences - * - * - * @return array - */ - public function getPreferences(Request $request) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - - return $this->json([ - 'posting:default:visibility' => $account['locked'] ? 'private' : 'public', - 'posting:default:sensitive' => false, - 'posting:default:language' => null, - 'reading:expand:media' => 'default', - 'reading:expand:spoilers' => false - ]); - } - - /** - * GET /api/v1/trends - * - * - * @return array - */ - public function getTrends(Request $request) - { - abort_if(!$request->user(), 403); - - return $this->json([]); - } - - /** - * GET /api/v1/announcements - * - * - * @return array - */ - public function getAnnouncements(Request $request) - { - abort_if(!$request->user(), 403); - - return $this->json([]); - } - - /** - * GET /api/v1/markers - * - * - * @return array - */ - public function getMarkers(Request $request) - { - abort_if(!$request->user(), 403); - - $type = $request->input('timeline'); - if(is_array($type)) { - $type = $type[0]; - } - if(!$type || !in_array($type, ['home', 'notifications'])) { - return $this->json([]); - } - $pid = $request->user()->profile_id; - return $this->json(MarkerService::get($pid, $type)); - } - - /** - * POST /api/v1/markers - * - * - * @return array - */ - public function setMarkers(Request $request) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $home = $request->input('home[last_read_id]'); - $notifications = $request->input('notifications[last_read_id]'); - - if($home) { - return $this->json(MarkerService::set($pid, 'home', $home)); - } - - if($notifications) { - return $this->json(MarkerService::set($pid, 'notifications', $notifications)); - } - - return $this->json([]); - } - - /** - * GET /api/v1/followed_tags - * - * - * @return array - */ - public function getFollowedTags(Request $request) - { - abort_if(!$request->user(), 403); - - $account = AccountService::get($request->user()->profile_id); - - $this->validate($request, [ - 'cursor' => 'sometimes', - 'limit' => 'sometimes|integer|min:1|max:200' - ]); - $limit = $request->input('limit', 100); - - $res = HashtagFollow::whereProfileId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit)->withQueryString(); - - $pagination = false; - $prevPage = $res->nextPageUrl(); - $nextPage = $res->previousPageUrl(); - if($nextPage && $prevPage) { - $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; - } else if($nextPage && !$prevPage) { - $pagination = '<' . $nextPage . '>; rel="next"'; - } else if(!$nextPage && $prevPage) { - $pagination = '<' . $prevPage . '>; rel="prev"'; - } - - if($pagination) { - return response()->json(FollowedTagResource::collection($res)->collection) - ->header('Link', $pagination); - } - return response()->json(FollowedTagResource::collection($res)->collection); - } - - /** - * POST /api/v1/tags/:id/follow - * - * - * @return object - */ - public function followHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - abort_if(!$tag, 422, 'Unknown hashtag'); - - abort_if( - HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, - 422, - 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' - ); - - $follows = HashtagFollow::updateOrCreate( - [ - 'profile_id' => $account['id'], - 'hashtag_id' => $tag->id - ], - [ - 'user_id' => $request->user()->id - ] - ); - - HashtagService::follow($pid, $tag->id); - HashtagFollowService::add($tag->id, $pid); - - return response()->json(FollowedTagResource::make($follows)->toArray($request)); - } - - /** - * POST /api/v1/tags/:id/unfollow - * - * - * @return object - */ - public function unfollowHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - abort_if(!$tag, 422, 'Unknown hashtag'); - - $follows = HashtagFollow::whereProfileId($pid) - ->whereHashtagId($tag->id) - ->first(); - - if(!$follows) { - return [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => false - ]; - } - - if($follows) { - HashtagService::unfollow($pid, $tag->id); - HashtagFollowService::unfollow($tag->id, $pid); - HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); - $follows->delete(); - } - - $res = FollowedTagResource::make($follows)->toArray($request); - $res['following'] = false; - return response()->json($res); - } - - /** - * GET /api/v1/tags/:id - * - * - * @return object - */ - public function getHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - if(!$tag) { - return [ - 'name' => $id, - 'url' => config('app.url') . '/i/web/hashtag/' . $id, - 'history' => [], - 'following' => false - ]; - } - - $res = [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => HashtagService::isFollowing($pid, $tag->id) - ]; - - if($request->has(self::PF_API_ENTITY_KEY)) { - $res['count'] = HashtagService::count($tag->id); - } - - return $this->json($res); - } + return $this->json($bookmarks, 200, $headers); + } + + /** + * POST /api/v1/statuses/{id}/bookmark + * + * + * + * @return StatusTransformer + */ + public function bookmarkStatus(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $status = Status::findOrFail($id); + $pid = $request->user()->profile_id; + + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + + if($status->scope == 'private') { + abort_if( + $pid !== $status->profile_id && !FollowerService::follows($pid, $status->profile_id), + 404, + 'Error: You cannot bookmark private posts from accounts you do not follow.' + ); + } + + Bookmark::firstOrCreate([ + 'status_id' => $status->id, + 'profile_id' => $pid + ]); + + BookmarkService::add($pid, $status->id); + + $res = StatusService::getMastodon($status->id, false); + $res['bookmarked'] = true; + + return $this->json($res); + } + + /** + * POST /api/v1/statuses/{id}/unbookmark + * + * + * + * @return StatusTransformer + */ + public function unbookmarkStatus(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $status = Status::findOrFail($id); + $pid = $request->user()->profile_id; + + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + + $bookmark = Bookmark::whereStatusId($status->id) + ->whereProfileId($pid) + ->first(); + + if($bookmark) { + BookmarkService::del($pid, $status->id); + $bookmark->delete(); + } + $res = StatusService::getMastodon($status->id, false); + $res['bookmarked'] = false; + + return $this->json($res); + } + + /** + * GET /api/v1/discover/posts + * + * + * @return array + */ + public function discoverPosts(Request $request) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'integer|min:1|max:40' + ]); + + $limit = $request->input('limit', 40); + $pid = $request->user()->profile_id; + $filters = UserFilterService::filters($pid); + $forYou = DiscoverService::getForYou(); + $posts = $forYou->take(50)->map(function($post) { + return StatusService::getMastodon($post); + }) + ->filter(function($post) use($filters) { + return $post && + isset($post['account']) && + isset($post['account']['id']) && + !in_array($post['account']['id'], $filters); + }) + ->take(12) + ->values(); + return $this->json(compact('posts')); + } + + /** + * GET /api/v2/statuses/{id}/replies + * + * + * @return array + */ + public function statusReplies(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $this->validate($request, [ + 'limit' => 'int|min:1|max:10', + 'sort' => 'in:all,newest,popular' + ]); + + $limit = $request->input('limit', 3); + $pid = $request->user()->profile_id; + $status = StatusService::getMastodon($id, false); + + abort_if(!$status, 404); + + if($status['visibility'] == 'private') { + if($pid != $status['account']['id']) { + abort_unless(FollowerService::follows($pid, $status['account']['id']), 404); + } + } + + $sortBy = $request->input('sort', 'all'); + + if($sortBy == 'all' && isset($status['replies_count']) && $status['replies_count'] && $request->has('refresh_cache')) { + if(!Cache::has('status:replies:all-rc:' . $id)) { + Cache::forget('status:replies:all:' . $id); + Cache::put('status:replies:all-rc:' . $id, true, 300); + } + } + + if($sortBy == 'all' && !$request->has('cursor')) { + $ids = Cache::remember('status:replies:all:' . $id, 3600, function() use($id) { + return DB::table('statuses') + ->where('in_reply_to_id', $id) + ->orderBy('id') + ->cursorPaginate(3); + }); + } else { + $ids = DB::table('statuses') + ->where('in_reply_to_id', $id) + ->when($sortBy, function($q, $sortBy) { + if($sortBy === 'all') { + return $q->orderBy('id'); + } + + if($sortBy === 'newest') { + return $q->orderByDesc('created_at'); + } + + if($sortBy === 'popular') { + return $q->orderByDesc('likes_count'); + } + }) + ->cursorPaginate($limit); + } + + $filters = UserFilterService::filters($pid); + $data = $ids->filter(function($post) use($filters) { + return !in_array($post->profile_id, $filters); + }) + ->map(function($post) use($pid) { + $status = StatusService::get($post->id, false); + + if(!$status || !isset($status['id'])) { + return false; + } + + $status['favourited'] = LikeService::liked($pid, $post->id); + return $status; + }) + ->map(function($post) { + if(isset($post['account']) && isset($post['account']['id'])) { + $account = AccountService::get($post['account']['id'], true); + $post['account'] = $account; + } + return $post; + }) + ->filter(function($post) { + return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']); + }) + ->values(); + + $res = [ + 'data' => $data, + 'next' => $ids->nextPageUrl() + ]; + + return $this->json($res); + } + + /** + * GET /api/v2/statuses/{id}/state + * + * + * @return array + */ + public function statusState(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $status = Status::findOrFail($id); + $pid = $request->user()->profile_id; + abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); + + return $this->json(StatusService::getState($status->id, $pid)); + } + + /** + * GET /api/v1.1/discover/accounts/popular + * + * + * @return array + */ + public function discoverAccountsPopular(Request $request) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() { + return DB::table('profiles') + ->where('is_private', false) + ->whereNull('status') + ->orderByDesc('profiles.followers_count') + ->limit(20) + ->get(); + }); + + $ids = $ids->map(function($profile) { + return AccountService::get($profile->id, true); + }) + ->filter(function($profile) use($pid) { + return $profile && isset($profile['id']); + }) + ->filter(function($profile) use($pid) { + return $profile['id'] != $pid; + }) + ->take(6) + ->values(); + + return $this->json($ids); + } + + /** + * GET /api/v1/preferences + * + * + * @return array + */ + public function getPreferences(Request $request) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + return $this->json([ + 'posting:default:visibility' => $account['locked'] ? 'private' : 'public', + 'posting:default:sensitive' => false, + 'posting:default:language' => null, + 'reading:expand:media' => 'default', + 'reading:expand:spoilers' => false + ]); + } + + /** + * GET /api/v1/trends + * + * + * @return array + */ + public function getTrends(Request $request) + { + abort_if(!$request->user(), 403); + + return $this->json([]); + } + + /** + * GET /api/v1/announcements + * + * + * @return array + */ + public function getAnnouncements(Request $request) + { + abort_if(!$request->user(), 403); + + return $this->json([]); + } + + /** + * GET /api/v1/markers + * + * + * @return array + */ + public function getMarkers(Request $request) + { + abort_if(!$request->user(), 403); + + $type = $request->input('timeline'); + if(is_array($type)) { + $type = $type[0]; + } + if(!$type || !in_array($type, ['home', 'notifications'])) { + return $this->json([]); + } + $pid = $request->user()->profile_id; + return $this->json(MarkerService::get($pid, $type)); + } + + /** + * POST /api/v1/markers + * + * + * @return array + */ + public function setMarkers(Request $request) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $home = $request->input('home[last_read_id]'); + $notifications = $request->input('notifications[last_read_id]'); + + if($home) { + return $this->json(MarkerService::set($pid, 'home', $home)); + } + + if($notifications) { + return $this->json(MarkerService::set($pid, 'notifications', $notifications)); + } + + return $this->json([]); + } + + /** + * GET /api/v1/followed_tags + * + * + * @return array + */ + public function getFollowedTags(Request $request) + { + abort_if(!$request->user(), 403); + + $account = AccountService::get($request->user()->profile_id); + + $this->validate($request, [ + 'cursor' => 'sometimes', + 'limit' => 'sometimes|integer|min:1|max:200' + ]); + $limit = $request->input('limit', 100); + + $res = HashtagFollow::whereProfileId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit)->withQueryString(); + + $pagination = false; + $prevPage = $res->nextPageUrl(); + $nextPage = $res->previousPageUrl(); + if($nextPage && $prevPage) { + $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; + } else if($nextPage && !$prevPage) { + $pagination = '<' . $nextPage . '>; rel="next"'; + } else if(!$nextPage && $prevPage) { + $pagination = '<' . $prevPage . '>; rel="prev"'; + } + + if($pagination) { + return response()->json(FollowedTagResource::collection($res)->collection) + ->header('Link', $pagination); + } + return response()->json(FollowedTagResource::collection($res)->collection); + } + + /** + * POST /api/v1/tags/:id/follow + * + * + * @return object + */ + public function followHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + abort_if( + HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, + 422, + 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' + ); + + $follows = HashtagFollow::updateOrCreate( + [ + 'profile_id' => $account['id'], + 'hashtag_id' => $tag->id + ], + [ + 'user_id' => $request->user()->id + ] + ); + + HashtagService::follow($pid, $tag->id); + HashtagFollowService::add($tag->id, $pid); + + return response()->json(FollowedTagResource::make($follows)->toArray($request)); + } + + /** + * POST /api/v1/tags/:id/unfollow + * + * + * @return object + */ + public function unfollowHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + $follows = HashtagFollow::whereProfileId($pid) + ->whereHashtagId($tag->id) + ->first(); + + if(!$follows) { + return [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => false + ]; + } + + if($follows) { + HashtagService::unfollow($pid, $tag->id); + HashtagFollowService::unfollow($tag->id, $pid); + HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); + $follows->delete(); + } + + $res = FollowedTagResource::make($follows)->toArray($request); + $res['following'] = false; + return response()->json($res); + } + + /** + * GET /api/v1/tags/:id + * + * + * @return object + */ + public function getHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + if(!$tag) { + return [ + 'name' => $id, + 'url' => config('app.url') . '/i/web/hashtag/' . $id, + 'history' => [], + 'following' => false + ]; + } + + $res = [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => HashtagService::isFollowing($pid, $tag->id) + ]; + + if($request->has(self::PF_API_ENTITY_KEY)) { + $res['count'] = HashtagService::count($tag->id); + } + + return $this->json($res); + } } From aa166ab11a0e1dafcfdcb44f6a4d8dbf0996bfaf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 17 Nov 2023 22:10:03 -0700 Subject: [PATCH 072/977] Update ApiV1Controller, move tags endpoints to TagsController --- app/Http/Controllers/Api/ApiV1Controller.php | 167 -------------- .../Controllers/Api/V1/TagsController.php | 207 ++++++++++++++++++ routes/api.php | 10 +- 3 files changed, 213 insertions(+), 171 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/TagsController.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index d43a867bd..84569fa5b 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3713,171 +3713,4 @@ class ApiV1Controller extends Controller return $this->json([]); } - - /** - * GET /api/v1/followed_tags - * - * - * @return array - */ - public function getFollowedTags(Request $request) - { - abort_if(!$request->user(), 403); - - $account = AccountService::get($request->user()->profile_id); - - $this->validate($request, [ - 'cursor' => 'sometimes', - 'limit' => 'sometimes|integer|min:1|max:200' - ]); - $limit = $request->input('limit', 100); - - $res = HashtagFollow::whereProfileId($account['id']) - ->orderByDesc('id') - ->cursorPaginate($limit)->withQueryString(); - - $pagination = false; - $prevPage = $res->nextPageUrl(); - $nextPage = $res->previousPageUrl(); - if($nextPage && $prevPage) { - $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; - } else if($nextPage && !$prevPage) { - $pagination = '<' . $nextPage . '>; rel="next"'; - } else if(!$nextPage && $prevPage) { - $pagination = '<' . $prevPage . '>; rel="prev"'; - } - - if($pagination) { - return response()->json(FollowedTagResource::collection($res)->collection) - ->header('Link', $pagination); - } - return response()->json(FollowedTagResource::collection($res)->collection); - } - - /** - * POST /api/v1/tags/:id/follow - * - * - * @return object - */ - public function followHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - abort_if(!$tag, 422, 'Unknown hashtag'); - - abort_if( - HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, - 422, - 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' - ); - - $follows = HashtagFollow::updateOrCreate( - [ - 'profile_id' => $account['id'], - 'hashtag_id' => $tag->id - ], - [ - 'user_id' => $request->user()->id - ] - ); - - HashtagService::follow($pid, $tag->id); - HashtagFollowService::add($tag->id, $pid); - - return response()->json(FollowedTagResource::make($follows)->toArray($request)); - } - - /** - * POST /api/v1/tags/:id/unfollow - * - * - * @return object - */ - public function unfollowHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - abort_if(!$tag, 422, 'Unknown hashtag'); - - $follows = HashtagFollow::whereProfileId($pid) - ->whereHashtagId($tag->id) - ->first(); - - if(!$follows) { - return [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => false - ]; - } - - if($follows) { - HashtagService::unfollow($pid, $tag->id); - HashtagFollowService::unfollow($tag->id, $pid); - HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); - $follows->delete(); - } - - $res = FollowedTagResource::make($follows)->toArray($request); - $res['following'] = false; - return response()->json($res); - } - - /** - * GET /api/v1/tags/:id - * - * - * @return object - */ - public function getHashtag(Request $request, $id) - { - abort_if(!$request->user(), 403); - - $pid = $request->user()->profile_id; - $account = AccountService::get($pid); - $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; - $tag = Hashtag::where('name', $operator, $id) - ->orWhere('slug', $operator, $id) - ->first(); - - if(!$tag) { - return [ - 'name' => $id, - 'url' => config('app.url') . '/i/web/hashtag/' . $id, - 'history' => [], - 'following' => false - ]; - } - - $res = [ - 'name' => $tag->name, - 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, - 'history' => [], - 'following' => HashtagService::isFollowing($pid, $tag->id) - ]; - - if($request->has(self::PF_API_ENTITY_KEY)) { - $res['count'] = HashtagService::count($tag->id); - } - - return $this->json($res); - } } diff --git a/app/Http/Controllers/Api/V1/TagsController.php b/app/Http/Controllers/Api/V1/TagsController.php new file mode 100644 index 000000000..7314ce09d --- /dev/null +++ b/app/Http/Controllers/Api/V1/TagsController.php @@ -0,0 +1,207 @@ +json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + /** + * GET /api/v1/tags/:id/related + * + * + * @return array + */ + public function relatedTags(Request $request, $tag) + { + abort_unless($request->user(), 403); + $tag = Hashtag::whereSlug($tag)->firstOrFail(); + return HashtagRelatedService::get($tag->id); + } + + /** + * POST /api/v1/tags/:id/follow + * + * + * @return object + */ + public function followHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + abort_if( + HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT, + 422, + 'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.' + ); + + $follows = HashtagFollow::updateOrCreate( + [ + 'profile_id' => $account['id'], + 'hashtag_id' => $tag->id + ], + [ + 'user_id' => $request->user()->id + ] + ); + + HashtagService::follow($pid, $tag->id); + HashtagFollowService::add($tag->id, $pid); + + return response()->json(FollowedTagResource::make($follows)->toArray($request)); + } + + /** + * POST /api/v1/tags/:id/unfollow + * + * + * @return object + */ + public function unfollowHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + abort_if(!$tag, 422, 'Unknown hashtag'); + + $follows = HashtagFollow::whereProfileId($pid) + ->whereHashtagId($tag->id) + ->first(); + + if(!$follows) { + return [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => false + ]; + } + + if($follows) { + HashtagService::unfollow($pid, $tag->id); + HashtagFollowService::unfollow($tag->id, $pid); + HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed'); + $follows->delete(); + } + + $res = FollowedTagResource::make($follows)->toArray($request); + $res['following'] = false; + return response()->json($res); + } + + /** + * GET /api/v1/tags/:id + * + * + * @return object + */ + public function getHashtag(Request $request, $id) + { + abort_if(!$request->user(), 403); + + $pid = $request->user()->profile_id; + $account = AccountService::get($pid); + $operator = config('database.default') == 'pgsql' ? 'ilike' : 'like'; + $tag = Hashtag::where('name', $operator, $id) + ->orWhere('slug', $operator, $id) + ->first(); + + if(!$tag) { + return [ + 'name' => $id, + 'url' => config('app.url') . '/i/web/hashtag/' . $id, + 'history' => [], + 'following' => false + ]; + } + + $res = [ + 'name' => $tag->name, + 'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug, + 'history' => [], + 'following' => HashtagService::isFollowing($pid, $tag->id) + ]; + + if($request->has(self::PF_API_ENTITY_KEY)) { + $res['count'] = HashtagService::count($tag->id); + } + + return $this->json($res); + } + + /** + * GET /api/v1/followed_tags + * + * + * @return array + */ + public function getFollowedTags(Request $request) + { + abort_if(!$request->user(), 403); + + $account = AccountService::get($request->user()->profile_id); + + $this->validate($request, [ + 'cursor' => 'sometimes', + 'limit' => 'sometimes|integer|min:1|max:200' + ]); + $limit = $request->input('limit', 100); + + $res = HashtagFollow::whereProfileId($account['id']) + ->orderByDesc('id') + ->cursorPaginate($limit) + ->withQueryString(); + + $pagination = false; + $prevPage = $res->nextPageUrl(); + $nextPage = $res->previousPageUrl(); + if($nextPage && $prevPage) { + $pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"'; + } else if($nextPage && !$prevPage) { + $pagination = '<' . $nextPage . '>; rel="next"'; + } else if(!$nextPage && $prevPage) { + $pagination = '<' . $prevPage . '>; rel="prev"'; + } + + if($pagination) { + return response()->json(FollowedTagResource::collection($res)->collection) + ->header('Link', $pagination); + } + return response()->json(FollowedTagResource::collection($res)->collection); + } +} diff --git a/routes/api.php b/routes/api.php index 23abfc323..27e566ab3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use Illuminate\Http\Request; use App\Http\Middleware\DeprecatedEndpoint; +use App\Http\Controllers\Api\V1\TagsController; $middleware = ['auth:api','validemail']; @@ -92,10 +93,11 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('markers', 'Api\ApiV1Controller@getMarkers')->middleware($middleware); Route::post('markers', 'Api\ApiV1Controller@setMarkers')->middleware($middleware); - Route::get('followed_tags', 'Api\ApiV1Controller@getFollowedTags')->middleware($middleware); - Route::post('tags/{id}/follow', 'Api\ApiV1Controller@followHashtag')->middleware($middleware); - Route::post('tags/{id}/unfollow', 'Api\ApiV1Controller@unfollowHashtag')->middleware($middleware); - Route::get('tags/{id}', 'Api\ApiV1Controller@getHashtag')->middleware($middleware); + Route::get('followed_tags', [TagsController::class, 'getFollowedTags'])->middleware($middleware); + Route::post('tags/{id}/follow', [TagsController::class, 'followHashtag'])->middleware($middleware); + Route::post('tags/{id}/unfollow', [TagsController::class, 'unfollowHashtag'])->middleware($middleware); + Route::get('tags/{id}/related', [TagsController::class, 'relatedTags'])->middleware($middleware); + Route::get('tags/{id}', [TagsController::class, 'getHashtag'])->middleware($middleware); Route::get('statuses/{id}/history', 'StatusEditController@history')->middleware($middleware); Route::put('statuses/{id}', 'StatusEditController@store')->middleware($middleware); From 176b4ed793b0690c05fa0103a77c0089c55bb594 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 17 Nov 2023 22:21:55 -0700 Subject: [PATCH 073/977] Add app:hashtag-related-generate command --- .../Commands/HashtagRelatedGenerate.php | 83 +++++++++++++++++++ app/Models/HashtagRelated.php | 2 + app/Services/HashtagRelatedService.php | 12 +-- 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 app/Console/Commands/HashtagRelatedGenerate.php diff --git a/app/Console/Commands/HashtagRelatedGenerate.php b/app/Console/Commands/HashtagRelatedGenerate.php new file mode 100644 index 000000000..0613495c9 --- /dev/null +++ b/app/Console/Commands/HashtagRelatedGenerate.php @@ -0,0 +1,83 @@ + 'Which hashtag should we generate related tags for?', + ]; + } + + /** + * Execute the console command. + */ + public function handle() + { + $tag = $this->argument('tag'); + $hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first(); + if(!$hashtag) { + $this->error('Hashtag not found, aborting...'); + exit; + } + + $this->info('Looking up #' . $tag . '...'); + + $tags = StatusHashtag::whereHashtagId($hashtag->id)->count(); + if(!$tags || $tags < 100) { + $this->error('Not enough posts found to generate related hashtags!'); + exit; + } + + $this->info('Found ' . $tags . ' posts that use that hashtag'); + $related = collect(HashtagRelatedService::fetchRelatedTags($tag)); + + $selected = multiselect( + label: 'Which tags do you want to generate?', + options: $related->pluck('name'), + required: true, + ); + + $filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all(); + $agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count'); + + HashtagRelated::updateOrCreate([ + 'hashtag_id' => $hashtag->id, + ], [ + 'related_tags' => array_values($filtered), + 'agg_score' => $agg_score, + 'last_calculated_at' => now() + ]); + + $this->info('Finished!'); + } +} diff --git a/app/Models/HashtagRelated.php b/app/Models/HashtagRelated.php index 42722a205..bbaa1c06c 100644 --- a/app/Models/HashtagRelated.php +++ b/app/Models/HashtagRelated.php @@ -9,6 +9,8 @@ class HashtagRelated extends Model { use HasFactory; + protected $guarded = []; + /** * The attributes that should be mutated to dates and other custom formats. * diff --git a/app/Services/HashtagRelatedService.php b/app/Services/HashtagRelatedService.php index 53a387b45..b96483987 100644 --- a/app/Services/HashtagRelatedService.php +++ b/app/Services/HashtagRelatedService.php @@ -8,10 +8,13 @@ use App\Models\HashtagRelated; class HashtagRelatedService { - public static function get($id) { - return HashtagRelated::whereHashtagId($id)->first(); + $tag = HashtagRelated::whereHashtagId($id)->first(); + if(!$tag) { + return []; + } + return $tag->related_tags; } public static function fetchRelatedTags($tag) @@ -20,15 +23,14 @@ class HashtagRelatedService ->select('h2.name', DB::raw('COUNT(*) as related_count')) ->join('status_hashtags as hs2', function ($join) { $join->on('status_hashtags.status_id', '=', 'hs2.status_id') - ->whereRaw('status_hashtags.hashtag_id != hs2.hashtag_id') - ->where('status_hashtags.created_at', '>', now()->subMonths(3)); + ->whereRaw('status_hashtags.hashtag_id != hs2.hashtag_id'); }) ->join('hashtags as h1', 'status_hashtags.hashtag_id', '=', 'h1.id') ->join('hashtags as h2', 'hs2.hashtag_id', '=', 'h2.id') ->where('h1.name', '=', $tag) ->groupBy('h2.name') ->orderBy('related_count', 'desc') - ->limit(10) + ->limit(30) ->get(); return $res; From d62a60a4eef2e217de508d3598e3a5e1107a7cb2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 17 Nov 2023 22:22:49 -0700 Subject: [PATCH 074/977] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77555a5bb..ff8f50772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610)) - Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb)) - Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6)) +- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) @@ -58,6 +59,8 @@ - Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85)) - Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8)) - Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d)) +- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b)) +- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From e5e3be0598f5ce2a8791f0c4a5c9c7304aeffced Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 17 Nov 2023 22:45:04 -0700 Subject: [PATCH 075/977] Update app:hashtag-related-generate command, add existing confirmation --- app/Console/Commands/HashtagRelatedGenerate.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/Console/Commands/HashtagRelatedGenerate.php b/app/Console/Commands/HashtagRelatedGenerate.php index 0613495c9..26fdb8b52 100644 --- a/app/Console/Commands/HashtagRelatedGenerate.php +++ b/app/Console/Commands/HashtagRelatedGenerate.php @@ -9,6 +9,7 @@ use App\Models\HashtagRelated; use App\Services\HashtagRelatedService; use Illuminate\Contracts\Console\PromptsForMissingInput; use function Laravel\Prompts\multiselect; +use function Laravel\Prompts\confirm; class HashtagRelatedGenerate extends Command implements PromptsForMissingInput { @@ -50,6 +51,16 @@ class HashtagRelatedGenerate extends Command implements PromptsForMissingInput exit; } + $exists = HashtagRelated::whereHashtagId($hashtag->id)->exists(); + + if($exists) { + $confirmed = confirm('Found existing related tags, do you want to regenerate them?'); + if(!$confirmed) { + $this->error('Aborting...'); + exit; + } + } + $this->info('Looking up #' . $tag . '...'); $tags = StatusHashtag::whereHashtagId($hashtag->id)->count(); From bcb88d5b0a8bea7a367ac4bd67f4038b1de1b074 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Nov 2023 01:11:12 -0700 Subject: [PATCH 076/977] Update StoryApiV1Controller, add self-carousel endpoint. Fixes #4352 --- .../Stories/StoryApiV1Controller.php | 722 ++++++++++-------- routes/api.php | 1 + 2 files changed, 424 insertions(+), 299 deletions(-) diff --git a/app/Http/Controllers/Stories/StoryApiV1Controller.php b/app/Http/Controllers/Stories/StoryApiV1Controller.php index db2b1f533..ca6a24791 100644 --- a/app/Http/Controllers/Stories/StoryApiV1Controller.php +++ b/app/Http/Controllers/Stories/StoryApiV1Controller.php @@ -24,358 +24,482 @@ use App\Http\Resources\StoryView as StoryViewResource; class StoryApiV1Controller extends Controller { - public function carousel(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $pid = $request->user()->profile_id; + const RECENT_KEY = 'pf:stories:recent-by-id:'; + const RECENT_TTL = 300; - if(config('database.default') == 'pgsql') { - $s = Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->get(); - } else { - $s = Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->orderBy('id') - ->get(); - } + public function carousel(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $pid = $request->user()->profile_id; - $nodes = $s->map(function($s) use($pid) { - $profile = AccountService::get($s->profile_id, true); - if(!$profile || !isset($profile['id'])) { - return false; - } + if(config('database.default') == 'pgsql') { + $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->map(function($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }) + ->unique('profile_id'); + }); + } else { + $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->orderBy('id') + ->get(); + }); + } - return [ - 'id' => (string) $s->id, - 'pid' => (string) $s->profile_id, - 'type' => $s->type, - 'src' => url(Storage::url($s->path)), - 'duration' => $s->duration ?? 3, - 'seen' => StoryService::hasSeen($pid, $s->id), - 'created_at' => $s->created_at->format('c') - ]; - }) - ->filter() - ->groupBy('pid') - ->map(function($item) use($pid) { - $profile = AccountService::get($item[0]['pid'], true); - $url = $profile['local'] ? url("/stories/{$profile['username']}") : - url("/i/rs/{$profile['id']}"); - return [ - 'id' => 'pfs:' . $profile['id'], - 'user' => [ - 'id' => (string) $profile['id'], - 'username' => $profile['username'], - 'username_acct' => $profile['acct'], - 'avatar' => $profile['avatar'], - 'local' => $profile['local'], - 'is_author' => $profile['id'] == $pid - ], - 'nodes' => $item, - 'url' => $url, - 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), - ]; - }) - ->sortBy('seen') - ->values(); + $nodes = $s->map(function($s) use($pid) { + $profile = AccountService::get($s->profile_id, true); + if(!$profile || !isset($profile['id'])) { + return false; + } - $res = [ - 'self' => [], - 'nodes' => $nodes, - ]; + return [ + 'id' => (string) $s->id, + 'pid' => (string) $s->profile_id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration ?? 3, + 'seen' => StoryService::hasSeen($pid, $s->id), + 'created_at' => $s->created_at->format('c') + ]; + }) + ->filter() + ->groupBy('pid') + ->map(function($item) use($pid) { + $profile = AccountService::get($item[0]['pid'], true); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); + return [ + 'id' => 'pfs:' . $profile['id'], + 'user' => [ + 'id' => (string) $profile['id'], + 'username' => $profile['username'], + 'username_acct' => $profile['acct'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'is_author' => $profile['id'] == $pid + ], + 'nodes' => $item, + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), + ]; + }) + ->sortBy('seen') + ->values(); - if(Story::whereProfileId($pid)->whereActive(true)->exists()) { - $selfStories = Story::whereProfileId($pid) - ->whereActive(true) - ->get() - ->map(function($s) use($pid) { - return [ - 'id' => (string) $s->id, - 'type' => $s->type, - 'src' => url(Storage::url($s->path)), - 'duration' => $s->duration, - 'seen' => true, - 'created_at' => $s->created_at->format('c') - ]; - }) - ->sortBy('id') - ->values(); - $selfProfile = AccountService::get($pid, true); - $res['self'] = [ - 'user' => [ - 'id' => (string) $selfProfile['id'], - 'username' => $selfProfile['acct'], - 'avatar' => $selfProfile['avatar'], - 'local' => $selfProfile['local'], - 'is_author' => true - ], + $res = [ + 'self' => [], + 'nodes' => $nodes, + ]; - 'nodes' => $selfStories, - ]; - } - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + if(Story::whereProfileId($pid)->whereActive(true)->exists()) { + $selfStories = Story::whereProfileId($pid) + ->whereActive(true) + ->get() + ->map(function($s) use($pid) { + return [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration, + 'seen' => true, + 'created_at' => $s->created_at->format('c') + ]; + }) + ->sortBy('id') + ->values(); + $selfProfile = AccountService::get($pid, true); + $res['self'] = [ + 'user' => [ + 'id' => (string) $selfProfile['id'], + 'username' => $selfProfile['acct'], + 'avatar' => $selfProfile['avatar'], + 'local' => $selfProfile['local'], + 'is_author' => true + ], - public function add(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + 'nodes' => $selfStories, + ]; + } + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - 'duration' => 'sometimes|integer|min:0|max:30' - ]); + public function selfCarousel(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $pid = $request->user()->profile_id; - $user = $request->user(); + if(config('database.default') == 'pgsql') { + $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->map(function($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }) + ->unique('profile_id'); + }); + } else { + $s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->orderBy('id') + ->get(); + }); + } - $count = Story::whereProfileId($user->profile_id) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + $nodes = $s->map(function($s) use($pid) { + $profile = AccountService::get($s->profile_id, true); + if(!$profile || !isset($profile['id'])) { + return false; + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + return [ + 'id' => (string) $s->id, + 'pid' => (string) $s->profile_id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration ?? 3, + 'seen' => StoryService::hasSeen($pid, $s->id), + 'created_at' => $s->created_at->format('c') + ]; + }) + ->filter() + ->groupBy('pid') + ->map(function($item) use($pid) { + $profile = AccountService::get($item[0]['pid'], true); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); + return [ + 'id' => 'pfs:' . $profile['id'], + 'user' => [ + 'id' => (string) $profile['id'], + 'username' => $profile['username'], + 'username_acct' => $profile['acct'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'is_author' => $profile['id'] == $pid + ], + 'nodes' => $item, + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), + ]; + }) + ->sortBy('seen') + ->values(); - $photo = $request->file('file'); - $path = $this->storeMedia($photo, $user); + $selfProfile = AccountService::get($pid, true); + $res = [ + 'self' => [ + 'user' => [ + 'id' => (string) $selfProfile['id'], + 'username' => $selfProfile['acct'], + 'avatar' => $selfProfile['avatar'], + 'local' => $selfProfile['local'], + 'is_author' => true + ], - $story = new Story(); - $story->duration = $request->input('duration', 3); - $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; - $story->mime = $photo->getMimeType(); - $story->path = $path; - $story->local = true; - $story->size = $photo->getSize(); - $story->bearcap_token = str_random(64); - $story->expires_at = now()->addMinutes(1440); - $story->save(); + 'nodes' => [], + ], + 'nodes' => $nodes, + ]; - $url = $story->path; + if(Story::whereProfileId($pid)->whereActive(true)->exists()) { + $selfStories = Story::whereProfileId($pid) + ->whereActive(true) + ->get() + ->map(function($s) use($pid) { + return [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'src' => url(Storage::url($s->path)), + 'duration' => $s->duration, + 'seen' => true, + 'created_at' => $s->created_at->format('c') + ]; + }) + ->sortBy('id') + ->values(); + $res['self']['nodes'] = $selfStories; + } + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - $res = [ - 'code' => 200, - 'msg' => 'Successfully added', - 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type - ]; + public function add(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - return $res; - } + $this->validate($request, [ + 'file' => function() { + return [ + 'required', + 'mimetypes:image/jpeg,image/png,video/mp4', + 'max:' . config_cache('pixelfed.max_photo_size'), + ]; + }, + 'duration' => 'sometimes|integer|min:0|max:30' + ]); - public function publish(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); - $this->validate($request, [ - 'media_id' => 'required', - 'duration' => 'required|integer|min:0|max:30', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $count = Story::whereProfileId($user->profile_id) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - $id = $request->input('media_id'); - $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); + if($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - $story->active = true; - $story->duration = $request->input('duration', 10); - $story->can_reply = $request->input('can_reply'); - $story->can_react = $request->input('can_react'); - $story->save(); + $photo = $request->file('file'); + $path = $this->storeMedia($photo, $user); - StoryService::delLatest($story->profile_id); - StoryFanout::dispatch($story)->onQueue('story'); - StoryService::addRotateQueue($story->id); + $story = new Story(); + $story->duration = $request->input('duration', 3); + $story->profile_id = $user->profile_id; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; + $story->mime = $photo->getMimeType(); + $story->path = $path; + $story->local = true; + $story->size = $photo->getSize(); + $story->bearcap_token = str_random(64); + $story->expires_at = now()->addMinutes(1440); + $story->save(); - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + $url = $story->path; - public function delete(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $res = [ + 'code' => 200, + 'msg' => 'Successfully added', + 'media_id' => (string) $story->id, + 'media_url' => url(Storage::url($url)) . '?v=' . time(), + 'media_type' => $story->type + ]; - $user = $request->user(); + return $res; + } - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - $story->active = false; - $story->save(); + public function publish(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - StoryDelete::dispatch($story)->onQueue('story'); + $this->validate($request, [ + 'media_id' => 'required', + 'duration' => 'required|integer|min:0|max:30', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean' + ]); - return [ - 'code' => 200, - 'msg' => 'Successfully deleted' - ]; - } + $id = $request->input('media_id'); + $user = $request->user(); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); - public function viewed(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $story->active = true; + $story->duration = $request->input('duration', 10); + $story->can_reply = $request->input('can_reply'); + $story->can_react = $request->input('can_react'); + $story->save(); - $this->validate($request, [ - 'id' => 'required|min:1', - ]); - $id = $request->input('id'); + StoryService::delLatest($story->profile_id); + StoryFanout::dispatch($story)->onQueue('story'); + StoryService::addRotateQueue($story->id); - $authed = $request->user()->profile; + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - $story = Story::with('profile') - ->findOrFail($id); - $exp = $story->expires_at; + public function delete(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $profile = $story->profile; + $user = $request->user(); - if($story->profile_id == $authed->id) { - return []; - } + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + $story->active = false; + $story->save(); - $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + StoryDelete::dispatch($story)->onQueue('story'); - $v = StoryView::firstOrCreate([ - 'story_id' => $id, - 'profile_id' => $authed->id - ]); + return [ + 'code' => 200, + 'msg' => 'Successfully deleted' + ]; + } - if($v->wasRecentlyCreated) { - Story::findOrFail($story->id)->increment('view_count'); + public function viewed(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - if($story->local == false) { - StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); - } - } + $this->validate($request, [ + 'id' => 'required|min:1', + ]); + $id = $request->input('id'); - Cache::forget('stories:recent:by_id:' . $authed->id); - StoryService::addSeen($authed->id, $story->id); - return ['code' => 200]; - } + $authed = $request->user()->profile; - public function comment(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'caption' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('caption'); + $story = Story::with('profile') + ->findOrFail($id); + $exp = $story->expires_at; - $story = Story::findOrFail($request->input('sid')); + $profile = $story->profile; - abort_if(!$story->can_reply, 422); + if($story->profile_id == $authed->id) { + return []; + } - $status = new Status; - $status->type = 'story:reply'; - $status->profile_id = $pid; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id - ]); - $status->save(); + $publicOnly = (bool) $profile->followedBy($authed); + abort_if(!$publicOnly, 403); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); + $v = StoryView::firstOrCreate([ + 'story_id' => $id, + 'profile_id' => $authed->id + ]); - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); + if($v->wasRecentlyCreated) { + Story::findOrFail($story->id)->increment('view_count'); - if($story->local) { - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - } else { - StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); - } + if($story->local == false) { + StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); + } + } - return [ - 'code' => 200, - 'msg' => 'Sent!' - ]; - } + Cache::forget('stories:recent:by_id:' . $authed->id); + StoryService::addSeen($authed->id, $story->id); + return ['code' => 200]; + } - protected function storeMedia($photo, $user) - { - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]) == false) { - abort(400, 'Invalid media type'); - return; - } + public function comment(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'caption' => 'required|string' + ]); + $pid = $request->user()->profile_id; + $text = $request->input('caption'); - $storagePath = MediaPathService::story($user->profile); - $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); - return $path; - } + $story = Story::findOrFail($request->input('sid')); - public function viewers(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(!$story->can_reply, 422); - $this->validate($request, [ - 'sid' => 'required|string|min:1|max:50' - ]); + $status = new Status; + $status->type = 'story:reply'; + $status->profile_id = $pid; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id + ]); + $status->save(); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text + ]); + $dm->save(); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); - $viewers = StoryView::whereStoryId($story->id) + if($story->local) { + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + } else { + StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); + } + + return [ + 'code' => 200, + 'msg' => 'Sent!' + ]; + } + + protected function storeMedia($photo, $user) + { + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), [ + 'image/jpeg', + 'image/png', + 'video/mp4' + ]) == false) { + abort(400, 'Invalid media type'); + return; + } + + $storagePath = MediaPathService::story($user->profile); + $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); + return $path; + } + + public function viewers(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + + $this->validate($request, [ + 'sid' => 'required|string|min:1|max:50' + ]); + + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); + + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); + + $viewers = StoryView::whereStoryId($story->id) ->orderByDesc('id') - ->cursorPaginate(10); + ->cursorPaginate(10); - return StoryViewResource::collection($viewers); - } + return StoryViewResource::collection($viewers); + } } diff --git a/routes/api.php b/routes/api.php index 27e566ab3..f1e5e7bd1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -313,6 +313,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::group(['prefix' => 'stories'], function () use($middleware) { Route::get('carousel', 'Stories\StoryApiV1Controller@carousel')->middleware($middleware); + Route::get('self-carousel', 'Stories\StoryApiV1Controller@selfCarousel')->middleware($middleware); Route::post('add', 'Stories\StoryApiV1Controller@add')->middleware($middleware); Route::post('publish', 'Stories\StoryApiV1Controller@publish')->middleware($middleware); Route::post('seen', 'Stories\StoryApiV1Controller@viewed')->middleware($middleware); From b0e8810a919988162552fbe19f0d8ff8036ba043 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 18 Nov 2023 01:13:55 -0700 Subject: [PATCH 077/977] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff8f50772..9d2e02309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ - Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8)) - Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d)) - Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) +- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 1ef885c1a14415bc4d82afd34555d3e26c63fa97 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 26 Nov 2023 04:30:48 -0700 Subject: [PATCH 078/977] Add migration to add state and other fields to places table --- ...39_add_state_and_score_to_places_table.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php diff --git a/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php b/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php new file mode 100644 index 000000000..d7f95aa3f --- /dev/null +++ b/database/migrations/2023_11_26_082439_add_state_and_score_to_places_table.php @@ -0,0 +1,34 @@ +string('state')->nullable()->index()->after('name'); + $table->tinyInteger('score')->default(0)->index()->after('long'); + $table->unsignedBigInteger('cached_post_count')->nullable(); + $table->timestamp('last_checked_at')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('places', function (Blueprint $table) { + $table->dropColumn('state'); + $table->dropColumn('score'); + $table->dropColumn('cached_post_count'); + $table->dropColumn('last_checked_at'); + }); + } +}; From a7320535e97173ee30f594a4950fe7cda80c8e8e Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Thu, 30 Nov 2023 00:19:04 +0000 Subject: [PATCH 079/977] #4791 Invalidate cache after adding a collection item for data consistency --- app/Http/Controllers/CollectionController.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index 6cd4bda57..15e4737e0 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -166,11 +166,7 @@ class CollectionController extends Controller 'order' => $count, ]); - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + CollectionService::deleteCollection($collection->id); $collection->updated_at = now(); $collection->save(); From 7cb075dbf9fcbab77e021782cf01d8415f8084dc Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Thu, 30 Nov 2023 00:20:08 +0000 Subject: [PATCH 080/977] #4790 User experience: add a post to a collection just right after deleting it from there --- resources/assets/js/components/CollectionComponent.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/assets/js/components/CollectionComponent.vue b/resources/assets/js/components/CollectionComponent.vue index 3f77cfc13..36e0050da 100644 --- a/resources/assets/js/components/CollectionComponent.vue +++ b/resources/assets/js/components/CollectionComponent.vue @@ -619,6 +619,9 @@ export default { this.posts = this.posts.filter(post => { return post.id != id; }); + this.ids = this.ids.filter(post_id => { + return post_id != id; + }); }, addRecentId(post) { @@ -630,6 +633,7 @@ export default { // window.location.reload(); this.closeModals(); this.posts.push(res.data); + this.ids.push(post.id); this.collection.post_count++; }).catch(err => { swal('Oops!', 'An error occured, please try selecting another post.', 'error'); From fe9b4c5a373202e870a72527f8ee7e29a035d08b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 3 Dec 2023 03:05:00 -0700 Subject: [PATCH 081/977] Update FollowServiceWarmCache --- app/Jobs/FollowPipeline/FollowServiceWarmCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php index 990236f69..0d3cca7ac 100644 --- a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php +++ b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php @@ -73,7 +73,7 @@ class FollowServiceWarmCache implements ShouldQueue if(Follower::whereProfileId($id)->orWhere('following_id', $id)->count()) { $following = []; $followers = []; - foreach(Follower::lazy() as $follow) { + foreach(Follower::where('following_id', $id)->orWhere('profile_id', $id)->lazyById(500) as $follow) { if($follow->following_id != $id && $follow->profile_id != $id) { continue; } From 8548294c7a2396ec531867fcdd6ffb814508faee Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 3 Dec 2023 03:06:32 -0700 Subject: [PATCH 082/977] Update HomeFeedPipeline, observe mutes/blocks during fanout --- .../HomeFeedPipeline/FeedFollowPipeline.php | 2 +- .../HomeFeedPipeline/FeedInsertPipeline.php | 27 +++++++++++++++++-- .../FeedInsertRemotePipeline.php | 23 +++++++++++++++- .../HashtagInsertFanoutPipeline.php | 7 ++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php index 64d646354..e386329ca 100644 --- a/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedFollowPipeline.php @@ -69,7 +69,7 @@ class FeedFollowPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing $actorId = $this->actorId; $followingId = $this->followingId; - $minId = SnowflakeService::byDate(now()->subMonths(6)); + $minId = SnowflakeService::byDate(now()->subWeeks(6)); $ids = Status::where('id', '>', $minId) ->where('profile_id', $followingId) diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index bf9c849a5..2456d2aa7 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -10,8 +10,10 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; +use App\UserFilter; use App\Services\FollowerService; use App\Services\HomeTimelineService; +use App\Services\StatusService; class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -64,11 +66,32 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function handle(): void { - $ids = FollowerService::localFollowerIds($this->pid); + $sid = $this->sid; + $status = StatusService::get($sid, false); + + if(!$status) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } HomeTimelineService::add($this->pid, $this->sid); + + + $ids = FollowerService::localFollowerIds($this->pid); + + if(!$ids || !count($ids)) { + return; + } + + $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + foreach($ids as $id) { - HomeTimelineService::add($id, $this->sid); + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $this->sid); + } } } } diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php index 579c290a7..738c09699 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php @@ -10,8 +10,10 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; +use App\UserFilter; use App\Services\FollowerService; use App\Services\HomeTimelineService; +use App\Services\StatusService; class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -64,10 +66,29 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces */ public function handle(): void { + $sid = $this->sid; + $status = StatusService::get($sid, false); + + if(!$status) { + return; + } + + if(!in_array($status['pf_type'], ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { + return; + } + $ids = FollowerService::localFollowerIds($this->pid); + if(!$ids || !count($ids)) { + return; + } + + $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + foreach($ids as $id) { - HomeTimelineService::add($id, $this->sid); + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $this->sid); + } } } } diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php index 581b8784f..2e6bf4758 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -10,6 +10,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use App\Hashtag; use App\StatusHashtag; +use App\UserFilter; use App\Services\HashtagFollowService; use App\Services\HomeTimelineService; use App\Services\StatusService; @@ -84,6 +85,8 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro return; } + $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + $ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id); if(!$ids || !count($ids)) { @@ -91,7 +94,9 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro } foreach($ids as $id) { - HomeTimelineService::add($id, $hashtag->status_id); + if(!in_array($id, $skipIds)) { + HomeTimelineService::add($id, $hashtag->status_id); + } } } } From fadb4d6ea4c5777d668699f864aafcca83739e5b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 3 Dec 2023 03:07:58 -0700 Subject: [PATCH 083/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2e02309..eca20cc63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ - Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d)) - Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b)) - Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b)) +- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a)) +- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 4a1363b929d2d53dd4607c32985bc1f5bf85debd Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 3 Dec 2023 22:18:38 -0700 Subject: [PATCH 084/977] Add WebPush --- composer.json | 1 + composer.lock | 1622 ++++++++++++----- config/webpush.php | 48 + ...041631_create_push_subscriptions_table.php | 36 + 4 files changed, 1257 insertions(+), 450 deletions(-) create mode 100644 config/webpush.php create mode 100644 database/migrations/2023_12_04_041631_create_push_subscriptions_table.php diff --git a/composer.json b/composer.json index 54328a674..285d38ccd 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "doctrine/dbal": "^3.0", "intervention/image": "^2.4", "jenssegers/agent": "^2.6", + "laravel-notification-channels/webpush": "^7.1", "laravel/framework": "^10.0", "laravel/helpers": "^1.1", "laravel/horizon": "^5.0", diff --git a/composer.lock b/composer.lock index ccf0148d6..12c097343 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "427b8abad9495b7a7bd2a489625a3881", + "content-hash": "74351c8a36870209c9bbfb76d158a10c", "packages": [ { "name": "aws/aws-crt-php", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "5545a4fa310aec39f54279fdacebcce33b3ff382" + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/5545a4fa310aec39f54279fdacebcce33b3ff382", - "reference": "5545a4fa310aec39f54279fdacebcce33b3ff382", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", "shasum": "" }, "require": { @@ -56,22 +56,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.3" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" }, - "time": "2023-10-16T20:10:06+00:00" + "time": "2023-11-08T00:42:13+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.285.4", + "version": "3.293.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c462af819d81cba49939949032b20799f5ef0fff" + "reference": "1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c462af819d81cba49939949032b20799f5ef0fff", - "reference": "c462af819d81cba49939949032b20799f5ef0fff", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4", + "reference": "1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4", "shasum": "" }, "require": { @@ -151,9 +151,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.285.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.293.2" }, - "time": "2023-11-10T19:25:49+00:00" + "time": "2023-12-01T19:06:15+00:00" }, { "name": "bacon/bacon-qr-code", @@ -424,6 +424,75 @@ }, "time": "2023-03-10T05:38:55+00:00" }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "67a77972b9f398ae7068dabacc39c08aeee170d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/67a77972b9f398ae7068dabacc39c08aeee170d5", + "reference": "67a77972b9f398ae7068dabacc39c08aeee170d5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-10-01T14:29:01+00:00" + }, { "name": "cboden/ratchet", "version": "v0.4.4", @@ -774,16 +843,16 @@ }, { "name": "doctrine/dbal", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2" + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/5b7bd66c9ff58c04c5474ab85edce442f8081cb2", - "reference": "5b7bd66c9ff58c04c5474ab85edce442f8081cb2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", + "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", "shasum": "" }, "require": { @@ -799,7 +868,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.35", + "phpstan/phpstan": "1.10.42", "phpstan/phpstan-strict-rules": "^1.5", "phpunit/phpunit": "9.6.13", "psalm/plugin-phpunit": "0.18.4", @@ -867,7 +936,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.7.1" + "source": "https://github.com/doctrine/dbal/tree/3.7.2" }, "funding": [ { @@ -883,7 +952,7 @@ "type": "tidelift" } ], - "time": "2023-10-06T05:06:20+00:00" + "time": "2023-11-19T08:06:58+00:00" }, { "name": "doctrine/deprecations", @@ -1368,20 +1437,20 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.16.0", + "version": "v4.17.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", - "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -1423,9 +1492,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" }, - "time": "2022-09-18T07:06:19+00:00" + "time": "2023-11-17T15:01:25+00:00" }, { "name": "facade/ignition-contracts", @@ -1480,6 +1549,82 @@ }, "time": "2020-10-16T08:27:54+00:00" }, + { + "name": "fgrosse/phpasn1", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/fgrosse/PHPASN1.git", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", + "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "~2.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "suggest": { + "ext-bcmath": "BCmath is the fallback extension for big integer calculations", + "ext-curl": "For loading OID information from the web if they have not bee defined statically", + "ext-gmp": "GMP is the preferred extension for big integer calculations", + "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FG\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Friedrich Große", + "email": "friedrich.grosse@gmail.com", + "homepage": "https://github.com/FGrosse", + "role": "Author" + }, + { + "name": "All contributors", + "homepage": "https://github.com/FGrosse/PHPASN1/contributors" + } + ], + "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", + "homepage": "https://github.com/FGrosse/PHPASN1", + "keywords": [ + "DER", + "asn.1", + "asn1", + "ber", + "binary", + "decoding", + "encoding", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/fgrosse/PHPASN1/issues", + "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" + }, + "abandoned": true, + "time": "2022-12-19T11:08:26+00:00" + }, { "name": "fig/http-message-util", "version": "1.1.5", @@ -1538,16 +1683,16 @@ }, { "name": "firebase/php-jwt", - "version": "v6.9.0", + "version": "v6.10.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "f03270e63eaccf3019ef0f32849c497385774e11" + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/f03270e63eaccf3019ef0f32849c497385774e11", - "reference": "f03270e63eaccf3019ef0f32849c497385774e11", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", "shasum": "" }, "require": { @@ -1595,9 +1740,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.9.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" }, - "time": "2023-10-05T00:24:42+00:00" + "time": "2023-12-01T16:26:39+00:00" }, { "name": "fruitcake/php-cors", @@ -1672,24 +1817,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.1", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", - "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.1" + "phpoption/phpoption": "^1.9.2" }, "require-dev": { - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "autoload": { @@ -1718,7 +1863,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" }, "funding": [ { @@ -1730,20 +1875,20 @@ "type": "tidelift" } ], - "time": "2023-02-25T20:23:15+00:00" + "time": "2023-11-12T22:16:48+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { @@ -1758,11 +1903,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1840,7 +1985,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -1856,28 +2001,28 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:20:53+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "type": "library", "extra": { @@ -1923,7 +2068,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.1" + "source": "https://github.com/guzzle/promises/tree/2.0.2" }, "funding": [ { @@ -1939,20 +2084,20 @@ "type": "tidelift" } ], - "time": "2023-08-03T15:11:55+00:00" + "time": "2023-12-03T20:19:20+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.1", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "shasum": "" }, "require": { @@ -1966,9 +2111,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2039,7 +2184,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.1" + "source": "https://github.com/guzzle/psr7/tree/2.6.2" }, "funding": [ { @@ -2055,32 +2200,38 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:13:57+00:00" + "time": "2023-12-03T20:05:35+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d" + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/61bf437fc2197f587f6857d3ff903a24f1731b5d", - "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.17" + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.19 || ^9.5.8", + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "uri-template/tests": "1.0.0" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { "psr-4": { "GuzzleHttp\\UriTemplate\\": "src" @@ -2119,7 +2270,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.2" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.3" }, "funding": [ { @@ -2135,7 +2286,7 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:19:19+00:00" + "time": "2023-12-03T19:50:20+00:00" }, { "name": "intervention/image", @@ -2357,17 +2508,75 @@ "time": "2020-06-13T08:05:20+00:00" }, { - "name": "laravel/framework", - "version": "v10.31.0", + "name": "laravel-notification-channels/webpush", + "version": "7.1.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "507ce9b28bce4b5e4140c28943092ca38e9a52e4" + "url": "https://github.com/laravel-notification-channels/webpush.git", + "reference": "b31f7d807d30c80e7391063291ebfe9683bb7de5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/507ce9b28bce4b5e4140c28943092ca38e9a52e4", - "reference": "507ce9b28bce4b5e4140c28943092ca38e9a52e4", + "url": "https://api.github.com/repos/laravel-notification-channels/webpush/zipball/b31f7d807d30c80e7391063291ebfe9683bb7de5", + "reference": "b31f7d807d30c80e7391063291ebfe9683bb7de5", + "shasum": "" + }, + "require": { + "illuminate/notifications": "^8.0|^9.0|^10.0", + "illuminate/support": "^8.0|^9.0|^10.0", + "minishlink/web-push": "^8.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "orchestra/testbench": "^6.0|^7.0|^8.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\WebPush\\WebPushServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cretu Eusebiu", + "email": "me@cretueusebiu.com", + "homepage": "http://cretueusebiu.com", + "role": "Developer" + } + ], + "description": "Web Push Notifications driver for Laravel.", + "homepage": "https://github.com/laravel-notification-channels/webpush", + "support": { + "issues": "https://github.com/laravel-notification-channels/webpush/issues", + "source": "https://github.com/laravel-notification-channels/webpush/tree/7.1.0" + }, + "time": "2023-03-14T11:20:02+00:00" + }, + { + "name": "laravel/framework", + "version": "v10.34.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "c581caa233e380610b34cc491490bfa147a3b62b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/c581caa233e380610b34cc491490bfa147a3b62b", + "reference": "c581caa233e380610b34cc491490bfa147a3b62b", "shasum": "" }, "require": { @@ -2468,7 +2677,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.12", + "orchestra/testbench-core": "^8.15.1", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", @@ -2556,7 +2765,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-11-07T13:48:30+00:00" + "time": "2023-11-28T19:06:27+00:00" }, { "name": "laravel/helpers", @@ -2616,16 +2825,16 @@ }, { "name": "laravel/horizon", - "version": "v5.21.3", + "version": "v5.21.4", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31" + "reference": "bdf58c84b592b83f62262cc6ca98b0debbbc308b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31", - "reference": "6e2b0bb3f8c5b653e8e3ba15661caea6d9613c31", + "url": "https://api.github.com/repos/laravel/horizon/zipball/bdf58c84b592b83f62262cc6ca98b0debbbc308b", + "reference": "bdf58c84b592b83f62262cc6ca98b0debbbc308b", "shasum": "" }, "require": { @@ -2688,9 +2897,9 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.21.3" + "source": "https://github.com/laravel/horizon/tree/v5.21.4" }, - "time": "2023-10-27T13:58:13+00:00" + "time": "2023-11-23T15:47:58+00:00" }, { "name": "laravel/passport", @@ -2829,16 +3038,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "076fe2cf128bd54b4341cdc6d49b95b34e101e4c" + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/076fe2cf128bd54b4341cdc6d49b95b34e101e4c", - "reference": "076fe2cf128bd54b4341cdc6d49b95b34e101e4c", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", "shasum": "" }, "require": { @@ -2885,7 +3094,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2023-10-17T13:38:16+00:00" + "time": "2023-11-08T14:08:06+00:00" }, { "name": "laravel/tinker", @@ -2958,16 +3167,16 @@ }, { "name": "laravel/ui", - "version": "v4.2.2", + "version": "v4.2.3", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "a58ec468db4a340b33f3426c778784717a2c144b" + "reference": "eb532ea096ca1c0298c87c19233daf011fda743a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/a58ec468db4a340b33f3426c778784717a2c144b", - "reference": "a58ec468db4a340b33f3426c778784717a2c144b", + "url": "https://api.github.com/repos/laravel/ui/zipball/eb532ea096ca1c0298c87c19233daf011fda743a", + "reference": "eb532ea096ca1c0298c87c19233daf011fda743a", "shasum": "" }, "require": { @@ -3014,9 +3223,9 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.2.2" + "source": "https://github.com/laravel/ui/tree/v4.2.3" }, - "time": "2023-05-09T19:47:28+00:00" + "time": "2023-11-23T14:44:22+00:00" }, { "name": "lcobucci/clock", @@ -3084,21 +3293,19 @@ }, { "name": "lcobucci/jwt", - "version": "5.1.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "f0031c07b96db6a0ca649206e7eacddb7e9d5908" + "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/f0031c07b96db6a0ca649206e7eacddb7e9d5908", - "reference": "f0031c07b96db6a0ca649206e7eacddb7e9d5908", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0ba88aed12c04bd2ed9924f500673f32b67a6211", + "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", "ext-openssl": "*", "ext-sodium": "*", "php": "~8.1.0 || ~8.2.0 || ~8.3.0", @@ -3143,7 +3350,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.1.0" + "source": "https://github.com/lcobucci/jwt/tree/5.2.0" }, "funding": [ { @@ -3155,7 +3362,7 @@ "type": "patreon" } ], - "time": "2023-10-31T06:41:47+00:00" + "time": "2023-11-20T21:17:42+00:00" }, { "name": "league/commonmark", @@ -3401,16 +3608,16 @@ }, { "name": "league/flysystem", - "version": "3.19.0", + "version": "3.22.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "1b2aa10f2326e0351399b8ce68e287d8e9209a83" + "reference": "d18526ee587f265f091f47bb83f1d9a001ef6f36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1b2aa10f2326e0351399b8ce68e287d8e9209a83", - "reference": "1b2aa10f2326e0351399b8ce68e287d8e9209a83", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d18526ee587f265f091f47bb83f1d9a001ef6f36", + "reference": "d18526ee587f265f091f47bb83f1d9a001ef6f36", "shasum": "" }, "require": { @@ -3438,7 +3645,7 @@ "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "microsoft/azure-storage-blob": "^1.1", - "phpseclib/phpseclib": "^3.0.14", + "phpseclib/phpseclib": "^3.0.34", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", "sabre/dav": "^4.3.1" @@ -3475,7 +3682,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.19.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.22.0" }, "funding": [ { @@ -3487,20 +3694,20 @@ "type": "github" } ], - "time": "2023-11-07T09:04:28+00:00" + "time": "2023-12-03T18:35:53+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.19.0", + "version": "3.22.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "03be643c8ed4dea811d946101be3bc875b5cf214" + "reference": "9808919ee5d819730d9582d4e1673e8d195c38d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/03be643c8ed4dea811d946101be3bc875b5cf214", - "reference": "03be643c8ed4dea811d946101be3bc875b5cf214", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/9808919ee5d819730d9582d4e1673e8d195c38d8", + "reference": "9808919ee5d819730d9582d4e1673e8d195c38d8", "shasum": "" }, "require": { @@ -3541,7 +3748,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.19.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.22.0" }, "funding": [ { @@ -3553,20 +3760,20 @@ "type": "github" } ], - "time": "2023-11-06T20:35:28+00:00" + "time": "2023-11-18T14:03:37+00:00" }, { "name": "league/flysystem-local", - "version": "3.19.0", + "version": "3.22.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "8d868217f9eeb4e9a7320db5ccad825e9a7a4076" + "reference": "42dfb4eaafc4accd248180f0dd66f17073b40c4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/8d868217f9eeb4e9a7320db5ccad825e9a7a4076", - "reference": "8d868217f9eeb4e9a7320db5ccad825e9a7a4076", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/42dfb4eaafc4accd248180f0dd66f17073b40c4c", + "reference": "42dfb4eaafc4accd248180f0dd66f17073b40c4c", "shasum": "" }, "require": { @@ -3601,7 +3808,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.19.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.22.0" }, "funding": [ { @@ -3613,7 +3820,7 @@ "type": "github" } ], - "time": "2023-11-06T20:35:28+00:00" + "time": "2023-11-18T20:52:53+00:00" }, { "name": "league/iso3166", @@ -3819,16 +4026,16 @@ }, { "name": "league/uri", - "version": "7.3.0", + "version": "7.4.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "36743c3961bb82bf93da91917b6bced0358a8d45" + "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/36743c3961bb82bf93da91917b6bced0358a8d45", - "reference": "36743c3961bb82bf93da91917b6bced0358a8d45", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bf414ba956d902f5d98bf9385fcf63954f09dce5", + "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5", "shasum": "" }, "require": { @@ -3897,7 +4104,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.3.0" + "source": "https://github.com/thephpleague/uri/tree/7.4.0" }, "funding": [ { @@ -3905,20 +4112,20 @@ "type": "github" } ], - "time": "2023-09-09T17:21:43+00:00" + "time": "2023-12-01T06:24:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.3.0", + "version": "7.4.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c409b60ed2245ff94c965a8c798a60166db53361" + "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c409b60ed2245ff94c965a8c798a60166db53361", - "reference": "c409b60ed2245ff94c965a8c798a60166db53361", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/bd8c487ec236930f7bbc42b8d374fa882fbba0f3", + "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3", "shasum": "" }, "require": { @@ -3981,7 +4188,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.3.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.0" }, "funding": [ { @@ -3989,7 +4196,74 @@ "type": "github" } ], - "time": "2023-09-09T17:21:43+00:00" + "time": "2023-11-24T15:40:42+00:00" + }, + { + "name": "minishlink/web-push", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/web-push-libs/web-push-php.git", + "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.0.1|^6.2", + "php": ">=8.0", + "spomky-labs/base64url": "^2.0", + "web-token/jwt-key-mgmt": "^2.0|^3.0.2", + "web-token/jwt-signature": "^2.0|^3.0.2", + "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2", + "web-token/jwt-util-ecc": "^2.0|^3.0.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.13.2", + "phpstan/phpstan": "^1.9.8", + "phpunit/phpunit": "^9.5.27" + }, + "suggest": { + "ext-gmp": "Optional for performance." + }, + "type": "library", + "autoload": { + "psr-4": { + "Minishlink\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Louis Lagrange", + "email": "lagrange.louis@gmail.com", + "homepage": "https://github.com/Minishlink" + } + ], + "description": "Web Push library for PHP", + "homepage": "https://github.com/web-push-libs/web-push-php", + "keywords": [ + "Push API", + "WebPush", + "notifications", + "push", + "web" + ], + "support": { + "issues": "https://github.com/web-push-libs/web-push-php/issues", + "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0" + }, + "time": "2023-01-10T17:14:44+00:00" }, { "name": "mobiledetect/mobiledetectlib", @@ -4222,19 +4496,20 @@ }, { "name": "nesbot/carbon", - "version": "2.71.0", + "version": "2.72.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "98276233188583f2ff845a0f992a235472d9466a" + "reference": "a6885fcbad2ec4360b0e200ee0da7d9b7c90786b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/98276233188583f2ff845a0f992a235472d9466a", - "reference": "98276233188583f2ff845a0f992a235472d9466a", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/a6885fcbad2ec4360b0e200ee0da7d9b7c90786b", + "reference": "a6885fcbad2ec4360b0e200ee0da7d9b7c90786b", "shasum": "" }, "require": { + "carbonphp/carbon-doctrine-types": "*", "ext-json": "*", "php": "^7.1.8 || ^8.0", "psr/clock": "^1.0", @@ -4246,8 +4521,8 @@ "psr/clock-implementation": "1.0" }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4", - "doctrine/orm": "^2.7", + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", "ondrejmirtes/better-reflection": "*", @@ -4324,7 +4599,7 @@ "type": "tidelift" } ], - "time": "2023-09-25T11:31:05+00:00" + "time": "2023-11-28T10:13:25+00:00" }, { "name": "nette/schema", @@ -4618,16 +4893,16 @@ }, { "name": "nyholm/psr7", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be" + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/3cb4d163b58589e47b35103e8e5e6a6a475b47be", - "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", "shasum": "" }, "require": { @@ -4680,7 +4955,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.0" + "source": "https://github.com/Nyholm/psr7/tree/1.8.1" }, "funding": [ { @@ -4692,7 +4967,7 @@ "type": "github" } ], - "time": "2023-05-02T11:26:24+00:00" + "time": "2023-11-13T09:31:12+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -5066,16 +5341,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.1", + "version": "1.9.2", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", - "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", "shasum": "" }, "require": { @@ -5083,7 +5358,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "type": "library", "extra": { @@ -5125,7 +5400,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" }, "funding": [ { @@ -5137,7 +5412,7 @@ "type": "tidelift" } ], - "time": "2023-02-25T19:38:58+00:00" + "time": "2023-11-12T21:59:55+00:00" }, { "name": "phpseclib/phpseclib", @@ -6495,16 +6770,16 @@ }, { "name": "react/dns", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f" + "reference": "c134600642fa615b46b41237ef243daa65bb64ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f", - "reference": "3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f", + "url": "https://api.github.com/repos/reactphp/dns/zipball/c134600642fa615b46b41237ef243daa65bb64ec", + "reference": "c134600642fa615b46b41237ef243daa65bb64ec", "shasum": "" }, "require": { @@ -6514,7 +6789,7 @@ "react/promise": "^3.0 || ^2.7 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", "react/promise-timer": "^1.9" }, @@ -6559,7 +6834,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.11.0" + "source": "https://github.com/reactphp/dns/tree/v1.12.0" }, "funding": [ { @@ -6567,20 +6842,20 @@ "type": "open_collective" } ], - "time": "2023-06-02T12:45:26+00:00" + "time": "2023-11-29T12:41:06+00:00" }, { "name": "react/event-loop", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05" + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6e7e587714fff7a83dcc7025aee42ab3b265ae05", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { @@ -6631,7 +6906,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.4.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { @@ -6639,7 +6914,7 @@ "type": "open_collective" } ], - "time": "2023-05-05T10:11:24+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { "name": "react/http", @@ -6735,24 +7010,24 @@ }, { "name": "react/promise", - "version": "v3.0.0", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "c86753c76fd3be465d93b308f18d189f01a22be4" + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4", - "reference": "c86753c76fd3be465d93b308f18d189f01a22be4", + "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.20 || 1.4.10", - "phpunit/phpunit": "^9.5 || ^7.5" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { @@ -6796,7 +7071,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.0.0" + "source": "https://github.com/reactphp/promise/tree/v3.1.0" }, "funding": [ { @@ -6804,7 +7079,7 @@ "type": "open_collective" } ], - "time": "2023-07-11T16:12:49+00:00" + "time": "2023-11-16T16:21:57+00:00" }, { "name": "react/socket", @@ -7145,16 +7420,16 @@ }, { "name": "spatie/laravel-backup", - "version": "8.4.0", + "version": "8.4.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "64f4b816c61f802e9f4c831a589c9d2e21573ddd" + "reference": "b79f790cc856e67cce012abf34bf1c9035085dc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/64f4b816c61f802e9f4c831a589c9d2e21573ddd", - "reference": "64f4b816c61f802e9f4c831a589c9d2e21573ddd", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/b79f790cc856e67cce012abf34bf1c9035085dc1", + "reference": "b79f790cc856e67cce012abf34bf1c9035085dc1", "shasum": "" }, "require": { @@ -7228,7 +7503,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/8.4.0" + "source": "https://github.com/spatie/laravel-backup/tree/8.4.1" }, "funding": [ { @@ -7240,7 +7515,7 @@ "type": "other" } ], - "time": "2023-10-17T15:51:49+00:00" + "time": "2023-11-20T08:21:45+00:00" }, { "name": "spatie/laravel-image-optimizer", @@ -7505,6 +7780,71 @@ ], "time": "2023-09-25T07:13:36+00:00" }, + { + "name": "spomky-labs/base64url", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11|^0.12", + "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", + "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", + "phpstan/phpstan-phpunit": "^0.11|^0.12", + "phpstan/phpstan-strict-rules": "^0.11|^0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/Decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/base64url/issues", + "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2020-11-03T09:10:25+00:00" + }, { "name": "stevebauman/purify", "version": "v6.0.2", @@ -7573,16 +7913,16 @@ }, { "name": "symfony/cache", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "ba33517043c22c94c7ab04b056476f6f86816cf8" + "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/ba33517043c22c94c7ab04b056476f6f86816cf8", - "reference": "ba33517043c22c94c7ab04b056476f6f86816cf8", + "url": "https://api.github.com/repos/symfony/cache/zipball/ac2d25f97b17eec6e19760b6b9962a4f7c44356a", + "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a", "shasum": "" }, "require": { @@ -7591,7 +7931,7 @@ "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.3.6" + "symfony/var-exporter": "^6.3.6|^7.0" }, "conflict": { "doctrine/dbal": "<2.13.1", @@ -7609,12 +7949,12 @@ "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/filesystem": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/messenger": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7649,7 +7989,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.3.8" + "source": "https://github.com/symfony/cache/tree/v6.4.0" }, "funding": [ { @@ -7665,20 +8005,20 @@ "type": "tidelift" } ], - "time": "2023-11-07T10:17:15+00:00" + "time": "2023-11-24T19:28:07+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b" + "reference": "1d74b127da04ffa87aa940abe15446fa89653778" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/ad945640ccc0ae6e208bcea7d7de4b39b569896b", - "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778", "shasum": "" }, "require": { @@ -7725,7 +8065,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" }, "funding": [ { @@ -7741,20 +8081,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2023-09-25T12:52:38+00:00" }, { "name": "symfony/console", - "version": "v6.3.8", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" + "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", - "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "url": "https://api.github.com/repos/symfony/console/zipball/a550a7c99daeedef3f9d23fb82e3531525ff11fd", + "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd", "shasum": "" }, "require": { @@ -7762,7 +8102,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0" + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/dependency-injection": "<5.4", @@ -7776,12 +8116,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/lock": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7815,7 +8159,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.8" + "source": "https://github.com/symfony/console/tree/v6.4.1" }, "funding": [ { @@ -7831,20 +8175,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T08:09:35+00:00" + "time": "2023-11-30T10:54:28+00:00" }, { "name": "symfony/css-selector", - "version": "v6.3.2", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "883d961421ab1709877c10ac99451632a3d6fa57" + "reference": "d036c6c0d0b09e24a14a35f8292146a658f986e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/883d961421ab1709877c10ac99451632a3d6fa57", - "reference": "883d961421ab1709877c10ac99451632a3d6fa57", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/d036c6c0d0b09e24a14a35f8292146a658f986e4", + "reference": "d036c6c0d0b09e24a14a35f8292146a658f986e4", "shasum": "" }, "require": { @@ -7880,7 +8224,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.3.2" + "source": "https://github.com/symfony/css-selector/tree/v6.4.0" }, "funding": [ { @@ -7896,11 +8240,11 @@ "type": "tidelift" } ], - "time": "2023-07-12T16:00:22+00:00" + "time": "2023-10-31T08:40:20+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -7947,7 +8291,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -7967,30 +8311,31 @@ }, { "name": "symfony/error-handler", - "version": "v6.3.5", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "1f69476b64fb47105c06beef757766c376b548c4" + "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/1f69476b64fb47105c06beef757766c376b548c4", - "reference": "1f69476b64fb47105c06beef757766c376b548c4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c873490a1c97b3a0a4838afc36ff36c112d02788", + "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0" + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "conflict": { - "symfony/deprecation-contracts": "<2.5" + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0" + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -8021,7 +8366,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.3.5" + "source": "https://github.com/symfony/error-handler/tree/v6.4.0" }, "funding": [ { @@ -8037,20 +8382,20 @@ "type": "tidelift" } ], - "time": "2023-09-12T06:57:20+00:00" + "time": "2023-10-18T09:43:34+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.3.2", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e" + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e", - "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d76d2632cfc2206eecb5ad2b26cd5934082941b6", + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6", "shasum": "" }, "require": { @@ -8067,13 +8412,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/error-handler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8101,7 +8446,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.0" }, "funding": [ { @@ -8117,11 +8462,11 @@ "type": "tidelift" } ], - "time": "2023-07-06T06:56:43+00:00" + "time": "2023-07-27T06:52:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", @@ -8177,7 +8522,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" }, "funding": [ { @@ -8197,23 +8542,23 @@ }, { "name": "symfony/finder", - "version": "v6.3.5", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", - "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -8241,7 +8586,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.5" + "source": "https://github.com/symfony/finder/tree/v6.4.0" }, "funding": [ { @@ -8257,20 +8602,20 @@ "type": "tidelift" } ], - "time": "2023-09-26T12:56:25+00:00" + "time": "2023-10-31T17:30:12+00:00" }, { "name": "symfony/http-client", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a" + "reference": "5c584530b77aa10ae216989ffc48b4bedc9c0b29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/0314e2d49939a9831929d6fc81c01c6df137fd0a", - "reference": "0314e2d49939a9831929d6fc81c01c6df137fd0a", + "url": "https://api.github.com/repos/symfony/http-client/zipball/5c584530b77aa10ae216989ffc48b4bedc9c0b29", + "reference": "5c584530b77aa10ae216989ffc48b4bedc9c0b29", "shasum": "" }, "require": { @@ -8299,10 +8644,11 @@ "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8333,7 +8679,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.3.8" + "source": "https://github.com/symfony/http-client/tree/v6.4.0" }, "funding": [ { @@ -8349,20 +8695,20 @@ "type": "tidelift" } ], - "time": "2023-11-06T18:31:59+00:00" + "time": "2023-11-28T20:55:58+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "3b66325d0176b4ec826bffab57c9037d759c31fb" + "reference": "1ee70e699b41909c209a0c930f11034b93578654" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/3b66325d0176b4ec826bffab57c9037d759c31fb", - "reference": "3b66325d0176b4ec826bffab57c9037d759c31fb", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", + "reference": "1ee70e699b41909c209a0c930f11034b93578654", "shasum": "" }, "require": { @@ -8411,7 +8757,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" }, "funding": [ { @@ -8427,20 +8773,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2023-07-30T20:28:31+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ce332676de1912c4389222987193c3ef38033df6" + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce332676de1912c4389222987193c3ef38033df6", - "reference": "ce332676de1912c4389222987193c3ef38033df6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/44a6d39a9cc11e154547d882d5aac1e014440771", + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771", "shasum": "" }, "require": { @@ -8455,12 +8801,12 @@ "require-dev": { "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.3", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^5.4|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8488,7 +8834,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.3.8" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.0" }, "funding": [ { @@ -8504,29 +8850,29 @@ "type": "tidelift" } ], - "time": "2023-11-07T10:17:15+00:00" + "time": "2023-11-20T16:41:16+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.3.8", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "929202375ccf44a309c34aeca8305408442ebcc1" + "reference": "2953274c16a229b3933ef73a6898e18388e12e1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/929202375ccf44a309c34aeca8305408442ebcc1", - "reference": "929202375ccf44a309c34aeca8305408442ebcc1", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2953274c16a229b3933ef73a6898e18388e12e1b", + "reference": "2953274c16a229b3933ef73a6898e18388e12e1b", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.3", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.3.4", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -8534,7 +8880,7 @@ "symfony/cache": "<5.4", "symfony/config": "<6.1", "symfony/console": "<5.4", - "symfony/dependency-injection": "<6.3.4", + "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<5.4", "symfony/form": "<5.4", "symfony/http-client": "<5.4", @@ -8544,7 +8890,7 @@ "symfony/translation": "<5.4", "symfony/translation-contracts": "<2.5", "symfony/twig-bridge": "<5.4", - "symfony/validator": "<5.4", + "symfony/validator": "<6.4", "symfony/var-dumper": "<6.3", "twig/twig": "<2.13" }, @@ -8553,26 +8899,26 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/clock": "^6.2", - "symfony/config": "^6.1", - "symfony/console": "^5.4|^6.0", - "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^6.3.4", - "symfony/dom-crawler": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0", - "symfony/property-access": "^5.4.5|^6.0.5", - "symfony/routing": "^5.4|^6.0", - "symfony/serializer": "^6.3", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/translation": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.3|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0", - "symfony/validator": "^6.3", - "symfony/var-exporter": "^6.2", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, "type": "library", @@ -8601,7 +8947,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.3.8" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.1" }, "funding": [ { @@ -8617,20 +8963,20 @@ "type": "tidelift" } ], - "time": "2023-11-10T13:47:32+00:00" + "time": "2023-12-01T17:02:02+00:00" }, { "name": "symfony/mailer", - "version": "v6.3.5", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06" + "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/d89611a7830d51b5e118bca38e390dea92f9ea06", - "reference": "d89611a7830d51b5e118bca38e390dea92f9ea06", + "url": "https://api.github.com/repos/symfony/mailer/zipball/ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", + "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", "shasum": "" }, "require": { @@ -8638,8 +8984,8 @@ "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/mime": "^6.2", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -8650,10 +8996,10 @@ "symfony/twig-bridge": "<6.2.1" }, "require-dev": { - "symfony/console": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/messenger": "^6.2", - "symfony/twig-bridge": "^6.2" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" }, "type": "library", "autoload": { @@ -8681,7 +9027,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.3.5" + "source": "https://github.com/symfony/mailer/tree/v6.4.0" }, "funding": [ { @@ -8697,32 +9043,32 @@ "type": "tidelift" } ], - "time": "2023-09-06T09:47:15+00:00" + "time": "2023-11-12T18:02:22+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.3.6", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "8d9741467c53750dc8ccda23a1cdb91cda732571" + "reference": "72d2f72f2016e559d0152188bef5a5dc9ebf5ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/8d9741467c53750dc8ccda23a1cdb91cda732571", - "reference": "8d9741467c53750dc8ccda23a1cdb91cda732571", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/72d2f72f2016e559d0152188bef5a5dc9ebf5ec7", + "reference": "72d2f72f2016e559d0152188bef5a5dc9ebf5ec7", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/mailer": "^5.4.21|^6.2.7" + "symfony/mailer": "^5.4.21|^6.2.7|^7.0" }, "conflict": { "symfony/http-foundation": "<6.2" }, "require-dev": { - "symfony/http-client": "^5.4|^6.0", - "symfony/webhook": "^6.3" + "symfony/http-client": "^6.3|^7.0", + "symfony/webhook": "^6.3|^7.0" }, "type": "symfony-mailer-bridge", "autoload": { @@ -8750,7 +9096,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.3.6" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.0" }, "funding": [ { @@ -8766,20 +9112,20 @@ "type": "tidelift" } ], - "time": "2023-10-12T13:32:47+00:00" + "time": "2023-11-06T17:20:05+00:00" }, { "name": "symfony/mime", - "version": "v6.3.5", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e" + "reference": "ca4f58b2ef4baa8f6cecbeca2573f88cd577d205" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/d5179eedf1cb2946dbd760475ebf05c251ef6a6e", - "reference": "d5179eedf1cb2946dbd760475ebf05c251ef6a6e", + "url": "https://api.github.com/repos/symfony/mime/zipball/ca4f58b2ef4baa8f6cecbeca2573f88cd577d205", + "reference": "ca4f58b2ef4baa8f6cecbeca2573f88cd577d205", "shasum": "" }, "require": { @@ -8793,16 +9139,16 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<5.4", - "symfony/serializer": "<6.2.13|>=6.3,<6.3.2" + "symfony/serializer": "<6.3.2" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/property-access": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "~6.2.13|^6.3.2" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.3.2|^7.0" }, "type": "library", "autoload": { @@ -8834,7 +9180,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.3.5" + "source": "https://github.com/symfony/mime/tree/v6.4.0" }, "funding": [ { @@ -8850,7 +9196,7 @@ "type": "tidelift" } ], - "time": "2023-09-29T06:59:36+00:00" + "time": "2023-10-17T11:49:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -9592,16 +9938,16 @@ }, { "name": "symfony/process", - "version": "v6.3.4", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", - "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "url": "https://api.github.com/repos/symfony/process/zipball/191703b1566d97a5425dc969e4350d32b8ef17aa", + "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa", "shasum": "" }, "require": { @@ -9633,7 +9979,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.4" + "source": "https://github.com/symfony/process/tree/v6.4.0" }, "funding": [ { @@ -9649,7 +9995,7 @@ "type": "tidelift" } ], - "time": "2023-08-07T10:39:22+00:00" + "time": "2023-11-17T21:06:49+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -9742,16 +10088,16 @@ }, { "name": "symfony/routing", - "version": "v6.3.5", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31" + "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/82616e59acd3e3d9c916bba798326cb7796d7d31", - "reference": "82616e59acd3e3d9c916bba798326cb7796d7d31", + "url": "https://api.github.com/repos/symfony/routing/zipball/0c95c164fdba18b12523b75e64199ca3503e6d40", + "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40", "shasum": "" }, "require": { @@ -9767,11 +10113,11 @@ "require-dev": { "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^6.2", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -9805,7 +10151,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.3.5" + "source": "https://github.com/symfony/routing/tree/v6.4.1" }, "funding": [ { @@ -9821,20 +10167,20 @@ "type": "tidelift" } ], - "time": "2023-09-20T16:05:51+00:00" + "time": "2023-12-01T14:54:37+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", "shasum": "" }, "require": { @@ -9887,7 +10233,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.0" }, "funding": [ { @@ -9903,20 +10249,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2023-07-30T20:28:31+00:00" }, { "name": "symfony/string", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "13880a87790c76ef994c91e87efb96134522577a" + "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", - "reference": "13880a87790c76ef994c91e87efb96134522577a", + "url": "https://api.github.com/repos/symfony/string/zipball/b45fcf399ea9c3af543a92edf7172ba21174d809", + "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809", "shasum": "" }, "require": { @@ -9930,11 +10276,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0" + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -9973,7 +10319,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.8" + "source": "https://github.com/symfony/string/tree/v6.4.0" }, "funding": [ { @@ -9989,20 +10335,20 @@ "type": "tidelift" } ], - "time": "2023-11-09T08:28:21+00:00" + "time": "2023-11-28T20:41:49+00:00" }, { "name": "symfony/translation", - "version": "v6.3.7", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "30212e7c87dcb79c83f6362b00bde0e0b1213499" + "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/30212e7c87dcb79c83f6362b00bde0e0b1213499", - "reference": "30212e7c87dcb79c83f6362b00bde0e0b1213499", + "url": "https://api.github.com/repos/symfony/translation/zipball/b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", + "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", "shasum": "" }, "require": { @@ -10027,17 +10373,17 @@ "require-dev": { "nikic/php-parser": "^4.13", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/intl": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0" + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10068,7 +10414,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.3.7" + "source": "https://github.com/symfony/translation/tree/v6.4.0" }, "funding": [ { @@ -10084,20 +10430,20 @@ "type": "tidelift" } ], - "time": "2023-10-28T23:11:45+00:00" + "time": "2023-11-29T08:14:36+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86" + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/02c24deb352fb0d79db5486c0c79905a85e37e86", - "reference": "02c24deb352fb0d79db5486c0c79905a85e37e86", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5", "shasum": "" }, "require": { @@ -10146,7 +10492,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.0" }, "funding": [ { @@ -10162,20 +10508,20 @@ "type": "tidelift" } ], - "time": "2023-05-30T17:17:10+00:00" + "time": "2023-07-25T15:08:44+00:00" }, { "name": "symfony/uid", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9" + "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/819fa5ac210fb7ddda4752b91a82f50be7493dd9", - "reference": "819fa5ac210fb7ddda4752b91a82f50be7493dd9", + "url": "https://api.github.com/repos/symfony/uid/zipball/8092dd1b1a41372110d06374f99ee62f7f0b9a92", + "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92", "shasum": "" }, "require": { @@ -10183,7 +10529,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0" + "symfony/console": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10220,7 +10566,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.3.8" + "source": "https://github.com/symfony/uid/tree/v6.4.0" }, "funding": [ { @@ -10236,20 +10582,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T08:07:48+00:00" + "time": "2023-10-31T08:18:17+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a" + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/81acabba9046550e89634876ca64bfcd3c06aa0a", - "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c40f7d17e91d8b407582ed51a2bbf83c52c367f6", + "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6", "shasum": "" }, "require": { @@ -10262,10 +10608,11 @@ }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/uid": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, "bin": [ @@ -10304,7 +10651,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.8" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.0" }, "funding": [ { @@ -10320,27 +10667,28 @@ "type": "tidelift" } ], - "time": "2023-11-08T10:42:36+00:00" + "time": "2023-11-09T08:28:32+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.3.6", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "374d289c13cb989027274c86206ddc63b16a2441" + "reference": "2d08ca6b9cc704dce525615d1e6d1788734f36d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/374d289c13cb989027274c86206ddc63b16a2441", - "reference": "374d289c13cb989027274c86206ddc63b16a2441", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/2d08ca6b9cc704dce525615d1e6d1788734f36d9", + "reference": "2d08ca6b9cc704dce525615d1e6d1788734f36d9", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/var-dumper": "^5.4|^6.0" + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10378,7 +10726,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.3.6" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.1" }, "funding": [ { @@ -10394,7 +10742,7 @@ "type": "tidelift" } ], - "time": "2023-10-13T09:16:49+00:00" + "time": "2023-11-30T10:32:10+00:00" }, { "name": "tightenco/collect", @@ -10505,31 +10853,31 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.5.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", - "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.0.2", - "php": "^7.1.3 || ^8.0", - "phpoption/phpoption": "^1.8", - "symfony/polyfill-ctype": "^1.23", - "symfony/polyfill-mbstring": "^1.23.1", - "symfony/polyfill-php80": "^1.23.1" + "graham-campbell/result-type": "^1.1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.2", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" }, "suggest": { "ext-filter": "Required to use the boolean validator." @@ -10541,7 +10889,7 @@ "forward-command": true }, "branch-alias": { - "dev-master": "5.5-dev" + "dev-master": "5.6-dev" } }, "autoload": { @@ -10573,7 +10921,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" }, "funding": [ { @@ -10585,7 +10933,7 @@ "type": "tidelift" } ], - "time": "2022-10-16T01:01:54+00:00" + "time": "2023-11-12T22:43:29+00:00" }, { "name": "voku/portable-ascii", @@ -10661,6 +11009,380 @@ ], "time": "2022-03-08T17:03:00+00:00" }, + { + "name": "web-token/jwt-core", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/4d956e786a4e35d54c74787ebff840a0311c5e83", + "reference": "4d956e786a4e35d54c74787ebff840a0311c5e83", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "paragonie/constant_time_encoding": "^2.4", + "php": ">=8.1" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/3.1.2" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-04T21:04:09+00:00" + }, + { + "name": "web-token/jwt-key-mgmt", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-key-mgmt.git", + "reference": "bf6dec304f2a718d70f7316e498c612317c59e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-key-mgmt/zipball/bf6dec304f2a718d70f7316e498c612317c59e08", + "reference": "bf6dec304f2a718d70f7316e498c612317c59e08", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "web-token/jwt-core": "^3.0" + }, + "suggest": { + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "php-http/httplug": "To enable JKU/X5U support.", + "php-http/message-factory": "To enable JKU/X5U support.", + "web-token/jwt-util-ecc": "To use EC key analyzers." + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\KeyManagement\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-key-mgmt/contributors" + } + ], + "description": "Key Management component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-key-mgmt/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2023-02-02T17:25:26+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "14b71230d9632564e356b785366ad36880964190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/14b71230d9632564e356b785366ad36880964190", + "reference": "14b71230d9632564e356b785366ad36880964190", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "web-token/jwt-core": "^3.0" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2023-02-02T17:25:26+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-ecdsa", + "version": "3.1.7", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-ecdsa.git", + "reference": "e09159600f19832cf4a68921e7299e564bc0eaf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-ecdsa/zipball/e09159600f19832cf4a68921e7299e564bc0eaf9", + "reference": "e09159600f19832cf4a68921e7299e564bc0eaf9", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "web-token/jwt-signature": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\Algorithm\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECDSA Based Signature Algorithms the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-ecdsa/tree/3.1.7" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2022-08-04T21:04:09+00:00" + }, + { + "name": "web-token/jwt-util-ecc", + "version": "3.2.8", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-util-ecc.git", + "reference": "b2337052dbee724d710c1fdb0d3609835a5f8609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-util-ecc/zipball/b2337052dbee724d710c1fdb0d3609835a5f8609", + "reference": "b2337052dbee724d710c1fdb0d3609835a5f8609", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11", + "php": ">=8.1" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\Util\\Ecc\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "ECC Tools for the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-util-ecc/tree/3.2.8" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2023-02-02T13:35:41+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", @@ -11928,16 +12650,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", "shasum": "" }, "require": { @@ -12011,7 +12733,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" }, "funding": [ { @@ -12027,7 +12749,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "sebastian/cli-parser", @@ -12995,16 +13717,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -13033,7 +13755,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -13041,7 +13763,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" } ], "aliases": [], diff --git a/config/webpush.php b/config/webpush.php new file mode 100644 index 000000000..bef3e6653 --- /dev/null +++ b/config/webpush.php @@ -0,0 +1,48 @@ + [ + 'subject' => env('VAPID_SUBJECT'), + 'public_key' => env('VAPID_PUBLIC_KEY'), + 'private_key' => env('VAPID_PRIVATE_KEY'), + 'pem_file' => env('VAPID_PEM_FILE'), + ], + + /** + * This is model that will be used to for push subscriptions. + */ + 'model' => \NotificationChannels\WebPush\PushSubscription::class, + + /** + * This is the name of the table that will be created by the migration and + * used by the PushSubscription model shipped with this package. + */ + 'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'), + + /** + * This is the database connection that will be used by the migration and + * the PushSubscription model shipped with this package. + */ + 'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')), + + /** + * The Guzzle client options used by Minishlink\WebPush. + */ + 'client_options' => [], + + /** + * Google Cloud Messaging. + * + * @deprecated + */ + 'gcm' => [ + 'key' => env('GCM_KEY'), + 'sender_id' => env('GCM_SENDER_ID'), + ], + +]; diff --git a/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php b/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php new file mode 100644 index 000000000..550a98f6a --- /dev/null +++ b/database/migrations/2023_12_04_041631_create_push_subscriptions_table.php @@ -0,0 +1,36 @@ +create(config('webpush.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->morphs('subscribable'); + $table->string('endpoint', 500)->unique(); + $table->string('public_key')->nullable(); + $table->string('auth_token')->nullable(); + $table->string('content_encoding')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::connection(config('webpush.database_connection'))->dropIfExists(config('webpush.table_name')); + } +} From 3204fb96694fcf54bcd747c24e9627d743b3b82d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 5 Dec 2023 00:48:14 -0700 Subject: [PATCH 085/977] Update FederationController, add proper following/follower counts --- app/Http/Controllers/FederationController.php | 431 +++++++++--------- 1 file changed, 218 insertions(+), 213 deletions(-) diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php index c4b5e86bf..0cf33d43e 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -3,17 +3,17 @@ namespace App\Http\Controllers; use App\Jobs\InboxPipeline\{ - DeleteWorker, - 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; @@ -24,243 +24,248 @@ 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; use App\Services\InstanceService; +use App\Services\AccountService; class FederationController extends Controller { - public function nodeinfoWellKnown() - { - abort_if(!config('federation.nodeinfo.enabled'), 404); - return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + public function nodeinfoWellKnown() + { + abort_if(!config('federation.nodeinfo.enabled'), 404); + return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin','*'); + } - public function nodeinfo() - { - abort_if(!config('federation.nodeinfo.enabled'), 404); - return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + public function nodeinfo() + { + abort_if(!config('federation.nodeinfo.enabled'), 404); + return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) + ->header('Access-Control-Allow-Origin','*'); + } - public function webfinger(Request $request) - { - if (!config('federation.webfinger.enabled') || - !$request->has('resource') || - !$request->filled('resource') - ) { - return response('', 400); - } + public function webfinger(Request $request) + { + if (!config('federation.webfinger.enabled') || + !$request->has('resource') || + !$request->filled('resource') + ) { + return response('', 400); + } - $resource = $request->input('resource'); - $domain = config('pixelfed.domain.app'); + $resource = $request->input('resource'); + $domain = config('pixelfed.domain.app'); - if(config('federation.activitypub.sharedInbox') && - $resource == 'acct:' . $domain . '@' . $domain) { - $res = [ - 'subject' => 'acct:' . $domain . '@' . $domain, - 'aliases' => [ - 'https://' . $domain . '/i/actor' - ], - 'links' => [ - [ - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => 'https://' . $domain . '/site/kb/instance-actor' - ], - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => 'https://' . $domain . '/i/actor' - ] - ] - ]; - return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); - } - $hash = hash('sha256', $resource); - $key = 'federation:webfinger:sha256:' . $hash; - if($cached = Cache::get($key)) { - return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); - } - if(strpos($resource, $domain) == false) { - return response('', 400); - } - $parsed = Nickname::normalizeProfileUrl($resource); - if(empty($parsed) || $parsed['domain'] !== $domain) { - return response('', 400); - } - $username = $parsed['username']; - $profile = Profile::whereNull('domain')->whereUsername($username)->first(); - if(!$profile || $profile->status !== null) { - return response('', 400); - } - $webfinger = (new Webfinger($profile))->generate(); - Cache::put($key, $webfinger, 1209600); + if(config('federation.activitypub.sharedInbox') && + $resource == 'acct:' . $domain . '@' . $domain) { + $res = [ + 'subject' => 'acct:' . $domain . '@' . $domain, + 'aliases' => [ + 'https://' . $domain . '/i/actor' + ], + 'links' => [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => 'https://' . $domain . '/site/kb/instance-actor' + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => 'https://' . $domain . '/i/actor' + ] + ] + ]; + return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); + } + $hash = hash('sha256', $resource); + $key = 'federation:webfinger:sha256:' . $hash; + if($cached = Cache::get($key)) { + return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); + } + if(strpos($resource, $domain) == false) { + return response('', 400); + } + $parsed = Nickname::normalizeProfileUrl($resource); + if(empty($parsed) || $parsed['domain'] !== $domain) { + return response('', 400); + } + $username = $parsed['username']; + $profile = Profile::whereNull('domain')->whereUsername($username)->first(); + if(!$profile || $profile->status !== null) { + return response('', 400); + } + $webfinger = (new Webfinger($profile))->generate(); + Cache::put($key, $webfinger, 1209600); - return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) - ->header('Access-Control-Allow-Origin','*'); - } + return response()->json($webfinger, 200, [], 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); + public function userOutbox(Request $request, $username) + { + abort_if(!config_cache('federation.activitypub.enabled'), 404); - if(!$request->wantsJson()) { - return redirect('/' . $username); - } + if(!$request->wantsJson()) { + return redirect('/' . $username); + } - $res = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox', - 'type' => 'OrderedCollection', - 'totalItems' => 0, - 'orderedItems' => [] - ]; + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox', + 'type' => 'OrderedCollection', + 'totalItems' => 0, + 'orderedItems' => [] + ]; - 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(); - if(!$payload || empty($payload)) { - return; - } - $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { - return; - } - $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { - return; - } + $headers = $request->headers->all(); + $payload = $request->getContent(); + if(!$payload || empty($payload)) { + return; + } + $obj = json_decode($payload, true, 8); + if(!isset($obj['id'])) { + return; + } + $domain = parse_url($obj['id'], PHP_URL_HOST); + if(in_array($domain, InstanceService::getBannedDomains())) { + return; + } - if(isset($obj['type']) && $obj['type'] === 'Delete') { - if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { - if($obj['object']['type'] === 'Person') { - if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); - return; - } - } + if(isset($obj['type']) && $obj['type'] === 'Delete') { + if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { + if($obj['object']['type'] === 'Person') { + if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); + return; + } + } - if($obj['object']['type'] === 'Tombstone') { - if(Status::whereObjectUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); - return; - } - } + if($obj['object']['type'] === 'Tombstone') { + if(Status::whereObjectUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); + return; + } + } - if($obj['object']['type'] === 'Story') { - dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); - return; - } - } - return; - } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { - dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow'); - } else { - dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); - } - return; - } + if($obj['object']['type'] === 'Story') { + dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); + return; + } + } + return; + } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { + dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow'); + } else { + dispatch(new InboxValidator($username, $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 sharedInbox(Request $request) + { + abort_if(!config_cache('federation.activitypub.enabled'), 404); + abort_if(!config('federation.activitypub.sharedInbox'), 404); - $headers = $request->headers->all(); - $payload = $request->getContent(); + $headers = $request->headers->all(); + $payload = $request->getContent(); - if(!$payload || empty($payload)) { - return; - } + if(!$payload || empty($payload)) { + return; + } - $obj = json_decode($payload, true, 8); - if(!isset($obj['id'])) { - return; - } + $obj = json_decode($payload, true, 8); + if(!isset($obj['id'])) { + return; + } - $domain = parse_url($obj['id'], PHP_URL_HOST); - if(in_array($domain, InstanceService::getBannedDomains())) { - return; - } + $domain = parse_url($obj['id'], PHP_URL_HOST); + if(in_array($domain, InstanceService::getBannedDomains())) { + return; + } - if(isset($obj['type']) && $obj['type'] === 'Delete') { - if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { - if($obj['object']['type'] === 'Person') { - if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); - return; - } - } + if(isset($obj['type']) && $obj['type'] === 'Delete') { + if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { + if($obj['object']['type'] === 'Person') { + if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); + return; + } + } - if($obj['object']['type'] === 'Tombstone') { - if(Status::whereObjectUrl($obj['object']['id'])->exists()) { - dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); - return; - } - } + if($obj['object']['type'] === 'Tombstone') { + if(Status::whereObjectUrl($obj['object']['id'])->exists()) { + dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); + return; + } + } - if($obj['object']['type'] === 'Story') { - dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); - return; - } - } - return; - } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { - dispatch(new InboxWorker($headers, $payload))->onQueue('follow'); - } else { - dispatch(new InboxWorker($headers, $payload))->onQueue('shared'); - } - return; - } + if($obj['object']['type'] === 'Story') { + dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); + return; + } + } + return; + } else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { + dispatch(new InboxWorker($headers, $payload))->onQueue('follow'); + } else { + dispatch(new InboxWorker($headers, $payload))->onQueue('shared'); + } + return; + } - public function userFollowing(Request $request, $username) - { - abort_if(!config_cache('federation.activitypub.enabled'), 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); - } + $id = AccountService::usernameToId($username); + abort_if(!$id, 404); + $account = AccountService::get($id); + abort_if(!$account || !isset($account['following_count']), 404); + $obj = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $request->getUri(), + 'type' => 'OrderedCollection', + 'totalItems' => $account['following_count'] ?? 0, + ]; + return response()->json($obj); + } - 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' => [] - ]; - - return response()->json($obj); - } + public function userFollowers(Request $request, $username) + { + abort_if(!config_cache('federation.activitypub.enabled'), 404); + $id = AccountService::usernameToId($username); + abort_if(!$id, 404); + $account = AccountService::get($id); + abort_if(!$account || !isset($account['followers_count']), 404); + $obj = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $request->getUri(), + 'type' => 'OrderedCollection', + 'totalItems' => $account['followers_count'] ?? 0, + ]; + return response()->json($obj); + } } From dec061f5ae546ec118dfb12b230304723123ffa7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 5 Dec 2023 00:55:41 -0700 Subject: [PATCH 086/977] Update FederationController, add proper statuses counts --- app/Http/Controllers/FederationController.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php index 0cf33d43e..6faea7050 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -124,12 +124,15 @@ class FederationController extends Controller return redirect('/' . $username); } + $id = AccountService::usernameToId($username); + abort_if(!$id, 404); + $account = AccountService::get($id); + abort_if(!$account || !isset($account['statuses_count']), 404); $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox', 'type' => 'OrderedCollection', - 'totalItems' => 0, - 'orderedItems' => [] + 'totalItems' => $account['statuses_count'] ?? 0, ]; return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json'); From a0157fce0c1ab287373924558f95962ae0df540f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 01:13:04 -0700 Subject: [PATCH 087/977] Update Inbox handler, fix missing object_url and uri fields for direct statuses --- app/Util/ActivityPub/Inbox.php | 15 ++++++--- ...d_direct_object_urls_to_statuses_table.php | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index e97eda34d..7cb25af82 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -282,8 +282,8 @@ class Inbox } if($actor->followers_count == 0) { - if(config('federation.activitypub.ingest.store_notes_without_followers')) { - } else if(FollowerService::followerCount($actor->id, true) == 0) { + if(config('federation.activitypub.ingest.store_notes_without_followers')) { + } else if(FollowerService::followerCount($actor->id, true) == 0) { return; } } @@ -401,6 +401,8 @@ class Inbox $status->visibility = 'direct'; $status->scope = 'direct'; $status->url = $activity['id']; + $status->uri = $activity['id']; + $status->object_url = $activity['id']; $status->in_reply_to_profile_id = $profile->id; $status->save(); @@ -703,12 +705,17 @@ class Inbox return; } $status = Status::whereProfileId($profile->id) - ->whereObjectUrl($id) + ->where(function($q) use($id) { + return $q->where('object_url', $id) + ->orWhere('url', $id); + }) ->first(); if(!$status) { return; } - FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + if($status->scope && $status->scope != 'direct') { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } RemoteStatusDelete::dispatch($status)->onQueue('high'); return; break; diff --git a/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php b/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php new file mode 100644 index 000000000..5211c658f --- /dev/null +++ b/database/migrations/2023_12_08_074345_add_direct_object_urls_to_statuses_table.php @@ -0,0 +1,33 @@ +whereNotNull('url')->whereNull('object_url')->lazyById(50, 'id') as $status) { + try { + $status->object_url = $status->url; + $status->uri = $status->url; + $status->save(); + } catch (Exception | UniqueConstraintViolationException $e) { + continue; + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; From 4cc66a838d8a69cb457e3f84c67c1e1135945d1d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 01:15:41 -0700 Subject: [PATCH 088/977] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca20cc63..3aa36a529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,9 @@ - Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b)) - Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a)) - Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c)) +- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) +- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) +- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From d848792ad417165913c12d9f1e0931f044ecf986 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 01:38:17 -0700 Subject: [PATCH 089/977] Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox --- app/Http/Controllers/DirectMessageController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 1f7e04e59..31f30f853 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -835,7 +835,7 @@ class DirectMessageController extends Controller public function remoteDelete($dm) { $profile = $dm->author; - $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; + $url = $dm->recipient->inbox_url; $body = [ '@context' => [ From 7f462a80556ae1bffcec37d5172046aa0bf5386f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 02:07:26 -0700 Subject: [PATCH 090/977] Update DirectMessageController, dispatch deliver and delete actions to the job queue --- .../Controllers/DirectMessageController.php | 7 ++-- .../DirectPipeline/DirectDeletePipeline.php | 42 +++++++++++++++++++ .../DirectPipeline/DirectDeliverPipeline.php | 42 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 app/Jobs/DirectPipeline/DirectDeletePipeline.php create mode 100644 app/Jobs/DirectPipeline/DirectDeliverPipeline.php diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 31f30f853..972422086 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -23,6 +23,8 @@ use App\Services\AccountService; use App\Services\StatusService; use App\Services\WebfingerService; use App\Models\Conversation; +use App\Jobs\DirectPipeline\DirectDeletePipeline; +use App\Jobs\DirectPipeline\DirectDeliverPipeline; class DirectMessageController extends Controller { @@ -829,7 +831,7 @@ class DirectMessageController extends Controller ] ]; - Helpers::sendSignedObject($profile, $url, $body); + DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high'); } public function remoteDelete($dm) @@ -852,7 +854,6 @@ class DirectMessageController extends Controller 'type' => 'Tombstone' ] ]; - - Helpers::sendSignedObject($profile, $url, $body); + DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high'); } } diff --git a/app/Jobs/DirectPipeline/DirectDeletePipeline.php b/app/Jobs/DirectPipeline/DirectDeletePipeline.php new file mode 100644 index 000000000..947806422 --- /dev/null +++ b/app/Jobs/DirectPipeline/DirectDeletePipeline.php @@ -0,0 +1,42 @@ +profile = $profile; + $this->url = $url; + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + Helpers::sendSignedObject($this->profile, $this->url, $this->payload); + } +} diff --git a/app/Jobs/DirectPipeline/DirectDeliverPipeline.php b/app/Jobs/DirectPipeline/DirectDeliverPipeline.php new file mode 100644 index 000000000..7d20a406e --- /dev/null +++ b/app/Jobs/DirectPipeline/DirectDeliverPipeline.php @@ -0,0 +1,42 @@ +profile = $profile; + $this->url = $url; + $this->payload = $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + Helpers::sendSignedObject($this->profile, $this->url, $this->payload); + } +} From d1c297d1adcca6cf8dea8218bbd663e8ab121217 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 02:36:56 -0700 Subject: [PATCH 091/977] Update DirectMessageController, revert delete delivery to sharedInbox --- app/Http/Controllers/DirectMessageController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 972422086..959b944ce 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -837,7 +837,7 @@ class DirectMessageController extends Controller public function remoteDelete($dm) { $profile = $dm->author; - $url = $dm->recipient->inbox_url; + $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; $body = [ '@context' => [ From 06bee36c522284d165f29eff317d2d0b495ff90e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 03:24:09 -0700 Subject: [PATCH 092/977] Update Inbox, improve story attribute collection --- app/Util/ActivityPub/Inbox.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 7cb25af82..cc39fafdd 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -713,8 +713,10 @@ class Inbox if(!$status) { return; } - if($status->scope && $status->scope != 'direct') { - FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + if($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { + if($status->type && !in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } } RemoteStatusDelete::dispatch($status)->onQueue('high'); return; @@ -985,9 +987,18 @@ class Inbox return; } + $url = $id; + + if(str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + $status = new Status; $status->profile_id = $actorProfile->id; $status->type = 'story:reaction'; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; $status->caption = $text; $status->rendered = $text; $status->scope = 'direct'; @@ -1094,11 +1105,20 @@ class Inbox return; } + $url = $id; + + if(str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + $status = new Status; $status->profile_id = $actorProfile->id; $status->type = 'story:reply'; $status->caption = $text; $status->rendered = $text; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; $status->scope = 'direct'; $status->visibility = 'direct'; $status->in_reply_to_profile_id = $story->profile_id; From 957bbbc2bdabd756b8737eaa05f5798eb7a47445 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 03:25:53 -0700 Subject: [PATCH 093/977] Update FeedInsertPipeline --- app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index 2456d2aa7..de7032faf 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -79,7 +79,6 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing HomeTimelineService::add($this->pid, $this->sid); - $ids = FollowerService::localFollowerIds($this->pid); if(!$ids || !count($ids)) { From 93a6f1e224408c553007bcf5a206895ab26b0849 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 03:26:32 -0700 Subject: [PATCH 094/977] formatting --- app/Util/ActivityPub/Inbox.php | 2450 ++++++++++++++++---------------- 1 file changed, 1225 insertions(+), 1225 deletions(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index cc39fafdd..05049a66f 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -4,20 +4,20 @@ namespace App\Util\ActivityPub; use Cache, DB, Log, Purify, Redis, Storage, Validator; use App\{ - Activity, - DirectMessage, - Follower, - FollowRequest, - Instance, - Like, - Notification, - Media, - Profile, - Status, - StatusHashtag, - Story, - StoryView, - UserFilter + Activity, + DirectMessage, + Follower, + FollowRequest, + Instance, + Like, + Notification, + Media, + Profile, + Status, + StatusHashtag, + Story, + StoryView, + UserFilter }; use Carbon\Carbon; use App\Util\ActivityPub\Helpers; @@ -53,1215 +53,1215 @@ use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; class Inbox { - protected $headers; - protected $profile; - protected $payload; - protected $logger; - - public function __construct($headers, $profile, $payload) - { - $this->headers = $headers; - $this->profile = $profile; - $this->payload = $payload; - } - - public function handle() - { - $this->handleVerb(); - return; - } - - public function handleVerb() - { - $verb = (string) $this->payload['type']; - switch ($verb) { - - case 'Add': - $this->handleAddActivity(); - break; - - case 'Create': - $this->handleCreateActivity(); - break; - - case 'Follow': - if(FollowValidator::validate($this->payload) == false) { return; } - $this->handleFollowActivity(); - break; - - case 'Announce': - if(AnnounceValidator::validate($this->payload) == false) { return; } - $this->handleAnnounceActivity(); - break; - - case 'Accept': - if(AcceptValidator::validate($this->payload) == false) { return; } - $this->handleAcceptActivity(); - break; - - case 'Delete': - $this->handleDeleteActivity(); - break; - - case 'Like': - if(LikeValidator::validate($this->payload) == false) { return; } - $this->handleLikeActivity(); - break; - - case 'Reject': - $this->handleRejectActivity(); - break; - - case 'Undo': - $this->handleUndoActivity(); - break; - - case 'View': - $this->handleViewActivity(); - break; - - case 'Story:Reaction': - $this->handleStoryReactionActivity(); - break; - - case 'Story:Reply': - $this->handleStoryReplyActivity(); - break; - - case 'Flag': - $this->handleFlagActivity(); - break; - - case 'Update': - $this->handleUpdateActivity(); - break; - - default: - // TODO: decide how to handle invalid verbs. - break; - } - } - - public function verifyNoteAttachment() - { - $activity = $this->payload['object']; - - if(isset($activity['inReplyTo']) && - !empty($activity['inReplyTo']) && - Helpers::validateUrl($activity['inReplyTo']) - ) { - // reply detected, skip attachment check - return true; - } - - $valid = Helpers::verifyAttachments($activity); - - return $valid; - } - - public function actorFirstOrCreate($actorUrl) - { - return Helpers::profileFetch($actorUrl); - } - - public function handleAddActivity() - { - // stories ;) - - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - if(!isset($obj['type'])) { - return; - } - - switch($obj['type']) { - case 'Story': - StoryFetch::dispatch($this->payload); - break; - } - - return; - } - - public function handleCreateActivity() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - if(!isset($activity['to'])) { - return; - } - $to = isset($activity['to']) ? $activity['to'] : []; - $cc = isset($activity['cc']) ? $activity['cc'] : []; - - if($activity['type'] == 'Question') { - $this->handlePollCreate(); - return; - } - - if( is_array($to) && - is_array($cc) && - count($to) == 1 && - count($cc) == 0 && - parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') - ) { - $this->handleDirectMessage(); - return; - } - - if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) { - $this->handleNoteReply(); - - } elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) { - if(!$this->verifyNoteAttachment()) { - return; - } - $this->handleNoteCreate(); - } - return; - } - - public function handleNoteReply() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - $inReplyTo = $activity['inReplyTo']; - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - - Helpers::statusFirstOrFetch($url, true); - return; - } - - public function handlePollCreate() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - Helpers::statusFirstOrFetch($url); - return; - } - - public function handleNoteCreate() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - if(!$actor || $actor->domain == null) { - return; - } - - if( isset($activity['inReplyTo']) && - isset($activity['name']) && - !isset($activity['content']) && - !isset($activity['attachment']) && - Helpers::validateLocalUrl($activity['inReplyTo']) - ) { - $this->handlePollVote(); - return; - } - - if($actor->followers_count == 0) { - if(config('federation.activitypub.ingest.store_notes_without_followers')) { - } else if(FollowerService::followerCount($actor->id, true) == 0) { - return; - } - } - - $hasUrl = isset($activity['url']); - $url = isset($activity['url']) ? $activity['url'] : $activity['id']; - - if($hasUrl) { - if(Status::whereUri($url)->exists()) { - return; - } - } else { - if(Status::whereObjectUrl($url)->exists()) { - return; - } - } - - Helpers::storeStatus( - $url, - $actor, - $activity - ); - return; - } - - public function handlePollVote() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - - if(!$actor) { - return; - } - - $status = Helpers::statusFetch($activity['inReplyTo']); - - if(!$status) { - return; - } - - $poll = $status->poll; - - if(!$poll) { - return; - } - - if(now()->gt($poll->expires_at)) { - return; - } - - $choices = $poll->poll_options; - $choice = array_search($activity['name'], $choices); - - if($choice === false) { - return; - } - - if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { - return; - } - - $vote = new PollVote; - $vote->status_id = $status->id; - $vote->profile_id = $actor->id; - $vote->poll_id = $poll->id; - $vote->choice = $choice; - $vote->uri = isset($activity['id']) ? $activity['id'] : null; - $vote->save(); - - $tallies = $poll->cached_tallies; - $tallies[$choice] = $tallies[$choice] + 1; - $poll->cached_tallies = $tallies; - $poll->votes_count = array_sum($tallies); - $poll->save(); - - PollService::del($status->id); - - return; - } - - public function handleDirectMessage() - { - $activity = $this->payload['object']; - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $profile = Profile::whereNull('domain') - ->whereUsername(array_last(explode('/', $activity['to'][0]))) - ->firstOrFail(); - - if(in_array($actor->id, $profile->blockedIds()->toArray())) { - return; - } - - $msg = $activity['content']; - $msgText = strip_tags($activity['content']); - - if(Str::startsWith($msgText, '@' . $profile->username)) { - $len = strlen('@' . $profile->username); - $msgText = substr($msgText, $len + 1); - } - - if($profile->user->settings->public_dm == false || $profile->is_private) { - if($profile->follows($actor) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - $status = new Status; - $status->profile_id = $actor->id; - $status->caption = $msgText; - $status->rendered = $msg; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->url = $activity['id']; - $status->uri = $activity['id']; - $status->object_url = $activity['id']; - $status->in_reply_to_profile_id = $profile->id; - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $profile->id; - $dm->from_id = $actor->id; - $dm->status_id = $status->id; - $dm->is_hidden = $hidden; - $dm->type = 'text'; - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $profile->id, - 'from_id' => $actor->id - ], - [ - 'type' => 'text', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if(count($activity['attachment'])) { - $photos = 0; - $videos = 0; - $allowed = explode(',', config_cache('pixelfed.media_types')); - $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length')); - foreach($activity['attachment'] as $a) { - $type = $a['mediaType']; - $url = $a['url']; - $valid = Helpers::validateUrl($url); - if(in_array($type, $allowed) == false || $valid == false) { - continue; - } - - $media = new Media(); - $media->remote_media = true; - $media->status_id = $status->id; - $media->profile_id = $status->profile_id; - $media->user_id = null; - $media->media_path = $url; - $media->remote_url = $url; - $media->mime = $type; - $media->save(); - if(explode('/', $type)[0] == 'image') { - $photos = $photos + 1; - } - if(explode('/', $type)[0] == 'video') { - $videos = $videos + 1; - } - } - - if($photos && $videos == 0) { - $dm->type = $photos == 1 ? 'photo' : 'photos'; - $dm->save(); - } - if($videos && $photos == 0) { - $dm->type = $videos == 1 ? 'video' : 'videos'; - $dm->save(); - } - } - - if(filter_var($msgText, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msgText)) { - $dm->type = 'link'; - $dm->meta = [ - 'domain' => parse_url($msgText, PHP_URL_HOST), - 'local' => parse_url($msgText, PHP_URL_HOST) == - parse_url(config('app.url'), PHP_URL_HOST) - ]; - $dm->save(); - } - } - - $nf = UserFilter::whereUserId($profile->id) - ->whereFilterableId($actor->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->exists(); - - if($profile->domain == null && $hidden == false && !$nf) { - $notification = new Notification(); - $notification->profile_id = $profile->id; - $notification->actor_id = $actor->id; - $notification->action = 'dm'; - $notification->item_id = $dm->id; - $notification->item_type = "App\DirectMessage"; - $notification->save(); - } - - return; - } - - public function handleFollowActivity() - { - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $target = $this->actorFirstOrCreate($this->payload['object']); - if(!$actor || !$target) { - return; - } - - if($actor->domain == null || $target->domain !== null) { - return; - } - - if( - Follower::whereProfileId($actor->id) - ->whereFollowingId($target->id) - ->exists() || - FollowRequest::whereFollowerId($actor->id) - ->whereFollowingId($target->id) - ->exists() - ) { - return; - } - - $blocks = UserFilterService::blocks($target->id); - if($blocks && in_array($actor->id, $blocks)) { - return; - } - - if($target->is_private == true) { - FollowRequest::updateOrCreate([ - 'follower_id' => $actor->id, - 'following_id' => $target->id, - ],[ - 'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray() - ]); - } else { - $follower = new Follower; - $follower->profile_id = $actor->id; - $follower->following_id = $target->id; - $follower->local_profile = empty($actor->domain); - $follower->save(); - - FollowPipeline::dispatch($follower); - FollowerService::add($actor->id, $target->id); - - // send Accept to remote profile - $accept = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $target->permalink().'#accepts/follows/' . $follower->id, - 'type' => 'Accept', - 'actor' => $target->permalink(), - 'object' => [ - 'id' => $this->payload['id'], - 'actor' => $actor->permalink(), - 'type' => 'Follow', - 'object' => $target->permalink() - ] - ]; - Helpers::sendSignedObject($target, $actor->inbox_url, $accept); - Cache::forget('profile:follower_count:'.$target->id); - Cache::forget('profile:follower_count:'.$actor->id); - Cache::forget('profile:following_count:'.$target->id); - Cache::forget('profile:following_count:'.$actor->id); - } - - return; - } - - public function handleAnnounceActivity() - { - $actor = $this->actorFirstOrCreate($this->payload['actor']); - $activity = $this->payload['object']; - - if(!$actor || $actor->domain == null) { - return; - } - - $parent = Helpers::statusFetch($activity); - - if(!$parent || empty($parent)) { - return; - } - - $blocks = UserFilterService::blocks($parent->profile_id); - if($blocks && in_array($actor->id, $blocks)) { - return; - } - - $status = Status::firstOrCreate([ - 'profile_id' => $actor->id, - 'reblog_of_id' => $parent->id, - 'type' => 'share' - ]); - - Notification::firstOrCreate( - [ - 'profile_id' => $parent->profile_id, - 'actor_id' => $actor->id, - 'action' => 'share', - 'item_id' => $parent->id, - 'item_type' => 'App\Status', - ] - ); - - $parent->reblogs_count = $parent->reblogs_count + 1; - $parent->save(); - - ReblogService::addPostReblog($parent->profile_id, $status->id); - - return; - } - - public function handleAcceptActivity() - { - $actor = $this->payload['object']['actor']; - $obj = $this->payload['object']['object']; - $type = $this->payload['object']['type']; - - if($type !== 'Follow') { - return; - } - - $actor = Helpers::validateLocalUrl($actor); - $target = Helpers::validateUrl($obj); - - if(!$actor || !$target) { - return; - } - - $actor = Helpers::profileFetch($actor); - $target = Helpers::profileFetch($target); - - if(!$actor || !$target) { - return; - } - - $request = FollowRequest::whereFollowerId($actor->id) - ->whereFollowingId($target->id) - ->whereIsRejected(false) - ->first(); - - if(!$request) { - return; - } - - $follower = Follower::firstOrCreate([ - 'profile_id' => $actor->id, - 'following_id' => $target->id, - ]); - FollowPipeline::dispatch($follower); - - $request->delete(); - - return; - } - - public function handleDeleteActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { - $profile = Profile::whereRemoteUrl($obj)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); - return; - } else { - if(!isset( - $obj['id'], - $this->payload['object'], - $this->payload['object']['id'], - $this->payload['object']['type'] - )) { - return; - } - $type = $this->payload['object']['type']; - $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']); - if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) { - return; - } - if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - $id = $this->payload['object']['id']; - switch ($type) { - case 'Person': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); - return; - break; - - case 'Tombstone': - $profile = Profile::whereRemoteUrl($actor)->first(); - if(!$profile || $profile->private_key != null) { - return; - } - $status = Status::whereProfileId($profile->id) - ->where(function($q) use($id) { - return $q->where('object_url', $id) - ->orWhere('url', $id); - }) - ->first(); - if(!$status) { - return; - } - if($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { - if($status->type && !in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { - FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - } - } - RemoteStatusDelete::dispatch($status)->onQueue('high'); - return; - break; - - case 'Story': - $story = Story::whereObjectId($id) - ->first(); - if($story) { - StoryExpire::dispatch($story)->onQueue('story'); - } - return; - break; - - default: - return; - break; - } - } - return; - } - - public function handleLikeActivity() - { - $actor = $this->payload['actor']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - $profile = self::actorFirstOrCreate($actor); - $obj = $this->payload['object']; - if(!Helpers::validateUrl($obj)) { - return; - } - $status = Helpers::statusFirstOrFetch($obj); - if(!$status || !$profile) { - return; - } - - $blocks = UserFilterService::blocks($status->profile_id); - if($blocks && in_array($profile->id, $blocks)) { - return; - } - - $like = Like::firstOrCreate([ - 'profile_id' => $profile->id, - 'status_id' => $status->id - ]); - - if($like->wasRecentlyCreated == true) { - $status->likes_count = $status->likes_count + 1; - $status->save(); - LikePipeline::dispatch($like); - } - - return; - } - - public function handleRejectActivity() - { - } - - public function handleUndoActivity() - { - $actor = $this->payload['actor']; - $profile = self::actorFirstOrCreate($actor); - $obj = $this->payload['object']; - - if(!$profile) { - return; - } - // TODO: Some implementations do not inline the object, skip for now - if(!$obj || !is_array($obj) || !isset($obj['type'])) { - return; - } - - switch ($obj['type']) { - case 'Accept': - break; - - case 'Announce': - if(is_array($obj) && isset($obj['object'])) { - $obj = $obj['object']; - } - if(!is_string($obj)) { - return; - } - if(Helpers::validateLocalUrl($obj)) { - $parsedId = last(explode('/', $obj)); - $status = Status::find($parsedId); - } else { - $status = Status::whereUri($obj)->first(); - } - if(!$status) { - return; - } - FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); - Status::whereProfileId($profile->id) - ->whereReblogOfId($status->id) - ->delete(); - ReblogService::removePostReblog($profile->id, $status->id); - Notification::whereProfileId($status->profile_id) - ->whereActorId($profile->id) - ->whereAction('share') - ->whereItemId($status->reblog_of_id) - ->whereItemType('App\Status') - ->forceDelete(); - break; - - case 'Block': - break; - - case 'Follow': - $following = self::actorFirstOrCreate($obj['object']); - if(!$following) { - return; - } - Follower::whereProfileId($profile->id) - ->whereFollowingId($following->id) - ->delete(); - Notification::whereProfileId($following->id) - ->whereActorId($profile->id) - ->whereAction('follow') - ->whereItemId($following->id) - ->whereItemType('App\Profile') - ->forceDelete(); - FollowerService::remove($profile->id, $following->id); - break; - - case 'Like': - $objectUri = $obj['object']; - if(!is_string($objectUri)) { - if(is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { - $objectUri = $objectUri['id']; - } else { - return; - } - } - $status = Helpers::statusFirstOrFetch($objectUri); - if(!$status) { - return; - } - Like::whereProfileId($profile->id) - ->whereStatusId($status->id) - ->forceDelete(); - Notification::whereProfileId($status->profile_id) - ->whereActorId($profile->id) - ->whereAction('like') - ->whereItemId($status->id) - ->whereItemType('App\Status') - ->forceDelete(); - break; - } - return; - } - - public function handleViewActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $actor = $this->payload['actor']; - $obj = $this->payload['object']; - - if(!Helpers::validateUrl($actor)) { - return; - } - - if(!$obj || !is_array($obj)) { - return; - } - - if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') { - return; - } - - if(!Helpers::validateLocalUrl($obj['object'])) { - return; - } - - $profile = Helpers::profileFetch($actor); - $storyId = Str::of($obj['object'])->explode('/')->last(); - - $story = Story::whereActive(true) - ->whereLocal(true) - ->find($storyId); - - if(!$story) { - return; - } - - if(!FollowerService::follows($profile->id, $story->profile_id)) { - return; - } - - $view = StoryView::firstOrCreate([ - 'story_id' => $story->id, - 'profile_id' => $profile->id - ]); - - if($view->wasRecentlyCreated == true) { - $story->view_count++; - $story->save(); - } - - return; - } - - public function handleStoryReactionActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['id'], - $this->payload['inReplyTo'], - $this->payload['content'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - $storyUrl = $this->payload['inReplyTo']; - $to = $this->payload['to']; - $text = Purify::clean($this->payload['content']); - - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { - return; - } - - if(!Helpers::validateLocalUrl($storyUrl)) { - return; - } - - if(!Helpers::validateLocalUrl($to)) { - return; - } - - if(Status::whereObjectUrl($id)->exists()) { - return; - } - - $storyId = Str::of($storyUrl)->explode('/')->last(); - $targetProfile = Helpers::profileFetch($to); - - $story = Story::whereProfileId($targetProfile->id) - ->find($storyId); - - if(!$story) { - return; - } - - if($story->can_react == false) { - return; - } - - $actorProfile = Helpers::profileFetch($actor); - - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { - return; - } - - $url = $id; - - if(str_ends_with($url, '/activity')) { - $url = substr($url, 0, -9); - } - - $status = new Status; - $status->profile_id = $actorProfile->id; - $status->type = 'story:reaction'; - $status->url = $url; - $status->uri = $url; - $status->object_url = $url; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'reaction' => $text - ]); - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $actorProfile->id; - $dm->type = 'story:react'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $targetProfile->username, - 'story_actor_username' => $actorProfile->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text - ]); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id - ], - [ - 'type' => 'story:react', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:react'; - $n->save(); - - return; - } - - public function handleStoryReplyActivity() - { - if(!isset( - $this->payload['actor'], - $this->payload['id'], - $this->payload['inReplyTo'], - $this->payload['content'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - $storyUrl = $this->payload['inReplyTo']; - $to = $this->payload['to']; - $text = Purify::clean($this->payload['content']); - - if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { - return; - } - - if(!Helpers::validateLocalUrl($storyUrl)) { - return; - } - - if(!Helpers::validateLocalUrl($to)) { - return; - } - - if(Status::whereObjectUrl($id)->exists()) { - return; - } - - $storyId = Str::of($storyUrl)->explode('/')->last(); - $targetProfile = Helpers::profileFetch($to); - - $story = Story::whereProfileId($targetProfile->id) - ->find($storyId); - - if(!$story) { - return; - } - - if($story->can_react == false) { - return; - } - - $actorProfile = Helpers::profileFetch($actor); - - if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { - return; - } - - $url = $id; - - if(str_ends_with($url, '/activity')) { - $url = substr($url, 0, -9); - } - - $status = new Status; - $status->profile_id = $actorProfile->id; - $status->type = 'story:reply'; - $status->caption = $text; - $status->rendered = $text; - $status->url = $url; - $status->uri = $url; - $status->object_url = $url; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'caption' => $text - ]); - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $actorProfile->id; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $targetProfile->username, - 'story_actor_username' => $actorProfile->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $actorProfile->id - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - - return; - } - - public function handleFlagActivity() - { - if(!isset( - $this->payload['id'], - $this->payload['type'], - $this->payload['actor'], - $this->payload['object'] - )) { - return; - } - - $id = $this->payload['id']; - $actor = $this->payload['actor']; - - if(Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { - return; - } - - $content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null; - $object = $this->payload['object']; - - if(empty($object) || (!is_array($object) && !is_string($object))) { - return; - } - - if(is_array($object) && count($object) > 100) { - return; - } - - $objects = collect([]); - $accountId = null; - - foreach($object as $objectUrl) { - if(!Helpers::validateLocalUrl($objectUrl)) { - continue; - } - - if(str_contains($objectUrl, '/users/')) { - $username = last(explode('/', $objectUrl)); - $profileId = Profile::whereUsername($username)->first(); - if($profileId) { - $accountId = $profileId->id; - } - } else if(str_contains($objectUrl, '/p/')) { - $postId = last(explode('/', $objectUrl)); - $objects->push($postId); - } else { - continue; - } - } - - if(!$accountId || !$objects->count()) { - return; - } - - $instanceHost = parse_url($id, PHP_URL_HOST); - - $instance = Instance::updateOrCreate([ - 'domain' => $instanceHost - ]); - - $report = new RemoteReport; - $report->status_ids = $objects->toArray(); - $report->comment = $content; - $report->account_id = $accountId; - $report->uri = $id; - $report->instance_id = $instance->id; - $report->report_meta = [ - 'actor' => $actor, - 'object' => $object - ]; - $report->save(); - - return; - } - - public function handleUpdateActivity() - { - $activity = $this->payload['object']; - - if(!isset($activity['type'], $activity['id'])) { - return; - } - - if(!Helpers::validateUrl($activity['id'])) { - return; - } - - if($activity['type'] === 'Note') { - if(Status::whereObjectUrl($activity['id'])->exists()) { - StatusRemoteUpdatePipeline::dispatch($activity); - } - } else if ($activity['type'] === 'Person') { - if(UpdatePersonValidator::validate($this->payload)) { - HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); - } - } - } + protected $headers; + protected $profile; + protected $payload; + protected $logger; + + public function __construct($headers, $profile, $payload) + { + $this->headers = $headers; + $this->profile = $profile; + $this->payload = $payload; + } + + public function handle() + { + $this->handleVerb(); + return; + } + + public function handleVerb() + { + $verb = (string) $this->payload['type']; + switch ($verb) { + + case 'Add': + $this->handleAddActivity(); + break; + + case 'Create': + $this->handleCreateActivity(); + break; + + case 'Follow': + if(FollowValidator::validate($this->payload) == false) { return; } + $this->handleFollowActivity(); + break; + + case 'Announce': + if(AnnounceValidator::validate($this->payload) == false) { return; } + $this->handleAnnounceActivity(); + break; + + case 'Accept': + if(AcceptValidator::validate($this->payload) == false) { return; } + $this->handleAcceptActivity(); + break; + + case 'Delete': + $this->handleDeleteActivity(); + break; + + case 'Like': + if(LikeValidator::validate($this->payload) == false) { return; } + $this->handleLikeActivity(); + break; + + case 'Reject': + $this->handleRejectActivity(); + break; + + case 'Undo': + $this->handleUndoActivity(); + break; + + case 'View': + $this->handleViewActivity(); + break; + + case 'Story:Reaction': + $this->handleStoryReactionActivity(); + break; + + case 'Story:Reply': + $this->handleStoryReplyActivity(); + break; + + case 'Flag': + $this->handleFlagActivity(); + break; + + case 'Update': + $this->handleUpdateActivity(); + break; + + default: + // TODO: decide how to handle invalid verbs. + break; + } + } + + public function verifyNoteAttachment() + { + $activity = $this->payload['object']; + + if(isset($activity['inReplyTo']) && + !empty($activity['inReplyTo']) && + Helpers::validateUrl($activity['inReplyTo']) + ) { + // reply detected, skip attachment check + return true; + } + + $valid = Helpers::verifyAttachments($activity); + + return $valid; + } + + public function actorFirstOrCreate($actorUrl) + { + return Helpers::profileFetch($actorUrl); + } + + public function handleAddActivity() + { + // stories ;) + + if(!isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + + if(!Helpers::validateUrl($actor)) { + return; + } + + if(!isset($obj['type'])) { + return; + } + + switch($obj['type']) { + case 'Story': + StoryFetch::dispatch($this->payload); + break; + } + + return; + } + + public function handleCreateActivity() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } + + if(!isset($activity['to'])) { + return; + } + $to = isset($activity['to']) ? $activity['to'] : []; + $cc = isset($activity['cc']) ? $activity['cc'] : []; + + if($activity['type'] == 'Question') { + $this->handlePollCreate(); + return; + } + + if( is_array($to) && + is_array($cc) && + count($to) == 1 && + count($cc) == 0 && + parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') + ) { + $this->handleDirectMessage(); + return; + } + + if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) { + $this->handleNoteReply(); + + } elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) { + if(!$this->verifyNoteAttachment()) { + return; + } + $this->handleNoteCreate(); + } + return; + } + + public function handleNoteReply() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } + + $inReplyTo = $activity['inReplyTo']; + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + + Helpers::statusFirstOrFetch($url, true); + return; + } + + public function handlePollCreate() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + Helpers::statusFirstOrFetch($url); + return; + } + + public function handleNoteCreate() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + if(!$actor || $actor->domain == null) { + return; + } + + if( isset($activity['inReplyTo']) && + isset($activity['name']) && + !isset($activity['content']) && + !isset($activity['attachment']) && + Helpers::validateLocalUrl($activity['inReplyTo']) + ) { + $this->handlePollVote(); + return; + } + + if($actor->followers_count == 0) { + if(config('federation.activitypub.ingest.store_notes_without_followers')) { + } else if(FollowerService::followerCount($actor->id, true) == 0) { + return; + } + } + + $hasUrl = isset($activity['url']); + $url = isset($activity['url']) ? $activity['url'] : $activity['id']; + + if($hasUrl) { + if(Status::whereUri($url)->exists()) { + return; + } + } else { + if(Status::whereObjectUrl($url)->exists()) { + return; + } + } + + Helpers::storeStatus( + $url, + $actor, + $activity + ); + return; + } + + public function handlePollVote() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + + if(!$actor) { + return; + } + + $status = Helpers::statusFetch($activity['inReplyTo']); + + if(!$status) { + return; + } + + $poll = $status->poll; + + if(!$poll) { + return; + } + + if(now()->gt($poll->expires_at)) { + return; + } + + $choices = $poll->poll_options; + $choice = array_search($activity['name'], $choices); + + if($choice === false) { + return; + } + + if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) { + return; + } + + $vote = new PollVote; + $vote->status_id = $status->id; + $vote->profile_id = $actor->id; + $vote->poll_id = $poll->id; + $vote->choice = $choice; + $vote->uri = isset($activity['id']) ? $activity['id'] : null; + $vote->save(); + + $tallies = $poll->cached_tallies; + $tallies[$choice] = $tallies[$choice] + 1; + $poll->cached_tallies = $tallies; + $poll->votes_count = array_sum($tallies); + $poll->save(); + + PollService::del($status->id); + + return; + } + + public function handleDirectMessage() + { + $activity = $this->payload['object']; + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $profile = Profile::whereNull('domain') + ->whereUsername(array_last(explode('/', $activity['to'][0]))) + ->firstOrFail(); + + if(in_array($actor->id, $profile->blockedIds()->toArray())) { + return; + } + + $msg = $activity['content']; + $msgText = strip_tags($activity['content']); + + if(Str::startsWith($msgText, '@' . $profile->username)) { + $len = strlen('@' . $profile->username); + $msgText = substr($msgText, $len + 1); + } + + if($profile->user->settings->public_dm == false || $profile->is_private) { + if($profile->follows($actor) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + $status = new Status; + $status->profile_id = $actor->id; + $status->caption = $msgText; + $status->rendered = $msg; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->url = $activity['id']; + $status->uri = $activity['id']; + $status->object_url = $activity['id']; + $status->in_reply_to_profile_id = $profile->id; + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $profile->id; + $dm->from_id = $actor->id; + $dm->status_id = $status->id; + $dm->is_hidden = $hidden; + $dm->type = 'text'; + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $profile->id, + 'from_id' => $actor->id + ], + [ + 'type' => 'text', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden + ] + ); + + if(count($activity['attachment'])) { + $photos = 0; + $videos = 0; + $allowed = explode(',', config_cache('pixelfed.media_types')); + $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length')); + foreach($activity['attachment'] as $a) { + $type = $a['mediaType']; + $url = $a['url']; + $valid = Helpers::validateUrl($url); + if(in_array($type, $allowed) == false || $valid == false) { + continue; + } + + $media = new Media(); + $media->remote_media = true; + $media->status_id = $status->id; + $media->profile_id = $status->profile_id; + $media->user_id = null; + $media->media_path = $url; + $media->remote_url = $url; + $media->mime = $type; + $media->save(); + if(explode('/', $type)[0] == 'image') { + $photos = $photos + 1; + } + if(explode('/', $type)[0] == 'video') { + $videos = $videos + 1; + } + } + + if($photos && $videos == 0) { + $dm->type = $photos == 1 ? 'photo' : 'photos'; + $dm->save(); + } + if($videos && $photos == 0) { + $dm->type = $videos == 1 ? 'video' : 'videos'; + $dm->save(); + } + } + + if(filter_var($msgText, FILTER_VALIDATE_URL)) { + if(Helpers::validateUrl($msgText)) { + $dm->type = 'link'; + $dm->meta = [ + 'domain' => parse_url($msgText, PHP_URL_HOST), + 'local' => parse_url($msgText, PHP_URL_HOST) == + parse_url(config('app.url'), PHP_URL_HOST) + ]; + $dm->save(); + } + } + + $nf = UserFilter::whereUserId($profile->id) + ->whereFilterableId($actor->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->exists(); + + if($profile->domain == null && $hidden == false && !$nf) { + $notification = new Notification(); + $notification->profile_id = $profile->id; + $notification->actor_id = $actor->id; + $notification->action = 'dm'; + $notification->item_id = $dm->id; + $notification->item_type = "App\DirectMessage"; + $notification->save(); + } + + return; + } + + public function handleFollowActivity() + { + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $target = $this->actorFirstOrCreate($this->payload['object']); + if(!$actor || !$target) { + return; + } + + if($actor->domain == null || $target->domain !== null) { + return; + } + + if( + Follower::whereProfileId($actor->id) + ->whereFollowingId($target->id) + ->exists() || + FollowRequest::whereFollowerId($actor->id) + ->whereFollowingId($target->id) + ->exists() + ) { + return; + } + + $blocks = UserFilterService::blocks($target->id); + if($blocks && in_array($actor->id, $blocks)) { + return; + } + + if($target->is_private == true) { + FollowRequest::updateOrCreate([ + 'follower_id' => $actor->id, + 'following_id' => $target->id, + ],[ + 'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray() + ]); + } else { + $follower = new Follower; + $follower->profile_id = $actor->id; + $follower->following_id = $target->id; + $follower->local_profile = empty($actor->domain); + $follower->save(); + + FollowPipeline::dispatch($follower); + FollowerService::add($actor->id, $target->id); + + // send Accept to remote profile + $accept = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $target->permalink().'#accepts/follows/' . $follower->id, + 'type' => 'Accept', + 'actor' => $target->permalink(), + 'object' => [ + 'id' => $this->payload['id'], + 'actor' => $actor->permalink(), + 'type' => 'Follow', + 'object' => $target->permalink() + ] + ]; + Helpers::sendSignedObject($target, $actor->inbox_url, $accept); + Cache::forget('profile:follower_count:'.$target->id); + Cache::forget('profile:follower_count:'.$actor->id); + Cache::forget('profile:following_count:'.$target->id); + Cache::forget('profile:following_count:'.$actor->id); + } + + return; + } + + public function handleAnnounceActivity() + { + $actor = $this->actorFirstOrCreate($this->payload['actor']); + $activity = $this->payload['object']; + + if(!$actor || $actor->domain == null) { + return; + } + + $parent = Helpers::statusFetch($activity); + + if(!$parent || empty($parent)) { + return; + } + + $blocks = UserFilterService::blocks($parent->profile_id); + if($blocks && in_array($actor->id, $blocks)) { + return; + } + + $status = Status::firstOrCreate([ + 'profile_id' => $actor->id, + 'reblog_of_id' => $parent->id, + 'type' => 'share' + ]); + + Notification::firstOrCreate( + [ + 'profile_id' => $parent->profile_id, + 'actor_id' => $actor->id, + 'action' => 'share', + 'item_id' => $parent->id, + 'item_type' => 'App\Status', + ] + ); + + $parent->reblogs_count = $parent->reblogs_count + 1; + $parent->save(); + + ReblogService::addPostReblog($parent->profile_id, $status->id); + + return; + } + + public function handleAcceptActivity() + { + $actor = $this->payload['object']['actor']; + $obj = $this->payload['object']['object']; + $type = $this->payload['object']['type']; + + if($type !== 'Follow') { + return; + } + + $actor = Helpers::validateLocalUrl($actor); + $target = Helpers::validateUrl($obj); + + if(!$actor || !$target) { + return; + } + + $actor = Helpers::profileFetch($actor); + $target = Helpers::profileFetch($target); + + if(!$actor || !$target) { + return; + } + + $request = FollowRequest::whereFollowerId($actor->id) + ->whereFollowingId($target->id) + ->whereIsRejected(false) + ->first(); + + if(!$request) { + return; + } + + $follower = Follower::firstOrCreate([ + 'profile_id' => $actor->id, + 'following_id' => $target->id, + ]); + FollowPipeline::dispatch($follower); + + $request->delete(); + + return; + } + + public function handleDeleteActivity() + { + if(!isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) { + $profile = Profile::whereRemoteUrl($obj)->first(); + if(!$profile || $profile->private_key != null) { + return; + } + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + return; + } else { + if(!isset( + $obj['id'], + $this->payload['object'], + $this->payload['object']['id'], + $this->payload['object']['type'] + )) { + return; + } + $type = $this->payload['object']['type']; + $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']); + if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) { + return; + } + if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + $id = $this->payload['object']['id']; + switch ($type) { + case 'Person': + $profile = Profile::whereRemoteUrl($actor)->first(); + if(!$profile || $profile->private_key != null) { + return; + } + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('inbox'); + return; + break; + + case 'Tombstone': + $profile = Profile::whereRemoteUrl($actor)->first(); + if(!$profile || $profile->private_key != null) { + return; + } + $status = Status::whereProfileId($profile->id) + ->where(function($q) use($id) { + return $q->where('object_url', $id) + ->orWhere('url', $id); + }) + ->first(); + if(!$status) { + return; + } + if($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { + if($status->type && !in_array($status->type, ['story:reaction', 'story:reply', 'reply'])) { + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + } + } + RemoteStatusDelete::dispatch($status)->onQueue('high'); + return; + break; + + case 'Story': + $story = Story::whereObjectId($id) + ->first(); + if($story) { + StoryExpire::dispatch($story)->onQueue('story'); + } + return; + break; + + default: + return; + break; + } + } + return; + } + + public function handleLikeActivity() + { + $actor = $this->payload['actor']; + + if(!Helpers::validateUrl($actor)) { + return; + } + + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + if(!Helpers::validateUrl($obj)) { + return; + } + $status = Helpers::statusFirstOrFetch($obj); + if(!$status || !$profile) { + return; + } + + $blocks = UserFilterService::blocks($status->profile_id); + if($blocks && in_array($profile->id, $blocks)) { + return; + } + + $like = Like::firstOrCreate([ + 'profile_id' => $profile->id, + 'status_id' => $status->id + ]); + + if($like->wasRecentlyCreated == true) { + $status->likes_count = $status->likes_count + 1; + $status->save(); + LikePipeline::dispatch($like); + } + + return; + } + + public function handleRejectActivity() + { + } + + public function handleUndoActivity() + { + $actor = $this->payload['actor']; + $profile = self::actorFirstOrCreate($actor); + $obj = $this->payload['object']; + + if(!$profile) { + return; + } + // TODO: Some implementations do not inline the object, skip for now + if(!$obj || !is_array($obj) || !isset($obj['type'])) { + return; + } + + switch ($obj['type']) { + case 'Accept': + break; + + case 'Announce': + if(is_array($obj) && isset($obj['object'])) { + $obj = $obj['object']; + } + if(!is_string($obj)) { + return; + } + if(Helpers::validateLocalUrl($obj)) { + $parsedId = last(explode('/', $obj)); + $status = Status::find($parsedId); + } else { + $status = Status::whereUri($obj)->first(); + } + if(!$status) { + return; + } + FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); + Status::whereProfileId($profile->id) + ->whereReblogOfId($status->id) + ->delete(); + ReblogService::removePostReblog($profile->id, $status->id); + Notification::whereProfileId($status->profile_id) + ->whereActorId($profile->id) + ->whereAction('share') + ->whereItemId($status->reblog_of_id) + ->whereItemType('App\Status') + ->forceDelete(); + break; + + case 'Block': + break; + + case 'Follow': + $following = self::actorFirstOrCreate($obj['object']); + if(!$following) { + return; + } + Follower::whereProfileId($profile->id) + ->whereFollowingId($following->id) + ->delete(); + Notification::whereProfileId($following->id) + ->whereActorId($profile->id) + ->whereAction('follow') + ->whereItemId($following->id) + ->whereItemType('App\Profile') + ->forceDelete(); + FollowerService::remove($profile->id, $following->id); + break; + + case 'Like': + $objectUri = $obj['object']; + if(!is_string($objectUri)) { + if(is_array($objectUri) && isset($objectUri['id']) && is_string($objectUri['id'])) { + $objectUri = $objectUri['id']; + } else { + return; + } + } + $status = Helpers::statusFirstOrFetch($objectUri); + if(!$status) { + return; + } + Like::whereProfileId($profile->id) + ->whereStatusId($status->id) + ->forceDelete(); + Notification::whereProfileId($status->profile_id) + ->whereActorId($profile->id) + ->whereAction('like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->forceDelete(); + break; + } + return; + } + + public function handleViewActivity() + { + if(!isset( + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $actor = $this->payload['actor']; + $obj = $this->payload['object']; + + if(!Helpers::validateUrl($actor)) { + return; + } + + if(!$obj || !is_array($obj)) { + return; + } + + if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') { + return; + } + + if(!Helpers::validateLocalUrl($obj['object'])) { + return; + } + + $profile = Helpers::profileFetch($actor); + $storyId = Str::of($obj['object'])->explode('/')->last(); + + $story = Story::whereActive(true) + ->whereLocal(true) + ->find($storyId); + + if(!$story) { + return; + } + + if(!FollowerService::follows($profile->id, $story->profile_id)) { + return; + } + + $view = StoryView::firstOrCreate([ + 'story_id' => $story->id, + 'profile_id' => $profile->id + ]); + + if($view->wasRecentlyCreated == true) { + $story->view_count++; + $story->save(); + } + + return; + } + + public function handleStoryReactionActivity() + { + if(!isset( + $this->payload['actor'], + $this->payload['id'], + $this->payload['inReplyTo'], + $this->payload['content'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + $storyUrl = $this->payload['inReplyTo']; + $to = $this->payload['to']; + $text = Purify::clean($this->payload['content']); + + if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { + return; + } + + if(!Helpers::validateLocalUrl($storyUrl)) { + return; + } + + if(!Helpers::validateLocalUrl($to)) { + return; + } + + if(Status::whereObjectUrl($id)->exists()) { + return; + } + + $storyId = Str::of($storyUrl)->explode('/')->last(); + $targetProfile = Helpers::profileFetch($to); + + $story = Story::whereProfileId($targetProfile->id) + ->find($storyId); + + if(!$story) { + return; + } + + if($story->can_react == false) { + return; + } + + $actorProfile = Helpers::profileFetch($actor); + + if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { + return; + } + + $url = $id; + + if(str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + + $status = new Status; + $status->profile_id = $actorProfile->id; + $status->type = 'story:reaction'; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'reaction' => $text + ]); + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $actorProfile->id; + $dm->type = 'story:react'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $targetProfile->username, + 'story_actor_username' => $actorProfile->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'reaction' => $text + ]); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $actorProfile->id + ], + [ + 'type' => 'story:react', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); + + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:react'; + $n->save(); + + return; + } + + public function handleStoryReplyActivity() + { + if(!isset( + $this->payload['actor'], + $this->payload['id'], + $this->payload['inReplyTo'], + $this->payload['content'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + $storyUrl = $this->payload['inReplyTo']; + $to = $this->payload['to']; + $text = Purify::clean($this->payload['content']); + + if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) { + return; + } + + if(!Helpers::validateLocalUrl($storyUrl)) { + return; + } + + if(!Helpers::validateLocalUrl($to)) { + return; + } + + if(Status::whereObjectUrl($id)->exists()) { + return; + } + + $storyId = Str::of($storyUrl)->explode('/')->last(); + $targetProfile = Helpers::profileFetch($to); + + $story = Story::whereProfileId($targetProfile->id) + ->find($storyId); + + if(!$story) { + return; + } + + if($story->can_react == false) { + return; + } + + $actorProfile = Helpers::profileFetch($actor); + + if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { + return; + } + + $url = $id; + + if(str_ends_with($url, '/activity')) { + $url = substr($url, 0, -9); + } + + $status = new Status; + $status->profile_id = $actorProfile->id; + $status->type = 'story:reply'; + $status->caption = $text; + $status->rendered = $text; + $status->url = $url; + $status->uri = $url; + $status->object_url = $url; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'caption' => $text + ]); + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $actorProfile->id; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $targetProfile->username, + 'story_actor_username' => $actorProfile->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text + ]); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $actorProfile->id + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); + + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + + return; + } + + public function handleFlagActivity() + { + if(!isset( + $this->payload['id'], + $this->payload['type'], + $this->payload['actor'], + $this->payload['object'] + )) { + return; + } + + $id = $this->payload['id']; + $actor = $this->payload['actor']; + + if(Helpers::validateLocalUrl($id) || parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) { + return; + } + + $content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null; + $object = $this->payload['object']; + + if(empty($object) || (!is_array($object) && !is_string($object))) { + return; + } + + if(is_array($object) && count($object) > 100) { + return; + } + + $objects = collect([]); + $accountId = null; + + foreach($object as $objectUrl) { + if(!Helpers::validateLocalUrl($objectUrl)) { + continue; + } + + if(str_contains($objectUrl, '/users/')) { + $username = last(explode('/', $objectUrl)); + $profileId = Profile::whereUsername($username)->first(); + if($profileId) { + $accountId = $profileId->id; + } + } else if(str_contains($objectUrl, '/p/')) { + $postId = last(explode('/', $objectUrl)); + $objects->push($postId); + } else { + continue; + } + } + + if(!$accountId || !$objects->count()) { + return; + } + + $instanceHost = parse_url($id, PHP_URL_HOST); + + $instance = Instance::updateOrCreate([ + 'domain' => $instanceHost + ]); + + $report = new RemoteReport; + $report->status_ids = $objects->toArray(); + $report->comment = $content; + $report->account_id = $accountId; + $report->uri = $id; + $report->instance_id = $instance->id; + $report->report_meta = [ + 'actor' => $actor, + 'object' => $object + ]; + $report->save(); + + return; + } + + public function handleUpdateActivity() + { + $activity = $this->payload['object']; + + if(!isset($activity['type'], $activity['id'])) { + return; + } + + if(!Helpers::validateUrl($activity['id'])) { + return; + } + + if($activity['type'] === 'Note') { + if(Status::whereObjectUrl($activity['id'])->exists()) { + StatusRemoteUpdatePipeline::dispatch($activity); + } + } else if ($activity['type'] === 'Person') { + if(UpdatePersonValidator::validate($this->payload)) { + HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); + } + } + } } From 9818656425c46c0c42d624a8dead09d68a127aa2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 04:27:09 -0700 Subject: [PATCH 095/977] Update DirectMessageController, dispatch local deletes to pipeline --- app/Http/Controllers/DirectMessageController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index 959b944ce..b706572b3 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -17,6 +17,7 @@ use App\{ use App\Services\MediaPathService; use App\Services\MediaBlocklistService; use App\Jobs\StatusPipeline\NewStatusPipeline; +use App\Jobs\StatusPipeline\StatusDelete; use Illuminate\Support\Str; use App\Util\ActivityPub\Helpers; use App\Services\AccountService; @@ -502,6 +503,8 @@ class DirectMessageController extends Controller if($recipient['local'] == false) { $dmc = $dm; $this->remoteDelete($dmc); + } else { + StatusDelete::dispatch($status)->onQueue('high'); } if(Conversation::whereStatusId($sid)->count()) { @@ -543,9 +546,6 @@ class DirectMessageController extends Controller StatusService::del($status->id, true); - $status->delete(); - $dm->delete(); - return [200]; } From 4c95306f1246bba3127e8977667fc6721ee2f66f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 04:44:45 -0700 Subject: [PATCH 096/977] Update StatusPipeline, fix Direct and Story notification deletion --- .../StatusPipeline/RemoteStatusDelete.php | 25 +++++++++++++++++-- app/Jobs/StatusPipeline/StatusDelete.php | 25 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index cb14288a1..78c41ed3d 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -40,6 +40,7 @@ use App\Services\CollectionService; use App\Services\StatusService; use App\Jobs\MediaPipeline\MediaDeletePipeline; use App\Jobs\ProfilePipeline\DecrementPostCount; +use App\Services\NotificationService; class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -137,14 +138,34 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing CollectionService::removeItem($col->collection_id, $col->object_id); $col->delete(); }); - DirectMessage::whereStatusId($status->id)->delete(); + $dms = DirectMessage::whereStatusId($status->id)->get(); + foreach($dms as $dm) { + $not = Notification::whereItemType('App\DirectMessage') + ->whereItemId($dm->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $dm->delete(); + } Like::whereStatusId($status->id)->forceDelete(); Media::whereStatusId($status->id) ->get() ->each(function($media) { MediaDeletePipeline::dispatch($media)->onQueue('mmo'); }); - MediaTag::where('status_id', $status->id)->delete(); + $mediaTags = MediaTag::where('status_id', $status->id)->get(); + foreach($mediaTags as $mtag) { + $not = Notification::whereItemType('App\MediaTag') + ->whereItemId($mtag->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $mtag->delete(); + } Mention::whereStatusId($status->id)->forceDelete(); Notification::whereItemType('App\Status') ->whereItemId($status->id) diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index 5b200fdf0..c0ced1368 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -35,6 +35,7 @@ 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 @@ -115,10 +116,30 @@ class StatusDelete implements ShouldQueue $col->delete(); }); - DirectMessage::whereStatusId($status->id)->delete(); + $dms = DirectMessage::whereStatusId($status->id)->get(); + foreach($dms as $dm) { + $not = Notification::whereItemType('App\DirectMessage') + ->whereItemId($dm->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $dm->delete(); + } Like::whereStatusId($status->id)->delete(); - MediaTag::where('status_id', $status->id)->delete(); + $mediaTags = MediaTag::where('status_id', $status->id)->get(); + foreach($mediaTags as $mtag) { + $not = Notification::whereItemType('App\MediaTag') + ->whereItemId($mtag->id) + ->first(); + if($not) { + NotificationService::del($not->profile_id, $not->id); + $not->forceDeleteQuietly(); + } + $mtag->delete(); + } Mention::whereStatusId($status->id)->forceDelete(); Notification::whereItemType('App\Status') From 4c3823b0c428e17a872edfd5da52782cd7261aef Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 04:51:29 -0700 Subject: [PATCH 097/977] Update Notifications.vue, fix deprecated DM action links for story activities --- resources/assets/components/sections/Notifications.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/assets/components/sections/Notifications.vue b/resources/assets/components/sections/Notifications.vue index b2c904184..1ddf522fc 100644 --- a/resources/assets/components/sections/Notifications.vue +++ b/resources/assets/components/sections/Notifications.vue @@ -87,12 +87,12 @@
@@ -216,7 +216,7 @@ methods: { init() { - if(this.retryAttempts == 3) { + if(this.retryAttempts == 1) { this.hasLoaded = true; this.isEmpty = true; clearTimeout(this.retryTimeout); From 0a0681199f985005467ab21a41564637dfd7d0cb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 04:56:21 -0700 Subject: [PATCH 098/977] Update ComposeModal, fix missing alttext post state --- resources/assets/js/components/ComposeModal.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 9ca6e0419..4ffd84666 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -1347,6 +1347,7 @@ export default { if(count.length) { swal('Missing media descriptions', 'You have enabled mandatory media descriptions. Please add media descriptions under Advanced settings to proceed. For more information, please see the media settings page.', 'warning'); + this.isPosting = false; return; } } From 822e9888bb08a89197c3621e80a64cee6bdbfbe8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 05:01:04 -0700 Subject: [PATCH 099/977] Update PhotoAlbumPresenter.vue, fix fullscreen mode --- .../presenter/PhotoAlbumPresenter.vue | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/resources/assets/js/components/presenter/PhotoAlbumPresenter.vue b/resources/assets/js/components/presenter/PhotoAlbumPresenter.vue index c83f11e20..3adda10df 100644 --- a/resources/assets/js/components/presenter/PhotoAlbumPresenter.vue +++ b/resources/assets/js/components/presenter/PhotoAlbumPresenter.vue @@ -26,8 +26,7 @@ .card-img-top { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; } .content-label-wrapper { - position: relative; + position: relative; } .content-label { - margin: 0; - position: absolute; - top:50%; - left:50%; - transform: translate(-50%, -50%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - z-index: 2; - background: rgba(0, 0, 0, 0.2) + margin: 0; + position: absolute; + top:50%; + left:50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + z-index: 2; + background: rgba(0, 0, 0, 0.2) } .album-wrapper { - position: relative; + position: relative; } From 9c43e7e26505700f98cd094d7b0b6cd00205b531 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 05:25:03 -0700 Subject: [PATCH 100/977] Update Timeline.vue, improve CHT pagination --- .../assets/components/sections/Timeline.vue | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/resources/assets/components/sections/Timeline.vue b/resources/assets/components/sections/Timeline.vue index dd2c7d2f0..6b064acea 100644 --- a/resources/assets/components/sections/Timeline.vue +++ b/resources/assets/components/sections/Timeline.vue @@ -186,7 +186,8 @@ sharesModalPost: {}, forceUpdateIdx: 0, showReblogBanner: false, - enablingReblogs: false + enablingReblogs: false, + baseApi: '/api/v1/pixelfed/timelines/', } }, @@ -201,6 +202,10 @@ return; }; } + if(window.App.config.ab.hasOwnProperty('cached_home_timeline')) { + const cht = window.App.config.ab.cached_home_timeline == true; + this.baseApi = cht ? '/api/v1/timelines/' : '/api/pixelfed/v1/timelines/'; + } this.fetchSettings(); }, @@ -247,7 +252,7 @@ fetchTimeline(scrollToTop = false) { let url, params; if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) { - url = `/api/v1/timelines/home`; + url = this.baseApi + `home`; params = { '_pe': 1, max_id: this.max_id, @@ -255,12 +260,17 @@ include_reblogs: true, } } else { - url = `/api/pixelfed/v1/timelines/${this.getScope()}`; + url = this.baseApi + this.getScope(); params = { max_id: this.max_id, limit: 6, + '_pe': 1, } } + if(this.getScope() === 'network') { + params.remote = true; + url = this.baseApi + `public`; + } axios.get(url, { params: params }).then(res => { @@ -278,7 +288,7 @@ this.max_id = Math.min(...ids); this.feed = res.data; - if(res.data.length !== 6) { + if(res.data.length < 4) { this.canLoadMore = false; this.showLoadMore = true; } @@ -306,7 +316,8 @@ let url, params; if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) { - url = `/api/v1/timelines/home`; + url = this.baseApi + `home`; + params = { '_pe': 1, max_id: this.max_id, @@ -314,12 +325,18 @@ include_reblogs: true, } } else { - url = `/api/pixelfed/v1/timelines/${this.getScope()}`; + url = this.baseApi + this.getScope(); params = { max_id: this.max_id, limit: 6, + '_pe': 1, } } + if(this.getScope() === 'network') { + params.remote = true; + url = this.baseApi + `public`; + + } axios.get(url, { params: params }).then(res => { From ed5e956a542521d82e28e538c3bdcf50a36329d4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 05:25:44 -0700 Subject: [PATCH 101/977] Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa36a529..4453cdec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,15 @@ - Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) - Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96)) - Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce)) +- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a)) +- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80)) +- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c)) +- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564)) +- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f)) +- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0)) +- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119)) +- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888)) +- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 38fee418a936998bd8257f7e9ef886bcf6d0da0f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 8 Dec 2023 05:48:04 -0700 Subject: [PATCH 102/977] Update DirectMessageController --- app/Http/Controllers/DirectMessageController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index b706572b3..df76d2ab9 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -546,6 +546,7 @@ class DirectMessageController extends Controller StatusService::del($status->id, true); + $status->forceDeleteQuietly(); return [200]; } From 041c01359bb40471e9ca467bac922ce95045e909 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 9 Dec 2023 23:53:02 -0700 Subject: [PATCH 103/977] Update HomeFeedPipeline, fix StatusService validation --- app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php | 2 +- app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php | 2 +- app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php | 2 +- app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index de7032faf..19a546e83 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -69,7 +69,7 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing $sid = $this->sid; $status = StatusService::get($sid, false); - if(!$status) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { return; } diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php index 738c09699..e24696bd8 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php @@ -69,7 +69,7 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces $sid = $this->sid; $status = StatusService::get($sid, false); - if(!$status) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { return; } diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php index 2e6bf4758..a200c06e8 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -77,7 +77,7 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro $sid = $hashtag->status_id; $status = StatusService::get($sid, false); - if(!$status) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { return; } diff --git a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php index ea9d87fbb..f1968e120 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagRemoveFanoutPipeline.php @@ -71,7 +71,7 @@ class HashtagRemoveFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro $hid = $this->hid; $status = StatusService::get($sid, false); - if(!$status) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { return; } From 33dbbe467d1f4652efd448878aea1323ab921aff Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 11 Dec 2023 01:34:46 -0700 Subject: [PATCH 104/977] Add Mutual Followers API endpoint --- .../Controllers/Api/ApiV1Dot1Controller.php | 16 + app/Services/FollowerService.php | 389 +++++++++--------- routes/api.php | 14 +- 3 files changed, 221 insertions(+), 198 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 298deb705..75d0fe984 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -20,6 +20,7 @@ use App\StatusArchived; use App\User; use App\UserSetting; use App\Services\AccountService; +use App\Services\FollowerService; use App\Services\StatusService; use App\Services\ProfileStatusService; use App\Services\LikeService; @@ -897,4 +898,19 @@ class ApiV1Dot1Controller extends Controller return [200]; } + + public function getMutualAccounts(Request $request, $id) + { + abort_if(!$request->user(), 403); + $account = AccountService::get($id, true); + if(!$account || !isset($account['id'])) { return []; } + $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id)) + ->map(function($accountId) { + return AccountService::get($accountId, true); + }) + ->filter() + ->take(24) + ->values(); + return $this->json($res); + } } diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 5525a5da2..8b5eeced9 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -6,222 +6,239 @@ use Illuminate\Support\Facades\Redis; use Cache; use DB; use App\{ - Follower, - Profile, - User + Follower, + Profile, + User }; use App\Jobs\FollowPipeline\FollowServiceWarmCache; class FollowerService { - const CACHE_KEY = 'pf:services:followers:'; - const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; - const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; - const FOLLOWING_KEY = 'pf:services:follow:following:id:'; - const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; - const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; + const CACHE_KEY = 'pf:services:followers:'; + const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:'; + const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:'; + const FOLLOWING_KEY = 'pf:services:follow:following:id:'; + const FOLLOWERS_KEY = 'pf:services:follow:followers:id:'; + const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:'; + const FOLLOWERS_INTER_KEY = 'pf:services:follow:followers:inter:id:'; - public static function add($actor, $target, $refresh = true) - { - $ts = (int) microtime(true); + public static function add($actor, $target, $refresh = true) + { + $ts = (int) microtime(true); if($refresh) { RelationshipService::refresh($actor, $target); } else { - RelationshipService::forget($actor, $target); + RelationshipService::forget($actor, $target); } - Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); - Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); - Cache::forget('profile:following:' . $actor); - } + Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target); + Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor); + Cache::forget('profile:following:' . $actor); + } - public static function remove($actor, $target) - { - Redis::zrem(self::FOLLOWING_KEY . $actor, $target); - Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); - Cache::forget('pf:services:follower:audience:' . $actor); - Cache::forget('pf:services:follower:audience:' . $target); - AccountService::del($actor); - AccountService::del($target); - RelationshipService::refresh($actor, $target); - Cache::forget('profile:following:' . $actor); - } + public static function remove($actor, $target) + { + Redis::zrem(self::FOLLOWING_KEY . $actor, $target); + Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); + Cache::forget('pf:services:follower:audience:' . $actor); + Cache::forget('pf:services:follower:audience:' . $target); + AccountService::del($actor); + AccountService::del($target); + RelationshipService::refresh($actor, $target); + Cache::forget('profile:following:' . $actor); + } - public static function followers($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'followers'); - return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); - } + public static function followers($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'followers'); + return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop); + } - public static function following($id, $start = 0, $stop = 10) - { - self::cacheSyncCheck($id, 'following'); - return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); - } + public static function following($id, $start = 0, $stop = 10) + { + self::cacheSyncCheck($id, 'following'); + return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop); + } - public static function followersPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::followers($id, $start, $end); - } + public static function followersPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::followers($id, $start, $end); + } - public static function followingPaginate($id, $page = 1, $limit = 10) - { - $start = $page == 1 ? 0 : $page * $limit - $limit; - $end = $start + ($limit - 1); - return self::following($id, $start, $end); - } + public static function followingPaginate($id, $page = 1, $limit = 10) + { + $start = $page == 1 ? 0 : $page * $limit - $limit; + $end = $start + ($limit - 1); + return self::following($id, $start, $end); + } - public static function followerCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'followers'); - } - return Redis::zCard(self::FOLLOWERS_KEY . $id); - } + public static function followerCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'followers'); + } + return Redis::zCard(self::FOLLOWERS_KEY . $id); + } - public static function followingCount($id, $warmCache = true) - { - if($warmCache) { - self::cacheSyncCheck($id, 'following'); - } - return Redis::zCard(self::FOLLOWING_KEY . $id); - } + public static function followingCount($id, $warmCache = true) + { + if($warmCache) { + self::cacheSyncCheck($id, 'following'); + } + return Redis::zCard(self::FOLLOWING_KEY . $id); + } - public static function follows(string $actor, string $target) - { - if($actor == $target) { - return false; - } + public static function follows(string $actor, string $target) + { + if($actor == $target) { + return false; + } - if(self::followerCount($target, false) && self::followingCount($actor, false)) { - self::cacheSyncCheck($target, 'followers'); - return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); - } else { - self::cacheSyncCheck($target, 'followers'); - self::cacheSyncCheck($actor, 'following'); - return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); - } - } + if(self::followerCount($target, false) && self::followingCount($actor, false)) { + self::cacheSyncCheck($target, 'followers'); + return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); + } else { + self::cacheSyncCheck($target, 'followers'); + self::cacheSyncCheck($actor, 'following'); + return Follower::whereProfileId($actor)->whereFollowingId($target)->exists(); + } + } - public static function cacheSyncCheck($id, $scope = 'followers') - { - if($scope === 'followers') { - if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - if($scope === 'following') { - if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { - return; - } - FollowServiceWarmCache::dispatch($id)->onQueue('low'); - } - return; - } + public static function cacheSyncCheck($id, $scope = 'followers') + { + if($scope === 'followers') { + if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + if($scope === 'following') { + if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) { + return; + } + FollowServiceWarmCache::dispatch($id)->onQueue('low'); + } + return; + } - public static function audience($profile, $scope = null) - { - return (new self)->getAudienceInboxes($profile, $scope); - } + public static function audience($profile, $scope = null) + { + return (new self)->getAudienceInboxes($profile, $scope); + } - public static function softwareAudience($profile, $software = 'pixelfed') - { - return collect(self::audience($profile)) - ->filter(function($inbox) use($software) { - $domain = parse_url($inbox, PHP_URL_HOST); - if(!$domain) { - return false; - } - return InstanceService::software($domain) === strtolower($software); - }) - ->unique() - ->values() - ->toArray(); - } + public static function softwareAudience($profile, $software = 'pixelfed') + { + return collect(self::audience($profile)) + ->filter(function($inbox) use($software) { + $domain = parse_url($inbox, PHP_URL_HOST); + if(!$domain) { + return false; + } + return InstanceService::software($domain) === strtolower($software); + }) + ->unique() + ->values() + ->toArray(); + } - protected function getAudienceInboxes($pid, $scope = null) - { - $key = 'pf:services:follower:audience:' . $pid; - $domains = Cache::remember($key, 432000, function() use($pid) { - $profile = Profile::whereNull(['status', 'domain'])->find($pid); - if(!$profile) { - return []; - } - return $profile - ->followers() - ->get() - ->map(function($follow) { - return $follow->sharedInbox ?? $follow->inbox_url; - }) - ->filter() - ->unique() - ->values(); - }); + protected function getAudienceInboxes($pid, $scope = null) + { + $key = 'pf:services:follower:audience:' . $pid; + $domains = Cache::remember($key, 432000, function() use($pid) { + $profile = Profile::whereNull(['status', 'domain'])->find($pid); + if(!$profile) { + return []; + } + return $profile + ->followers() + ->get() + ->map(function($follow) { + return $follow->sharedInbox ?? $follow->inbox_url; + }) + ->filter() + ->unique() + ->values(); + }); - if(!$domains || !$domains->count()) { - return []; - } + if(!$domains || !$domains->count()) { + return []; + } - $banned = InstanceService::getBannedDomains(); + $banned = InstanceService::getBannedDomains(); - if(!$banned || count($banned) === 0) { - return $domains->toArray(); - } + if(!$banned || count($banned) === 0) { + return $domains->toArray(); + } - $res = $domains->filter(function($domain) use($banned) { - $parsed = parse_url($domain, PHP_URL_HOST); - return !in_array($parsed, $banned); - }) - ->values() - ->toArray(); + $res = $domains->filter(function($domain) use($banned) { + $parsed = parse_url($domain, PHP_URL_HOST); + return !in_array($parsed, $banned); + }) + ->values() + ->toArray(); - return $res; - } + return $res; + } - public static function mutualCount($pid, $mid) - { - return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->count(); - }); - } + public static function mutualCount($pid, $mid) + { + return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->count(); + }); + } - public static function mutualIds($pid, $mid, $limit = 3) - { - $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; - return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { - return DB::table('followers as u') - ->join('followers as s', 'u.following_id', '=', 's.following_id') - ->where('s.profile_id', $mid) - ->where('u.profile_id', $pid) - ->limit($limit) - ->pluck('s.following_id') - ->toArray(); - }); - } + public static function mutualIds($pid, $mid, $limit = 3) + { + $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit; + return Cache::remember($key, 3600, function() use($pid, $mid, $limit) { + return DB::table('followers as u') + ->join('followers as s', 'u.following_id', '=', 's.following_id') + ->where('s.profile_id', $mid) + ->where('u.profile_id', $pid) + ->limit($limit) + ->pluck('s.following_id') + ->toArray(); + }); + } - public static function delCache($id) - { - Redis::del(self::CACHE_KEY . $id); - Redis::del(self::FOLLOWING_KEY . $id); - Redis::del(self::FOLLOWERS_KEY . $id); - Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); - Cache::forget(self::FOLLOWING_SYNC_KEY . $id); - } + public static function mutualAccounts($actorId, $profileId) + { + if($actorId == $profileId) { + return []; + } + $actorKey = self::FOLLOWING_KEY . $actorId; + $profileKey = self::FOLLOWERS_KEY . $profileId; + $key = self::FOLLOWERS_INTER_KEY . $actorId . ':' . $profileId; + $res = Redis::zinterstore($key, [$actorKey, $profileKey]); + if($res) { + return Redis::zrange($key, 0, -1); + } else { + return []; + } + } - public static function localFollowerIds($pid, $limit = 0) - { - $key = self::FOLLOWERS_LOCAL_KEY . $pid; - $res = Cache::remember($key, 7200, function() use($pid) { - return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); - }); - return $limit ? - $res->take($limit)->values()->toArray() : - $res->values()->toArray(); - } + public static function delCache($id) + { + Redis::del(self::CACHE_KEY . $id); + Redis::del(self::FOLLOWING_KEY . $id); + Redis::del(self::FOLLOWERS_KEY . $id); + Cache::forget(self::FOLLOWERS_SYNC_KEY . $id); + Cache::forget(self::FOLLOWING_SYNC_KEY . $id); + } + + public static function localFollowerIds($pid, $limit = 0) + { + $key = self::FOLLOWERS_LOCAL_KEY . $pid; + $res = Cache::remember($key, 7200, function() use($pid) { + return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort(); + }); + return $limit ? + $res->take($limit)->values()->toArray() : + $res->values()->toArray(); + } } diff --git a/routes/api.php b/routes/api.php index f1e5e7bd1..10a4363c2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -111,12 +111,9 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'v1.1'], function() use($middleware) { - $reportMiddleware = $middleware; - $reportMiddleware[] = DeprecatedEndpoint::class; - Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($reportMiddleware); + Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($middleware); Route::group(['prefix' => 'accounts'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('timelines/home', 'Api\ApiV1Controller@timelineHome')->middleware($middleware); Route::delete('avatar', 'Api\ApiV1Dot1Controller@deleteAvatar')->middleware($middleware); Route::get('{id}/posts', 'Api\ApiV1Dot1Controller@accountPosts')->middleware($middleware); @@ -125,10 +122,10 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('two-factor', 'Api\ApiV1Dot1Controller@accountTwoFactor')->middleware($middleware); Route::get('emails-from-pixelfed', 'Api\ApiV1Dot1Controller@accountEmailsFromPixelfed')->middleware($middleware); Route::get('apps-and-applications', 'Api\ApiV1Dot1Controller@accountApps')->middleware($middleware); + Route::get('mutuals/{id}', 'Api\ApiV1Dot1Controller@getMutualAccounts')->middleware($middleware); }); Route::group(['prefix' => 'collections'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('accounts/{id}', 'CollectionController@getUserCollections')->middleware($middleware); Route::get('items/{id}', 'CollectionController@getItems')->middleware($middleware); Route::get('view/{id}', 'CollectionController@getCollection')->middleware($middleware); @@ -139,7 +136,6 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'direct'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('thread', 'DirectMessageController@thread')->middleware($middleware); Route::post('thread/send', 'DirectMessageController@create')->middleware($middleware); Route::delete('thread/message', 'DirectMessageController@delete')->middleware($middleware); @@ -151,19 +147,16 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'archive'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::post('add/{id}', 'Api\ApiV1Dot1Controller@archive')->middleware($middleware); Route::post('remove/{id}', 'Api\ApiV1Dot1Controller@unarchive')->middleware($middleware); Route::get('list', 'Api\ApiV1Dot1Controller@archivedPosts')->middleware($middleware); }); Route::group(['prefix' => 'places'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('posts/{id}/{slug}', 'Api\ApiV1Dot1Controller@placesById')->middleware($middleware); }); Route::group(['prefix' => 'stories'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('carousel', 'Stories\StoryApiV1Controller@carousel')->middleware($middleware); Route::post('add', 'Stories\StoryApiV1Controller@add')->middleware($middleware); Route::post('publish', 'Stories\StoryApiV1Controller@publish')->middleware($middleware); @@ -173,20 +166,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { }); Route::group(['prefix' => 'compose'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('search/location', 'ComposeController@searchLocation')->middleware($middleware); Route::get('settings', 'ComposeController@composeSettings')->middleware($middleware); }); Route::group(['prefix' => 'discover'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular')->middleware($middleware); Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware); Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware); }); Route::group(['prefix' => 'directory'], function () use($middleware) { - $middleware[] = DeprecatedEndpoint::class; Route::get('listing', 'PixelfedDirectoryController@get'); }); From 6dceb6f05b8d742c569da99b646749858698896b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 11 Dec 2023 01:37:11 -0700 Subject: [PATCH 105/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4453cdec6..86b3f52e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb)) - Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6)) - Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7)) +- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) @@ -75,6 +76,7 @@ - Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119)) - Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888)) - Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2)) +- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 759a439334d67d0ef60dbce2077e0148dead3c95 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 11 Dec 2023 04:09:33 -0700 Subject: [PATCH 106/977] Update Inbox, improve tombstone query efficiency --- app/Util/ActivityPub/Inbox.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 05049a66f..0dd4722e9 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -404,7 +404,7 @@ class Inbox $status->uri = $activity['id']; $status->object_url = $activity['id']; $status->in_reply_to_profile_id = $profile->id; - $status->save(); + $status->saveQuietly(); $dm = new DirectMessage; $dm->to_id = $profile->id; @@ -704,13 +704,15 @@ class Inbox if(!$profile || $profile->private_key != null) { return; } - $status = Status::whereProfileId($profile->id) - ->where(function($q) use($id) { - return $q->where('object_url', $id) - ->orWhere('url', $id); - }) - ->first(); + + $status = Status::where('object_url', $id)->first(); if(!$status) { + $status = Status::where('url', $id)->first(); + if(!$status) { + return; + } + } + if($status->profile_id != $profile->id) { return; } if($status->scope && in_array($status->scope, ['public', 'unlisted', 'private'])) { From 8d98e3dc971ddfcebd5a58ee84e66ab0d9b6ad90 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 11 Dec 2023 04:10:13 -0700 Subject: [PATCH 107/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b3f52e3..f6e608d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ - Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888)) - Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2)) - Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135)) +- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From ff92015c87817f90e0f7140bd5cb2694a21cf0c1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 12 Dec 2023 23:07:25 -0700 Subject: [PATCH 108/977] Add migration --- ...d_uploaded_to_s3_to_import_posts_table.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php diff --git a/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php b/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php new file mode 100644 index 000000000..c7d5cdcbd --- /dev/null +++ b/database/migrations/2023_12_13_060425_add_uploaded_to_s3_to_import_posts_table.php @@ -0,0 +1,28 @@ +boolean('uploaded_to_s3')->default(false)->index()->after('skip_missing_media'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('import_posts', function (Blueprint $table) { + $table->dropColumn('uploaded_to_s3'); + }); + } +}; From 85839b220ad801747a2dac813e7e01ef2b160077 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 13 Dec 2023 04:46:49 -0700 Subject: [PATCH 109/977] Update cache/session config --- config/cache.php | 24 ++++++++++++++++++++++++ config/session.php | 27 ++++++++++++++++++++------- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/config/cache.php b/config/cache.php index b2a854623..452e3dd4d 100644 --- a/config/cache.php +++ b/config/cache.php @@ -36,17 +36,20 @@ return [ 'array' => [ 'driver' => 'array', + 'serialize' => false, ], 'database' => [ 'driver' => 'database', 'table' => 'cache', 'connection' => null, + 'lock_connection' => null, ], 'file' => [ 'driver' => 'file', 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), ], 'memcached' => [ @@ -70,6 +73,7 @@ return [ 'redis' => [ 'driver' => 'redis', + 'lock_connection' => 'default', 'client' => env('REDIS_CLIENT', 'phpredis'), 'default' => [ @@ -83,6 +87,25 @@ return [ ], + 'redis:session' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'prefix' => 'pf_session', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + ], /* @@ -101,4 +124,5 @@ return [ str_slug(env('APP_NAME', 'laravel'), '_').'_cache' ), + 'limiter' => env('CACHE_LIMITER_DRIVER', 'redis'), ]; diff --git a/config/session.php b/config/session.php index 1b692e3a4..d3e982bd4 100644 --- a/config/session.php +++ b/config/session.php @@ -70,7 +70,7 @@ return [ | */ - 'connection' => null, + 'connection' => env('SESSION_CONNECTION'), /* |-------------------------------------------------------------------------- @@ -96,7 +96,7 @@ return [ | */ - 'store' => null, + 'store' => env('SESSION_STORE'), /* |-------------------------------------------------------------------------- @@ -109,7 +109,7 @@ return [ | */ - 'lottery' => [2, 1000], + 'lottery' => [2, 100], /* |-------------------------------------------------------------------------- @@ -161,7 +161,7 @@ return [ | */ - 'secure' => true, + 'secure' => env('SESSION_SECURE_COOKIE', true), /* |-------------------------------------------------------------------------- @@ -183,12 +183,25 @@ return [ | | This option determines how your cookies behave when cross-site requests | take place, and can be used to mitigate CSRF attacks. By default, we - | do not enable this as other CSRF protection services are in place. + | will set this value to "lax" since this is a secure default value. | - | Supported: "lax", "strict" + | Supported: "lax", "strict", "none", null | */ - 'same_site' => null, + 'same_site' => env('SESSION_SAME_SITE_COOKIES', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => false, ]; From ebbd98e743b317232300d4089ac1f9c53184da99 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 13 Dec 2023 06:30:05 -0700 Subject: [PATCH 110/977] Update AccountService, add setLastActive method --- app/Services/AccountService.php | 376 +++++++++++++++++--------------- 1 file changed, 195 insertions(+), 181 deletions(-) diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index ea64855ce..fa1613cae 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -15,209 +15,223 @@ use Illuminate\Support\Str; class AccountService { - const CACHE_KEY = 'pf:services:account:'; + const CACHE_KEY = 'pf:services:account:'; - public static function get($id, $softFail = false) - { - $res = Cache::remember(self::CACHE_KEY . $id, 43200, function() use($id) { - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $profile = Profile::find($id); - if(!$profile || $profile->status === 'delete') { - return null; - } - $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); - return $fractal->createData($resource)->toArray(); - }); + public static function get($id, $softFail = false) + { + $res = Cache::remember(self::CACHE_KEY . $id, 43200, function() use($id) { + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $profile = Profile::find($id); + if(!$profile || $profile->status === 'delete') { + return null; + } + $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); + return $fractal->createData($resource)->toArray(); + }); - if(!$res) { - return $softFail ? null : abort(404); - } - return $res; - } + if(!$res) { + return $softFail ? null : abort(404); + } + return $res; + } - public static function getMastodon($id, $softFail = false) - { - $account = self::get($id, $softFail); - if(!$account) { - return null; - } + public static function getMastodon($id, $softFail = false) + { + $account = self::get($id, $softFail); + if(!$account) { + return null; + } - if(config('exp.emc') == false) { - return $account; - } + if(config('exp.emc') == false) { + return $account; + } - unset( - $account['header_bg'], - $account['is_admin'], - $account['last_fetched_at'], - $account['local'], - $account['location'], - $account['note_text'], - $account['pronouns'], - $account['website'] - ); + unset( + $account['header_bg'], + $account['is_admin'], + $account['last_fetched_at'], + $account['local'], + $account['location'], + $account['note_text'], + $account['pronouns'], + $account['website'] + ); - $account['avatar_static'] = $account['avatar']; - $account['bot'] = false; - $account['emojis'] = []; - $account['fields'] = []; - $account['header'] = url('/storage/headers/missing.png'); - $account['header_static'] = url('/storage/headers/missing.png'); - $account['last_status_at'] = null; + $account['avatar_static'] = $account['avatar']; + $account['bot'] = false; + $account['emojis'] = []; + $account['fields'] = []; + $account['header'] = url('/storage/headers/missing.png'); + $account['header_static'] = url('/storage/headers/missing.png'); + $account['last_status_at'] = null; - return $account; - } + return $account; + } - public static function del($id) - { - Cache::forget('pf:activitypub:user-object:by-id:' . $id); - return Cache::forget(self::CACHE_KEY . $id); - } + public static function del($id) + { + Cache::forget('pf:activitypub:user-object:by-id:' . $id); + return Cache::forget(self::CACHE_KEY . $id); + } - public static function settings($id) - { - return Cache::remember('profile:compose:settings:' . $id, 604800, function() use($id) { - $settings = UserSetting::whereUserId($id)->first(); - if(!$settings) { - return self::defaultSettings(); - } - return collect($settings) - ->filter(function($item, $key) { - return in_array($key, array_keys(self::defaultSettings())) == true; - }) - ->map(function($item, $key) { - if($key == 'compose_settings') { - $cs = self::defaultSettings()['compose_settings']; - $ms = is_array($item) ? $item : []; - return array_merge($cs, $ms); - } + public static function settings($id) + { + return Cache::remember('profile:compose:settings:' . $id, 604800, function() use($id) { + $settings = UserSetting::whereUserId($id)->first(); + if(!$settings) { + return self::defaultSettings(); + } + return collect($settings) + ->filter(function($item, $key) { + return in_array($key, array_keys(self::defaultSettings())) == true; + }) + ->map(function($item, $key) { + if($key == 'compose_settings') { + $cs = self::defaultSettings()['compose_settings']; + $ms = is_array($item) ? $item : []; + return array_merge($cs, $ms); + } - if($key == 'other') { - $other = self::defaultSettings()['other']; - $mo = is_array($item) ? $item : []; - return array_merge($other, $mo); - } - return $item; - }); - }); - } + if($key == 'other') { + $other = self::defaultSettings()['other']; + $mo = is_array($item) ? $item : []; + return array_merge($other, $mo); + } + return $item; + }); + }); + } - public static function canEmbed($id) - { - return self::settings($id)['other']['disable_embeds'] == false; - } + public static function canEmbed($id) + { + return self::settings($id)['other']['disable_embeds'] == false; + } - public static function defaultSettings() - { - return [ - 'crawlable' => true, - 'public_dm' => false, - 'reduce_motion' => false, - 'high_contrast_mode' => false, - 'video_autoplay' => false, - 'show_profile_follower_count' => true, - 'show_profile_following_count' => true, - 'compose_settings' => [ - 'default_scope' => 'public', - 'default_license' => 1, - 'media_descriptions' => false - ], - 'other' => [ - 'advanced_atom' => false, - 'disable_embeds' => false, - 'mutual_mention_notifications' => false, - 'hide_collections' => false, - 'hide_like_counts' => false, - 'hide_groups' => false, - 'hide_stories' => false, - 'disable_cw' => false, - ] - ]; - } + public static function defaultSettings() + { + return [ + 'crawlable' => true, + 'public_dm' => false, + 'reduce_motion' => false, + 'high_contrast_mode' => false, + 'video_autoplay' => false, + 'show_profile_follower_count' => true, + 'show_profile_following_count' => true, + 'compose_settings' => [ + 'default_scope' => 'public', + 'default_license' => 1, + 'media_descriptions' => false + ], + 'other' => [ + 'advanced_atom' => false, + 'disable_embeds' => false, + 'mutual_mention_notifications' => false, + 'hide_collections' => false, + 'hide_like_counts' => false, + 'hide_groups' => false, + 'hide_stories' => false, + 'disable_cw' => false, + ] + ]; + } - public static function syncPostCount($id) - { - $profile = Profile::find($id); + public static function syncPostCount($id) + { + $profile = Profile::find($id); - if(!$profile) { - return false; - } + if(!$profile) { + return false; + } - $key = self::CACHE_KEY . 'pcs:' . $id; + $key = self::CACHE_KEY . 'pcs:' . $id; - if(Cache::has($key)) { - return; - } + if(Cache::has($key)) { + return; + } - $count = Status::whereProfileId($id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->whereIn('scope', ['public', 'unlisted', 'private']) - ->count(); + $count = Status::whereProfileId($id) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->whereIn('scope', ['public', 'unlisted', 'private']) + ->count(); - $profile->status_count = $count; - $profile->save(); + $profile->status_count = $count; + $profile->save(); - Cache::put($key, 1, 900); - return true; - } + Cache::put($key, 1, 900); + return true; + } - public static function usernameToId($username) - { - $key = self::CACHE_KEY . 'u2id:' . hash('sha256', $username); - return Cache::remember($key, 900, function() use($username) { - $s = Str::of($username); - if($s->contains('@') && !$s->startsWith('@')) { - $username = "@{$username}"; - } - $profile = DB::table('profiles') - ->whereUsername($username) - ->first(); - if(!$profile) { - return null; - } - return (string) $profile->id; - }); - } + public static function usernameToId($username) + { + $key = self::CACHE_KEY . 'u2id:' . hash('sha256', $username); + return Cache::remember($key, 14400, function() use($username) { + $s = Str::of($username); + if($s->contains('@') && !$s->startsWith('@')) { + $username = "@{$username}"; + } + $profile = DB::table('profiles') + ->whereUsername($username) + ->first(); + if(!$profile) { + return null; + } + return (string) $profile->id; + }); + } - public static function hiddenFollowers($id) - { - $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { - return false; - } + public static function hiddenFollowers($id) + { + $account = self::get($id, true); + if(!$account || !isset($account['local']) || $account['local'] == false) { + return false; + } - return Cache::remember('pf:acct:settings:hidden-followers:' . $id, 43200, function() use($id) { - $user = User::whereProfileId($id)->first(); - if(!$user) { - return false; - } - $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { - return $settings->show_profile_follower_count == false; - } - return false; - }); - } + return Cache::remember('pf:acct:settings:hidden-followers:' . $id, 43200, function() use($id) { + $user = User::whereProfileId($id)->first(); + if(!$user) { + return false; + } + $settings = UserSetting::whereUserId($user->id)->first(); + if($settings) { + return $settings->show_profile_follower_count == false; + } + return false; + }); + } - public static function hiddenFollowing($id) - { - $account = self::get($id, true); - if(!$account || !isset($account['local']) || $account['local'] == false) { - return false; - } + public static function hiddenFollowing($id) + { + $account = self::get($id, true); + if(!$account || !isset($account['local']) || $account['local'] == false) { + return false; + } - return Cache::remember('pf:acct:settings:hidden-following:' . $id, 43200, function() use($id) { - $user = User::whereProfileId($id)->first(); - if(!$user) { - return false; - } - $settings = UserSetting::whereUserId($user->id)->first(); - if($settings) { - return $settings->show_profile_following_count == false; - } - return false; - }); - } + return Cache::remember('pf:acct:settings:hidden-following:' . $id, 43200, function() use($id) { + $user = User::whereProfileId($id)->first(); + if(!$user) { + return false; + } + $settings = UserSetting::whereUserId($user->id)->first(); + if($settings) { + return $settings->show_profile_following_count == false; + } + return false; + }); + } + + public static function setLastActive($id = false) + { + if(!$id) { return; } + $key = 'user:last_active_at:id:' . $id; + if(!Cache::has($key)) { + $user = User::find($id); + if(!$user) { return; } + $user->last_active_at = now(); + $user->save(); + Cache::put($key, 1, 14400); + } + return; + } } From b6419545491623648e410b5a73b10887325ece3d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 13 Dec 2023 06:49:00 -0700 Subject: [PATCH 111/977] Update ApiV1Controller, set last_active_at --- app/Http/Controllers/Api/ApiV1Controller.php | 61 ++++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 84569fa5b..f5e1202c0 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -189,6 +189,7 @@ class ApiV1Controller extends Controller abort_if(!$user, 403); abort_if($user->status != null, 403); + AccountService::setLastActive($user->id); $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id); @@ -247,6 +248,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + AccountService::setLastActive($user->id); $profile = $user->profile; $settings = $user->settings; @@ -754,6 +756,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $target = Profile::where('id', '!=', $user->profile_id) ->whereNull('status') @@ -838,6 +841,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $target = Profile::where('id', '!=', $user->profile_id) ->whereNull('status') @@ -941,6 +945,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + AccountService::setLastActive($user->id); $query = $request->input('q'); $limit = $request->input('limit') ?? 20; $resolve = (bool) $request->input('resolve', false); @@ -1013,6 +1018,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; + AccountService::setLastActive($user->id); if(intval($id) === intval($pid)) { abort(400, 'You cannot block yourself'); @@ -1105,6 +1111,7 @@ class ApiV1Controller extends Controller $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; + AccountService::setLastActive($user->id); if(intval($id) === intval($pid)) { abort(400, 'You cannot unblock yourself'); @@ -1237,6 +1244,8 @@ class ApiV1Controller extends Controller $user = $request->user(); + AccountService::setLastActive($user->id); + $status = StatusService::getMastodon($id, false); abort_unless($status, 400); @@ -1296,6 +1305,8 @@ class ApiV1Controller extends Controller $user = $request->user(); + AccountService::setLastActive($user->id); + $status = Status::findOrFail($id); if(intval($status->profile_id) !== intval($user->profile_id)) { @@ -1611,6 +1622,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + AccountService::setLastActive($user->id); if($user->last_active_at == null) { return []; @@ -1732,6 +1744,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + AccountService::setLastActive($user->id); $media = Media::whereUserId($user->id) ->whereProfileId($user->profile_id) @@ -1778,6 +1791,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $media = Media::whereUserId($user->id) ->whereNull('status_id') @@ -1821,6 +1835,8 @@ class ApiV1Controller extends Controller return []; } + AccountService::setLastActive($user->id); + if(empty($request->file('file'))) { return response('', 422); } @@ -2065,6 +2081,8 @@ class ApiV1Controller extends Controller 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, + 'types[]' => 'sometimes|array', + 'type' => 'sometimes|string|in:mention,reblog,follow,favourite' ]); $pid = $request->user()->profile_id; @@ -2078,28 +2096,28 @@ class ApiV1Controller extends Controller $min = 1; } + $types = $request->input('types'); + $maxId = null; $minId = null; + AccountService::setLastActive($request->user()->id); - if($max) { - $res = NotificationService::getMaxMastodon($pid, $max, $limit); - $ids = NotificationService::getRankedMaxId($pid, $max, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } - } else { - $res = NotificationService::getMinMastodon($pid, $min ?? $since, $limit); - $ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit); - if(!empty($ids)) { - $maxId = max($ids); - $minId = min($ids); - } + $res = $max ? + NotificationService::getMaxMastodon($pid, $max, $limit) : + NotificationService::getMinMastodon($pid, $min ?? $since, $limit); + $ids = $max ? + NotificationService::getRankedMaxId($pid, $max, $limit) : + NotificationService::getRankedMinId($pid, $min ?? $since, $limit); + if(!empty($ids)) { + $maxId = max($ids); + $minId = min($ids); } - if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) { - Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); - NotificationService::warmCache($pid, 400, true); + if(empty($res)) { + if(!Cache::has('pf:services:notifications:hasSynced:'.$pid)) { + Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600); + NotificationService::warmCache($pid, 400, true); + } } $baseUrl = config('app.url') . '/api/v1/notifications?limit=' . $limit . '&'; @@ -2153,6 +2171,7 @@ class ApiV1Controller extends Controller $inTypes = $includeReblogs ? ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] : ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']; + AccountService::setLastActive($request->user()->id); if(config('exp.cached_home_timeline')) { $paddedLimit = $includeReblogs ? $limit + 10 : $limit + 50; @@ -2402,6 +2421,7 @@ class ApiV1Controller extends Controller $remote = $request->has('remote'); $local = $request->has('local'); $filtered = $user ? UserFilterService::filters($user->profile_id) : []; + AccountService::setLastActive($user->id); if($remote && config('instance.timeline.network.cached')) { Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { @@ -2593,7 +2613,7 @@ class ApiV1Controller extends Controller public function statusById(Request $request, $id) { abort_if(!$request->user(), 403); - + AccountService::setLastActive($request->user()->id); $pid = $request->user()->profile_id; $res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false); @@ -2637,6 +2657,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $pid = $user->profile_id; $status = StatusService::getMastodon($id, false); @@ -3107,7 +3128,7 @@ class ApiV1Controller extends Controller public function statusDelete(Request $request, $id) { abort_if(!$request->user(), 403); - + AccountService::setLastActive($request->user()->id); $status = Status::whereProfileId($request->user()->profile->id) ->findOrFail($id); @@ -3135,6 +3156,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); if(intval($status->profile_id) !== intval($user->profile_id)) { @@ -3181,6 +3203,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); if(intval($status->profile_id) !== intval($user->profile_id)) { From f22a36fe308f9d51b67ae738d78bb9e78ea6cdb3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 13 Dec 2023 06:49:51 -0700 Subject: [PATCH 112/977] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e608d69..93b545a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,8 @@ - Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2)) - Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135)) - Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393)) +- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7)) +- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 5cea5aab3c18965a8bbe74a95c2283a3385f37ab Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 04:56:22 -0700 Subject: [PATCH 113/977] Add Domain Blocks --- app/Models/UserDomainBlock.php | 21 ++++++++++++++ config/instance.php | 3 +- ...052413_create_user_domain_blocks_table.php | 29 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 app/Models/UserDomainBlock.php create mode 100644 database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php diff --git a/app/Models/UserDomainBlock.php b/app/Models/UserDomainBlock.php new file mode 100644 index 000000000..900e026f2 --- /dev/null +++ b/app/Models/UserDomainBlock.php @@ -0,0 +1,21 @@ +belongsTo(Profile::class, 'profile_id'); + } +} diff --git a/config/instance.php b/config/instance.php index 5161ecb80..6357afe63 100644 --- a/config/instance.php +++ b/config/instance.php @@ -110,7 +110,8 @@ return [ 'user_filters' => [ 'max_user_blocks' => env('PF_MAX_USER_BLOCKS', 50), - 'max_user_mutes' => env('PF_MAX_USER_MUTES', 50) + 'max_user_mutes' => env('PF_MAX_USER_MUTES', 50), + 'max_domain_blocks' => env('PF_MAX_DOMAIN_BLOCKS', 50), ], 'reports' => [ diff --git a/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php new file mode 100644 index 000000000..4cacbfcae --- /dev/null +++ b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php @@ -0,0 +1,29 @@ +id(); + $table->unsignedBigInteger('profile_id')->index(); + $table->string('domain'); + $table->unique(['profile_id', 'domain'], 'user_domain_blocks_by_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_domain_blocks'); + } +}; From 2136ffe3d8c73cac48f83f7e416b60a6602a8f49 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:30:52 -0700 Subject: [PATCH 114/977] Add localization --- resources/lang/en/profile.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lang/en/profile.php b/resources/lang/en/profile.php index 7851852a2..3206a94bc 100644 --- a/resources/lang/en/profile.php +++ b/resources/lang/en/profile.php @@ -12,4 +12,6 @@ return [ 'status.disabled.header' => 'Profile Unavailable', 'status.disabled.body' => 'Sorry, this profile is not available at the moment. Please try again shortly.', + + 'block.domain.max' => 'Max limit of domain blocks reached! You can only block :max domains at a time. Ask your admin to adjust this limit.' ]; From 2438324369edc1e0bef831015a2ed2f60c5a1e1d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:32:21 -0700 Subject: [PATCH 115/977] Add template-vue settings blade view --- .../views/settings/template-vue.blade.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 resources/views/settings/template-vue.blade.php diff --git a/resources/views/settings/template-vue.blade.php b/resources/views/settings/template-vue.blade.php new file mode 100644 index 000000000..adcbb34c2 --- /dev/null +++ b/resources/views/settings/template-vue.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.app') + +@section('content') +@if (session('status')) +
+ {{ session('status') }} +
+@endif +@if ($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+@endif +@if (session('error')) +
+ {{ session('error') }} +
+@endif + +
+
+
+
+
+ @include('settings.partial.sidebar') +
+ @yield('section') +
+
+
+
+
+
+ +@endsection From cef451e588c781df4e565a5b17613a443c4f0c15 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:36:59 -0700 Subject: [PATCH 116/977] Update routes --- resources/views/settings/privacy.blade.php | 5 +- .../views/settings/privacy/blocked.blade.php | 68 +++++++++---------- .../views/settings/privacy/muted.blade.php | 62 ++++++++--------- routes/web.php | 1 + 4 files changed, 64 insertions(+), 72 deletions(-) diff --git a/resources/views/settings/privacy.blade.php b/resources/views/settings/privacy.blade.php index 57f83c664..a1212141a 100644 --- a/resources/views/settings/privacy.blade.php +++ b/resources/views/settings/privacy.blade.php @@ -8,8 +8,9 @@
diff --git a/resources/views/settings/privacy/blocked.blade.php b/resources/views/settings/privacy/blocked.blade.php index e3b2eab2b..8906eae7d 100644 --- a/resources/views/settings/privacy/blocked.blade.php +++ b/resources/views/settings/privacy/blocked.blade.php @@ -2,40 +2,36 @@ @section('section') -
-

Blocked Users

-
-
- - @if($users->count() > 0) -
    - @foreach($users as $user) -
  • -
    - {{$user->username}} - - - @csrf - - - - -
    -
  • - @endforeach -
-
- {{$users->links()}} -
- @else -

You are not blocking any accounts.

- @endif +
+
+

+

Blocked Accounts

+
+
+
-@endsection \ No newline at end of file +@if($users->count() > 0) +
+ @foreach($users as $user) +
+
+ {{$user->username}} + +
+ @csrf + + +
+
+
+
+ @endforeach +
+
+ {{$users->links()}} +
+@else +

You are not blocking any accounts.

+@endif + +@endsection diff --git a/resources/views/settings/privacy/muted.blade.php b/resources/views/settings/privacy/muted.blade.php index a13b9b716..152b8d641 100644 --- a/resources/views/settings/privacy/muted.blade.php +++ b/resources/views/settings/privacy/muted.blade.php @@ -1,41 +1,35 @@ @extends('settings.template') @section('section') - -
-

Muted Users

-
-
- - @if($users->count() > 0) -
    +
    +
    +

    +

    Muted Accounts

    +
    +
    +
    +@if($users->count() > 0) +
    @foreach($users as $user) -
  • -
    - {{$user->username}} - -
    - @csrf - - -
    -
    -
    -
  • +
    +
    + {{$user->username}} + +
    + @csrf + + +
    +
    +
    +
    @endforeach -
-
+
+
{{$users->links()}} -
- @else -

You are not muting any accounts.

- @endif +
+@else +

You are not muting any accounts.

+@endif -@endsection \ No newline at end of file +@endsection diff --git a/routes/web.php b/routes/web.php index b823b8729..b8149a605 100644 --- a/routes/web.php +++ b/routes/web.php @@ -489,6 +489,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate'); Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users'); Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate'); + Route::get('privacy/domain-blocks', 'SettingsController@domainBlocks')->name('settings.privacy.domain-blocks'); Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances'); Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore'); Route::post('privacy/blocked-instances/unblock', 'SettingsController@blockedInstanceUnblock')->name('settings.privacy.blocked-instances.unblock'); From 28da44beecd258c6e4f96a7c073b7ba374ab3723 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:42:56 -0700 Subject: [PATCH 117/977] Update PrivacySettings, add domainBlocks --- .../Controllers/Settings/PrivacySettings.php | 45 +++++-------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index 9a5febe83..bd2222d48 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -14,6 +14,7 @@ use App\Util\Lexer\PrettyNumber; use App\Util\ActivityPub\Helpers; use Auth, Cache, DB; use Illuminate\Http\Request; +use App\Models\UserDomainBlock; trait PrivacySettings { @@ -149,47 +150,25 @@ trait PrivacySettings public function blockedInstances() { - $pid = Auth::user()->profile->id; - $filters = UserFilter::whereUserId($pid) - ->whereFilterableType('App\Instance') - ->whereFilterType('block') - ->orderByDesc('id') - ->paginate(10); - return view('settings.privacy.blocked-instances', compact('filters')); + // deprecated + abort(404); + } + + public function domainBlocks() + { + return view('settings.privacy.domain-blocks'); } public function blockedInstanceStore(Request $request) { - $this->validate($request, [ - 'domain' => 'required|url|min:1|max:120' - ]); - $domain = $request->input('domain'); - if(Helpers::validateUrl($domain) == false) { - return abort(400, 'Invalid domain'); - } - $domain = parse_url($domain, PHP_URL_HOST); - $instance = Instance::firstOrCreate(['domain' => $domain]); - $filter = new UserFilter; - $filter->user_id = Auth::user()->profile->id; - $filter->filterable_id = $instance->id; - $filter->filterable_type = 'App\Instance'; - $filter->filter_type = 'block'; - $filter->save(); - return response()->json(['msg' => 200]); + // deprecated + abort(404); } public function blockedInstanceUnblock(Request $request) { - $this->validate($request, [ - 'id' => 'required|integer|min:1' - ]); - $pid = Auth::user()->profile->id; - - $filter = UserFilter::whereFilterableType('App\Instance') - ->whereUserId($pid) - ->findOrFail($request->input('id')); - $filter->delete(); - return redirect(route('settings.privacy.blocked-instances')); + // deprecated + abort(404); } public function blockedKeywords() From 63c9ebe81f3ca1ae347575f83cd83ae20b3e1fa2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:43:37 -0700 Subject: [PATCH 118/977] Update api routes --- routes/api.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routes/api.php b/routes/api.php index 10a4363c2..af40e27bc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -51,9 +51,9 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('blocks', 'Api\ApiV1Controller@accountBlocks')->middleware($middleware); Route::get('conversations', 'Api\ApiV1Controller@conversations')->middleware($middleware); Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis'); - Route::get('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware); - Route::post('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware); - Route::delete('domain_blocks', 'Api\ApiV1Controller@accountDomainBlocks')->middleware($middleware); + Route::get('domain_blocks', 'Api\V1\DomainBlockController@index')->middleware($middleware); + Route::post('domain_blocks', 'Api\V1\DomainBlockController@store')->middleware($middleware); + Route::delete('domain_blocks', 'Api\V1\DomainBlockController@delete')->middleware($middleware); Route::get('endorsements', 'Api\ApiV1Controller@accountEndorsements')->middleware($middleware); Route::get('favourites', 'Api\ApiV1Controller@accountFavourites')->middleware($middleware); Route::get('filters', 'Api\ApiV1Controller@accountFilters')->middleware($middleware); From 28da107f66b16c93fc2d983b3979571f1cef5526 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 05:56:37 -0700 Subject: [PATCH 119/977] Add DomainBlockController --- .../Api/V1/DomainBlockController.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/DomainBlockController.php diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php new file mode 100644 index 000000000..5a6178e39 --- /dev/null +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -0,0 +1,96 @@ +json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } + + public function index(Request $request) + { + abort_unless($request->user(), 403); + $this->validate($request, [ + 'limit' => 'sometimes|integer|min:1|max:200' + ]); + $limit = $request->input('limit', 100); + $id = $request->user()->profile_id; + $filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit); + $links = null; + $headers = []; + + if($filters->nextCursor()) { + $links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"'; + } + + if($filters->previousCursor()) { + if($links != null) { + $links .= ', '; + } + $links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"'; + } + + if($links) { + $headers = ['Link' => $links]; + } + return $this->json($filters->pluck('domain'), 200, $headers); + } + + public function store(Request $request) + { + abort_unless($request->user(), 403); + + $this->validate($request, [ + 'domain' => 'required|active_url|min:1|max:120' + ]); + + $pid = $request->user()->profile_id; + + $domain = trim($request->input('domain')); + + if(Helpers::validateUrl($domain) == false) { + return abort(500, 'Invalid domain or already blocked by server admins'); + } + + $domain = parse_url($domain, PHP_URL_HOST); + + abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server'); + + $existingCount = UserDomainBlock::whereProfileId($pid)->count(); + $maxLimit = config('instance.user_filters.max_domain_blocks'); + $errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]); + + abort_if($existingCount >= $maxLimit, 400, $errorMsg); + + $block = UserDomainBlock::updateOrInsert([ + 'profile_id' => $pid, + 'domain' => $domain + ]); + + return $this->json([]); + } + + public function delete(Request $request) + { + abort_unless($request->user(), 403); + + $this->validate($request, [ + 'domain' => 'required|min:1|max:120' + ]); + + $pid = $request->user()->profile_id; + + $domain = trim($request->input('domain')); + + $filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete(); + + return $this->json([]); + } +} From e5d789e0ab51ff55d64d559829f4fd6999946bda Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 06:01:43 -0700 Subject: [PATCH 120/977] Add domain blocks setting view --- .../settings/privacy/domain-blocks.blade.php | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 resources/views/settings/privacy/domain-blocks.blade.php diff --git a/resources/views/settings/privacy/domain-blocks.blade.php b/resources/views/settings/privacy/domain-blocks.blade.php new file mode 100644 index 000000000..d93e0b58e --- /dev/null +++ b/resources/views/settings/privacy/domain-blocks.blade.php @@ -0,0 +1,272 @@ +@extends('settings.template-vue') + +@section('section') +
+
+
+

+

Domain Blocks

+
+
+ +

You can block entire domains, this prevents users on that instance from interacting with your content and from you seeing content from that domain on public feeds.

+ +
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + + + + + +
+
+
+ + + +
+
+

You are not blocking any domains.

+
+
+
+@endsection + +@push('scripts') + +@endpush From d3f032b2ec1cbef33066ec131961eb31dc4a76b6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 06:11:13 -0700 Subject: [PATCH 121/977] Update FollowerService, add quickCheck to follows method for non cold-boot checks --- app/Services/FollowerService.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index 8b5eeced9..dcb7a1158 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -89,12 +89,16 @@ class FollowerService return Redis::zCard(self::FOLLOWING_KEY . $id); } - public static function follows(string $actor, string $target) + public static function follows(string $actor, string $target, $quickCheck = false) { if($actor == $target) { return false; } + if($quickCheck) { + return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); + } + if(self::followerCount($target, false) && self::followingCount($actor, false)) { self::cacheSyncCheck($target, 'followers'); return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor); From 60e053c93671d0e790cde02aa1c51bc011cca17a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 06:22:56 -0700 Subject: [PATCH 122/977] Update ApiV1Controller, update discoverAccountsPopular method --- app/Http/Controllers/Api/ApiV1Controller.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index f5e1202c0..e429a8681 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3619,25 +3619,31 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id; - $ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() { + $ids = Cache::remember('api:v1.1:discover:accounts:popular', 3600, function() { return DB::table('profiles') ->where('is_private', false) ->whereNull('status') ->orderByDesc('profiles.followers_count') - ->limit(20) + ->limit(30) ->get(); }); - + $filters = UserFilterService::filters($pid); $ids = $ids->map(function($profile) { return AccountService::get($profile->id, true); }) ->filter(function($profile) use($pid) { - return $profile && isset($profile['id']); + return $profile && isset($profile['id'], $profile['locked']) && !$profile['locked']; }) ->filter(function($profile) use($pid) { return $profile['id'] != $pid; }) - ->take(6) + ->filter(function($profile) use($pid) { + return !FollowerService::follows($pid, $profile['id'], true); + }) + ->filter(function($profile) use($filters) { + return !in_array($profile['id'], $filters); + }) + ->take(16) ->values(); return $this->json($ids); From 7016d195202e335253ab1143c1212434e23193ee Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 16 Dec 2023 17:04:16 -0700 Subject: [PATCH 123/977] Update Privacy Settings view, change button to Blocked Domains and add l10n --- resources/lang/en/profile.php | 6 +++++- resources/views/settings/privacy.blade.php | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/lang/en/profile.php b/resources/lang/en/profile.php index 3206a94bc..04a2bcd90 100644 --- a/resources/lang/en/profile.php +++ b/resources/lang/en/profile.php @@ -13,5 +13,9 @@ return [ 'status.disabled.header' => 'Profile Unavailable', 'status.disabled.body' => 'Sorry, this profile is not available at the moment. Please try again shortly.', - 'block.domain.max' => 'Max limit of domain blocks reached! You can only block :max domains at a time. Ask your admin to adjust this limit.' + 'block.domain.max' => 'Max limit of domain blocks reached! You can only block :max domains at a time. Ask your admin to adjust this limit.', + + 'mutedAccounts' => 'Muted Accounts', + 'blockedAccounts' => 'Blocked Accounts', + 'blockedDomains' => 'Blocked Domains', ]; diff --git a/resources/views/settings/privacy.blade.php b/resources/views/settings/privacy.blade.php index a1212141a..369ddbbb4 100644 --- a/resources/views/settings/privacy.blade.php +++ b/resources/views/settings/privacy.blade.php @@ -8,9 +8,9 @@
From e7c08fbbb2ed55acc04d2bd47a510c618319490e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:32:48 -0700 Subject: [PATCH 124/977] Update AccountService, add blocksDomain method --- app/Services/AccountService.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index fa1613cae..98e878845 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -7,6 +7,7 @@ use App\Profile; use App\Status; use App\User; use App\UserSetting; +use App\Models\UserDomainBlock; use App\Transformer\Api\AccountTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; @@ -234,4 +235,13 @@ class AccountService } return; } + + public static function blocksDomain($pid, $domain = false) + { + if(!$domain) { + return; + } + + return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists(); + } } From a7f96d81949af8029edcbbaabc50ad63030485f9 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:34:53 -0700 Subject: [PATCH 125/977] Update Inbox, add user domain blocks to Direct Message handler --- app/Util/ActivityPub/Inbox.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 0dd4722e9..c0ad390b8 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -372,7 +372,11 @@ class Inbox ->whereUsername(array_last(explode('/', $activity['to'][0]))) ->firstOrFail(); - if(in_array($actor->id, $profile->blockedIds()->toArray())) { + if(!$actor || in_array($actor->id, $profile->blockedIds()->toArray())) { + return; + } + + if(AccountService::blocksDomain($profile->id, $actor->domain) == true) { return; } From c89dc45e8d8010282a5605537a665151d6c7790c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:37:34 -0700 Subject: [PATCH 126/977] Update Inbox, add user domain blocks to Follow handler --- app/Util/ActivityPub/Inbox.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index c0ad390b8..176890e70 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -514,6 +514,10 @@ class Inbox return; } + if(AccountService::blocksDomain($target->id, $actor->domain) == true) { + return; + } + if( Follower::whereProfileId($actor->id) ->whereFollowingId($target->id) From 279fb28e2a8d5749fb590b89ed47ff4c89ea9379 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:42:21 -0700 Subject: [PATCH 127/977] Update Inbox, add user domain blocks to Announce handler --- app/Util/ActivityPub/Inbox.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 176890e70..6818a8e87 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -589,6 +589,10 @@ class Inbox return; } + if(AccountService::blocksDomain($parent->profile_id, $actor->domain) == true) { + return; + } + $blocks = UserFilterService::blocks($parent->profile_id); if($blocks && in_array($actor->id, $blocks)) { return; From 3fbf8f159ef24ba202cd727916724fe782eaa50d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:46:09 -0700 Subject: [PATCH 128/977] Update Inbox, add user domain blocks to Accept handler --- app/Util/ActivityPub/Inbox.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 6818a8e87..15d7d3445 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -646,6 +646,10 @@ class Inbox return; } + if(AccountService::blocksDomain($target->id, $actor->domain) == true) { + return; + } + $request = FollowRequest::whereFollowerId($actor->id) ->whereFollowingId($target->id) ->whereIsRejected(false) From e32e50da7bc2a5c4d05379854752f3a780990628 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:47:03 -0700 Subject: [PATCH 129/977] Update Inbox, add user domain blocks to Like handler --- app/Util/ActivityPub/Inbox.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 15d7d3445..defb75fa4 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -775,6 +775,10 @@ class Inbox return; } + if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } + $blocks = UserFilterService::blocks($status->profile_id); if($blocks && in_array($profile->id, $blocks)) { return; From 491468612f99dc5ae88fde79672767a74f943a8e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:49:31 -0700 Subject: [PATCH 130/977] Update Inbox, add user domain blocks to Undo handler --- app/Util/ActivityPub/Inbox.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index defb75fa4..09676301f 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -836,6 +836,9 @@ class Inbox if(!$status) { return; } + if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } FeedRemoveRemotePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); Status::whereProfileId($profile->id) ->whereReblogOfId($status->id) @@ -857,6 +860,9 @@ class Inbox if(!$following) { return; } + if(AccountService::blocksDomain($following->id, $profile->domain) == true) { + return; + } Follower::whereProfileId($profile->id) ->whereFollowingId($following->id) ->delete(); @@ -882,6 +888,9 @@ class Inbox if(!$status) { return; } + if(AccountService::blocksDomain($status->profile_id, $profile->domain) == true) { + return; + } Like::whereProfileId($profile->id) ->whereStatusId($status->id) ->forceDelete(); From 8a0ceaf801af614457ae65197d18bb0fe69372d2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 18 Dec 2023 22:57:53 -0700 Subject: [PATCH 131/977] Update Inbox, add user domain blocks to Story reaction handlers --- app/Util/ActivityPub/Inbox.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 09676301f..b8bb780c8 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -944,6 +944,10 @@ class Inbox return; } + if(AccountService::blocksDomain($story->profile_id, $profile->domain) == true) { + return; + } + if(!FollowerService::follows($profile->id, $story->profile_id)) { return; } @@ -1014,6 +1018,10 @@ class Inbox $actorProfile = Helpers::profileFetch($actor); + if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + return; + } + if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { return; } @@ -1132,6 +1140,11 @@ class Inbox $actorProfile = Helpers::profileFetch($actor); + + if(AccountService::blocksDomain($targetProfile->id, $actorProfile->domain) == true) { + return; + } + if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) { return; } From 819e7d3b328e8a14361a4df78976d0cb41f27ce7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 01:10:48 -0700 Subject: [PATCH 132/977] Add FeedRemoveDomainPipeline --- .../FeedRemoveDomainPipeline.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php diff --git a/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php new file mode 100644 index 000000000..2168ee054 --- /dev/null +++ b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php @@ -0,0 +1,92 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("hts:feed:remove:domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if(!config('exp.cached_home_timeline')) { + return; + } + if(!$this->pid || !$this->domain) { + return; + } + $domain = strtolower($this->domain); + $pid = $this->pid; + $posts = HomeTimelineService::get($pid, '0', '-1'); + + foreach($posts as $post) { + $status = StatusService::get($post, false); + if(!$status || !isset($status['url'])) { + HomeTimelineService::rem($pid, $post); + continue; + } + $host = strtolower(parse_url($status['url'], PHP_URL_HOST)); + if($host === strtolower(config('pixelfed.domain.app')) || !$host) { + continue; + } + if($host === $domain) { + HomeTimelineService::rem($pid, $status['id']); + } + } + } +} From 5c1591fdffbdb1e842d7e2049be6ad967f485864 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 01:20:14 -0700 Subject: [PATCH 133/977] Add job batches migration --- ..._12_19_081928_create_job_batches_table.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 database/migrations/2023_12_19_081928_create_job_batches_table.php diff --git a/database/migrations/2023_12_19_081928_create_job_batches_table.php b/database/migrations/2023_12_19_081928_create_job_batches_table.php new file mode 100644 index 000000000..50e38c20f --- /dev/null +++ b/database/migrations/2023_12_19_081928_create_job_batches_table.php @@ -0,0 +1,35 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_batches'); + } +}; From a492a95a0eb4e2a75d84d9da1cc807d8c3d85bb2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 04:01:41 -0700 Subject: [PATCH 134/977] Update AdminShadowFilter, fix deleted profile bug --- app/Http/Controllers/AdminShadowFilterController.php | 3 ++- app/Models/AdminShadowFilter.php | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/AdminShadowFilterController.php b/app/Http/Controllers/AdminShadowFilterController.php index 461e1d0c2..e181be5c1 100644 --- a/app/Http/Controllers/AdminShadowFilterController.php +++ b/app/Http/Controllers/AdminShadowFilterController.php @@ -19,7 +19,8 @@ class AdminShadowFilterController extends Controller { $filter = $request->input('filter'); $searchQuery = $request->input('q'); - $filters = AdminShadowFilter::when($filter, function($q, $filter) { + $filters = AdminShadowFilter::whereHas('profile') + ->when($filter, function($q, $filter) { if($filter == 'all') { return $q; } else if($filter == 'inactive') { diff --git a/app/Models/AdminShadowFilter.php b/app/Models/AdminShadowFilter.php index f98086f7f..8a163feeb 100644 --- a/app/Models/AdminShadowFilter.php +++ b/app/Models/AdminShadowFilter.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Services\AccountService; +use App\Profile; class AdminShadowFilter extends Model { @@ -24,4 +25,9 @@ class AdminShadowFilter extends Model return; } + + public function profile() + { + return $this->belongsTo(Profile::class, 'item_id'); + } } From 1664a5bc52fc62dc4f5c099cc75c189ccc4b0e27 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 05:46:06 -0700 Subject: [PATCH 135/977] Update FollowerService, add $silent param to remove method to more efficently purge relationships --- app/Services/FollowerService.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php index dcb7a1158..cec8f7068 100644 --- a/app/Services/FollowerService.php +++ b/app/Services/FollowerService.php @@ -35,16 +35,18 @@ class FollowerService Cache::forget('profile:following:' . $actor); } - public static function remove($actor, $target) + public static function remove($actor, $target, $silent = false) { Redis::zrem(self::FOLLOWING_KEY . $actor, $target); Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); - Cache::forget('pf:services:follower:audience:' . $actor); - Cache::forget('pf:services:follower:audience:' . $target); - AccountService::del($actor); - AccountService::del($target); - RelationshipService::refresh($actor, $target); - Cache::forget('profile:following:' . $actor); + if($silent !== true) { + AccountService::del($actor); + AccountService::del($target); + RelationshipService::refresh($actor, $target); + Cache::forget('profile:following:' . $actor); + } else { + RelationshipService::forget($actor, $target); + } } public static function followers($id, $start = 0, $stop = 10) From 484a377a449c7d1c59e304c158e7c1514ec60649 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 06:02:58 -0700 Subject: [PATCH 136/977] Add ProfilePurgeFollowersByDomain pipeline job --- .../ProfilePurgeFollowersByDomain.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php diff --git a/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php b/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php new file mode 100644 index 000000000..24fcdc832 --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfilePurgeFollowersByDomain.php @@ -0,0 +1,119 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("followers:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + + $pid = $this->pid; + $domain = $this->domain; + + $query = 'SELECT f.* + FROM followers f + JOIN profiles p ON p.id = f.profile_id OR p.id = f.following_id + WHERE (f.profile_id = ? OR f.following_id = ?) + AND p.domain = ?;'; + $params = [$pid, $pid, $domain]; + + foreach(DB::cursor($query, $params) as $n) { + if(!$n || !$n->id) { + continue; + } + $follower = Follower::find($n->id); + if($follower->following_id == $pid && $follower->profile_id) { + FollowerService::remove($follower->profile_id, $pid, true); + $follower->delete(); + } else if ($follower->profile_id == $pid && $follower->following_id) { + FollowerService::remove($follower->following_id, $pid, true); + $follower->delete(); + } + } + + $profile = Profile::find($pid); + + $followerCount = DB::table('profiles') + ->join('followers', 'profiles.id', '=', 'followers.following_id') + ->where('followers.following_id', $pid) + ->count(); + + $followingCount = DB::table('profiles') + ->join('followers', 'profiles.id', '=', 'followers.following_id') + ->where('followers.profile_id', $pid) + ->count(); + + $profile->followers_count = $followerCount; + $profile->following_count = $followingCount; + $profile->save(); + + AccountService::del($profile->id); + } +} From 9d621108b06d1470369f647d13f46c1af174d0c4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 06:03:36 -0700 Subject: [PATCH 137/977] Add ProfilePurgeNotificationsByDomain pipeline job --- .../ProfilePurgeNotificationsByDomain.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php diff --git a/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php b/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php new file mode 100644 index 000000000..ea5a45e4a --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfilePurgeNotificationsByDomain.php @@ -0,0 +1,91 @@ +pid . ':d-' . $this->domain; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("notify:v1:purge-by-domain:{$this->pid}:d-{$this->domain}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct($pid, $domain) + { + $this->pid = $pid; + $this->domain = $domain; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if ($this->batch()->cancelled()) { + return; + } + + $pid = $this->pid; + $domain = $this->domain; + + $query = 'SELECT notifications.* + FROM profiles + JOIN notifications on profiles.id = notifications.actor_id + WHERE notifications.profile_id = ? + AND profiles.domain = ?'; + $params = [$pid, $domain]; + + foreach(DB::cursor($query, $params) as $n) { + if(!$n || !$n->id) { + continue; + } + Notification::where('id', $n->id)->delete(); + NotificationService::del($pid, $n->id); + } + } +} From 54adbeb0592d4a3b1ad1d66f48227ca09f0c8d7b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 06:04:03 -0700 Subject: [PATCH 138/977] Update FeedRemoveDomainPipeline, make batchable --- app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php index 2168ee054..018ea3794 100644 --- a/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedRemoveDomainPipeline.php @@ -2,6 +2,7 @@ namespace App\Jobs\HomeFeedPipeline; +use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; @@ -15,7 +16,7 @@ use App\Services\HomeTimelineService; class FeedRemoveDomainPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $pid; protected $domain; @@ -67,6 +68,11 @@ class FeedRemoveDomainPipeline implements ShouldQueue, ShouldBeUniqueUntilProces if(!config('exp.cached_home_timeline')) { return; } + + if ($this->batch()->cancelled()) { + return; + } + if(!$this->pid || !$this->domain) { return; } From 87bba03d23b60d7e1940b26ac0f633a0cfaaeb5d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 06:24:51 -0700 Subject: [PATCH 139/977] Update DomainBlockController, dispatch jobies --- .../Api/V1/DomainBlockController.php | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php index 5a6178e39..53a209ed9 100644 --- a/app/Http/Controllers/Api/V1/DomainBlockController.php +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -6,6 +6,12 @@ use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Models\UserDomainBlock; use App\Util\ActivityPub\Helpers; +use Illuminate\Bus\Batch; +use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Cache; +use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline; +use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain; +use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain; class DomainBlockController extends Controller { @@ -59,7 +65,7 @@ class DomainBlockController extends Controller return abort(500, 'Invalid domain or already blocked by server admins'); } - $domain = parse_url($domain, PHP_URL_HOST); + $domain = strtolower(parse_url($domain, PHP_URL_HOST)); abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server'); @@ -69,11 +75,23 @@ class DomainBlockController extends Controller abort_if($existingCount >= $maxLimit, 400, $errorMsg); - $block = UserDomainBlock::updateOrInsert([ + $block = UserDomainBlock::updateOrCreate([ 'profile_id' => $pid, 'domain' => $domain ]); + if($block->wasRecentlyCreated) { + Bus::batch([ + [ + new FeedRemoveDomainPipeline($pid, $domain), + new ProfilePurgeNotificationsByDomain($pid, $domain), + new ProfilePurgeFollowersByDomain($pid, $domain) + ] + ])->allowFailures()->onQueue('feed')->dispatch(); + + Cache::forget('profile:following:' . $pid); + } + return $this->json([]); } @@ -87,7 +105,7 @@ class DomainBlockController extends Controller $pid = $request->user()->profile_id; - $domain = trim($request->input('domain')); + $domain = strtolower(trim($request->input('domain'))); $filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete(); From 795132df1830dd99d5cb0d09de121ddbb3fbad39 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 19 Dec 2023 06:25:39 -0700 Subject: [PATCH 140/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b545a30..2f75a72a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ - Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393)) - Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7)) - Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545)) +- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a)) +- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From dd16189fc81ab0b0501fa61894d12985089b7316 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 20 Dec 2023 23:10:57 -0700 Subject: [PATCH 141/977] Update ImageResize job, add more logging --- app/Jobs/ImageOptimizePipeline/ImageResize.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Jobs/ImageOptimizePipeline/ImageResize.php b/app/Jobs/ImageOptimizePipeline/ImageResize.php index 9bb896a40..c1b4ea7f0 100644 --- a/app/Jobs/ImageOptimizePipeline/ImageResize.php +++ b/app/Jobs/ImageOptimizePipeline/ImageResize.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Log; class ImageResize implements ShouldQueue { @@ -46,6 +47,7 @@ class ImageResize implements ShouldQueue } $path = storage_path('app/'.$media->media_path); if (!is_file($path) || $media->skip_optimize) { + Log::info('Tried to optimize media that does not exist or is not readable. ' . $path); return; } @@ -57,6 +59,7 @@ class ImageResize implements ShouldQueue $img = new Image(); $img->resizeImage($media); } catch (Exception $e) { + Log::error($e); } ImageThumbnail::dispatch($media)->onQueue('mmo'); From ae1db1e3ab079fc6c8965df6761031e3b4a49baf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 20 Dec 2023 23:17:27 -0700 Subject: [PATCH 142/977] Update migration --- .../2023_12_16_052413_create_user_domain_blocks_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php index 4cacbfcae..16f8f3fb2 100644 --- a/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php +++ b/database/migrations/2023_12_16_052413_create_user_domain_blocks_table.php @@ -14,7 +14,7 @@ return new class extends Migration Schema::create('user_domain_blocks', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('profile_id')->index(); - $table->string('domain'); + $table->string('domain')->index(); $table->unique(['profile_id', 'domain'], 'user_domain_blocks_by_id'); }); } From 0455dd1996269eb4450f9f778f3ae4159e719c5a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 20 Dec 2023 23:55:26 -0700 Subject: [PATCH 143/977] Update UserFilter model, add user relation --- app/UserFilter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/UserFilter.php b/app/UserFilter.php index b0af2d777..dfa0d4662 100644 --- a/app/UserFilter.php +++ b/app/UserFilter.php @@ -33,4 +33,9 @@ class UserFilter extends Model { return $this->belongsTo(Instance::class, 'filterable_id'); } + + public function user() + { + return $this->belongsTo(Profile::class, 'user_id'); + } } From 29aa87c282f141a788a5b95b7c8312a12966c14e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 00:17:20 -0700 Subject: [PATCH 144/977] Update HomeFeedPipeline jobs, add domain block filtering --- .../HomeFeedPipeline/FeedInsertPipeline.php | 22 +++++++++++++++++-- .../FeedInsertRemotePipeline.php | 22 +++++++++++++++++-- .../HashtagInsertFanoutPipeline.php | 18 +++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php index 19a546e83..4237a7b1a 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertPipeline.php @@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\UserFilter; +use App\Models\UserDomainBlock; use App\Services\FollowerService; use App\Services\HomeTimelineService; use App\Services\StatusService; @@ -69,7 +70,7 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing $sid = $this->sid; $status = StatusService::get($sid, false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { return; } @@ -85,7 +86,24 @@ class FeedInsertPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing return; } - $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile') + ->whereFilterableId($status['account']['id']) + ->whereIn('filter_type', ['mute', 'block']) + ->pluck('user_id') + ->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); foreach($ids as $id) { if(!in_array($id, $skipIds)) { diff --git a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php index e24696bd8..6c4ce0c35 100644 --- a/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php +++ b/app/Jobs/HomeFeedPipeline/FeedInsertRemotePipeline.php @@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use App\UserFilter; +use App\Models\UserDomainBlock; use App\Services\FollowerService; use App\Services\HomeTimelineService; use App\Services\StatusService; @@ -69,7 +70,7 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces $sid = $this->sid; $status = StatusService::get($sid, false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { return; } @@ -83,7 +84,24 @@ class FeedInsertRemotePipeline implements ShouldQueue, ShouldBeUniqueUntilProces return; } - $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile') + ->whereFilterableId($status['account']['id']) + ->whereIn('filter_type', ['mute', 'block']) + ->pluck('user_id') + ->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); foreach($ids as $id) { if(!in_array($id, $skipIds)) { diff --git a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php index a200c06e8..eca598e49 100644 --- a/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php +++ b/app/Jobs/HomeFeedPipeline/HashtagInsertFanoutPipeline.php @@ -11,6 +11,7 @@ use Illuminate\Queue\SerializesModels; use App\Hashtag; use App\StatusHashtag; use App\UserFilter; +use App\Models\UserDomainBlock; use App\Services\HashtagFollowService; use App\Services\HomeTimelineService; use App\Services\StatusService; @@ -77,7 +78,7 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro $sid = $hashtag->status_id; $status = StatusService::get($sid, false); - if(!$status || !isset($status['account']) || !isset($status['account']['id'])) { + if(!$status || !isset($status['account']) || !isset($status['account']['id'], $status['url'])) { return; } @@ -85,7 +86,20 @@ class HashtagInsertFanoutPipeline implements ShouldQueue, ShouldBeUniqueUntilPro return; } - $skipIds = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + $skipIds = []; + + if(strtolower(config('pixelfed.domain.app')) !== $domain) { + $skipIds = UserDomainBlock::where('domain', $domain)->pluck('profile_id')->toArray(); + } + + $filters = UserFilter::whereFilterableType('App\Profile')->whereFilterableId($status['account']['id'])->whereIn('filter_type', ['mute', 'block'])->pluck('user_id')->toArray(); + + if($filters && count($filters)) { + $skipIds = array_merge($skipIds, $filters); + } + + $skipIds = array_unique(array_values($skipIds)); $ids = HashtagFollowService::getPidByHid($hashtag->hashtag_id); From b3148b788eb81135e95c32e7631972a65e62f768 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 00:21:33 -0700 Subject: [PATCH 145/977] Update HomeTimelineService, add domain blocks filtering to warmCache method --- app/Services/HomeTimelineService.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Services/HomeTimelineService.php b/app/Services/HomeTimelineService.php index 6a2db0482..08d990591 100644 --- a/app/Services/HomeTimelineService.php +++ b/app/Services/HomeTimelineService.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Redis; use App\Follower; use App\Status; +use App\Models\UserDomainBlock; class HomeTimelineService { @@ -81,6 +82,8 @@ class HomeTimelineService $following = array_diff($following, $filters); } + $domainBlocks = UserDomainBlock::whereProfileId($id)->pluck('domain')->toArray(); + $ids = Status::where('id', '>', $minId) ->whereIn('profile_id', $following) ->whereNull(['in_reply_to_id', 'reblog_of_id']) @@ -91,6 +94,16 @@ class HomeTimelineService ->pluck('id'); foreach($ids as $pid) { + $status = StatusService::get($pid, false); + if(!$status || !isset($status['account'], $status['url'])) { + continue; + } + if($domainBlocks && count($domainBlocks)) { + $domain = strtolower(parse_url($status['url'], PHP_URL_HOST)); + if(in_array($domain, $domainBlocks)) { + continue; + } + } self::add($id, $pid); } From 6d55cb27eed3bace3168e4350e4deac0192ed8dc Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 00:42:26 -0700 Subject: [PATCH 146/977] Update UserFilterService, add domainBlocks method --- app/Services/UserFilterService.php | 271 +++++++++++++++-------------- 1 file changed, 143 insertions(+), 128 deletions(-) diff --git a/app/Services/UserFilterService.php b/app/Services/UserFilterService.php index 1dcdc819a..5673db60c 100644 --- a/app/Services/UserFilterService.php +++ b/app/Services/UserFilterService.php @@ -4,145 +4,160 @@ namespace App\Services; use Cache; use App\UserFilter; +use App\Models\UserDomainBlock; use Illuminate\Support\Facades\Redis; class UserFilterService { - const USER_MUTES_KEY = 'pf:services:mutes:ids:'; - const USER_BLOCKS_KEY = 'pf:services:blocks:ids:'; + const USER_MUTES_KEY = 'pf:services:mutes:ids:'; + const USER_BLOCKS_KEY = 'pf:services:blocks:ids:'; + const USER_DOMAIN_KEY = 'pf:services:domain-blocks:ids:'; - public static function mutes(int $profile_id) - { - $key = self::USER_MUTES_KEY . $profile_id; - $warm = Cache::has($key . ':cached-v0'); - if($warm) { - return Redis::zrevrange($key, 0, -1) ?? []; - } else { - if(Redis::zrevrange($key, 0, -1)) { - return Redis::zrevrange($key, 0, -1); - } - $ids = UserFilter::whereFilterType('mute') - ->whereUserId($profile_id) - ->pluck('filterable_id') - ->map(function($id) { - $acct = AccountService::get($id, true); - if(!$acct) { - return false; - } - return $acct['id']; - }) - ->filter(function($res) { - return $res; - }) - ->values() - ->toArray(); - foreach ($ids as $muted_id) { - Redis::zadd($key, (int) $muted_id, (int) $muted_id); - } - Cache::set($key . ':cached-v0', 1, 7776000); - return $ids; - } - } + public static function mutes(int $profile_id) + { + $key = self::USER_MUTES_KEY . $profile_id; + $warm = Cache::has($key . ':cached-v0'); + if($warm) { + return Redis::zrevrange($key, 0, -1) ?? []; + } else { + if(Redis::zrevrange($key, 0, -1)) { + return Redis::zrevrange($key, 0, -1); + } + $ids = UserFilter::whereFilterType('mute') + ->whereUserId($profile_id) + ->pluck('filterable_id') + ->map(function($id) { + $acct = AccountService::get($id, true); + if(!$acct) { + return false; + } + return $acct['id']; + }) + ->filter(function($res) { + return $res; + }) + ->values() + ->toArray(); + foreach ($ids as $muted_id) { + Redis::zadd($key, (int) $muted_id, (int) $muted_id); + } + Cache::set($key . ':cached-v0', 1, 7776000); + return $ids; + } + } - public static function blocks(int $profile_id) - { - $key = self::USER_BLOCKS_KEY . $profile_id; - $warm = Cache::has($key . ':cached-v0'); - if($warm) { - return Redis::zrevrange($key, 0, -1) ?? []; - } else { - if(Redis::zrevrange($key, 0, -1)) { - return Redis::zrevrange($key, 0, -1); - } - $ids = UserFilter::whereFilterType('block') - ->whereUserId($profile_id) - ->pluck('filterable_id') - ->map(function($id) { - $acct = AccountService::get($id, true); - if(!$acct) { - return false; - } - return $acct['id']; - }) - ->filter(function($res) { - return $res; - }) - ->values() - ->toArray(); - foreach ($ids as $blocked_id) { - Redis::zadd($key, (int) $blocked_id, (int) $blocked_id); - } - Cache::set($key . ':cached-v0', 1, 7776000); - return $ids; - } - } + public static function blocks(int $profile_id) + { + $key = self::USER_BLOCKS_KEY . $profile_id; + $warm = Cache::has($key . ':cached-v0'); + if($warm) { + return Redis::zrevrange($key, 0, -1) ?? []; + } else { + if(Redis::zrevrange($key, 0, -1)) { + return Redis::zrevrange($key, 0, -1); + } + $ids = UserFilter::whereFilterType('block') + ->whereUserId($profile_id) + ->pluck('filterable_id') + ->map(function($id) { + $acct = AccountService::get($id, true); + if(!$acct) { + return false; + } + return $acct['id']; + }) + ->filter(function($res) { + return $res; + }) + ->values() + ->toArray(); + foreach ($ids as $blocked_id) { + Redis::zadd($key, (int) $blocked_id, (int) $blocked_id); + } + Cache::set($key . ':cached-v0', 1, 7776000); + return $ids; + } + } - public static function filters(int $profile_id) - { - return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id))); - } + public static function filters(int $profile_id) + { + return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id))); + } - public static function mute(int $profile_id, int $muted_id) - { - if($profile_id == $muted_id) { - return false; - } - $key = self::USER_MUTES_KEY . $profile_id; - $mutes = self::mutes($profile_id); - $exists = in_array($muted_id, $mutes); - if(!$exists) { - Redis::zadd($key, $muted_id, $muted_id); - } - return true; - } + public static function mute(int $profile_id, int $muted_id) + { + if($profile_id == $muted_id) { + return false; + } + $key = self::USER_MUTES_KEY . $profile_id; + $mutes = self::mutes($profile_id); + $exists = in_array($muted_id, $mutes); + if(!$exists) { + Redis::zadd($key, $muted_id, $muted_id); + } + return true; + } - public static function unmute(int $profile_id, string $muted_id) - { - if($profile_id == $muted_id) { - return false; - } - $key = self::USER_MUTES_KEY . $profile_id; - $mutes = self::mutes($profile_id); - $exists = in_array($muted_id, $mutes); - if($exists) { - Redis::zrem($key, $muted_id); - } - return true; - } + public static function unmute(int $profile_id, string $muted_id) + { + if($profile_id == $muted_id) { + return false; + } + $key = self::USER_MUTES_KEY . $profile_id; + $mutes = self::mutes($profile_id); + $exists = in_array($muted_id, $mutes); + if($exists) { + Redis::zrem($key, $muted_id); + } + return true; + } - public static function block(int $profile_id, int $blocked_id) - { - if($profile_id == $blocked_id) { - return false; - } - $key = self::USER_BLOCKS_KEY . $profile_id; - $exists = in_array($blocked_id, self::blocks($profile_id)); - if(!$exists) { - Redis::zadd($key, $blocked_id, $blocked_id); - } - return true; - } + public static function block(int $profile_id, int $blocked_id) + { + if($profile_id == $blocked_id) { + return false; + } + $key = self::USER_BLOCKS_KEY . $profile_id; + $exists = in_array($blocked_id, self::blocks($profile_id)); + if(!$exists) { + Redis::zadd($key, $blocked_id, $blocked_id); + } + return true; + } - public static function unblock(int $profile_id, string $blocked_id) - { - if($profile_id == $blocked_id) { - return false; - } - $key = self::USER_BLOCKS_KEY . $profile_id; - $exists = in_array($blocked_id, self::blocks($profile_id)); - if($exists) { - Redis::zrem($key, $blocked_id); - } - return $exists; - } + public static function unblock(int $profile_id, string $blocked_id) + { + if($profile_id == $blocked_id) { + return false; + } + $key = self::USER_BLOCKS_KEY . $profile_id; + $exists = in_array($blocked_id, self::blocks($profile_id)); + if($exists) { + Redis::zrem($key, $blocked_id); + } + return $exists; + } - public static function blockCount(int $profile_id) - { - return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); - } + public static function blockCount(int $profile_id) + { + return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); + } - public static function muteCount(int $profile_id) - { - return Redis::zcard(self::USER_MUTES_KEY . $profile_id); - } + public static function muteCount(int $profile_id) + { + return Redis::zcard(self::USER_MUTES_KEY . $profile_id); + } + + public static function domainBlocks($pid, $purge = false) + { + if($purge) { + Cache::forget(self::USER_DOMAIN_KEY . $pid); + } + return Cache::remember( + self::USER_DOMAIN_KEY . $pid, + 21600, + function() use($pid) { + return UserDomainBlock::whereProfileId($pid)->pluck('domain')->toArray(); + }); + } } From 6d8121413865c00f6ccc8e46ff2e367bb603bd66 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 00:44:54 -0700 Subject: [PATCH 147/977] Update DomainBlockController, purge domainBlocks cache --- app/Http/Controllers/Api/V1/DomainBlockController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php index 53a209ed9..2186c0936 100644 --- a/app/Http/Controllers/Api/V1/DomainBlockController.php +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Models\UserDomainBlock; use App\Util\ActivityPub\Helpers; +use App\Services\UserFilterService; use Illuminate\Bus\Batch; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Cache; @@ -90,6 +91,7 @@ class DomainBlockController extends Controller ])->allowFailures()->onQueue('feed')->dispatch(); Cache::forget('profile:following:' . $pid); + UserFilterService::domainBlocks($pid, true); } return $this->json([]); @@ -109,6 +111,8 @@ class DomainBlockController extends Controller $filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete(); + UserFilterService::domainBlocks($pid, true); + return $this->json([]); } } From 21947835f8969a6100d275897bc84f337b958994 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 00:46:24 -0700 Subject: [PATCH 148/977] Update ApiV1Controller, use domainBlock filtering on public/network feeds --- app/Http/Controllers/Api/ApiV1Controller.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index e429a8681..be77b5606 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -31,6 +31,7 @@ use App\{ UserSetting, UserFilter, }; +use App\Models\UserDomainBlock; use League\Fractal; use App\Transformer\Api\Mastodon\v1\{ AccountTransformer, @@ -2422,6 +2423,7 @@ class ApiV1Controller extends Controller $local = $request->has('local'); $filtered = $user ? UserFilterService::filters($user->profile_id) : []; AccountService::setLastActive($user->id); + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); if($remote && config('instance.timeline.network.cached')) { Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { @@ -2496,6 +2498,13 @@ class ApiV1Controller extends Controller ->filter(function($s) use($filtered) { return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; }) + ->filter(function($s) use($domainBlocks) { + if(!$domainBlocks || !count($domainBlocks)) { + return $s; + } + $domain = strtolower(parse_url($s['url'], PHP_URL_HOST)); + return !in_array($domain, $domainBlocks); + }) ->take($limit) ->values(); From c3f16c87a31eedb50e1070ec0f00c3d8cb6f6882 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 01:05:49 -0700 Subject: [PATCH 149/977] Update SearchApiV2Service, add user domain blocks filtering --- app/Services/SearchApiV2Service.php | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/Services/SearchApiV2Service.php b/app/Services/SearchApiV2Service.php index 90691f0bd..f926c2c27 100644 --- a/app/Services/SearchApiV2Service.php +++ b/app/Services/SearchApiV2Service.php @@ -95,7 +95,15 @@ class SearchApiV2Service if(substr($webfingerQuery, 0, 1) !== '@') { $webfingerQuery = '@' . $webfingerQuery; } - $banned = InstanceService::getBannedDomains(); + $banned = InstanceService::getBannedDomains() ?? []; + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + if($domainBlocks && count($domainBlocks)) { + $banned = array_unique( + array_values( + array_merge($banned, $domainBlocks) + ) + ); + } $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like'; $results = Profile::select('username', 'id', 'followers_count', 'domain') ->where('username', $operator, $query) @@ -172,8 +180,18 @@ class SearchApiV2Service 'hashtags' => [], 'statuses' => [], ]; + $user = request()->user(); $mastodonMode = self::$mastodonMode; $query = urldecode($this->query->input('q')); + $banned = InstanceService::getBannedDomains(); + $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + if($domainBlocks && count($domainBlocks)) { + $banned = array_unique( + array_values( + array_merge($banned, $domainBlocks) + ) + ); + } if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) { $default['accounts'] = $this->accounts(substr($query, 1)); return $default; @@ -197,7 +215,11 @@ class SearchApiV2Service } catch (\Exception $e) { return $default; } - if($res && isset($res['id'])) { + if($res && isset($res['id'], $res['url'])) { + $domain = strtolower(parse_url($res['url'], PHP_URL_HOST)); + if(in_array($domain, $banned)) { + return $default; + } $default['accounts'][] = $res; return $default; } else { @@ -212,6 +234,10 @@ class SearchApiV2Service return $default; } if($res && isset($res['id'])) { + $domain = strtolower(parse_url($res['url'], PHP_URL_HOST)); + if(in_array($domain, $banned)) { + return $default; + } $default['accounts'][] = $res; return $default; } else { @@ -221,6 +247,9 @@ class SearchApiV2Service if($sid = Status::whereUri($query)->first()) { $s = StatusService::get($sid->id, false); + if(!$s) { + return $default; + } if(in_array($s['visibility'], ['public', 'unlisted'])) { $default['statuses'][] = $s; return $default; @@ -229,7 +258,7 @@ class SearchApiV2Service try { $res = ActivityPubFetchService::get($query); - $banned = InstanceService::getBannedDomains(); + if($res) { $json = json_decode($res, true); From fcbcd7ec73bb89bb56156fbc0dc929994444751e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 01:53:49 -0700 Subject: [PATCH 150/977] Update Delete pipelines, delete status hashtags quietly --- app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php | 5 +---- app/Jobs/StatusPipeline/RemoteStatusDelete.php | 5 +---- app/Jobs/StatusPipeline/StatusDelete.php | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php index 353509c6c..824323cda 100644 --- a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php +++ b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php @@ -76,10 +76,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue }); Mention::whereStatusId($status->id)->forceDelete(); Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete(); - $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); - foreach($statusHashtags as $stag) { - $stag->delete(); - } + StatusHashtag::whereStatusId($status->id)->deleteQuietly(); StatusView::whereStatusId($status->id)->delete(); Status::whereReblogOfId($status->id)->forceDelete(); $status->forceDelete(); diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index 78c41ed3d..9898d3c82 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -174,10 +174,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing ->whereObjectId($status->id) ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); - foreach($statusHashtags as $stag) { - $stag->delete(); - } + StatusHashtag::whereStatusId($status->id)->deleteQuietly(); StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index c0ced1368..a053bfe75 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -151,10 +151,7 @@ class StatusDelete implements ShouldQueue ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - $statusHashtags = StatusHashtag::whereStatusId($status->id)->get(); - foreach($statusHashtags as $stag) { - $stag->delete(); - } + StatusHashtag::whereStatusId($status->id)->deleteQuietly(); StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); From 89b8e87477caedfe4cec4534a370be184b8a9190 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 02:03:15 -0700 Subject: [PATCH 151/977] Update ApiV1Controller, apply user domain blocks filtering to hashtag timelines --- app/Http/Controllers/Api/ApiV1Controller.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index be77b5606..798d9ee55 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3285,6 +3285,7 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 20); $onlyMedia = $request->input('only_media', true); $pe = $request->has(self::PF_API_ENTITY_KEY); + $pid = $request->user()->profile_id; if($min || $max) { $minMax = SnowflakeService::byDate(now()->subMonths(6)); @@ -3296,7 +3297,8 @@ class ApiV1Controller extends Controller } } - $filters = UserFilterService::filters($request->user()->profile_id); + $filters = UserFilterService::filters($pid); + $domainBlocks = UserFilterService::domainBlocks($pid); if(!$min && !$max) { $id = 1; @@ -3322,10 +3324,11 @@ class ApiV1Controller extends Controller if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) { return false; } - return $i && isset($i['account']); + return $i && isset($i['account'], $i['url']); }) - ->filter(function($i) use($filters) { - return !in_array($i['account']['id'], $filters); + ->filter(function($i) use($filters, $domainBlocks) { + $domain = strtolower(parse_url($i['url'], PHP_URL_HOST)); + return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks); }) ->values() ->toArray(); From 5169936062d801936ac688c0f4e220c4794f5e62 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 02:05:26 -0700 Subject: [PATCH 152/977] Update MarkerService, fix php deprecation warning --- app/Services/MarkerService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/MarkerService.php b/app/Services/MarkerService.php index 6b407b567..130f0b017 100644 --- a/app/Services/MarkerService.php +++ b/app/Services/MarkerService.php @@ -13,7 +13,7 @@ class MarkerService return Cache::get(self::CACHE_KEY . $timeline . ':' . $profileId); } - public static function set($profileId, $timeline = 'home', $entityId) + public static function set($profileId, $timeline = 'home', $entityId = false) { $existing = self::get($profileId, $timeline); $key = self::CACHE_KEY . $timeline . ':' . $profileId; From 6c39df7fb38781efd0b7464d96ace0cb0912add1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 02:08:44 -0700 Subject: [PATCH 153/977] Update Inbox, import AccountService --- app/Util/ActivityPub/Inbox.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index b8bb780c8..e26f0a48c 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -39,6 +39,7 @@ use App\Util\ActivityPub\Validator\Like as LikeValidator; use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator; use App\Util\ActivityPub\Validator\UpdatePersonValidator; +use App\Services\AccountService; use App\Services\PollService; use App\Services\FollowerService; use App\Services\ReblogService; From e98df1196f079ccad4e44897457f035b8efeadcf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 03:34:31 -0700 Subject: [PATCH 154/977] Add migration --- ...1_103223_purge_deleted_status_hashtags.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php diff --git a/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php b/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php new file mode 100644 index 000000000..bf2acc34e --- /dev/null +++ b/database/migrations/2023_12_21_103223_purge_deleted_status_hashtags.php @@ -0,0 +1,25 @@ +lazyById(200)->each->deleteQuietly(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; From 3e28cf661ba683a51256363de6deba4b372aa32f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 03:35:47 -0700 Subject: [PATCH 155/977] Add user domain block commands --- app/Console/Commands/AddUserDomainBlock.php | 99 +++++++++++++++++++ .../Commands/DeleteUserDomainBlock.php | 88 +++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 app/Console/Commands/AddUserDomainBlock.php create mode 100644 app/Console/Commands/DeleteUserDomainBlock.php diff --git a/app/Console/Commands/AddUserDomainBlock.php b/app/Console/Commands/AddUserDomainBlock.php new file mode 100644 index 000000000..33f441cdc --- /dev/null +++ b/app/Console/Commands/AddUserDomainBlock.php @@ -0,0 +1,99 @@ +validateDomain($domain); + $this->processBlocks($domain); + return; + } + + protected function validateDomain($domain) + { + if(!strpos($domain, '.')) { + $this->error('Invalid domain'); + return; + } + + if(str_starts_with($domain, 'https://')) { + $domain = str_replace('https://', '', $domain); + } + + if(str_starts_with($domain, 'http://')) { + $domain = str_replace('http://', '', $domain); + } + + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); + if(!$valid) { + $this->error('Invalid domain'); + return; + } + + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + + if($domain === config('pixelfed.domain.app')) { + $this->error('Invalid domain'); + return; + } + + $confirmed = confirm('Are you sure you want to block ' . $domain . '?'); + if(!$confirmed) { + return; + } + + return $domain; + } + + protected function processBlocks($domain) + { + progress( + label: 'Updating user domain blocks...', + steps: User::lazyById(500), + callback: fn ($user) => $this->performTask($user, $domain), + ); + } + + protected function performTask($user, $domain) + { + if(!$user->profile_id || $user->delete_after) { + return; + } + + if($user->status != null && $user->status != 'disabled') { + return; + } + + UserDomainBlock::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'domain' => $domain + ]); + } +} diff --git a/app/Console/Commands/DeleteUserDomainBlock.php b/app/Console/Commands/DeleteUserDomainBlock.php new file mode 100644 index 000000000..80c139f2b --- /dev/null +++ b/app/Console/Commands/DeleteUserDomainBlock.php @@ -0,0 +1,88 @@ +validateDomain($domain); + $this->processUnblocks($domain); + return; + } + + protected function validateDomain($domain) + { + if(!strpos($domain, '.')) { + $this->error('Invalid domain'); + return; + } + + if(str_starts_with($domain, 'https://')) { + $domain = str_replace('https://', '', $domain); + } + + if(str_starts_with($domain, 'http://')) { + $domain = str_replace('http://', '', $domain); + } + + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); + if(!$valid) { + $this->error('Invalid domain'); + return; + } + + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + + if($domain === config('pixelfed.domain.app')) { + $this->error('Invalid domain'); + return; + } + + $confirmed = confirm('Are you sure you want to unblock ' . $domain . '?'); + if(!$confirmed) { + return; + } + + return $domain; + } + + protected function processUnblocks($domain) + { + progress( + label: 'Updating user domain blocks...', + steps: UserDomainBlock::whereDomain($domain)->lazyById(500), + callback: fn ($domainBlock) => $this->performTask($domainBlock), + ); + } + + protected function performTask($domainBlock) + { + $domainBlock->deleteQuietly(); + } +} From f3f0175c8494d9bf45931e02fa8c6ccb9558968d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 03:47:23 -0700 Subject: [PATCH 156/977] Add DefaultDomainBlock model + migration --- app/Models/DefaultDomainBlock.php | 13 +++++++++ ...103_create_default_domain_blocks_table.php | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 app/Models/DefaultDomainBlock.php create mode 100644 database/migrations/2023_12_21_104103_create_default_domain_blocks_table.php diff --git a/app/Models/DefaultDomainBlock.php b/app/Models/DefaultDomainBlock.php new file mode 100644 index 000000000..d90816a32 --- /dev/null +++ b/app/Models/DefaultDomainBlock.php @@ -0,0 +1,13 @@ +id(); + $table->string('domain')->unique()->index(); + $table->text('note')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('default_domain_blocks'); + } +}; From 519c7a3735c7893cee93486dd5a27a43fee6e3d1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 03:48:08 -0700 Subject: [PATCH 157/977] Update domain block commands --- app/Console/Commands/AddUserDomainBlock.php | 5 +++++ app/Console/Commands/DeleteUserDomainBlock.php | 3 +++ 2 files changed, 8 insertions(+) diff --git a/app/Console/Commands/AddUserDomainBlock.php b/app/Console/Commands/AddUserDomainBlock.php index 33f441cdc..7128879e1 100644 --- a/app/Console/Commands/AddUserDomainBlock.php +++ b/app/Console/Commands/AddUserDomainBlock.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use App\User; +use App\Models\DefaultDomainBlock; use App\Models\UserDomainBlock; use function Laravel\Prompts\text; use function Laravel\Prompts\confirm; @@ -31,6 +32,7 @@ class AddUserDomainBlock extends Command public function handle() { $domain = text('Enter domain you want to block'); + $domain = strtolower($domain); $domain = $this->validateDomain($domain); $this->processBlocks($domain); return; @@ -74,6 +76,9 @@ class AddUserDomainBlock extends Command protected function processBlocks($domain) { + DefaultDomainBlock::updateOrCreate([ + 'domain' => $domain + ]); progress( label: 'Updating user domain blocks...', steps: User::lazyById(500), diff --git a/app/Console/Commands/DeleteUserDomainBlock.php b/app/Console/Commands/DeleteUserDomainBlock.php index 80c139f2b..9cc1c1ded 100644 --- a/app/Console/Commands/DeleteUserDomainBlock.php +++ b/app/Console/Commands/DeleteUserDomainBlock.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use App\User; +use App\Models\DefaultDomainBlock; use App\Models\UserDomainBlock; use function Laravel\Prompts\text; use function Laravel\Prompts\confirm; @@ -31,6 +32,7 @@ class DeleteUserDomainBlock extends Command public function handle() { $domain = text('Enter domain you want to unblock'); + $domain = strtolower($domain); $domain = $this->validateDomain($domain); $this->processUnblocks($domain); return; @@ -74,6 +76,7 @@ class DeleteUserDomainBlock extends Command protected function processUnblocks($domain) { + DefaultDomainBlock::whereDomain($domain)->delete(); progress( label: 'Updating user domain blocks...', steps: UserDomainBlock::whereDomain($domain)->lazyById(500), From fa0380ac3be5534ef6f113347fd9feaec8e88d8c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 04:47:09 -0700 Subject: [PATCH 158/977] Update UserObserver, add default domain blocks logic --- app/Observers/UserObserver.php | 207 +++++++++++++++++++++------------ 1 file changed, 131 insertions(+), 76 deletions(-) diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index ec4ef9f34..d587bd7e8 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -7,90 +7,52 @@ use App\Follower; use App\Profile; use App\User; use App\UserSetting; +use App\Services\UserFilterService; +use App\Models\DefaultDomainBlock; +use App\Models\UserDomainBlock; use App\Jobs\FollowPipeline\FollowPipeline; use DB; use App\Services\FollowerService; class UserObserver { - /** - * Listen to the User created event. - * - * @param \App\User $user - * - * @return void - */ - public function saved(User $user) - { - if($user->status == 'deleted') { - return; - } + /** + * Handle the notification "created" event. + * + * @param \App\User $user + * @return void + */ + public function created(User $user): void + { + $this->handleUser($user); + } - if(Profile::whereUsername($user->username)->exists()) { - return; + /** + * Listen to the User saved event. + * + * @param \App\User $user + * + * @return void + */ + public function saved(User $user) + { + $this->handleUser($user); + } + + /** + * Listen to the User updated event. + * + * @param \App\User $user + * + * @return void + */ + public function updated(User $user): void + { + $this->handleUser($user); + if($user->profile) { + $this->applyDefaultDomainBlocks($user); } - - if (empty($user->profile)) { - $profile = DB::transaction(function() use($user) { - $profile = new Profile(); - $profile->user_id = $user->id; - $profile->username = $user->username; - $profile->name = $user->name; - $pkiConfig = [ - 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]; - $pki = openssl_pkey_new($pkiConfig); - openssl_pkey_export($pki, $pki_private); - $pki_public = openssl_pkey_get_details($pki); - $pki_public = $pki_public['key']; - - $profile->private_key = $pki_private; - $profile->public_key = $pki_public; - $profile->save(); - return $profile; - }); - - DB::transaction(function() use($user, $profile) { - $user = User::findOrFail($user->id); - $user->profile_id = $profile->id; - $user->save(); - - CreateAvatar::dispatch($profile); - }); - - if(config_cache('account.autofollow') == true) { - $names = config_cache('account.autofollow_usernames'); - $names = explode(',', $names); - - if(!$names || !last($names)) { - return; - } - - $profiles = Profile::whereIn('username', $names)->get(); - - if($profiles) { - foreach($profiles as $p) { - $follower = new Follower; - $follower->profile_id = $profile->id; - $follower->following_id = $p->id; - $follower->save(); - - FollowPipeline::dispatch($follower); - } - } - } - } - - if (empty($user->settings)) { - DB::transaction(function() use($user) { - UserSetting::firstOrCreate([ - 'user_id' => $user->id - ]); - }); - } - } + } /** * Handle the user "deleted" event. @@ -102,4 +64,97 @@ class UserObserver { FollowerService::delCache($user->profile_id); } + + protected function handleUser($user) + { + if(in_array($user->status, ['deleted', 'delete'])) { + return; + } + + if(Profile::whereUsername($user->username)->exists()) { + return; + } + + if (empty($user->profile)) { + $profile = DB::transaction(function() use($user) { + $profile = new Profile(); + $profile->user_id = $user->id; + $profile->username = $user->username; + $profile->name = $user->name; + $pkiConfig = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + $pki = openssl_pkey_new($pkiConfig); + openssl_pkey_export($pki, $pki_private); + $pki_public = openssl_pkey_get_details($pki); + $pki_public = $pki_public['key']; + + $profile->private_key = $pki_private; + $profile->public_key = $pki_public; + $profile->save(); + $this->applyDefaultDomainBlocks($user); + return $profile; + }); + + + DB::transaction(function() use($user, $profile) { + $user = User::findOrFail($user->id); + $user->profile_id = $profile->id; + $user->save(); + + CreateAvatar::dispatch($profile); + }); + + if(config_cache('account.autofollow') == true) { + $names = config_cache('account.autofollow_usernames'); + $names = explode(',', $names); + + if(!$names || !last($names)) { + return; + } + + $profiles = Profile::whereIn('username', $names)->get(); + + if($profiles) { + foreach($profiles as $p) { + $follower = new Follower; + $follower->profile_id = $profile->id; + $follower->following_id = $p->id; + $follower->save(); + + FollowPipeline::dispatch($follower); + } + } + } + } + + if (empty($user->settings)) { + DB::transaction(function() use($user) { + UserSetting::firstOrCreate([ + 'user_id' => $user->id + ]); + }); + } + } + + protected function applyDefaultDomainBlocks($user) + { + if($user->profile_id == null) { + return; + } + $defaultDomainBlocks = DefaultDomainBlock::pluck('domain')->toArray(); + + if(!$defaultDomainBlocks || !count($defaultDomainBlocks)) { + return; + } + + foreach($defaultDomainBlocks as $domain) { + UserDomainBlock::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'domain' => strtolower(trim($domain)) + ]); + } + } } From d8f46f47a1106b5c5d78efaa4a67abb9696b965f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 04:48:56 -0700 Subject: [PATCH 159/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f75a72a2..8ab35fec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6)) - Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7)) - Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46)) +- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) From 73a0f528ab6cc7a5e272ec2dff2c1df37e1f39d3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 05:00:35 -0700 Subject: [PATCH 160/977] Update user domain block commands --- app/Console/Commands/AddUserDomainBlock.php | 10 ++++++---- app/Console/Commands/DeleteUserDomainBlock.php | 15 ++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/Console/Commands/AddUserDomainBlock.php b/app/Console/Commands/AddUserDomainBlock.php index 7128879e1..6d5c192bf 100644 --- a/app/Console/Commands/AddUserDomainBlock.php +++ b/app/Console/Commands/AddUserDomainBlock.php @@ -34,6 +34,10 @@ class AddUserDomainBlock extends Command $domain = text('Enter domain you want to block'); $domain = strtolower($domain); $domain = $this->validateDomain($domain); + if(!$domain || empty($domain)) { + $this->error('Invalid domain'); + return; + } $this->processBlocks($domain); return; } @@ -41,7 +45,6 @@ class AddUserDomainBlock extends Command protected function validateDomain($domain) { if(!strpos($domain, '.')) { - $this->error('Invalid domain'); return; } @@ -53,14 +56,13 @@ class AddUserDomainBlock extends Command $domain = str_replace('http://', '', $domain); } + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); if(!$valid) { - $this->error('Invalid domain'); return; } - $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); - if($domain === config('pixelfed.domain.app')) { $this->error('Invalid domain'); return; diff --git a/app/Console/Commands/DeleteUserDomainBlock.php b/app/Console/Commands/DeleteUserDomainBlock.php index 9cc1c1ded..405b6fe76 100644 --- a/app/Console/Commands/DeleteUserDomainBlock.php +++ b/app/Console/Commands/DeleteUserDomainBlock.php @@ -34,6 +34,10 @@ class DeleteUserDomainBlock extends Command $domain = text('Enter domain you want to unblock'); $domain = strtolower($domain); $domain = $this->validateDomain($domain); + if(!$domain || empty($domain)) { + $this->error('Invalid domain'); + return; + } $this->processUnblocks($domain); return; } @@ -41,7 +45,6 @@ class DeleteUserDomainBlock extends Command protected function validateDomain($domain) { if(!strpos($domain, '.')) { - $this->error('Invalid domain'); return; } @@ -53,16 +56,14 @@ class DeleteUserDomainBlock extends Command $domain = str_replace('http://', '', $domain); } + $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); + $valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE); if(!$valid) { - $this->error('Invalid domain'); return; } - $domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST)); - if($domain === config('pixelfed.domain.app')) { - $this->error('Invalid domain'); return; } @@ -77,6 +78,10 @@ class DeleteUserDomainBlock extends Command protected function processUnblocks($domain) { DefaultDomainBlock::whereDomain($domain)->delete(); + if(!UserDomainBlock::whereDomain($domain)->count()) { + $this->info('No results found!'); + return; + } progress( label: 'Updating user domain blocks...', steps: UserDomainBlock::whereDomain($domain)->lazyById(500), From 1be21c76f38940509f2282c65db6288ba7dcf989 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 21 Dec 2023 06:18:51 -0700 Subject: [PATCH 161/977] Fix StatusHashtag delete bug --- app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php | 2 +- app/Jobs/StatusPipeline/RemoteStatusDelete.php | 2 +- app/Jobs/StatusPipeline/StatusDelete.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php index 824323cda..4969fca2f 100644 --- a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php +++ b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php @@ -76,7 +76,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue }); Mention::whereStatusId($status->id)->forceDelete(); Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->deleteQuietly(); + StatusHashtag::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete(); Status::whereReblogOfId($status->id)->forceDelete(); $status->forceDelete(); diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index 9898d3c82..07a2f6236 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -174,7 +174,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing ->whereObjectId($status->id) ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->deleteQuietly(); + StatusHashtag::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); diff --git a/app/Jobs/StatusPipeline/StatusDelete.php b/app/Jobs/StatusPipeline/StatusDelete.php index a053bfe75..dbbfad5ac 100644 --- a/app/Jobs/StatusPipeline/StatusDelete.php +++ b/app/Jobs/StatusPipeline/StatusDelete.php @@ -151,7 +151,7 @@ class StatusDelete implements ShouldQueue ->delete(); StatusArchived::whereStatusId($status->id)->delete(); - StatusHashtag::whereStatusId($status->id)->deleteQuietly(); + StatusHashtag::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete(); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); From adfaa2b1404e5a66cde66a563951fc24577e01f4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 25 Dec 2023 00:30:05 -0700 Subject: [PATCH 162/977] Update AP ProfileTransformer, add published attribute --- app/Transformer/ActivityPub/ProfileTransformer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Transformer/ActivityPub/ProfileTransformer.php b/app/Transformer/ActivityPub/ProfileTransformer.php index cdd4eb82d..45d22cd11 100644 --- a/app/Transformer/ActivityPub/ProfileTransformer.php +++ b/app/Transformer/ActivityPub/ProfileTransformer.php @@ -40,6 +40,7 @@ class ProfileTransformer extends Fractal\TransformerAbstract 'url' => $profile->url(), 'manuallyApprovesFollowers' => (bool) $profile->is_private, 'indexable' => (bool) $profile->indexable, + 'published' => $profile->created_at->format('Y-m-d') . 'T00:00:00Z', 'publicKey' => [ 'id' => $profile->permalink().'#main-key', 'owner' => $profile->permalink(), From f66b9fe74e1bfc7842ab9d0fac21c05847bbf188 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 25 Dec 2023 00:31:05 -0700 Subject: [PATCH 163/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab35fec3..10b13f261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ - Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545)) - Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a)) - Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc)) +- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 7dbdbf15a5fcfcd11726b4f3fabffa53cf08bb72 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 27 Dec 2023 02:51:47 -0700 Subject: [PATCH 164/977] Add Roles & Parental Controls --- app/Http/Controllers/Api/ApiV1Controller.php | 10 ++ app/Http/Controllers/UserRolesController.php | 10 ++ app/Models/UserRoles.php | 23 ++++ app/Providers/AuthServiceProvider.php | 2 +- app/Services/UserRoleService.php | 111 ++++++++++++++++++ ...3_12_27_081801_create_user_roles_table.php | 30 +++++ ...27_082024_add_has_roles_to_users_table.php | 28 +++++ 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/UserRolesController.php create mode 100644 app/Models/UserRoles.php create mode 100644 app/Services/UserRoleService.php create mode 100644 database/migrations/2023_12_27_081801_create_user_roles_table.php create mode 100644 database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 798d9ee55..6f314e0b3 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -98,6 +98,7 @@ use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; use App\Services\DiscoverService; use App\Services\CustomEmojiService; use App\Services\MarkerService; +use App\Services\UserRoleService; use App\Models\Conversation; use App\Jobs\FollowPipeline\FollowAcceptPipeline; use App\Jobs\FollowPipeline\FollowRejectPipeline; @@ -1623,6 +1624,8 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); if($user->last_active_at == null) { @@ -1792,6 +1795,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $media = Media::whereUserId($user->id) @@ -1831,6 +1835,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); if($user->last_active_at == null) { return []; @@ -2419,8 +2424,13 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 20; $user = $request->user(); + $remote = $request->has('remote'); $local = $request->has('local'); + $userRoleKey = $remote ? 'can-view-network-feed' : 'can-view-public-feed'; + if($user->has_roles && !UserRoleService::can($userRoleKey, $user->id)) { + return []; + } $filtered = $user ? UserFilterService::filters($user->profile_id) : []; AccountService::setLastActive($user->id); $domainBlocks = UserFilterService::domainBlocks($user->profile_id); diff --git a/app/Http/Controllers/UserRolesController.php b/app/Http/Controllers/UserRolesController.php new file mode 100644 index 000000000..dbd34d0da --- /dev/null +++ b/app/Http/Controllers/UserRolesController.php @@ -0,0 +1,10 @@ + 'array' + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 0ec8c1895..8e1a6a98d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -14,7 +14,7 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - 'App\Model' => 'App\Policies\ModelPolicy', + // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php new file mode 100644 index 000000000..fef5356c2 --- /dev/null +++ b/app/Services/UserRoleService.php @@ -0,0 +1,111 @@ +first()) { + return $roles->roles; + } + + return self::defaultRoles(); + } + + public static function roleKeys() + { + return array_keys(self::defaultRoles()); + } + + public static function defaultRoles() + { + return [ + 'account-force-private' => true, + 'account-ignore-follow-requests' => true, + + 'can-view-public-feed' => true, + 'can-view-network-feed' => true, + 'can-view-discover' => true, + + 'can-post' => true, + 'can-comment' => true, + 'can-like' => true, + 'can-share' => true, + + 'can-follow' => false, + 'can-make-public' => false, + ]; + } + + public static function getRoles($id) + { + $myRoles = self::get($id); + $roleData = collect(self::roleData()) + ->map(function($role, $k) use($myRoles) { + $role['value'] = $myRoles[$k]; + return $role; + }) + ->toArray(); + return $roleData; + } + + public static function roleData() + { + return [ + 'account-force-private' => [ + 'title' => 'Force Private Account', + 'action' => 'Prevent changing account from private' + ], + 'account-ignore-follow-requests' => [ + 'title' => 'Ignore Follow Requests', + 'action' => 'Hide follow requests and associated notifications' + ], + 'can-view-public-feed' => [ + 'title' => 'Hide Public Feed', + 'action' => 'Hide the public feed timeline' + ], + 'can-view-network-feed' => [ + 'title' => 'Hide Network Feed', + 'action' => 'Hide the network feed timeline' + ], + 'can-view-discover' => [ + 'title' => 'Hide Discover', + 'action' => 'Hide the discover feature' + ], + 'can-post' => [ + 'title' => 'Can post', + 'action' => 'Allows new posts to be shared' + ], + 'can-comment' => [ + 'title' => 'Can comment', + 'action' => 'Allows new comments to be posted' + ], + 'can-like' => [ + 'title' => 'Can Like', + 'action' => 'Allows the ability to like posts and comments' + ], + 'can-share' => [ + 'title' => 'Can Share', + 'action' => 'Allows the ability to share posts and comments' + ], + 'can-follow' => [ + 'title' => 'Can Follow', + 'action' => 'Allows the ability to follow accounts' + ], + 'can-make-public' => [ + 'title' => 'Can make account public', + 'action' => 'Allows the ability to make account public' + ], + ]; + } +} diff --git a/database/migrations/2023_12_27_081801_create_user_roles_table.php b/database/migrations/2023_12_27_081801_create_user_roles_table.php new file mode 100644 index 000000000..4e6af18d0 --- /dev/null +++ b/database/migrations/2023_12_27_081801_create_user_roles_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('profile_id')->unique()->index(); + $table->unsignedInteger('user_id')->unique()->index(); + $table->json('roles')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_roles'); + } +}; diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php new file mode 100644 index 000000000..c60b81deb --- /dev/null +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -0,0 +1,28 @@ +boolean('has_roles')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('has_roles'); + }); + } +}; From 98211d3620fb5e22e9712f2fe24e202304a26c1a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 28 Dec 2023 23:46:59 +0000 Subject: [PATCH 165/977] refactor Dockerfile and Docker workflow --- .dockerignore | 3 +- .editorconfig | 4 + .github/workflows/build-docker.yml | 125 ------- .github/workflows/docker.yml | 176 ++++++++++ .hadolint.yaml | 5 + config/filesystems.php | 2 +- contrib/docker/Dockerfile | 320 ++++++++++++++++++ contrib/docker/Dockerfile.apache | 100 ------ contrib/docker/Dockerfile.fpm | 90 ----- contrib/docker/README.md | 214 ++++++++++++ .../apache/conf-available/remoteip.conf | 4 + .../apache/docker-entrypoint.d/.gitkeep | 0 contrib/docker/docker-entrypoint.sh | 45 +++ .../docker/fpm/docker-entrypoint.d/.gitkeep | 0 contrib/docker/nginx/Procfile | 2 + contrib/docker/nginx/default-http.conf | 49 +++ .../docker/nginx/docker-entrypoint.d/.gitkeep | 0 contrib/docker/php.production.ini | 6 +- .../shared/docker-entrypoint.d/00-storage.sh | 13 + .../shared/docker-entrypoint.d/01-cache.sh | 13 + .../shared/docker-entrypoint.d/02-horizon.sh | 6 + contrib/docker/shared/lib.sh | 13 + contrib/docker/start.apache.sh | 15 - contrib/docker/start.fpm.sh | 15 - .../views/admin/diagnostics/home.blade.php | 2 +- 25 files changed, 870 insertions(+), 352 deletions(-) delete mode 100644 .github/workflows/build-docker.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .hadolint.yaml create mode 100644 contrib/docker/Dockerfile delete mode 100644 contrib/docker/Dockerfile.apache delete mode 100644 contrib/docker/Dockerfile.fpm create mode 100644 contrib/docker/README.md create mode 100644 contrib/docker/apache/conf-available/remoteip.conf create mode 100644 contrib/docker/apache/docker-entrypoint.d/.gitkeep create mode 100755 contrib/docker/docker-entrypoint.sh create mode 100644 contrib/docker/fpm/docker-entrypoint.d/.gitkeep create mode 100644 contrib/docker/nginx/Procfile create mode 100644 contrib/docker/nginx/default-http.conf create mode 100644 contrib/docker/nginx/docker-entrypoint.d/.gitkeep create mode 100755 contrib/docker/shared/docker-entrypoint.d/00-storage.sh create mode 100755 contrib/docker/shared/docker-entrypoint.d/01-cache.sh create mode 100755 contrib/docker/shared/docker-entrypoint.d/02-horizon.sh create mode 100644 contrib/docker/shared/lib.sh delete mode 100755 contrib/docker/start.apache.sh delete mode 100755 contrib/docker/start.fpm.sh diff --git a/.dockerignore b/.dockerignore index 70376cdf4..8e25f3cbc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ data -Dockerfile -contrib/docker/Dockerfile.* +contrib/docker/Dockerfile docker-compose*.yml .dockerignore .git diff --git a/.editorconfig b/.editorconfig index 1cd7d1077..0eda619e8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +[{*.yml,*.yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index 34f31cf08..000000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Build Docker image - -on: - workflow_dispatch: - push: - branches: - - dev - tags: - - '*' - pull_request: - paths: - - .github/workflows/build-docker.yml - - contrib/docker/Dockerfile.apache - - contrib/docker/Dockerfile.fpm -permissions: - contents: read - -jobs: - build-docker-apache: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.apache - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - latest=auto - suffix=-apache - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.apache - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - - build-docker-fpm: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Docker Lint - uses: hadolint/hadolint-action@v3.0.0 - with: - dockerfile: contrib/docker/Dockerfile.fpm - failure-threshold: error - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - secrets: inherit - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: github.event_name != 'pull_request' - - - name: Fetch tags - uses: docker/metadata-action@v4 - secrets: inherit - id: meta - with: - images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed - flavor: | - suffix=-fpm - tags: | - type=edge,branch=dev - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Build and push Docker image - uses: docker/build-push-action@v3 - with: - context: . - file: contrib/docker/Dockerfile.fpm - platforms: linux/amd64,linux/arm64 - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..b71c08c87 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,176 @@ +--- +name: Docker + +on: + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push + push: + branches: + - dev + - jippi-fork # TODO(jippi): remove me before merge + tags: + - "*" + + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request + pull_request: + types: + - labeled + - opened + - ready_for_review + - reopened + - synchronize + +jobs: + lint: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Docker Lint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: contrib/docker/Dockerfile + failure-threshold: error + + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + matrix: + php_version: + - 8.1 + - 8.2 + - 8.3 + target_runtime: + - apache + - fpm + - nginx + php_base: + - apache + - fpm + + # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude + exclude: + # Broken for imagick on arm64 due to https://github.com/Imagick/imagick/pull/641 + # Could probably figure out how to do a matrix only ignoring 8.3 + linux/arm64, but this is easier atm + - php_version: 8.3 + + # targeting [apache] runtime with [fpm] base type doesn't make sense + - target_runtime: apache + php_base: fpm + + # targeting [fpm] runtime with [apache] base type doesn't make sense + - target_runtime: fpm + php_base: apache + + # targeting [nginx] runtime with [apache] base type doesn't make sense + - target_runtime: nginx + php_base: apache + + # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior + concurrency: + group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }} + cancel-in-progress: true + + permissions: + contents: read + packages: write + + env: + # Set the repo variable [DOCKER_HUB_USERNAME] to override the default at https://github.com///settings/variables/actions + # + # NOTE: no login attempt will happen with Docker Hub until this secret is set + DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default at https://github.com///settings/variables/actions + # + # NOTE: no login attempt will happen with Docker Hub until this secret is set + DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_REPO] to override the default at https://github.com///settings/variables/actions + # + # NOTE: no login attempt will happen with Docker Hub until this secret is set + DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }} + + # For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN] + # set to your Personal Access Token at https://github.com///settings/secrets/actions + # + # NOTE: no login attempt will happen with Docker Hub until this secret is set + HAS_DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN != '' }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + with: + version: v0.12.0 # *or* newer, needed for annotations to work + + - name: Log in to the GitHub Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub registry (conditionally) + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + if: ${{ env.HAS_DOCKER_HUB_TOKEN == true }} + + - name: Docker meta + uses: docker/metadata-action@v5 + id: meta + with: + images: | + name=ghcr.io/${{ github.repository }},enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_TOKEN }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=edge,branch=dev + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: contrib/docker/Dockerfile + target: ${{ matrix.target_runtime }}-runtime + platforms: linux/amd64,linux/arm64 + builder: ${{ steps.buildx.outputs.name }} + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + push: true + sbom: true + provenance: true + build-args: | + PHP_VERSION=${{ matrix.php_version }} + PHP_BASE_TYPE=${{ matrix.php_base }} + cache-from: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + cache-to: type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 000000000..cbb62ca47 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,5 @@ +ignored: + - DL3002 # warning: Last USER should not be root + - DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install ` use `apt-get install =` + - SC2046 # warning: Quote this to prevent word splitting. + - SC2086 # info: Double quote to prevent globbing and word splitting. diff --git a/config/filesystems.php b/config/filesystems.php index 6817d5e34..d5247c980 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -72,7 +72,7 @@ return [ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), - 'visibility' => 'public', + 'visibility' => env('AWS_VISIBILITY', 'public'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 000000000..0a15bc6b5 --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,320 @@ +# syntax=docker/dockerfile:1 +# See https://hub.docker.com/r/docker/dockerfile + +####################################################### +# Configuration +####################################################### + +ARG COMPOSER_VERSION="2.6" +ARG NGINX_VERSION=1.25.3 +ARG FOREGO_VERSION=0.17.2 +ARG PECL_EXTENSIONS_EXTRA="" +ARG PECL_EXTENSIONS="imagick redis" +ARG PHP_BASE_TYPE="apache" +ARG PHP_DATABASE_EXTENSIONS="pdo_pgsql pdo_mysql" +ARG PHP_DEBIAN_RELEASE="bullseye" +ARG PHP_EXTENSIONS_EXTRA="" +ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" +ARG PHP_VERSION="8.1" +ARG APT_PACKAGES_EXTRA="" + +# GPG key for nginx apt repository +ARG NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 + +# GPP key path for nginx apt repository +ARG NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg + +####################################################### +# Docker "copy from" images +####################################################### + +# Composer docker image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM composer:${COMPOSER_VERSION} AS composer-image + +# nginx webserver from Docker Hub. +# Used to copy some docker-entrypoint files for [nginx-runtime] +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM nginx:${NGINX_VERSION} AS nginx-image + +# Forego is a Procfile "runner" that makes it trival to run multiple +# processes under a simple init / PID 1 process. +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/nginx-proxy/forego +FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image + +####################################################### +# Base image +####################################################### + +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base + +ARG PHP_VERSION +ARG PHP_DEBIAN_RELEASE +ARG APT_PACKAGES_EXTRA + +ARG TARGETPLATFORM +ARG BUILDKIT_SBOM_SCAN_STAGE=true + +ENV DEBIAN_FRONTEND=noninteractive + +# Ensure we run all scripts through 'bash' rather than 'sh' +SHELL ["/bin/bash", "-c"] + +RUN set -ex \ + && mkdir -pv /var/www/ \ + && chown -R 33:33 /var/www + +WORKDIR /var/www/ + +# Install package dependencies +RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ +<<-SCRIPT + #!/bin/bash + set -ex -o errexit -o nounset -o pipefail + + # ensure we keep apt cache around in a Docker environment + rm -f /etc/apt/apt.conf.d/docker-clean + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache + + # Standard packages + standardPackages=( + apt-utils + ca-certificates + gettext-base + git + gnupg1 + gosu + libcurl4-openssl-dev + libzip-dev + locales + locales-all + nano + procps + unzip + zip + ) + + # Image Optimization + imageOptimization=( + gifsicle + jpegoptim + optipng + pngquant + ) + + # Image Processing + imageProcessing=( + libjpeg62-turbo-dev + libmagickwand-dev + libpng-dev + ) + + # Required for GD + gdDependencies=( + libwebp-dev + libwebp6 + libxpm-dev + libxpm4 + ) + + # Video Processing + videoProcessing=( + ffmpeg + ) + + # Database + databaseDependencies=( + libpq-dev + libsqlite3-dev + ) + + apt-get update + + apt-get upgrade -y + + apt-get install -y --no-install-recommends \ + ${standardPackages[*]} \ + ${imageOptimization[*]} \ + ${imageProcessing[*]} \ + ${gdDependencies[*]} \ + ${videoProcessing[*]} \ + ${databaseDependencies[*]} \ + ${APT_PACKAGES_EXTRA} +SCRIPT + +# update locales +RUN set -ex \ + && locale-gen \ + && update-locale + +####################################################### +# PHP: extensions +####################################################### + +FROM base AS php-extensions + +ARG PECL_EXTENSIONS +ARG PECL_EXTENSIONS_EXTRA +ARG PHP_DATABASE_EXTENSIONS +ARG PHP_DEBIAN_RELEASE +ARG PHP_EXTENSIONS +ARG PHP_EXTENSIONS_EXTRA +ARG PHP_VERSION +ARG TARGETPLATFORM + +RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ + set -ex \ + # Grab the PHP source code so we can compile against it + && docker-php-source extract \ + # Install pecl extensions + && pecl install ${PECL_EXTENSIONS} ${PECL_EXTENSIONS_EXTRA} \ + # PHP GD extensions + && docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + --with-xpm \ + # PHP extensions (dependencies) + && docker-php-ext-install -j$(nproc) ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_DATABASE_EXTENSIONS} \ + # Enable all extensions + && docker-php-ext-enable ${PECL_EXTENSIONS} ${PECL_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_DATABASE_EXTENSIONS} + +####################################################### +# PHP: composer and source code +####################################################### + +FROM base AS composer-and-src + +ARG PHP_VERSION +ARG PHP_DEBIAN_RELEASE +ARG TARGETPLATFORM + +# Make sure composer cache is targeting our cache mount later +ENV COMPOSER_CACHE_DIR=/cache/composer + +# Don't enforce any memory limits for composer +ENV COMPOSER_MEMORY_LIMIT=-1 + +# Disable interactvitity from composer +ENV COMPOSER_NO_INTERACTION=1 + +# Copy composer from https://hub.docker.com/_/composer +COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer + +#! Changing user to 33 +USER 33:33 + +# Copy over only composer related files so docker layer cache isn't invalidated on PHP file changes +COPY --link --chown=33:33 composer.json composer.lock /var/www/ + +# Install composer dependencies +# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) +RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/cache/composer \ + set -ex \ + && composer install --prefer-dist --no-autoloader --ignore-platform-reqs + +# Copy all other files over +COPY --link --chown=33:33 . /var/www/ + +# Generate optimized autoloader now that we have all files around +RUN set -ex \ + && composer dump-autoload --optimize + +#! Changing back to root +USER root:root + +####################################################### +# Runtime: base +####################################################### + +FROM base AS shared-runtime + +COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/php/extensions +COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php +COPY --link --from=composer-and-src --chown=33:33 /var/www /var/www +COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego +COPY --link contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" + +# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 +RUN set -ex \ + && cp --recursive --link --preserve=all storage storage.skel \ + && rm -rf html && ln -s public html + +COPY --link contrib/docker/docker-entrypoint.sh /docker-entrypoint.sh +COPY --link contrib/docker/shared/lib.sh /lib.sh +COPY --link contrib/docker/shared/docker-entrypoint.d /docker-entrypoint.d/ + +ENTRYPOINT ["/docker-entrypoint.sh"] + +VOLUME /var/www/storage /var/www/bootstrap + +####################################################### +# Runtime: apache +####################################################### + +FROM shared-runtime AS apache-runtime + +COPY --link contrib/docker/apache/conf-available/remoteip.conf /etc/apache2/conf-available/remoteip.conf +COPY --link contrib/docker/apache/docker-entrypoint.d /docker-entrypoint.d/ + +RUN set -ex \ + && a2enmod rewrite remoteip proxy proxy_http \ + && a2enconf remoteip + +CMD ["apache2-foreground"] + +EXPOSE 80 + +####################################################### +# Runtime: fpm +####################################################### + +FROM shared-runtime AS fpm-runtime + +COPY --link contrib/docker/fpm/docker-entrypoint.d /docker-entrypoint.d/ + +CMD ["php-fpm"] + +EXPOSE 9000 + +####################################################### +# Runtime: nginx +####################################################### + +FROM shared-runtime AS nginx-runtime + +ARG NGINX_GPGKEY +ARG NGINX_GPGKEY_PATH +ARG NGINX_VERSION +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION +ARG TARGETPLATFORM + +# Install nginx dependencies +RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + set -ex \ + && gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \ + && gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \ + && echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + nginx=${NGINX_VERSION}* + +# copy docker entrypoints from the *real* nginx image directly +COPY --link --from=nginx-image /docker-entrypoint.d /docker-entrypoint.d/ +COPY --link contrib/docker/nginx/docker-entrypoint.d /docker-entrypoint.d/ +COPY --link contrib/docker/nginx/default-http.conf /etc/nginx/templates/default.conf.template +COPY --link contrib/docker/nginx/Procfile . + +EXPOSE 80 + +STOPSIGNAL SIGQUIT + +CMD ["forego", "start", "-r"] diff --git a/contrib/docker/Dockerfile.apache b/contrib/docker/Dockerfile.apache deleted file mode 100644 index 9c33aee17..000000000 --- a/contrib/docker/Dockerfile.apache +++ /dev/null @@ -1,100 +0,0 @@ -FROM php:8.1-apache-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp6 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#APACHE Bootstrap - && a2enmod rewrite remoteip \ - && {\ - echo RemoteIPHeader X-Real-IP ;\ - echo RemoteIPTrustedProxy 10.0.0.0/8 ;\ - echo RemoteIPTrustedProxy 172.16.0.0/12 ;\ - echo RemoteIPTrustedProxy 192.168.0.0/16 ;\ - echo SetEnvIf X-Forwarded-Proto "https" HTTPS=on ;\ - } > /etc/apache2/conf-available/remoteip.conf \ - && a2enconf remoteip \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.apache.sh"] diff --git a/contrib/docker/Dockerfile.fpm b/contrib/docker/Dockerfile.fpm deleted file mode 100644 index 0b8e5c113..000000000 --- a/contrib/docker/Dockerfile.fpm +++ /dev/null @@ -1,90 +0,0 @@ -FROM php:8.1-fpm-bullseye - -ENV COMPOSER_MEMORY_LIMIT=-1 -ARG DEBIAN_FRONTEND=noninteractive -WORKDIR /var/www/ - -# Get Composer binary -COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer - -# Install package dependencies -RUN apt-get update \ - && apt-get upgrade -y \ -# && apt-get install -y --no-install-recommends apt-utils \ - && apt-get install -y --no-install-recommends \ -## Standard - locales \ - locales-all \ - git \ - gosu \ - zip \ - unzip \ - libzip-dev \ - libcurl4-openssl-dev \ -## Image Optimization - optipng \ - pngquant \ - jpegoptim \ - gifsicle \ -## Image Processing - libjpeg62-turbo-dev \ - libpng-dev \ - libmagickwand-dev \ -# Required for GD - libxpm4 \ - libxpm-dev \ - libwebp6 \ - libwebp-dev \ -## Video Processing - ffmpeg \ -## Database -# libpq-dev \ -# libsqlite3-dev \ - mariadb-client \ -# Locales Update - && sed -i '/en_US/s/^#//g' /etc/locale.gen \ - && locale-gen \ - && update-locale \ -# Install PHP extensions - && docker-php-source extract \ -#PHP Imagemagick extensions - && pecl install imagick \ - && docker-php-ext-enable imagick \ -# PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - && docker-php-ext-install -j$(nproc) gd \ -#PHP Redis extensions - && pecl install redis \ - && docker-php-ext-enable redis \ -#PHP Database extensions - && docker-php-ext-install pdo_mysql \ -#pdo_pgsql pdo_sqlite \ -#PHP extensions (dependencies) - && docker-php-ext-configure intl \ - && docker-php-ext-install -j$(nproc) intl bcmath zip pcntl exif curl \ -#Cleanup - && docker-php-source delete \ - && apt-get autoremove --purge -y \ - && apt-get clean \ - && rm -rf /var/cache/apt \ - && rm -rf /var/lib/apt/lists/ - -# Use the default production configuration -COPY contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" - -COPY . /var/www/ -# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 -RUN cp -r storage storage.skel \ - && composer install --prefer-dist --no-interaction --no-ansi --optimize-autoloader \ - && rm -rf html && ln -s public html \ - && chown -R www-data:www-data /var/www - -RUN php artisan horizon:publish - -VOLUME /var/www/storage /var/www/bootstrap - -CMD ["/var/www/contrib/docker/start.fpm.sh"] diff --git a/contrib/docker/README.md b/contrib/docker/README.md new file mode 100644 index 000000000..441477fe4 --- /dev/null +++ b/contrib/docker/README.md @@ -0,0 +1,214 @@ +# Pixelfed Docker images + +## Runtimes + +The Pixelfed Dockerfile support multiple target *runtimes* ([Apache](#apache), [Nginx + FPM](#nginx), and [fpm](#fpm)). + +You can consider a *runtime* target as individual Dockerfiles, but instead, all of them are build from the same optimized Dockerfile, sharing +90% of their configuration and packages. + +### Apache + +Building a custom Pixelfed Docker image using Apache + mod_php can be achieved the following way. + +#### docker build (Apache) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target apache-runtime \ + --tag / \ + . +``` + +#### docker compose (Apache) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: apache-runtime +``` + +### Nginx + +Building a custom Pixelfed Docker image using nginx + FPM can be achieved the following way. + +#### docker build (nginx) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target nginx-runtime \ + --build-arg 'PHP_BASE_TYPE=fpm' \ + --tag / \ + . +``` + +#### docker compose (nginx) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: nginx-runtime + args: + PHP_BASE_TYPE: fpm +``` + +### FPM + +Building a custom Pixelfed Docker image using FPM (only) can be achieved the following way. + +#### docker build (fpm) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target fpm-runtime \ + --build-arg 'PHP_BASE_TYPE=fpm' \ + --tag / \ + . +``` + +#### docker compose (fpm) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: fpm-runtime + args: + PHP_BASE_TYPE: fpm +``` + +## Build settings (arguments) + +The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). + +Using *build arguments* allow us to create a flexible and more maintainable Dockerfile, supporting [multiple runtimes](#runtimes) ([FPM](#fpm), [Nginx](#nginx), [Apache + mod_php](#apache)) and end-user flexibility without having to fork or copy the Dockerfile. + +*Build arguments* can be configured using `--build-arg 'name=value'` for `docker build`, `docker compose build` and `docker buildx build`. For `docker-compose.yml` the `args` key for [`build`](https://docs.docker.com/compose/compose-file/compose-file-v3/#build) can be used. + +### `PHP_VERSION` + +The `PHP` version to use when building the runtime container. + +Any valid Docker Hub PHP version is acceptable here, as long as it's [published to Docker Hub](https://hub.docker.com/_/php/tags) + +**Example values**: + +* `8` will use the latest version of PHP 8 +* `8.1` will use the latest version of PHP 8.1 +* `8.2.14` will use PHP 8.2.14 +* `latest` will use whatever is the latest PHP version + +**Default value**: `8.1` + +### `PECL_EXTENSIONS` + +PECL extensions to install via `pecl install` + +Use [PECL_EXTENSIONS_EXTRA](#pecl_extensions_extra) if you want to add *additional* extenstions. + +Only change this setting if you want to change the baseline extensions. + +See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `imagick redis` + +### `PECL_EXTENSIONS_EXTRA` + +Extra PECL extensions (separated by space) to install via `pecl install` + +See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `""` + +### `PHP_EXTENSIONS` + +PHP Extensions to install via `docker-php-ext-install`. + +**NOTE:** use [`PHP_EXTENSIONS_EXTRA`](#php_extensions_extra) if you want to add *additional* extensions, only override this if you want to change the baseline extensions. + +See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information + +**Default value**: `intl bcmath zip pcntl exif curl gd` + +### `PHP_EXTENSIONS_EXTRA` + +Extra PHP Extensions (separated by space) to install via `docker-php-ext-install`. + +See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `""` + +### `PHP_DATABASE_EXTENSIONS` + +PHP database extensions to install. + +By default we install both `pgsql` and `mysql` since it's more convinient (and adds very little build time! but can be overwritten here if required. + +**Default value**: `pdo_pgsql pdo_mysql` + +### `COMPOSER_VERSION` + +The version of Composer to install. + +Please see the [Docker Hub `composer` page](https://hub.docker.com/_/composer) for valid values. + +**Default value**: `2.6` + +### `APT_PACKAGES_EXTRA` + +Extra APT packages (separated by space) that should be installed inside the image by `apt-get install` + +**Default value**: `""` + +### `NGINX_VERSION` + +Version of `nginx` to when targeting [`nginx-runtime`](#nginx). + +Please see the [Docker Hub `nginx` page](https://hub.docker.com/_/nginx) for available versions. + +**Default value**: `1.25.3` + +### `PHP_BASE_TYPE` + +The `PHP` base image layer to use when building the runtime container. + +When targeting + +* [`apache-runtime`](#apache) use `apache` +* [`fpm-runtime`](#fpm) use `fpm` +* [`nginx-runtime`](#nginx) use `fpm` + +**Valid values**: + +* `apache` +* `fpm` +* `cli` + +**Default value**: `apache` + +### `PHP_DEBIAN_RELEASE` + +The `Debian` Operation System version to use. + +**Valid values**: + +* `bullseye` +* `bookworm` + +**Default value**: `bullseye` diff --git a/contrib/docker/apache/conf-available/remoteip.conf b/contrib/docker/apache/conf-available/remoteip.conf new file mode 100644 index 000000000..1632f8e43 --- /dev/null +++ b/contrib/docker/apache/conf-available/remoteip.conf @@ -0,0 +1,4 @@ +RemoteIPHeader X-Real-IP +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 diff --git a/contrib/docker/apache/docker-entrypoint.d/.gitkeep b/contrib/docker/apache/docker-entrypoint.d/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/docker/docker-entrypoint.sh b/contrib/docker/docker-entrypoint.sh new file mode 100755 index 000000000..706c38317 --- /dev/null +++ b/contrib/docker/docker-entrypoint.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# vim:sw=4:ts=4:et + +set -e + +source /lib.sh + +mkdir -p /docker-entrypoint.d/ + +if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + entrypoint_log "/docker-entrypoint.d/ is not empty, will attempt to perform configuration" + + entrypoint_log "looking for shell scripts in /docker-entrypoint.d/" + find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.envsh) + if [ -x "$f" ]; then + entrypoint_log "Sourcing $f"; + . "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "Ignoring $f, not executable"; + fi + ;; + + *.sh) + if [ -x "$f" ]; then + entrypoint_log "Launching $f"; + "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "Ignoring $f, not executable"; + fi + ;; + + *) entrypoint_log "Ignoring $f";; + esac + done + + entrypoint_log "Configuration complete; ready for start up" +else + entrypoint_log "No files found in /docker-entrypoint.d/, skipping configuration" +fi + +exec "$@" diff --git a/contrib/docker/fpm/docker-entrypoint.d/.gitkeep b/contrib/docker/fpm/docker-entrypoint.d/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/docker/nginx/Procfile b/contrib/docker/nginx/Procfile new file mode 100644 index 000000000..bd375bf6a --- /dev/null +++ b/contrib/docker/nginx/Procfile @@ -0,0 +1,2 @@ +fpm: php-fpm +nginx: nginx -g "daemon off;" diff --git a/contrib/docker/nginx/default-http.conf b/contrib/docker/nginx/default-http.conf new file mode 100644 index 000000000..8182dba7f --- /dev/null +++ b/contrib/docker/nginx/default-http.conf @@ -0,0 +1,49 @@ +server { + listen 80 default_server; + + server_name ${APP_DOMAIN}; + root /var/www/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + index index.html index.htm index.php; + + charset utf-8; + client_max_body_size 100M; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/contrib/docker/nginx/docker-entrypoint.d/.gitkeep b/contrib/docker/nginx/docker-entrypoint.d/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/docker/php.production.ini b/contrib/docker/php.production.ini index b84839ff5..2a1df3988 100644 --- a/contrib/docker/php.production.ini +++ b/contrib/docker/php.production.ini @@ -363,7 +363,7 @@ zend.enable_gc = On ; Allows to include or exclude arguments from stack traces generated for exceptions ; Default: Off -; In production, it is recommended to turn this setting on to prohibit the output +; In production, it is recommended to turn this setting on to prohibit the output ; of sensitive information in stack traces zend.exception_ignore_args = On @@ -679,7 +679,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = 64M +post_max_size = 95M ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,7 +831,7 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = 64M +upload_max_filesize = 95M ; Maximum number of files that can be uploaded via a single request max_file_uploads = 20 diff --git a/contrib/docker/shared/docker-entrypoint.d/00-storage.sh b/contrib/docker/shared/docker-entrypoint.d/00-storage.sh new file mode 100755 index 000000000..079a7887c --- /dev/null +++ b/contrib/docker/shared/docker-entrypoint.d/00-storage.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +source /lib.sh + +entrypoint_log "==> Create the storage tree if needed" +as_www_user cp --recursive storage.skel/* storage/ + +entrypoint_log "==> Ensure storage is linked" +as_www_user php artisan storage:link + +entrypoint_log "==> Ensure permissions are correct" +chown --recursive www-data:www-data storage/ bootstrap/ diff --git a/contrib/docker/shared/docker-entrypoint.d/01-cache.sh b/contrib/docker/shared/docker-entrypoint.d/01-cache.sh new file mode 100755 index 000000000..df2466b27 --- /dev/null +++ b/contrib/docker/shared/docker-entrypoint.d/01-cache.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +source /lib.sh + +entrypoint_log "==> config:cache" +as_www_user php artisan config:cache + +entrypoint_log "==> route:cache" +as_www_user php artisan route:cache + +entrypoint_log "==> view:cache" +as_www_user php artisan view:cache diff --git a/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh b/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh new file mode 100755 index 000000000..4afd1ea4a --- /dev/null +++ b/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e +source /lib.sh + +as_www_user php artisan horizon:publish diff --git a/contrib/docker/shared/lib.sh b/contrib/docker/shared/lib.sh new file mode 100644 index 000000000..3e7ef0f91 --- /dev/null +++ b/contrib/docker/shared/lib.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +function entrypoint_log() { + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "/docker-entrypoint.sh: $@" + fi +} + +function as_www_user() { + su --preserve-environment www-data --shell /bin/bash --command "${*}" +} diff --git a/contrib/docker/start.apache.sh b/contrib/docker/start.apache.sh deleted file mode 100755 index 4fb19e476..000000000 --- a/contrib/docker/start.apache.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run Apache -apache2-foreground diff --git a/contrib/docker/start.fpm.sh b/contrib/docker/start.fpm.sh deleted file mode 100755 index 199489fc6..000000000 --- a/contrib/docker/start.fpm.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Create the storage tree if needed and fix permissions -cp -r storage.skel/* storage/ -chown -R www-data:www-data storage/ bootstrap/ - -# Refresh the environment -php artisan config:cache -php artisan storage:link -php artisan horizon:publish -php artisan route:cache -php artisan view:cache - -# Finally run FPM -php-fpm diff --git a/resources/views/admin/diagnostics/home.blade.php b/resources/views/admin/diagnostics/home.blade.php index bf2b5d742..db44a2332 100644 --- a/resources/views/admin/diagnostics/home.blade.php +++ b/resources/views/admin/diagnostics/home.blade.php @@ -654,7 +654,7 @@ MEDIA MEDIA_EXIF_DATABASE - {{config_cache('media.exif.batabase') ? '✅ true' : '❌ false' }} + {{config_cache('media.exif.database') ? '✅ true' : '❌ false' }} From 7b6c9c7428d704552e38d42314272a1b83967a51 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 1 Jan 2024 16:19:24 -0700 Subject: [PATCH 166/977] Update migrations --- .../migrations/2023_12_27_081801_create_user_roles_table.php | 1 + .../2023_12_27_082024_add_has_roles_to_users_table.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/database/migrations/2023_12_27_081801_create_user_roles_table.php b/database/migrations/2023_12_27_081801_create_user_roles_table.php index 4e6af18d0..59b8ab390 100644 --- a/database/migrations/2023_12_27_081801_create_user_roles_table.php +++ b/database/migrations/2023_12_27_081801_create_user_roles_table.php @@ -16,6 +16,7 @@ return new class extends Migration $table->unsignedBigInteger('profile_id')->unique()->index(); $table->unsignedInteger('user_id')->unique()->index(); $table->json('roles')->nullable(); + $table->json('meta')->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php index c60b81deb..09246e37b 100644 --- a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -13,6 +13,8 @@ return new class extends Migration { Schema::table('users', function (Blueprint $table) { $table->boolean('has_roles')->default(false); + $table->unsignedInteger('parent_id')->nullable(); + $table->tinyInteger('role_id')->unsigned()->nullable()->index(); }); } @@ -23,6 +25,8 @@ return new class extends Migration { Schema::table('users', function (Blueprint $table) { $table->dropColumn('has_roles'); + $table->dropColumn('parent_id'); + $table->dropColumn('role_id'); }); } }; From d39946b045be826ef0079d9a8a06c7312f7ec93a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:04:27 -0700 Subject: [PATCH 167/977] Update ApiV1Controller, add permissions check --- app/Http/Controllers/Api/ApiV1Controller.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 6f314e0b3..c1dd8cbf4 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1245,6 +1245,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); @@ -1306,6 +1307,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); @@ -3175,6 +3177,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); @@ -3222,6 +3225,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); @@ -3272,6 +3276,13 @@ class ApiV1Controller extends Controller '_pe' => 'sometimes' ]); + $user = $request->user(); + abort_if( + $user->has_roles && !UserRoleService::can('can-view-hashtag-feed', $user->id), + 403, + 'Invalid permissions for this action' + ); + if(config('database.default') === 'pgsql') { $tag = Hashtag::where('name', 'ilike', $hashtag) ->orWhere('slug', 'ilike', $hashtag) From 75b0f2dda043b26046f40417d9c1440734335bcf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:06:18 -0700 Subject: [PATCH 168/977] Update ComposeController, add permissions check --- app/Http/Controllers/ComposeController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index 9be50f346..e79625861 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -54,6 +54,7 @@ use App\Util\Lexer\Autolink; use App\Util\Lexer\Extractor; use App\Util\Media\License; use Image; +use App\Services\UserRoleService; class ComposeController extends Controller { @@ -92,6 +93,7 @@ class ComposeController extends Controller $user = Auth::user(); $profile = $user->profile; + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $limitKey = 'compose:rate-limit:media-upload:' . $user->id; $limitTtl = now()->addMinutes(15); @@ -184,6 +186,7 @@ class ComposeController extends Controller ]); $user = Auth::user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $limitKey = 'compose:rate-limit:media-updates:' . $user->id; $limitTtl = now()->addMinutes(15); From cbe75ce8712f6d703b67738579e22dfa6226e1c2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:06:54 -0700 Subject: [PATCH 169/977] Update UserRolesController --- app/Http/Controllers/UserRolesController.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/UserRolesController.php b/app/Http/Controllers/UserRolesController.php index dbd34d0da..65a71d19d 100644 --- a/app/Http/Controllers/UserRolesController.php +++ b/app/Http/Controllers/UserRolesController.php @@ -3,8 +3,21 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use App\Services\UserRoleService; class UserRolesController extends Controller { - // + public function __construct() + { + $this->middleware('auth'); + } + + public function getRoles(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + return UserRoleService::getRoles($request->user()->id); + } } From 0ef6812709fa36ed8ae79da144ba114fcdc02fe6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:07:42 -0700 Subject: [PATCH 170/977] Update UserRoleService, add useDefaultFallback parameter --- app/Services/UserRoleService.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php index fef5356c2..500a4666e 100644 --- a/app/Services/UserRoleService.php +++ b/app/Services/UserRoleService.php @@ -6,12 +6,19 @@ use App\Models\UserRoles; class UserRoleService { - public static function can($action, $id) + public static function can($action, $id, $useDefaultFallback = true) { + $default = self::defaultRoles(); $roles = self::get($id); - - return in_array($action, $roles) ? $roles[$action] : null; - } + return + in_array($action, array_keys($roles)) ? + $roles[$action] : + ( + $useDefaultFallback ? + $default[$action] : + false + ); + } public static function get($id) { @@ -36,6 +43,7 @@ class UserRoleService 'can-view-public-feed' => true, 'can-view-network-feed' => true, 'can-view-discover' => true, + 'can-view-hashtag-feed' => false, 'can-post' => true, 'can-comment' => true, From fd44c80ce9723cf91ece4767539f0c503b2f833f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 3 Jan 2024 01:54:30 -0700 Subject: [PATCH 171/977] Update meta tags, improve descriptions and seo/og tags --- app/Services/AccountService.php | 35 ++++++++++++++++ resources/views/layouts/app.blade.php | 12 +++--- resources/views/profile/show.blade.php | 35 +++++++++++----- resources/views/status/show.blade.php | 56 +++++++++++++++++++++----- 4 files changed, 110 insertions(+), 28 deletions(-) diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index 98e878845..5ffc1e9b5 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -13,6 +13,7 @@ use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; +use \NumberFormatter; class AccountService { @@ -244,4 +245,38 @@ class AccountService return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists(); } + + public static function formatNumber($num) { + if(!$num || $num < 1) { + return "0"; + } + $num = intval($num); + $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1); + + if ($num >= 1000000000) { + return $formatter->format($num / 1000000000) . 'B'; + } else if ($num >= 1000000) { + return $formatter->format($num / 1000000) . 'M'; + } elseif ($num >= 1000) { + return $formatter->format($num / 1000) . 'K'; + } else { + return $formatter->format($num); + } + } + + public static function getMetaDescription($id) + { + $account = self::get($id, true); + + if(!$account) return ""; + + $posts = self::formatNumber($account['statuses_count']) . ' Posts, '; + $following = self::formatNumber($account['following_count']) . ' Following, '; + $followers = self::formatNumber($account['followers_count']) . ' Followers'; + $note = $account['note'] && strlen($account['note']) ? + ' · ' . \Purify::clean(strip_tags(str_replace("\n", '', str_replace("\r", '', $account['note'])))) : + ''; + return $posts . $following . $followers . $note; + } } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 94f90ed97..0136842bb 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -12,9 +12,9 @@ {{ $title ?? config_cache('app.name') }} - - - + + + @stack('meta') @@ -73,9 +73,9 @@ {{ $title ?? config('app.name', 'Pixelfed') }} - - - + + + @stack('meta') diff --git a/resources/views/profile/show.blade.php b/resources/views/profile/show.blade.php index 5107c0ab3..dfa8edabb 100644 --- a/resources/views/profile/show.blade.php +++ b/resources/views/profile/show.blade.php @@ -1,4 +1,13 @@ -@extends('layouts.app',['title' => $profile->username . " on " . config('app.name')]) +@extends('layouts.app', [ + 'title' => $profile->name . ' (@' . $acct . ') - Pixelfed', + 'ogTitle' => $profile->name . ' (@' . $acct . ')', + 'ogType' => 'profile' +]) + +@php +$acct = $profile->username . '@' . config('pixelfed.domain.app'); +$metaDescription = \App\Services\AccountService::getMetaDescription($profile->id); +@endphp @section('content') @if (session('error')) @@ -8,9 +17,6 @@ @endif -@if($profile->website) -{{$profile->website}} -@endif
- + @endsection -@push('meta') - - - - - - @if($status->viewType() == "video" || $status->viewType() == "video:album") - - @endif +@push('meta')@if($mediaCount && $s['pf_type'] === "photo" || $s['pf_type'] === "photo:album") + + @elseif($mediaCount && $s['pf_type'] === "video" || $s['pf_type'] === "video:album") + @endif + + + + + @endpush @push('scripts') From 5087a878854cb30b147e527985bb89e5180dfb4f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 3 Jan 2024 02:00:47 -0700 Subject: [PATCH 172/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10b13f261..13baca035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ - Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a)) - Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc)) - Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1)) +- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From bca248499417963d3e859b46de88fe9ca1aba3d0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 3 Jan 2024 04:11:29 -0700 Subject: [PATCH 173/977] Update Webfinger util, add avatar entity. Fixes #1629 --- app/Util/Webfinger/Webfinger.php | 91 +++++++++++++++++++------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/app/Util/Webfinger/Webfinger.php b/app/Util/Webfinger/Webfinger.php index 879103332..c900358e6 100644 --- a/app/Util/Webfinger/Webfinger.php +++ b/app/Util/Webfinger/Webfinger.php @@ -4,43 +4,60 @@ namespace App\Util\Webfinger; class Webfinger { - protected $user; - protected $subject; - protected $aliases; - protected $links; + protected $user; + protected $subject; + protected $aliases; + protected $links; - public function __construct($user) - { - $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST); - $this->aliases = [ - $user->url(), - $user->permalink(), - ]; - $this->links = [ - [ - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => $user->url(), - ], - [ - 'rel' => 'http://schemas.google.com/g/2010#updates-from', - 'type' => 'application/atom+xml', - 'href' => $user->permalink('.atom'), - ], - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $user->permalink(), - ], - ]; - } + public function __construct($user) + { + $avatar = $user ? $user->avatarUrl() : url('/storage/avatars/default.jpg'); + $avatarPath = parse_url($avatar, PHP_URL_PATH); + $extension = pathinfo($avatarPath, PATHINFO_EXTENSION); + $mimeTypes = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'svg' => 'image/svg', + ]; + $avatarType = $mimeTypes[$extension] ?? 'application/octet-stream'; - public function generate() - { - return [ - 'subject' => $this->subject, - 'aliases' => $this->aliases, - 'links' => $this->links, - ]; - } + $this->subject = 'acct:'.$user->username.'@'.parse_url(config('app.url'), PHP_URL_HOST); + $this->aliases = [ + $user->url(), + $user->permalink(), + ]; + $this->links = [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $user->url(), + ], + [ + 'rel' => 'http://schemas.google.com/g/2010#updates-from', + 'type' => 'application/atom+xml', + 'href' => $user->permalink('.atom'), + ], + [ + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $user->permalink(), + ], + [ + 'rel' => 'http://webfinger.net/rel/avatar', + 'type' => $avatarType, + 'href' => $avatar, + ], + ]; + } + + public function generate() + { + return [ + 'subject' => $this->subject, + 'aliases' => $this->aliases, + 'links' => $this->links, + ]; + } } From b19d3a20dd4db3c8bf4d221746986e37306dab38 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 11:00:45 +0000 Subject: [PATCH 174/977] only run kernel tasks on one server lifted from https://github.com/pixelfed/pixelfed/pull/4634 --- app/Console/Kernel.php | 88 +++++++++---------- .../shared/docker-entrypoint.d/00-storage.sh | 2 +- .../shared/docker-entrypoint.d/01-cache.sh | 2 +- .../shared/docker-entrypoint.d/02-horizon.sh | 2 +- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 046924eb6..2b6510e35 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -7,53 +7,53 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { - /** - * The Artisan commands provided by your application. - * - * @var array - */ - protected $commands = [ - // - ]; + /** + * The Artisan commands provided by your application. + * + * @var array + */ + protected $commands = [ + // + ]; - /** - * Define the application's command schedule. - * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - * - * @return void - */ - protected function schedule(Schedule $schedule) - { - $schedule->command('media:optimize')->hourlyAt(40); - $schedule->command('media:gc')->hourlyAt(5); - $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->command('story:gc')->everyFiveMinutes(); - $schedule->command('gc:failedjobs')->dailyAt(3); - $schedule->command('gc:passwordreset')->dailyAt('09:41'); - $schedule->command('gc:sessions')->twiceDaily(13, 23); + /** + * Define the application's command schedule. + * + * @param \Illuminate\Console\Scheduling\Schedule $schedule + * + * @return void + */ + protected function schedule(Schedule $schedule) + { + $schedule->command('media:optimize')->hourlyAt(40)->onOneServer(); + $schedule->command('media:gc')->hourlyAt(5)->onOneServer(); + $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); + $schedule->command('story:gc')->everyFiveMinutes()->onOneServer(); + $schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer(); + $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); + $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); - if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { - $schedule->command('media:s3gc')->hourlyAt(15); - } + if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { + $schedule->command('media:s3gc')->hourlyAt(15)->onOneServer(); + } - if(config('import.instagram.enabled')) { - $schedule->command('app:transform-imports')->everyFourMinutes(); - $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51); - $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37); - $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); - } - } + if (config('import.instagram.enabled')) { + $schedule->command('app:transform-imports')->everyFourMinutes()->onOneServer(); + $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer(); + $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer(); + $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer(); + } + } - /** - * Register the commands for the application. - * - * @return void - */ - protected function commands() - { - $this->load(__DIR__.'/Commands'); + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__ . '/Commands'); - require base_path('routes/console.php'); - } + require base_path('routes/console.php'); + } } diff --git a/contrib/docker/shared/docker-entrypoint.d/00-storage.sh b/contrib/docker/shared/docker-entrypoint.d/00-storage.sh index 079a7887c..860ec0425 100755 --- a/contrib/docker/shared/docker-entrypoint.d/00-storage.sh +++ b/contrib/docker/shared/docker-entrypoint.d/00-storage.sh @@ -1,6 +1,6 @@ #!/bin/bash +set -o errexit -o nounset -o pipefail -set -e source /lib.sh entrypoint_log "==> Create the storage tree if needed" diff --git a/contrib/docker/shared/docker-entrypoint.d/01-cache.sh b/contrib/docker/shared/docker-entrypoint.d/01-cache.sh index df2466b27..06e440802 100755 --- a/contrib/docker/shared/docker-entrypoint.d/01-cache.sh +++ b/contrib/docker/shared/docker-entrypoint.d/01-cache.sh @@ -1,6 +1,6 @@ #!/bin/bash +set -o errexit -o nounset -o pipefail -set -e source /lib.sh entrypoint_log "==> config:cache" diff --git a/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh b/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh index 4afd1ea4a..04227cf40 100755 --- a/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh +++ b/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh @@ -1,6 +1,6 @@ #!/bin/bash +set -o errexit -o nounset -o pipefail -set -e source /lib.sh as_www_user php artisan horizon:publish From cf080dda099fcd1b30d00192aeef974f75494e76 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 11:01:56 +0000 Subject: [PATCH 175/977] rename init files --- .../shared/docker-entrypoint.d/{00-storage.sh => 10-storage.sh} | 0 .../shared/docker-entrypoint.d/{01-cache.sh => 20-cache.sh} | 0 .../shared/docker-entrypoint.d/{02-horizon.sh => 30-horizon.sh} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename contrib/docker/shared/docker-entrypoint.d/{00-storage.sh => 10-storage.sh} (100%) rename contrib/docker/shared/docker-entrypoint.d/{01-cache.sh => 20-cache.sh} (100%) rename contrib/docker/shared/docker-entrypoint.d/{02-horizon.sh => 30-horizon.sh} (100%) diff --git a/contrib/docker/shared/docker-entrypoint.d/00-storage.sh b/contrib/docker/shared/docker-entrypoint.d/10-storage.sh similarity index 100% rename from contrib/docker/shared/docker-entrypoint.d/00-storage.sh rename to contrib/docker/shared/docker-entrypoint.d/10-storage.sh diff --git a/contrib/docker/shared/docker-entrypoint.d/01-cache.sh b/contrib/docker/shared/docker-entrypoint.d/20-cache.sh similarity index 100% rename from contrib/docker/shared/docker-entrypoint.d/01-cache.sh rename to contrib/docker/shared/docker-entrypoint.d/20-cache.sh diff --git a/contrib/docker/shared/docker-entrypoint.d/02-horizon.sh b/contrib/docker/shared/docker-entrypoint.d/30-horizon.sh similarity index 100% rename from contrib/docker/shared/docker-entrypoint.d/02-horizon.sh rename to contrib/docker/shared/docker-entrypoint.d/30-horizon.sh From f390c3c3e9a26d17fa3a8eaec2275fb88859049a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 11:11:16 +0000 Subject: [PATCH 176/977] install all database extensions by default lifted from https://github.com/pixelfed/pixelfed/pull/4172 --- .editorconfig | 16 +++++++++++++++- contrib/docker/Dockerfile | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0eda619e8..b6ffb5b1c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -root = true +root = false [*] indent_size = 4 @@ -11,3 +11,17 @@ insert_final_newline = true [{*.yml,*.yaml}] indent_style = space indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 4 + +shell_variant = bash +binary_next_line = true +case-indent = true +switch_case_indent = true +space_redirects = true +keep_padding = true +function_next_line = true +simplify = true +space-redirects = true diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index 0a15bc6b5..7715dc7b3 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -11,7 +11,7 @@ ARG FOREGO_VERSION=0.17.2 ARG PECL_EXTENSIONS_EXTRA="" ARG PECL_EXTENSIONS="imagick redis" ARG PHP_BASE_TYPE="apache" -ARG PHP_DATABASE_EXTENSIONS="pdo_pgsql pdo_mysql" +ARG PHP_DATABASE_EXTENSIONS="pdo_pgsql pdo_mysql pdo_sqlite" ARG PHP_DEBIAN_RELEASE="bullseye" ARG PHP_EXTENSIONS_EXTRA="" ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" From 6244511cf8f48e4df78cd721ddefaf29c0372005 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 11:20:22 +0000 Subject: [PATCH 177/977] don't hardcode UID/GID for runtime --- contrib/docker/Dockerfile | 18 ++++++++++++------ .../shared/docker-entrypoint.d/10-storage.sh | 6 +++--- .../{30-horizon.sh => 20-horizon.sh} | 2 +- .../{20-cache.sh => 30-cache.sh} | 10 +++++----- contrib/docker/shared/lib.sh | 4 ++-- 5 files changed, 23 insertions(+), 17 deletions(-) rename contrib/docker/shared/docker-entrypoint.d/{30-horizon.sh => 20-horizon.sh} (60%) rename contrib/docker/shared/docker-entrypoint.d/{20-cache.sh => 30-cache.sh} (58%) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index 7715dc7b3..d35276db6 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -17,6 +17,8 @@ ARG PHP_EXTENSIONS_EXTRA="" ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" ARG PHP_VERSION="8.1" ARG APT_PACKAGES_EXTRA="" +ARG RUNTIME_UID=33 +ARG RUNTIME_GID=33 # GPG key for nginx apt repository ARG NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 @@ -56,6 +58,8 @@ FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base ARG PHP_VERSION ARG PHP_DEBIAN_RELEASE ARG APT_PACKAGES_EXTRA +ARG RUNTIME_UID +ARG RUNTIME_GID ARG TARGETPLATFORM ARG BUILDKIT_SBOM_SCAN_STAGE=true @@ -67,7 +71,7 @@ SHELL ["/bin/bash", "-c"] RUN set -ex \ && mkdir -pv /var/www/ \ - && chown -R 33:33 /var/www + && chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www WORKDIR /var/www/ @@ -193,6 +197,8 @@ FROM base AS composer-and-src ARG PHP_VERSION ARG PHP_DEBIAN_RELEASE +ARG RUNTIME_UID +ARG RUNTIME_GID ARG TARGETPLATFORM # Make sure composer cache is targeting our cache mount later @@ -207,11 +213,11 @@ ENV COMPOSER_NO_INTERACTION=1 # Copy composer from https://hub.docker.com/_/composer COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer -#! Changing user to 33 -USER 33:33 +#! Changing user to runtime user +USER ${RUNTIME_UID}:${RUNTIME_GID} # Copy over only composer related files so docker layer cache isn't invalidated on PHP file changes -COPY --link --chown=33:33 composer.json composer.lock /var/www/ +COPY --link --chown=${RUNTIME_UID}:${RUNTIME_GID} composer.json composer.lock /var/www/ # Install composer dependencies # NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) @@ -220,7 +226,7 @@ RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE} && composer install --prefer-dist --no-autoloader --ignore-platform-reqs # Copy all other files over -COPY --link --chown=33:33 . /var/www/ +COPY --link --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ # Generate optimized autoloader now that we have all files around RUN set -ex \ @@ -237,7 +243,7 @@ FROM base AS shared-runtime COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/php/extensions COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php -COPY --link --from=composer-and-src --chown=33:33 /var/www /var/www +COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego COPY --link contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" diff --git a/contrib/docker/shared/docker-entrypoint.d/10-storage.sh b/contrib/docker/shared/docker-entrypoint.d/10-storage.sh index 860ec0425..8357688c1 100755 --- a/contrib/docker/shared/docker-entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/docker-entrypoint.d/10-storage.sh @@ -4,10 +4,10 @@ set -o errexit -o nounset -o pipefail source /lib.sh entrypoint_log "==> Create the storage tree if needed" -as_www_user cp --recursive storage.skel/* storage/ +as_runtime_user cp --recursive storage.skel/* storage/ entrypoint_log "==> Ensure storage is linked" -as_www_user php artisan storage:link +as_runtime_user php artisan storage:link entrypoint_log "==> Ensure permissions are correct" -chown --recursive www-data:www-data storage/ bootstrap/ +chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} storage/ bootstrap/ diff --git a/contrib/docker/shared/docker-entrypoint.d/30-horizon.sh b/contrib/docker/shared/docker-entrypoint.d/20-horizon.sh similarity index 60% rename from contrib/docker/shared/docker-entrypoint.d/30-horizon.sh rename to contrib/docker/shared/docker-entrypoint.d/20-horizon.sh index 04227cf40..9db54ba35 100755 --- a/contrib/docker/shared/docker-entrypoint.d/30-horizon.sh +++ b/contrib/docker/shared/docker-entrypoint.d/20-horizon.sh @@ -3,4 +3,4 @@ set -o errexit -o nounset -o pipefail source /lib.sh -as_www_user php artisan horizon:publish +as_runtime_user php artisan horizon:publish diff --git a/contrib/docker/shared/docker-entrypoint.d/20-cache.sh b/contrib/docker/shared/docker-entrypoint.d/30-cache.sh similarity index 58% rename from contrib/docker/shared/docker-entrypoint.d/20-cache.sh rename to contrib/docker/shared/docker-entrypoint.d/30-cache.sh index 06e440802..3eb87b6bb 100755 --- a/contrib/docker/shared/docker-entrypoint.d/20-cache.sh +++ b/contrib/docker/shared/docker-entrypoint.d/30-cache.sh @@ -3,11 +3,11 @@ set -o errexit -o nounset -o pipefail source /lib.sh -entrypoint_log "==> config:cache" -as_www_user php artisan config:cache - entrypoint_log "==> route:cache" -as_www_user php artisan route:cache +as_runtime_user php artisan route:cache entrypoint_log "==> view:cache" -as_www_user php artisan view:cache +as_runtime_user php artisan view:cache + +entrypoint_log "==> config:cache" +as_runtime_user php artisan config:cache diff --git a/contrib/docker/shared/lib.sh b/contrib/docker/shared/lib.sh index 3e7ef0f91..8253ed085 100644 --- a/contrib/docker/shared/lib.sh +++ b/contrib/docker/shared/lib.sh @@ -8,6 +8,6 @@ function entrypoint_log() { fi } -function as_www_user() { - su --preserve-environment www-data --shell /bin/bash --command "${*}" +function as_runtime_user() { + su --preserve-environment ${RUNTIME_UID} --shell /bin/bash --command "${*}" } From 0aee66810d61d4f35bcbf6ff1fc2d2f18221be9c Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 11:28:00 +0000 Subject: [PATCH 178/977] fix editorconfig --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b6ffb5b1c..0510b0d44 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -root = false +root = true [*] indent_size = 4 From 7dcca09c65cc937563f4573d031768d11b94240a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 13:07:01 +0000 Subject: [PATCH 179/977] a bit of refactoring --- .dockerignore | 1 - contrib/docker/Dockerfile | 160 +++++++---------------- contrib/docker/install/base.sh | 81 ++++++++++++ contrib/docker/install/php-extensions.sh | 32 +++++ 4 files changed, 162 insertions(+), 112 deletions(-) create mode 100755 contrib/docker/install/base.sh create mode 100755 contrib/docker/install/php-extensions.sh diff --git a/.dockerignore b/.dockerignore index 8e25f3cbc..bc5af5a21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ data -contrib/docker/Dockerfile docker-compose*.yml .dockerignore .git diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index d35276db6..e432216ed 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -8,17 +8,25 @@ ARG COMPOSER_VERSION="2.6" ARG NGINX_VERSION=1.25.3 ARG FOREGO_VERSION=0.17.2 -ARG PECL_EXTENSIONS_EXTRA="" -ARG PECL_EXTENSIONS="imagick redis" -ARG PHP_BASE_TYPE="apache" -ARG PHP_DATABASE_EXTENSIONS="pdo_pgsql pdo_mysql pdo_sqlite" -ARG PHP_DEBIAN_RELEASE="bullseye" -ARG PHP_EXTENSIONS_EXTRA="" -ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" + +# PHP base configuration ARG PHP_VERSION="8.1" -ARG APT_PACKAGES_EXTRA="" -ARG RUNTIME_UID=33 -ARG RUNTIME_GID=33 +ARG PHP_BASE_TYPE="apache" +ARG PHP_DEBIAN_RELEASE="bullseye" +ARG RUNTIME_UID=33 # often called 'www-data' +ARG RUNTIME_GID=33 # often called 'www-data' + +# APT extra packages +ARG APT_PACKAGES_EXTRA= + +# Extensions installed via [pecl install] +ARG PHP_PECL_EXTENSIONS="" +ARG PHP_PECL_EXTENSIONS_EXTRA= + +# Extensions installed via [docker-php-ext-install] +ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" +ARG PHP_EXTENSIONS_EXTRA= +ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite" # GPG key for nginx apt repository ARG NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 @@ -75,87 +83,13 @@ RUN set -ex \ WORKDIR /var/www/ -# Install package dependencies +ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} + +# Install and configure base layer +COPY contrib/docker/install/base.sh /install/base.sh RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ -<<-SCRIPT - #!/bin/bash - set -ex -o errexit -o nounset -o pipefail - - # ensure we keep apt cache around in a Docker environment - rm -f /etc/apt/apt.conf.d/docker-clean - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache - - # Standard packages - standardPackages=( - apt-utils - ca-certificates - gettext-base - git - gnupg1 - gosu - libcurl4-openssl-dev - libzip-dev - locales - locales-all - nano - procps - unzip - zip - ) - - # Image Optimization - imageOptimization=( - gifsicle - jpegoptim - optipng - pngquant - ) - - # Image Processing - imageProcessing=( - libjpeg62-turbo-dev - libmagickwand-dev - libpng-dev - ) - - # Required for GD - gdDependencies=( - libwebp-dev - libwebp6 - libxpm-dev - libxpm4 - ) - - # Video Processing - videoProcessing=( - ffmpeg - ) - - # Database - databaseDependencies=( - libpq-dev - libsqlite3-dev - ) - - apt-get update - - apt-get upgrade -y - - apt-get install -y --no-install-recommends \ - ${standardPackages[*]} \ - ${imageOptimization[*]} \ - ${imageProcessing[*]} \ - ${gdDependencies[*]} \ - ${videoProcessing[*]} \ - ${databaseDependencies[*]} \ - ${APT_PACKAGES_EXTRA} -SCRIPT - -# update locales -RUN set -ex \ - && locale-gen \ - && update-locale + /install/base.sh ####################################################### # PHP: extensions @@ -163,37 +97,35 @@ RUN set -ex \ FROM base AS php-extensions -ARG PECL_EXTENSIONS -ARG PECL_EXTENSIONS_EXTRA -ARG PHP_DATABASE_EXTENSIONS +ARG PHP_EXTENSIONS_DATABASE ARG PHP_DEBIAN_RELEASE ARG PHP_EXTENSIONS ARG PHP_EXTENSIONS_EXTRA +ARG PHP_PECL_EXTENSIONS +ARG PHP_PECL_EXTENSIONS_EXTRA ARG PHP_VERSION ARG TARGETPLATFORM +ENV PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} +ENV PHP_DEBIAN_RELEASE=${PHP_DEBIAN_RELEASE} +ENV PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} +ENV PHP_EXTENSIONS=${PHP_EXTENSIONS} +ENV PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} +ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} +ENV PHP_VERSION=${PHP_VERSION} +ENV TARGETPLATFORM=${TARGETPLATFORM} + +COPY contrib/docker/install/php-extensions.sh /install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ - set -ex \ - # Grab the PHP source code so we can compile against it - && docker-php-source extract \ - # Install pecl extensions - && pecl install ${PECL_EXTENSIONS} ${PECL_EXTENSIONS_EXTRA} \ - # PHP GD extensions - && docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm \ - # PHP extensions (dependencies) - && docker-php-ext-install -j$(nproc) ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_DATABASE_EXTENSIONS} \ - # Enable all extensions - && docker-php-ext-enable ${PECL_EXTENSIONS} ${PECL_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_DATABASE_EXTENSIONS} + --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + /install/php-extensions.sh ####################################################### # PHP: composer and source code ####################################################### -FROM base AS composer-and-src +FROM php-extensions AS composer-and-src ARG PHP_VERSION ARG PHP_DEBIAN_RELEASE @@ -241,6 +173,12 @@ USER root:root FROM base AS shared-runtime +ARG RUNTIME_UID +ARG RUNTIME_GID + +ENV RUNTIME_UID=${RUNTIME_UID} +ENV RUNTIME_GID=${RUNTIME_GID} + COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/php/extensions COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www @@ -252,9 +190,9 @@ RUN set -ex \ && cp --recursive --link --preserve=all storage storage.skel \ && rm -rf html && ln -s public html -COPY --link contrib/docker/docker-entrypoint.sh /docker-entrypoint.sh -COPY --link contrib/docker/shared/lib.sh /lib.sh -COPY --link contrib/docker/shared/docker-entrypoint.d /docker-entrypoint.d/ +COPY contrib/docker/docker-entrypoint.sh /docker-entrypoint.sh +COPY contrib/docker/shared/lib.sh /lib.sh +COPY contrib/docker/shared/docker-entrypoint.d /docker-entrypoint.d/ ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/contrib/docker/install/base.sh b/contrib/docker/install/base.sh new file mode 100755 index 000000000..b0e3d7b6d --- /dev/null +++ b/contrib/docker/install/base.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +# Ensure we keep apt cache around in a Docker environment +rm -f /etc/apt/apt.conf.d/docker-clean +echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache + +# Don't install recommended packages by default +echo 'APT::Install-Recommends "false";' >>/etc/apt/apt.conf + +# Don't install suggested packages by default +echo 'APT::Install-Suggests "false";' >>/etc/apt/apt.conf + +# Standard packages +declare -ra standardPackages=( + apt-utils + ca-certificates + gettext-base + git + gnupg1 + gosu + libcurl4-openssl-dev + libzip-dev + locales + locales-all + nano + procps + unzip + zip + software-properties-common +) + +# Image Optimization +declare -ra imageOptimization=( + gifsicle + jpegoptim + optipng + pngquant +) + +# Image Processing +declare -ra imageProcessing=( + libjpeg62-turbo-dev + libmagickwand-dev + libpng-dev +) + +# Required for GD +declare -ra gdDependencies=( + libwebp-dev + libwebp6 + libxpm-dev + libxpm4 +) + +# Video Processing +declare -ra videoProcessing=( + ffmpeg +) + +# Database +declare -ra databaseDependencies=( + libpq-dev + libsqlite3-dev +) + +apt-get update + +apt-get upgrade -y + +apt-get install -y \ + ${standardPackages[*]} \ + ${imageOptimization[*]} \ + ${imageProcessing[*]} \ + ${gdDependencies[*]} \ + ${videoProcessing[*]} \ + ${databaseDependencies[*]} \ + ${APT_PACKAGES_EXTRA} + +locale-gen +update-locale diff --git a/contrib/docker/install/php-extensions.sh b/contrib/docker/install/php-extensions.sh new file mode 100755 index 000000000..1cb86fd77 --- /dev/null +++ b/contrib/docker/install/php-extensions.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -ex -o errexit -o nounset -o pipefail + +# Grab the PHP source code so we can compile against it +docker-php-source extract + +# PHP GD extensions +docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + --with-xpm + +# Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] +# this can also overwirte the [gd] configure above by simply running it again +if [[ -f /install/php-extension-configure.sh ]]; then + if [ !-x "$f" ]; then + echo >&2 "ERROR: found /install/php-extension-configure.sh but its not executable - please [chmod +x] the file!" + exit 1 + fi + + /install/php-extension-configure.sh +fi + +# Install pecl extensions +pecl install ${PHP_PECL_EXTENSIONS} ${PHP_PECL_EXTENSIONS_EXTRA} + +# PHP extensions (dependencies) +docker-php-ext-install -j$(nproc) ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS_DATABASE} + +# Enable all extensions +docker-php-ext-enable ${PHP_PECL_EXTENSIONS} ${PHP_PECL_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS_DATABASE} From c369ef50a7e479a18f35e66933e300b2132a869f Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 16:08:01 +0000 Subject: [PATCH 180/977] more refactoring for templating --- contrib/docker/Dockerfile | 27 +++---- .../etc/apache2}/conf-available/remoteip.conf | 4 + contrib/docker/docker-entrypoint.sh | 45 ----------- .../docker/fpm/docker-entrypoint.d/.gitkeep | 0 .../docker-entrypoint.d => fpm/root}/.gitkeep | 0 contrib/docker/nginx/default-http.conf | 49 ------------ .../docker/nginx/docker-entrypoint.d/.gitkeep | 0 .../nginx/root/etc/nginx/conf.d/default.conf | 49 ++++++++++++ .../shared/docker-entrypoint.d/10-storage.sh | 13 ---- .../shared/docker-entrypoint.d/30-cache.sh | 13 ---- contrib/docker/shared/lib.sh | 13 ---- .../docker/entrypoint.d/04-defaults.envsh | 26 +++++++ .../root/docker/entrypoint.d/05-templating.sh | 40 ++++++++++ .../root/docker/entrypoint.d/10-storage.sh | 13 ++++ .../docker/entrypoint.d}/20-horizon.sh | 4 +- .../root/docker/entrypoint.d/30-cache.sh | 13 ++++ .../docker/shared/root/docker/entrypoint.sh | 50 +++++++++++++ contrib/docker/shared/root/docker/helpers.sh | 75 +++++++++++++++++++ .../{ => shared/root/docker}/install/base.sh | 0 .../root/docker}/install/php-extensions.sh | 0 .../templates/usr/local/etc/php/php.ini} | 6 +- 21 files changed, 286 insertions(+), 154 deletions(-) rename contrib/docker/apache/{ => root/etc/apache2}/conf-available/remoteip.conf (56%) delete mode 100755 contrib/docker/docker-entrypoint.sh delete mode 100644 contrib/docker/fpm/docker-entrypoint.d/.gitkeep rename contrib/docker/{apache/docker-entrypoint.d => fpm/root}/.gitkeep (100%) delete mode 100644 contrib/docker/nginx/default-http.conf delete mode 100644 contrib/docker/nginx/docker-entrypoint.d/.gitkeep create mode 100644 contrib/docker/nginx/root/etc/nginx/conf.d/default.conf delete mode 100755 contrib/docker/shared/docker-entrypoint.d/10-storage.sh delete mode 100755 contrib/docker/shared/docker-entrypoint.d/30-cache.sh delete mode 100644 contrib/docker/shared/lib.sh create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh rename contrib/docker/shared/{docker-entrypoint.d => root/docker/entrypoint.d}/20-horizon.sh (52%) create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh create mode 100755 contrib/docker/shared/root/docker/entrypoint.sh create mode 100644 contrib/docker/shared/root/docker/helpers.sh rename contrib/docker/{ => shared/root/docker}/install/base.sh (100%) rename contrib/docker/{ => shared/root/docker}/install/php-extensions.sh (100%) rename contrib/docker/{php.production.ini => shared/root/docker/templates/usr/local/etc/php/php.ini} (99%) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index e432216ed..d3cbca764 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -86,10 +86,10 @@ WORKDIR /var/www/ ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} # Install and configure base layer -COPY contrib/docker/install/base.sh /install/base.sh +COPY contrib/docker/shared/root/docker/install/base.sh /docker/install/base.sh RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ - /install/base.sh + /docker/install/base.sh ####################################################### # PHP: extensions @@ -115,11 +115,11 @@ ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} ENV PHP_VERSION=${PHP_VERSION} ENV TARGETPLATFORM=${TARGETPLATFORM} -COPY contrib/docker/install/php-extensions.sh /install/php-extensions.sh +COPY contrib/docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ - /install/php-extensions.sh + /docker/install/php-extensions.sh ####################################################### # PHP: composer and source code @@ -183,18 +183,15 @@ COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/p COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego -COPY --link contrib/docker/php.production.ini "$PHP_INI_DIR/php.ini" # for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 RUN set -ex \ && cp --recursive --link --preserve=all storage storage.skel \ && rm -rf html && ln -s public html -COPY contrib/docker/docker-entrypoint.sh /docker-entrypoint.sh -COPY contrib/docker/shared/lib.sh /lib.sh -COPY contrib/docker/shared/docker-entrypoint.d /docker-entrypoint.d/ +COPY contrib/docker/shared/root / -ENTRYPOINT ["/docker-entrypoint.sh"] +ENTRYPOINT ["/docker/entrypoint.sh"] VOLUME /var/www/storage /var/www/bootstrap @@ -204,8 +201,7 @@ VOLUME /var/www/storage /var/www/bootstrap FROM shared-runtime AS apache-runtime -COPY --link contrib/docker/apache/conf-available/remoteip.conf /etc/apache2/conf-available/remoteip.conf -COPY --link contrib/docker/apache/docker-entrypoint.d /docker-entrypoint.d/ +COPY contrib/docker/apache/root / RUN set -ex \ && a2enmod rewrite remoteip proxy proxy_http \ @@ -221,7 +217,7 @@ EXPOSE 80 FROM shared-runtime AS fpm-runtime -COPY --link contrib/docker/fpm/docker-entrypoint.d /docker-entrypoint.d/ +COPY contrib/docker/fpm/root / CMD ["php-fpm"] @@ -252,10 +248,9 @@ RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE nginx=${NGINX_VERSION}* # copy docker entrypoints from the *real* nginx image directly -COPY --link --from=nginx-image /docker-entrypoint.d /docker-entrypoint.d/ -COPY --link contrib/docker/nginx/docker-entrypoint.d /docker-entrypoint.d/ -COPY --link contrib/docker/nginx/default-http.conf /etc/nginx/templates/default.conf.template -COPY --link contrib/docker/nginx/Procfile . +COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ +COPY contrib/docker/nginx/root / +COPY contrib/docker/nginx/Procfile . EXPOSE 80 diff --git a/contrib/docker/apache/conf-available/remoteip.conf b/contrib/docker/apache/root/etc/apache2/conf-available/remoteip.conf similarity index 56% rename from contrib/docker/apache/conf-available/remoteip.conf rename to contrib/docker/apache/root/etc/apache2/conf-available/remoteip.conf index 1632f8e43..516da9f5d 100644 --- a/contrib/docker/apache/conf-available/remoteip.conf +++ b/contrib/docker/apache/root/etc/apache2/conf-available/remoteip.conf @@ -1,4 +1,8 @@ RemoteIPHeader X-Real-IP + +# All private IPs as outlined in rfc1918 +# +# See: https://datatracker.ietf.org/doc/html/rfc1918 RemoteIPTrustedProxy 10.0.0.0/8 RemoteIPTrustedProxy 172.16.0.0/12 RemoteIPTrustedProxy 192.168.0.0/16 diff --git a/contrib/docker/docker-entrypoint.sh b/contrib/docker/docker-entrypoint.sh deleted file mode 100755 index 706c38317..000000000 --- a/contrib/docker/docker-entrypoint.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# vim:sw=4:ts=4:et - -set -e - -source /lib.sh - -mkdir -p /docker-entrypoint.d/ - -if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - entrypoint_log "/docker-entrypoint.d/ is not empty, will attempt to perform configuration" - - entrypoint_log "looking for shell scripts in /docker-entrypoint.d/" - find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do - case "$f" in - *.envsh) - if [ -x "$f" ]; then - entrypoint_log "Sourcing $f"; - . "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "Ignoring $f, not executable"; - fi - ;; - - *.sh) - if [ -x "$f" ]; then - entrypoint_log "Launching $f"; - "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "Ignoring $f, not executable"; - fi - ;; - - *) entrypoint_log "Ignoring $f";; - esac - done - - entrypoint_log "Configuration complete; ready for start up" -else - entrypoint_log "No files found in /docker-entrypoint.d/, skipping configuration" -fi - -exec "$@" diff --git a/contrib/docker/fpm/docker-entrypoint.d/.gitkeep b/contrib/docker/fpm/docker-entrypoint.d/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/contrib/docker/apache/docker-entrypoint.d/.gitkeep b/contrib/docker/fpm/root/.gitkeep similarity index 100% rename from contrib/docker/apache/docker-entrypoint.d/.gitkeep rename to contrib/docker/fpm/root/.gitkeep diff --git a/contrib/docker/nginx/default-http.conf b/contrib/docker/nginx/default-http.conf deleted file mode 100644 index 8182dba7f..000000000 --- a/contrib/docker/nginx/default-http.conf +++ /dev/null @@ -1,49 +0,0 @@ -server { - listen 80 default_server; - - server_name ${APP_DOMAIN}; - root /var/www/public; - - add_header X-Frame-Options "SAMEORIGIN"; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Content-Type-Options "nosniff"; - - access_log /dev/stdout; - error_log /dev/stderr warn; - - index index.html index.htm index.php; - - charset utf-8; - client_max_body_size 100M; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { - access_log off; - log_not_found off; - } - - location = /robots.txt { - access_log off; - log_not_found off; - } - - error_page 404 /index.php; - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - - include fastcgi_params; - - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} diff --git a/contrib/docker/nginx/docker-entrypoint.d/.gitkeep b/contrib/docker/nginx/docker-entrypoint.d/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf b/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf new file mode 100644 index 000000000..af5a66b77 --- /dev/null +++ b/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf @@ -0,0 +1,49 @@ +server { + listen 80 default_server; + + server_name ${APP_DOMAIN}; + root /var/www/public; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + index index.html index.htm index.php; + + charset utf-8; + client_max_body_size ${POST_MAX_SIZE}; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/contrib/docker/shared/docker-entrypoint.d/10-storage.sh b/contrib/docker/shared/docker-entrypoint.d/10-storage.sh deleted file mode 100755 index 8357688c1..000000000 --- a/contrib/docker/shared/docker-entrypoint.d/10-storage.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail - -source /lib.sh - -entrypoint_log "==> Create the storage tree if needed" -as_runtime_user cp --recursive storage.skel/* storage/ - -entrypoint_log "==> Ensure storage is linked" -as_runtime_user php artisan storage:link - -entrypoint_log "==> Ensure permissions are correct" -chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} storage/ bootstrap/ diff --git a/contrib/docker/shared/docker-entrypoint.d/30-cache.sh b/contrib/docker/shared/docker-entrypoint.d/30-cache.sh deleted file mode 100755 index 3eb87b6bb..000000000 --- a/contrib/docker/shared/docker-entrypoint.d/30-cache.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail - -source /lib.sh - -entrypoint_log "==> route:cache" -as_runtime_user php artisan route:cache - -entrypoint_log "==> view:cache" -as_runtime_user php artisan view:cache - -entrypoint_log "==> config:cache" -as_runtime_user php artisan config:cache diff --git a/contrib/docker/shared/lib.sh b/contrib/docker/shared/lib.sh deleted file mode 100644 index 8253ed085..000000000 --- a/contrib/docker/shared/lib.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -e - -function entrypoint_log() { - if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "/docker-entrypoint.sh: $@" - fi -} - -function as_runtime_user() { - su --preserve-environment ${RUNTIME_UID} --shell /bin/bash --command "${*}" -} diff --git a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh new file mode 100755 index 000000000..2244be9b8 --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -0,0 +1,26 @@ +#!/bin/bash + +# NOTE: +# +# this file is *sourced* not run by the entrypoint runner +# so any environment values set here will be accessible to all sub-processes +# and future entrypoint.d scripts +# + +set_identity "${BASH_SOURCE[0]}" + +load-config-files + +: ${POST_MAX_SIZE_BUFFER:=1M} +log "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" +buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") +log "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" + +log "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" +log " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" +log " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" +log " POST_MAX_SIZE_BUFFER=${buffer}" +: ${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((${MAX_PHOTO_SIZE} * ${MAX_ALBUM_LENGTH}) + ${buffer})))} +log "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" + +export POST_MAX_SIZE diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh new file mode 100755 index 000000000..4b9f9014a --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -0,0 +1,40 @@ +#!/bin/bash +source /docker/helpers.sh + +set_identity "$0" + +auto_envsubst() { + local template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" + local output_dir="${ENVSUBST_OUTPUT_DIR:-}" + local filter="${ENVSUBST_FILTER:-}" + local template defined_envs relative_path output_path output_dir subdir + + # load all dot-env files + load-config-files + + # export all dot-env variables so they are available in templating + export ${seen_dot_env_variables[@]} + + defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" "$output_path" + done +} + +auto_envsubst + +exit 0 diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh new file mode 100755 index 000000000..c814a3df4 --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -0,0 +1,13 @@ +#!/bin/bash +source /docker/helpers.sh + +set_identity "$0" + +log "Create the storage tree if needed" +as_runtime_user cp --recursive storage.skel/* storage/ + +log "Ensure storage is linked" +as_runtime_user php artisan storage:link + +log "Ensure permissions are correct" +chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} storage/ bootstrap/ diff --git a/contrib/docker/shared/docker-entrypoint.d/20-horizon.sh b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh similarity index 52% rename from contrib/docker/shared/docker-entrypoint.d/20-horizon.sh rename to contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh index 9db54ba35..7c1d8fdc6 100755 --- a/contrib/docker/shared/docker-entrypoint.d/20-horizon.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -o errexit -o nounset -o pipefail +source /docker/helpers.sh -source /lib.sh +set_identity "$0" as_runtime_user php artisan horizon:publish diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh new file mode 100755 index 000000000..e561daef9 --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -0,0 +1,13 @@ +#!/bin/bash +source /docker/helpers.sh + +set_identity "$0" + +log "==> route:cache" +as_runtime_user php artisan route:cache + +log "==> view:cache" +as_runtime_user php artisan view:cache + +log "==> config:cache" +as_runtime_user php artisan config:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh new file mode 100755 index 000000000..0964980bc --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e -o errexit -o nounset -o pipefail + +[[ -n ${ENTRYPOINT_DEBUG:-} ]] && set -x + +declare -g ME="$0" +declare -gr ENTRYPOINT_ROOT=/docker/entrypoint.d/ + +source /docker/helpers.sh + +# ensure the entrypoint folder exists +mkdir -p "${ENTRYPOINT_ROOT}" + +if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + log "looking for shell scripts in /docker/entrypoint.d/" + find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.envsh) + if [ -x "$f" ]; then + log "Sourcing $f" + source "$f" + resetore_identity + else + # warn on shell scripts without exec bit + log_warning "Ignoring $f, not executable" + fi + ;; + + *.sh) + if [ -x "$f" ]; then + log "Launching $f" + "$f" + else + # warn on shell scripts without exec bit + log_warning "Ignoring $f, not executable" + fi + ;; + + *) + log_warning "Ignoring $f" + ;; + esac + done + + log "Configuration complete; ready for start up" +else + log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" +fi + +exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh new file mode 100644 index 000000000..fc8691324 --- /dev/null +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e -o errexit -o nounset -o pipefail + +declare -g error_message_color="\033[1;31m" +declare -g warn_message_color="\033[1;34m" +declare -g color_clear="\033[1;0m" +declare -g log_prefix= +declare -g old_log_prefix= +declare -ra dot_env_files=( + /var/www/.env.docker + /var/www/.env +) +declare -a seen_dot_env_variables=() + +function set_identity() { + old_log_prefix="${log_prefix}" + log_prefix="ENTRYPOINT - [${1}] - " +} + +function resetore_identity() { + log_prefix="${old_log_prefix}" +} + +function as_runtime_user() { + su --preserve-environment $(id -un ${RUNTIME_UID}) --shell /bin/bash --command "${*}" +} + +# @description Display the given error message with its line number on stderr and exit with error. +# @arg $message string A error message. +function log_error() { + echo -e "${error_message_color}${log_prefix}ERROR - ${1}${color_clear}" >/dev/stderr +} + +# @description Display the given error message with its line number on stderr and exit with error. +# @arg $message string A error message. +# @exitcode 1 +function log_error_and_exit() { + log_error "$1" + + exit 1 +} + +# @description Display the given warning message with its line number on stderr. +# @arg $message string A warning message. +function log_warning() { + echo -e "${warn_message_color}${log_prefix}WARNING - ${1}${color_clear}" >/dev/stderr +} + +function log() { + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "${log_prefix}$@" + fi +} + +function load-config-files() { + # Associative array (aka map/disctrionary) holding the unique keys found in dot-env files + local -A _tmp_dot_env_keys + + for f in "${dot_env_files[@]}"; do + if [ ! -e "$f" ]; then + log_warning "Could not source file [${f}]: does not exists" + continue + fi + + log "Sourcing ${f}" + source "${f}" + + # find all keys in the dot-env file and store them in our temp associative array + for k in "$(grep -v '^#' "${f}" | sed -E 's/(.*)=.*/\1/' | xargs)"; do + _tmp_dot_env_keys[$k]=1 + done + done + + seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) +} diff --git a/contrib/docker/install/base.sh b/contrib/docker/shared/root/docker/install/base.sh similarity index 100% rename from contrib/docker/install/base.sh rename to contrib/docker/shared/root/docker/install/base.sh diff --git a/contrib/docker/install/php-extensions.sh b/contrib/docker/shared/root/docker/install/php-extensions.sh similarity index 100% rename from contrib/docker/install/php-extensions.sh rename to contrib/docker/shared/root/docker/install/php-extensions.sh diff --git a/contrib/docker/php.production.ini b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini similarity index 99% rename from contrib/docker/php.production.ini rename to contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 2a1df3988..1a9f6b598 100644 --- a/contrib/docker/php.production.ini +++ b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -679,7 +679,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = 95M +post_max_size = ${POST_MAX_SIZE} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,10 +831,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = 95M +upload_max_filesize = ${POST_MAX_SIZE} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = 20 +max_file_uploads = ${MAX_ALBUM_LENGTH} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; From e05575283a1fa299b126d83cb189b6291b213354 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 16:12:18 +0000 Subject: [PATCH 181/977] update docs --- contrib/docker/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/docker/README.md b/contrib/docker/README.md index 441477fe4..9dd46d06e 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -116,11 +116,11 @@ Any valid Docker Hub PHP version is acceptable here, as long as it's [published **Default value**: `8.1` -### `PECL_EXTENSIONS` +### `PHP_PECL_EXTENSIONS` PECL extensions to install via `pecl install` -Use [PECL_EXTENSIONS_EXTRA](#pecl_extensions_extra) if you want to add *additional* extenstions. +Use [PHP_PECL_EXTENSIONS_EXTRA](#php_pecl_extensions_extra) if you want to add *additional* extenstions. Only change this setting if you want to change the baseline extensions. @@ -128,7 +128,7 @@ See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_ **Default value**: `imagick redis` -### `PECL_EXTENSIONS_EXTRA` +### `PHP_PECL_EXTENSIONS_EXTRA` Extra PECL extensions (separated by space) to install via `pecl install` @@ -154,13 +154,13 @@ See the [`How to install more PHP extensions` documentation on Docker Hub](https **Default value**: `""` -### `PHP_DATABASE_EXTENSIONS` +### `PHP_EXTENSIONS_DATABASE` PHP database extensions to install. By default we install both `pgsql` and `mysql` since it's more convinient (and adds very little build time! but can be overwritten here if required. -**Default value**: `pdo_pgsql pdo_mysql` +**Default value**: `pdo_pgsql pdo_mysql pdo_sqlite` ### `COMPOSER_VERSION` From a08a5e7cde5682c8afd7eac727c2398447038902 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 20:55:04 +0000 Subject: [PATCH 182/977] more docs and rework --- contrib/docker/Dockerfile | 41 +++++++- contrib/docker/README.md | 66 +++++++++++++ .../templates}/conf.d/default.conf | 4 +- .../root/docker/entrypoint.d/05-templating.sh | 53 ++++++----- .../root/docker/entrypoint.d/10-storage.sh | 3 - .../root/docker/entrypoint.d/30-cache.sh | 5 - .../docker/shared/root/docker/entrypoint.sh | 93 ++++++++++++------- contrib/docker/shared/root/docker/helpers.sh | 35 ++++++- .../docker/shared/root/docker/install/base.sh | 7 +- .../templates/usr/local/etc/php/php.ini | 6 +- 10 files changed, 226 insertions(+), 87 deletions(-) rename contrib/docker/nginx/root/{etc/nginx => docker/templates}/conf.d/default.conf (90%) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index d3cbca764..471ac5820 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -5,14 +5,29 @@ # Configuration ####################################################### +# See: https://github.com/composer/composer/releases ARG COMPOSER_VERSION="2.6" + +# See: https://nginx.org/ ARG NGINX_VERSION=1.25.3 + +# See: https://github.com/ddollar/forego ARG FOREGO_VERSION=0.17.2 +# See: https://github.com/hairyhenderson/gomplate +ARG GOMPLATE_VERSION=v3.11.6 + +### # PHP base configuration +### + +# See: https://hub.docker.com/_/php/tags ARG PHP_VERSION="8.1" + +# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants ARG PHP_BASE_TYPE="apache" ARG PHP_DEBIAN_RELEASE="bullseye" + ARG RUNTIME_UID=33 # often called 'www-data' ARG RUNTIME_GID=33 # often called 'www-data' @@ -57,17 +72,31 @@ FROM nginx:${NGINX_VERSION} AS nginx-image # See: https://github.com/nginx-proxy/forego FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image +# gomplate-image grabs the gomplate binary from GitHub releases +# +# It's in its own layer so it can be fetched in parallel with other build steps +FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image + +ARG BUILDARCH +ARG BUILDOS +ARG GOMPLATE_VERSION + +RUN set -ex \ + && curl --silent --show-error --location --output /usr/local/bin/gomplate https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${BUILDOS}-${BUILDARCH} \ + && chmod +x /usr/local/bin/gomplate \ + && /usr/local/bin/gomplate --version + ####################################################### # Base image ####################################################### FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base -ARG PHP_VERSION -ARG PHP_DEBIAN_RELEASE ARG APT_PACKAGES_EXTRA -ARG RUNTIME_UID +ARG PHP_DEBIAN_RELEASE +ARG PHP_VERSION ARG RUNTIME_GID +ARG RUNTIME_UID ARG TARGETPLATFORM ARG BUILDKIT_SBOM_SCAN_STAGE=true @@ -173,8 +202,11 @@ USER root:root FROM base AS shared-runtime -ARG RUNTIME_UID +ARG BUILDARCH +ARG BUILDOS +ARG GOMPLATE_VERSION ARG RUNTIME_GID +ARG RUNTIME_UID ENV RUNTIME_UID=${RUNTIME_UID} ENV RUNTIME_GID=${RUNTIME_GID} @@ -183,6 +215,7 @@ COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/p COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego +COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate # for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 RUN set -ex \ diff --git a/contrib/docker/README.md b/contrib/docker/README.md index 9dd46d06e..a0b07f5fa 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -93,6 +93,72 @@ services: PHP_BASE_TYPE: fpm ``` +## Customizing your `Dockerfile` + +### Running commands on container start + +#### Description + +When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint) script will + +1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). +1. Check if the file is executable. + 1. If the file is not executable, print an error and exit the container. +1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). +1. If the file has the extension `.sh` the file will be run like a normal script. +1. Any other file extension will log a warning and will be ignored. + +#### Included scripts + +* `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. +* `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. +* `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. +* `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured +* `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed + +#### Disabling entrypoint or individual scripts + +To disable the entire entrypoint you can set the variable `ENTRYPOINT_SKIP=1`. + +To disable individual entrypoint scripts you can add the filename to the space (`" "`) separated variable `ENTRYPOINT_SKIP_SCRIPTS`. (example: `ENTRYPOINT_SKIP_SCRIPTS="10-storage.sh 30-cache.sh"`) + +### Templating + +The Docker container can do some basic templating (more like variable replacement) as part of the entrypoint scripts via [gomplate](https://docs.gomplate.ca/). + +Any file put in the `/docker/templates/` directory will be templated and written to the right directory. + +#### File path examples + +1. To template `/usr/local/etc/php/php.ini` in the container put the source file in `/docker/templates/usr/local/etc/php/php.ini`. +1. To template `/a/fantastic/example.txt` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. +1. To template `/some/path/anywhere` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. + +#### Available variables + +Variables available for templating are sourced (in order, so *last* source takes precedence) like this: + +1. `env:` in your `docker-compose.yml` or `-e` in your `docker run` / `docker compose run` +1. Any exported variables in `.envsh` files loaded *before* `05-templating.sh` (e.g. any file with `04-`, `03-`, `02-`, `01-` or `00-` prefix) +1. All key/value pairs in `/var/www/.env.docker` +1. All key/value pairs in `/var/www/.env` + +#### Template guide 101 + +Please see the [gomplate documentation](https://docs.gomplate.ca/) for a more comprehensive overview. + +The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: + +* `{{ getenv "VAR_NAME" }}` print an environment variable and **fail** if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) +* `{{ getenv "VAR_NAME" "default" }}` print an environment variable and print `default` if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) + +The script will *fail* if you reference a variable that does not exist (and don't have a default value) in a template. + +Please see the + +* [gomplate syntax documentation](https://docs.gomplate.ca/syntax/) +* [gomplate functions documentation](https://docs.gomplate.ca/functions/) + ## Build settings (arguments) The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). diff --git a/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf b/contrib/docker/nginx/root/docker/templates/conf.d/default.conf similarity index 90% rename from contrib/docker/nginx/root/etc/nginx/conf.d/default.conf rename to contrib/docker/nginx/root/docker/templates/conf.d/default.conf index af5a66b77..671332e78 100644 --- a/contrib/docker/nginx/root/etc/nginx/conf.d/default.conf +++ b/contrib/docker/nginx/root/docker/templates/conf.d/default.conf @@ -1,7 +1,7 @@ server { listen 80 default_server; - server_name ${APP_DOMAIN}; + server_name {{ getenv "APP_DOMAIN" }}; root /var/www/public; add_header X-Frame-Options "SAMEORIGIN"; @@ -14,7 +14,7 @@ server { index index.html index.htm index.php; charset utf-8; - client_max_body_size ${POST_MAX_SIZE}; + client_max_body_size {{ getenv "POST_MAX_SIZE" }}; location / { try_files $uri $uri/ /index.php?$query_string; diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 4b9f9014a..468b10617 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -3,38 +3,37 @@ source /docker/helpers.sh set_identity "$0" -auto_envsubst() { - local template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" - local output_dir="${ENVSUBST_OUTPUT_DIR:-}" - local filter="${ENVSUBST_FILTER:-}" - local template defined_envs relative_path output_path output_dir subdir +declare template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" +declare output_dir="${ENVSUBST_OUTPUT_DIR:-}" +declare filter="${ENVSUBST_FILTER:-}" +declare template defined_envs relative_path output_path output_dir subdir - # load all dot-env files - load-config-files +# load all dot-env files +load-config-files - # export all dot-env variables so they are available in templating - export ${seen_dot_env_variables[@]} +: ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1} - defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" "$output_path" - done -} + log "Running [gomplate] on [$template] --> [$output_path]" + cat "$template" | gomplate >"$output_path" -auto_envsubst - -exit 0 + # Show the diff from the envsubst command + if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF} = 1 ]]; then + git --no-pager diff "$template" "${output_path}" || : + fi +done diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index c814a3df4..a35eeb5d3 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -3,10 +3,7 @@ source /docker/helpers.sh set_identity "$0" -log "Create the storage tree if needed" as_runtime_user cp --recursive storage.skel/* storage/ - -log "Ensure storage is linked" as_runtime_user php artisan storage:link log "Ensure permissions are correct" diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh index e561daef9..11965fcea 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -3,11 +3,6 @@ source /docker/helpers.sh set_identity "$0" -log "==> route:cache" as_runtime_user php artisan route:cache - -log "==> view:cache" as_runtime_user php artisan view:cache - -log "==> config:cache" as_runtime_user php artisan config:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index 0964980bc..dbc8aa1b5 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -1,50 +1,71 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail -[[ -n ${ENTRYPOINT_DEBUG:-} ]] && set -x +: ${ENTRYPOINT_SKIP:=0} +: ${ENTRYPOINT_SKIP_SCRIPTS:=""} +: ${ENTRYPOINT_DEBUG:=0} +: ${ENTRYPOINT_ROOT:="/docker/entrypoint.d/"} -declare -g ME="$0" -declare -gr ENTRYPOINT_ROOT=/docker/entrypoint.d/ +export ENTRYPOINT_ROOT -source /docker/helpers.sh +if [[ ${ENTRYPOINT_SKIP} == 0 ]]; then + [[ ${ENTRYPOINT_DEBUG} == 1 ]] && set -x -# ensure the entrypoint folder exists -mkdir -p "${ENTRYPOINT_ROOT}" + source /docker/helpers.sh -if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - log "looking for shell scripts in /docker/entrypoint.d/" - find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do - case "$f" in - *.envsh) - if [ -x "$f" ]; then - log "Sourcing $f" - source "$f" - resetore_identity - else - # warn on shell scripts without exec bit - log_warning "Ignoring $f, not executable" + declare -a skip_scripts=() + IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" + + declare script_name + + # ensure the entrypoint folder exists + mkdir -p "${ENTRYPOINT_ROOT}" + + if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + log "looking for shell scripts in /docker/entrypoint.d/" + + find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do + script_name="$(get_script_name $f)" + if array_value_exists skip_scripts "${script_name}"; then + log_warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + + continue fi - ;; - *.sh) - if [ -x "$f" ]; then - log "Launching $f" - "$f" - else - # warn on shell scripts without exec bit - log_warning "Ignoring $f, not executable" - fi - ;; + case "$f" in + *.envsh) + if [ -x "$f" ]; then + log "Sourcing $f" - *) - log_warning "Ignoring $f" - ;; - esac - done + source "$f" - log "Configuration complete; ready for start up" -else - log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + resetore_identity + else + # warn on shell scripts without exec bit + log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + fi + ;; + + *.sh) + if [ -x "$f" ]; then + log "Launching $f" + "$f" + else + # warn on shell scripts without exec bit + log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + fi + ;; + + *) + log_warning "Ignoring $f" + ;; + esac + done + + log "Configuration complete; ready for start up" + else + log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + fi fi exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index fc8691324..f1ca33d09 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -10,11 +10,11 @@ declare -ra dot_env_files=( /var/www/.env.docker /var/www/.env ) -declare -a seen_dot_env_variables=() +declare -ga seen_dot_env_variables=() function set_identity() { old_log_prefix="${log_prefix}" - log_prefix="ENTRYPOINT - [${1}] - " + log_prefix="ENTRYPOINT - [$(get_script_name $1)] - " } function resetore_identity() { @@ -22,7 +22,23 @@ function resetore_identity() { } function as_runtime_user() { - su --preserve-environment $(id -un ${RUNTIME_UID}) --shell /bin/bash --command "${*}" + local -i exit_code + local target_user + + target_user=$(id -un ${RUNTIME_UID}) + + log "👷 Running [${*}] as [${target_user}]" + + su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + exit_code=$? + + if [[ $exit_code != 0 ]]; then + log_error "❌ Error!" + return $exit_code + fi + + log "✅ OK!" + return $exit_code } # @description Display the given error message with its line number on stderr and exit with error. @@ -53,7 +69,7 @@ function log() { } function load-config-files() { - # Associative array (aka map/disctrionary) holding the unique keys found in dot-env files + # Associative array (aka map/dictionary) holding the unique keys found in dot-env files local -A _tmp_dot_env_keys for f in "${dot_env_files[@]}"; do @@ -73,3 +89,14 @@ function load-config-files() { seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) } + +function array_value_exists() { + local -nr validOptions=$1 + local -r providedValue="\<${2}\>" + + [[ ${validOptions[*]} =~ $providedValue ]] +} + +function get_script_name() { + echo "${1#"$ENTRYPOINT_ROOT"}" +} diff --git a/contrib/docker/shared/root/docker/install/base.sh b/contrib/docker/shared/root/docker/install/base.sh index b0e3d7b6d..b9b37b031 100755 --- a/contrib/docker/shared/root/docker/install/base.sh +++ b/contrib/docker/shared/root/docker/install/base.sh @@ -15,7 +15,7 @@ echo 'APT::Install-Suggests "false";' >>/etc/apt/apt.conf declare -ra standardPackages=( apt-utils ca-certificates - gettext-base + curl git gnupg1 gosu @@ -25,9 +25,10 @@ declare -ra standardPackages=( locales-all nano procps - unzip - zip software-properties-common + unzip + wget + zip ) # Image Optimization diff --git a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 1a9f6b598..81ba3d207 100644 --- a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -679,7 +679,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = ${POST_MAX_SIZE} +post_max_size = {{ getenv "POST_MAX_SIZE" }} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -831,10 +831,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = ${POST_MAX_SIZE} +upload_max_filesize = {{ getenv "POST_MAX_SIZE" }} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = ${MAX_ALBUM_LENGTH} +max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" }} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; From ce34e4d04625c02c40eaef6314e225354c103ee5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 21:21:00 +0000 Subject: [PATCH 183/977] more docs and rework --- contrib/docker/Dockerfile | 25 ++++--- .../docker/entrypoint.d/04-defaults.envsh | 16 ++-- .../root/docker/entrypoint.d/05-templating.sh | 6 +- .../root/docker/entrypoint.d/10-storage.sh | 8 +- .../root/docker/entrypoint.d/20-horizon.sh | 4 +- .../root/docker/entrypoint.d/30-cache.sh | 8 +- .../docker/shared/root/docker/entrypoint.sh | 51 +++++++------ contrib/docker/shared/root/docker/helpers.sh | 73 +++++++++++-------- 8 files changed, 106 insertions(+), 85 deletions(-) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index 471ac5820..708d8247b 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -154,7 +154,7 @@ RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TA # PHP: composer and source code ####################################################### -FROM php-extensions AS composer-and-src +FROM base AS composer-and-src ARG PHP_VERSION ARG PHP_DEBIAN_RELEASE @@ -178,7 +178,7 @@ COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer USER ${RUNTIME_UID}:${RUNTIME_GID} # Copy over only composer related files so docker layer cache isn't invalidated on PHP file changes -COPY --link --chown=${RUNTIME_UID}:${RUNTIME_GID} composer.json composer.lock /var/www/ +COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} composer.json composer.lock /var/www/ # Install composer dependencies # NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) @@ -187,14 +187,7 @@ RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE} && composer install --prefer-dist --no-autoloader --ignore-platform-reqs # Copy all other files over -COPY --link --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ - -# Generate optimized autoloader now that we have all files around -RUN set -ex \ - && composer dump-autoload --optimize - -#! Changing back to root -USER root:root +COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ ####################################################### # Runtime: base @@ -213,9 +206,19 @@ ENV RUNTIME_GID=${RUNTIME_GID} COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/php/extensions COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php -COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate +COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer +COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www + +#! Changing user to runtime user +USER ${RUNTIME_UID}:${RUNTIME_GID} + +# Generate optimized autoloader now that we have all files around +RUN set -ex \ + && composer dump-autoload --optimize + +USER root # for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 RUN set -ex \ diff --git a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index 2244be9b8..00415b22f 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -7,20 +7,20 @@ # and future entrypoint.d scripts # -set_identity "${BASH_SOURCE[0]}" +entrypoint-set-name "${BASH_SOURCE[0]}" load-config-files : ${POST_MAX_SIZE_BUFFER:=1M} -log "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" +log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") -log "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" +log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" -log "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" -log " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" -log " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" -log " POST_MAX_SIZE_BUFFER=${buffer}" +log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" +log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" +log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" +log-info " POST_MAX_SIZE_BUFFER=${buffer}" : ${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((${MAX_PHOTO_SIZE} * ${MAX_ALBUM_LENGTH}) + ${buffer})))} -log "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" +log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" export POST_MAX_SIZE diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 468b10617..22975a905 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -1,7 +1,7 @@ #!/bin/bash source /docker/helpers.sh -set_identity "$0" +entrypoint-set-name "$0" declare template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" declare output_dir="${ENVSUBST_OUTPUT_DIR:-}" @@ -23,13 +23,13 @@ find "$template_dir" -follow -type f -print | while read -r template; do output_dir=$(dirname "$output_path") if [ ! -w "$output_dir" ]; then - log_error_and_exit "ERROR: $template_dir exists, but $output_dir is not writable" + log-error-and-exit "ERROR: $template_dir exists, but $output_dir is not writable" fi # create a subdirectory where the template file exists mkdir -p "$output_dir/$subdir" - log "Running [gomplate] on [$template] --> [$output_path]" + log-info "Running [gomplate] on [$template] --> [$output_path]" cat "$template" | gomplate >"$output_path" # Show the diff from the envsubst command diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index a35eeb5d3..14f66dc27 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -1,10 +1,10 @@ #!/bin/bash source /docker/helpers.sh -set_identity "$0" +entrypoint-set-name "$0" -as_runtime_user cp --recursive storage.skel/* storage/ -as_runtime_user php artisan storage:link +run-as-runtime-user cp --recursive storage.skel/* storage/ +run-as-runtime-user php artisan storage:link -log "Ensure permissions are correct" +log-info "Ensure permissions are correct" chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} storage/ bootstrap/ diff --git a/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh index 7c1d8fdc6..2d3394746 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -1,6 +1,6 @@ #!/bin/bash source /docker/helpers.sh -set_identity "$0" +entrypoint-set-name "$0" -as_runtime_user php artisan horizon:publish +run-as-runtime-user php artisan horizon:publish diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh index 11965fcea..5c0f20bb8 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -1,8 +1,8 @@ #!/bin/bash source /docker/helpers.sh -set_identity "$0" +entrypoint-set-name "$0" -as_runtime_user php artisan route:cache -as_runtime_user php artisan view:cache -as_runtime_user php artisan config:cache +run-as-runtime-user php artisan route:cache +run-as-runtime-user php artisan view:cache +run-as-runtime-user php artisan config:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index dbc8aa1b5..4f2e2eb90 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -13,7 +13,9 @@ if [[ ${ENTRYPOINT_SKIP} == 0 ]]; then source /docker/helpers.sh - declare -a skip_scripts=() + entrypoint-set-name "entrypoint.sh" + + declare -a skip_scripts IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" declare script_name @@ -22,49 +24,52 @@ if [[ ${ENTRYPOINT_SKIP} == 0 ]]; then mkdir -p "${ENTRYPOINT_ROOT}" if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - log "looking for shell scripts in /docker/entrypoint.d/" + log-info "looking for shell scripts in /docker/entrypoint.d/" - find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r f; do - script_name="$(get_script_name $f)" - if array_value_exists skip_scripts "${script_name}"; then - log_warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do + script_name="$(get-entrypoint-script-name $file)" + + if in-array "${script_name}" skip_scripts; then + log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" continue fi - case "$f" in + case "${file}" in *.envsh) - if [ -x "$f" ]; then - log "Sourcing $f" - - source "$f" - - resetore_identity - else + if ! is-executable "${file}"; then # warn on shell scripts without exec bit - log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi + + log-info "Sourcing [${file}]" + + source "${file}" + + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-name ;; *.sh) - if [ -x "$f" ]; then - log "Launching $f" - "$f" - else + if ! is-executable "${file}"; then # warn on shell scripts without exec bit - log_error_and_exit "File [$f] is not executable (please 'chmod +x' it)" + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi + + log-info "Running [${file}]" + "${file}" ;; *) - log_warning "Ignoring $f" + log-warning "Ignoring unrecognized file [${file}]" ;; esac done - log "Configuration complete; ready for start up" + log-info "Configuration complete; ready for start up" else - log_warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + log-warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" fi fi diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index f1ca33d09..5fb04d7a9 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -1,70 +1,79 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail +# Some splash of color for important messages declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" declare -g color_clear="\033[1;0m" + +# Current and previous log prefix declare -g log_prefix= -declare -g old_log_prefix= +declare -g log_prefix_previous= + +# dot-env files to source when reading config declare -ra dot_env_files=( /var/www/.env.docker /var/www/.env ) + +# environment keys seen when source dot files (so we can [export] them) declare -ga seen_dot_env_variables=() -function set_identity() { - old_log_prefix="${log_prefix}" - log_prefix="ENTRYPOINT - [$(get_script_name $1)] - " +function entrypoint-set-name() { + log_prefix_previous="${log_prefix}" + log_prefix="ENTRYPOINT - [$(get-entrypoint-script-name $1)] - " } -function resetore_identity() { - log_prefix="${old_log_prefix}" +function entrypoint-restore-name() { + log_prefix="${log_prefix_previous}" } -function as_runtime_user() { +function run-as-runtime-user() { local -i exit_code local target_user target_user=$(id -un ${RUNTIME_UID}) - log "👷 Running [${*}] as [${target_user}]" + log-info "👷 Running [${*}] as [${target_user}]" su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" exit_code=$? if [[ $exit_code != 0 ]]; then - log_error "❌ Error!" + log-error "❌ Error!" return $exit_code fi - log "✅ OK!" + log-info "✅ OK!" return $exit_code } -# @description Display the given error message with its line number on stderr and exit with error. +# @description Print the given error message to stderr # @arg $message string A error message. -function log_error() { - echo -e "${error_message_color}${log_prefix}ERROR - ${1}${color_clear}" >/dev/stderr +function log-error() { + echo -e "${error_message_color}${log_prefix}ERROR - ${*}${color_clear}" >/dev/stderr } -# @description Display the given error message with its line number on stderr and exit with error. -# @arg $message string A error message. +# @description Print the given error message to stderr and exit 1 +# @arg $@ string A error message. # @exitcode 1 -function log_error_and_exit() { - log_error "$1" +function log-error-and-exit() { + log-error "$@" exit 1 } -# @description Display the given warning message with its line number on stderr. -# @arg $message string A warning message. -function log_warning() { - echo -e "${warn_message_color}${log_prefix}WARNING - ${1}${color_clear}" >/dev/stderr +# @description Print the given warning message to stderr +# @arg $@ string A warning message. +function log-warning() { + echo -e "${warn_message_color}${log_prefix}WARNING - ${*}${color_clear}" >/dev/stderr } -function log() { +# @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A warning message. +function log-info() { if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "${log_prefix}$@" + echo "${log_prefix}$*" fi } @@ -74,11 +83,11 @@ function load-config-files() { for f in "${dot_env_files[@]}"; do if [ ! -e "$f" ]; then - log_warning "Could not source file [${f}]: does not exists" + log-warning "Could not source file [${f}]: does not exists" continue fi - log "Sourcing ${f}" + log-info "Sourcing ${f}" source "${f}" # find all keys in the dot-env file and store them in our temp associative array @@ -90,13 +99,17 @@ function load-config-files() { seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) } -function array_value_exists() { - local -nr validOptions=$1 - local -r providedValue="\<${2}\>" +function in-array() { + local -r needle="\<${1}\>" + local -nr haystack=$2 - [[ ${validOptions[*]} =~ $providedValue ]] + [[ ${haystack[*]} =~ $needle ]] } -function get_script_name() { +function is-executable() { + [[ -x "$1" ]] +} + +function get-entrypoint-script-name() { echo "${1#"$ENTRYPOINT_ROOT"}" } From f2c84971363e169f8c0cd2b525c41d88eeffc9aa Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 21:55:24 +0000 Subject: [PATCH 184/977] more clanup --- contrib/docker/Dockerfile | 5 +- .../docker/entrypoint.d/04-defaults.envsh | 6 +- .../root/docker/entrypoint.d/05-templating.sh | 56 +++++++---- .../docker/shared/root/docker/entrypoint.sh | 96 +++++++++---------- contrib/docker/shared/root/docker/helpers.sh | 12 +++ 5 files changed, 100 insertions(+), 75 deletions(-) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index 708d8247b..cdff0d916 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -126,9 +126,9 @@ RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TA FROM base AS php-extensions -ARG PHP_EXTENSIONS_DATABASE ARG PHP_DEBIAN_RELEASE ARG PHP_EXTENSIONS +ARG PHP_EXTENSIONS_DATABASE ARG PHP_EXTENSIONS_EXTRA ARG PHP_PECL_EXTENSIONS ARG PHP_PECL_EXTENSIONS_EXTRA @@ -136,13 +136,10 @@ ARG PHP_VERSION ARG TARGETPLATFORM ENV PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} -ENV PHP_DEBIAN_RELEASE=${PHP_DEBIAN_RELEASE} ENV PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} ENV PHP_EXTENSIONS=${PHP_EXTENSIONS} ENV PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} -ENV PHP_VERSION=${PHP_VERSION} -ENV TARGETPLATFORM=${TARGETPLATFORM} COPY contrib/docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ diff --git a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index 00415b22f..3f1a77843 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -2,20 +2,23 @@ # NOTE: # -# this file is *sourced* not run by the entrypoint runner +# This file is *sourced* not run by the entrypoint runner # so any environment values set here will be accessible to all sub-processes # and future entrypoint.d scripts # +# We also don't need to source `helpers.sh` since it's already available entrypoint-set-name "${BASH_SOURCE[0]}" load-config-files +# We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead : ${POST_MAX_SIZE_BUFFER:=1M} log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" +# Automatically calculate the [post_max_size] value for [php.ini] and [nginx] log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LENGTH}) + {POST_MAX_SIZE_BUFFER}]" log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" @@ -23,4 +26,5 @@ log-info " POST_MAX_SIZE_BUFFER=${buffer}" : ${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((${MAX_PHOTO_SIZE} * ${MAX_ALBUM_LENGTH}) + ${buffer})))} log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" +# NOTE: must export the value so it's available in other scripts! export POST_MAX_SIZE diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 22975a905..89b6f0504 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -3,37 +3,53 @@ source /docker/helpers.sh entrypoint-set-name "$0" -declare template_dir="${ENVSUBST_TEMPLATE_DIR:-/docker/templates}" -declare output_dir="${ENVSUBST_OUTPUT_DIR:-}" -declare filter="${ENVSUBST_FILTER:-}" -declare template defined_envs relative_path output_path output_dir subdir - -# load all dot-env files -load-config-files - +# Show [git diff] of templates being rendered (will help verify output) : ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1} +# Directory where templates can be found +: ${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/} +# Root path to write template template_files to (default is '', meaning it will be written to /) +: ${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=} + +declare template_file relative_template_file_path output_file_dir + +# load all dot-env config files +load-config-files # export all dot-env variables so they are available in templating export ${seen_dot_env_variables[@]} -find "$template_dir" -follow -type f -print | while read -r template; do - relative_path="${template#"$template_dir/"}" - subdir=$(dirname "$relative_path") - output_path="$output_dir/${relative_path}" - output_dir=$(dirname "$output_path") +find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do + # Example: template_file=/docker/templates/usr/local/etc/php/php.ini - if [ ! -w "$output_dir" ]; then - log-error-and-exit "ERROR: $template_dir exists, but $output_dir is not writable" + # The file path without the template dir prefix ($ENTRYPOINT_TEMPLATE_DIR) + # + # Example: /usr/local/etc/php/php.ini + relative_template_file_path="${template_file#"${ENTRYPOINT_TEMPLATE_DIR}"}" + + # Adds optional prefix to the output file path + # + # Example: /usr/local/etc/php/php.ini + output_file_path="${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX}/${relative_template_file_path}" + + # Remove the file from the path + # + # Example: /usr/local/etc/php + output_file_dir=$(dirname "${output_file_path}") + + # Ensure the output directory is writable + if ! is-writable "${output_file_dir}"; then + log-error-and-exit "${output_file_dir} is not writable" fi - # create a subdirectory where the template file exists - mkdir -p "$output_dir/$subdir" + # Create the output directory if it doesn't exists + ensure-directory "${output_file_dir}" - log-info "Running [gomplate] on [$template] --> [$output_path]" - cat "$template" | gomplate >"$output_path" + # Render the template + log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" + cat "${template_file}" | gomplate >"${output_file_path}" # Show the diff from the envsubst command if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF} = 1 ]]; then - git --no-pager diff "$template" "${output_path}" || : + git --no-pager diff "${template_file}" "${output_file_path}" || : fi done diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index 4f2e2eb90..501fbced7 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -1,76 +1,72 @@ #!/bin/bash -set -e -o errexit -o nounset -o pipefail +if [[ ${ENTRYPOINT_SKIP:=0} != 0 ]]; then + exec "$@" +fi -: ${ENTRYPOINT_SKIP:=0} -: ${ENTRYPOINT_SKIP_SCRIPTS:=""} -: ${ENTRYPOINT_DEBUG:=0} : ${ENTRYPOINT_ROOT:="/docker/entrypoint.d/"} +: ${ENTRYPOINT_SKIP_SCRIPTS:=""} export ENTRYPOINT_ROOT -if [[ ${ENTRYPOINT_SKIP} == 0 ]]; then - [[ ${ENTRYPOINT_DEBUG} == 1 ]] && set -x +source /docker/helpers.sh - source /docker/helpers.sh +entrypoint-set-name "entrypoint.sh" - entrypoint-set-name "entrypoint.sh" +declare -a skip_scripts +IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" - declare -a skip_scripts - IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" +declare script_name - declare script_name +# ensure the entrypoint folder exists +mkdir -p "${ENTRYPOINT_ROOT}" - # ensure the entrypoint folder exists - mkdir -p "${ENTRYPOINT_ROOT}" +if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + log-info "looking for shell scripts in /docker/entrypoint.d/" - if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - log-info "looking for shell scripts in /docker/entrypoint.d/" + find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do + script_name="$(get-entrypoint-script-name $file)" - find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do - script_name="$(get-entrypoint-script-name $file)" + if in-array "${script_name}" skip_scripts; then + log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" - if in-array "${script_name}" skip_scripts; then - log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + continue + fi - continue + case "${file}" in + *.envsh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - case "${file}" in - *.envsh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi + log-info "Sourcing [${file}]" - log-info "Sourcing [${file}]" + source "${file}" - source "${file}" + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-name + ;; - # the sourced file will (should) than the log prefix, so this restores our own - # "global" log prefix once the file is done being sourced - entrypoint-restore-name - ;; + *.sh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi - *.sh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi + log-info "Running [${file}]" + "${file}" + ;; - log-info "Running [${file}]" - "${file}" - ;; + *) + log-warning "Ignoring unrecognized file [${file}]" + ;; + esac + done - *) - log-warning "Ignoring unrecognized file [${file}]" - ;; - esac - done - - log-info "Configuration complete; ready for start up" - else - log-warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" - fi + log-info "Configuration complete; ready for start up" +else + log-warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" fi exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index 5fb04d7a9..1a4f346b4 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -1,6 +1,10 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail +: ${ENTRYPOINT_DEBUG:=0} + +[[ ${ENTRYPOINT_DEBUG} == 1 ]] && set -x + # Some splash of color for important messages declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" @@ -110,6 +114,14 @@ function is-executable() { [[ -x "$1" ]] } +function is-writable() { + [[ -w "$1" ]] +} + +function ensure-directory() { + mkdir -pv "$@" +} + function get-entrypoint-script-name() { echo "${1#"$ENTRYPOINT_ROOT"}" } From c64571e46d1b3034bfcd55d2399b28d1e17d485e Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 22:16:25 +0000 Subject: [PATCH 185/977] more docs --- .../root/docker/entrypoint.d/05-templating.sh | 2 +- .../docker/shared/root/docker/entrypoint.sh | 110 ++++++++++-------- contrib/docker/shared/root/docker/helpers.sh | 47 +++++++- 3 files changed, 104 insertions(+), 55 deletions(-) diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 89b6f0504..4e6060e43 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -42,7 +42,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat fi # Create the output directory if it doesn't exists - ensure-directory "${output_file_dir}" + ensure-directory-exists "${output_file_dir}" # Render the template log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index 501fbced7..c1e3064f3 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -1,72 +1,80 @@ #!/bin/bash +# short curcuit the entrypoint if $ENTRYPOINT_SKIP isn't set to 0 if [[ ${ENTRYPOINT_SKIP:=0} != 0 ]]; then exec "$@" fi +# Directory where entrypoint scripts lives : ${ENTRYPOINT_ROOT:="/docker/entrypoint.d/"} -: ${ENTRYPOINT_SKIP_SCRIPTS:=""} - export ENTRYPOINT_ROOT +# Space separated list of scripts the entrypoint runner should skip +: ${ENTRYPOINT_SKIP_SCRIPTS:=""} + +# Load helper scripts source /docker/helpers.sh +# Set the entrypoint name for logging entrypoint-set-name "entrypoint.sh" +# Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup declare -a skip_scripts IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" -declare script_name - -# ensure the entrypoint folder exists +# Ensure the entrypoint root folder exists mkdir -p "${ENTRYPOINT_ROOT}" -if /usr/bin/find "${ENTRYPOINT_ROOT}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - log-info "looking for shell scripts in /docker/entrypoint.d/" - - find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do - script_name="$(get-entrypoint-script-name $file)" - - if in-array "${script_name}" skip_scripts; then - log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" - - continue - fi - - case "${file}" in - *.envsh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi - - log-info "Sourcing [${file}]" - - source "${file}" - - # the sourced file will (should) than the log prefix, so this restores our own - # "global" log prefix once the file is done being sourced - entrypoint-restore-name - ;; - - *.sh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi - - log-info "Running [${file}]" - "${file}" - ;; - - *) - log-warning "Ignoring unrecognized file [${file}]" - ;; - esac - done - - log-info "Configuration complete; ready for start up" -else +# If ENTRYPOINT_ROOT directory is empty, warn and run the regular command +if is-directory-empty "${ENTRYPOINT_ROOT}"; then log-warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" + + exec "$@" fi +# Start scanning for entrypoint.d files to source or run +log-info "looking for shell scripts in [${ENTRYPOINT_ROOT}]" + +find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do + # Skip the script if it's in the skip-script list + if in-array $(get-entrypoint-script-name "${file}") skip_scripts; then + log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + + continue + fi + + # Inspect the file extension of the file we're processing + case "${file}" in + *.envsh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "Sourcing [${file}]" + + source "${file}" + + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-name + ;; + + *.sh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi + + log-info "Running [${file}]" + "${file}" + ;; + + *) + log-warning "Ignoring unrecognized file [${file}]" + ;; + esac +done + +log-info "Configuration complete; ready for start up" + exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index 1a4f346b4..ce9ab2661 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -23,15 +23,22 @@ declare -ra dot_env_files=( # environment keys seen when source dot files (so we can [export] them) declare -ga seen_dot_env_variables=() +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-name] +# @arg $1 string The name (or path) of the entrypoint script being run function entrypoint-set-name() { log_prefix_previous="${log_prefix}" log_prefix="ENTRYPOINT - [$(get-entrypoint-script-name $1)] - " } +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-name] function entrypoint-restore-name() { log_prefix="${log_prefix_previous}" } +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails function run-as-runtime-user() { local -i exit_code local target_user @@ -54,12 +61,14 @@ function run-as-runtime-user() { # @description Print the given error message to stderr # @arg $message string A error message. +# @stderr The error message provided with log prefix function log-error() { echo -e "${error_message_color}${log_prefix}ERROR - ${*}${color_clear}" >/dev/stderr } # @description Print the given error message to stderr and exit 1 # @arg $@ string A error message. +# @stderr The error message provided with log prefix # @exitcode 1 function log-error-and-exit() { log-error "$@" @@ -69,18 +78,22 @@ function log-error-and-exit() { # @description Print the given warning message to stderr # @arg $@ string A warning message. +# @stderr The warning message provided with log prefix function log-warning() { echo -e "${warn_message_color}${log_prefix}WARNING - ${*}${color_clear}" >/dev/stderr } -# @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set -# @arg $@ string A warning message. +# @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stdout The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS function log-info() { if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then echo "${log_prefix}$*" fi } +# @description Loads the dot-env files used by Docker and track the keys present in the configuration. +# @sets seen_dot_env_variables array List of config keys discovered during loading function load-config-files() { # Associative array (aka map/dictionary) holding the unique keys found in dot-env files local -A _tmp_dot_env_keys @@ -103,6 +116,11 @@ function load-config-files() { seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) } +# @description Checks if $needle exists in $haystack +# @arg $1 string The needle (value) to search for +# @arg $2 array The haystack (array) to search in +# @exitcode 0 If $needle was found in $haystack +# @exitcode 1 If $needle was *NOT* found in $haystack function in-array() { local -r needle="\<${1}\>" local -nr haystack=$2 @@ -110,18 +128,41 @@ function in-array() { [[ ${haystack[*]} =~ $needle ]] } +# @description Checks if $1 has executable bit set or not +# @arg $1 string The path to check +# @exitcode 0 If $1 has executable bit +# @exitcode 1 If $1 does *NOT* have executable bit function is-executable() { [[ -x "$1" ]] } +# @description Checks if $1 is writable or not +# @arg $1 string The path to check +# @exitcode 0 If $1 is writable +# @exitcode 1 If $1 is *NOT* writable function is-writable() { [[ -w "$1" ]] } -function ensure-directory() { +# @description Checks if $1 contains any files or not +# @arg $1 string The path to check +# @exitcode 0 If $1 contains files +# @exitcode 1 If $1 does *NOT* contain files +function is-directory-empty() { + ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v +} + +# @description Ensures a directory exists (via mkdir) +# @arg $1 string The path to create +# @exitcode 0 If $1 If the path exists *or* was created +# @exitcode 1 If $1 If the path does *NOT* exists and could *NOT* be created +function ensure-directory-exists() { mkdir -pv "$@" } +# @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_ROOT prefix +# @arg $1 string The path to manipulate +# @stdout The relative path to the entrypoint script function get-entrypoint-script-name() { echo "${1#"$ENTRYPOINT_ROOT"}" } From c12ef66c5642d43b1a2efa64c03bf6d6b131c2d3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 22:33:41 +0000 Subject: [PATCH 186/977] opt-in fixing of user/group ownership of files --- contrib/docker/README.md | 11 +++++++++- .../root/docker/entrypoint.d/10-storage.sh | 6 +++--- .../entrypoint.d/15-storage-permissions.sh | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh diff --git a/contrib/docker/README.md b/contrib/docker/README.md index a0b07f5fa..b07a9b1b7 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -103,7 +103,7 @@ When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/ 1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). 1. Check if the file is executable. - 1. If the file is not executable, print an error and exit the container. + 1. If the file is *not* executable, print an error and exit the container. 1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). 1. If the file has the extension `.sh` the file will be run like a normal script. 1. Any other file extension will log a warning and will be ignored. @@ -159,6 +159,15 @@ Please see the * [gomplate syntax documentation](https://docs.gomplate.ca/syntax/) * [gomplate functions documentation](https://docs.gomplate.ca/functions/) +### Fixing ownership on startup + +You can set the environment variable `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. + +The variable is a space-delimited list shown below and accepts both relative and absolute paths: + +* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` +* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` + ## Build settings (arguments) The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index 14f66dc27..83e0abf34 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -3,8 +3,8 @@ source /docker/helpers.sh entrypoint-set-name "$0" +# Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions run-as-runtime-user cp --recursive storage.skel/* storage/ -run-as-runtime-user php artisan storage:link -log-info "Ensure permissions are correct" -chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} storage/ bootstrap/ +# Ensure storage linkk are correctly configured +run-as-runtime-user php artisan storage:link diff --git a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh new file mode 100755 index 000000000..0a67e3fad --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh @@ -0,0 +1,21 @@ +#!/bin/bash +source /docker/helpers.sh + +entrypoint-set-name "$0" + +# Optionally fix ownership of configured paths +: ${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""} + +declare -a ensure_ownership_paths=() +IFS=' ' read -a ensure_ownership_paths <<<"$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS" + +if [[ ${#ensure_ownership_paths} == 0 ]]; then + log-info "No paths has been configured for ownership fixes via [\$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS]." + + exit 0 +fi + +for path in "${ensure_ownership_paths[@]}"; do + log-info "Ensure ownership of [${path}] correct" + chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" +done From 895b51fd9ff059c3a469ad510ab0127f2e8c7b87 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 23:04:25 +0000 Subject: [PATCH 187/977] more tweaks --- contrib/docker/README.md | 12 +++-- .../root/docker/entrypoint.d/10-storage.sh | 2 +- .../entrypoint.d/15-storage-permissions.sh | 8 ++-- .../docker/shared/root/docker/entrypoint.sh | 5 +++ contrib/docker/shared/root/docker/helpers.sh | 44 ++++++++++++++++--- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/contrib/docker/README.md b/contrib/docker/README.md index b07a9b1b7..2797b95ff 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -108,6 +108,12 @@ When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/ 1. If the file has the extension `.sh` the file will be run like a normal script. 1. Any other file extension will log a warning and will be ignored. +#### Debugging + +You can set environment variable `ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. + +You can also `docker exec` or `docker run` into a container and run `/` + #### Included scripts * `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. @@ -145,7 +151,7 @@ Variables available for templating are sourced (in order, so *last* source takes #### Template guide 101 -Please see the [gomplate documentation](https://docs.gomplate.ca/) for a more comprehensive overview. +Please see the [`gomplate` documentation](https://docs.gomplate.ca/) for a more comprehensive overview. The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: @@ -156,8 +162,8 @@ The script will *fail* if you reference a variable that does not exist (and don' Please see the -* [gomplate syntax documentation](https://docs.gomplate.ca/syntax/) -* [gomplate functions documentation](https://docs.gomplate.ca/functions/) +* [`gomplate` syntax documentation](https://docs.gomplate.ca/syntax/) +* [`gomplate` functions documentation](https://docs.gomplate.ca/functions/) ### Fixing ownership on startup diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index 83e0abf34..add20b5d6 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -4,7 +4,7 @@ source /docker/helpers.sh entrypoint-set-name "$0" # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions -run-as-runtime-user cp --recursive storage.skel/* storage/ +run-as-runtime-user cp --recursive storage.skel/ storage/ # Ensure storage linkk are correctly configured run-as-runtime-user php artisan storage:link diff --git a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh index 0a67e3fad..d5844d661 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh @@ -7,15 +7,15 @@ entrypoint-set-name "$0" : ${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""} declare -a ensure_ownership_paths=() -IFS=' ' read -a ensure_ownership_paths <<<"$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS" +IFS=' ' read -a ensure_ownership_paths <<<"${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS}" -if [[ ${#ensure_ownership_paths} == 0 ]]; then +if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then log-info "No paths has been configured for ownership fixes via [\$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS]." exit 0 fi for path in "${ensure_ownership_paths[@]}"; do - log-info "Ensure ownership of [${path}] correct" - chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" + log-info "Ensure ownership of [${path}] is correct" + run-as-current-user chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" done diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index c1e3064f3..e17a1c42b 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -50,7 +50,9 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi + log-info log-info "Sourcing [${file}]" + log-info source "${file}" @@ -65,7 +67,10 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi + log-info log-info "Running [${file}]" + log-info + "${file}" ;; diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index ce9ab2661..d80dc0d24 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -1,9 +1,7 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail -: ${ENTRYPOINT_DEBUG:=0} - -[[ ${ENTRYPOINT_DEBUG} == 1 ]] && set -x +[[ ${ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x # Some splash of color for important messages declare -g error_message_color="\033[1;31m" @@ -40,14 +38,37 @@ function entrypoint-restore-name() { # @exitcode 0 if the command succeeeds # @exitcode 1 if the command fails function run-as-runtime-user() { + run-command-as "$(id -un ${RUNTIME_UID})" "${@}" +} + +# @description Run a command as the [runtime user] +# @arg $@ string The command to run +# @exitcode 0 if the command succeeeds +# @exitcode 1 if the command fails +function run-as-current-user() { + run-command-as "$(id -un)" "${@}" +} + +# @description Run a command as the a named user +# @arg $1 string The user to run the command as +# @arg $@ string The command to run +# @exitcode 0 If the command succeeeds +# @exitcode 1 If the command fails +function run-command-as() { local -i exit_code local target_user - target_user=$(id -un ${RUNTIME_UID}) + target_user=${1} + shift - log-info "👷 Running [${*}] as [${target_user}]" + log-info-stderr "👷 Running [${*}] as [${target_user}]" + + if [[ ${target_user} != "root" ]]; then + su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + else + "${@}" + fi - su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" exit_code=$? if [[ $exit_code != 0 ]]; then @@ -55,7 +76,7 @@ function run-as-runtime-user() { return $exit_code fi - log-info "✅ OK!" + log-info-stderr "✅ OK!" return $exit_code } @@ -92,6 +113,15 @@ function log-info() { fi } +# @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set +# @arg $@ string A info message. +# @stderr The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS +function log-info-stderr() { + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "${log_prefix}$*" + fi +} + # @description Loads the dot-env files used by Docker and track the keys present in the configuration. # @sets seen_dot_env_variables array List of config keys discovered during loading function load-config-files() { From d13895a3e0530011bf6210388dffd2614e4f5675 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 4 Jan 2024 23:15:46 +0000 Subject: [PATCH 188/977] add 15-storage-permissions.sh to the docs --- contrib/docker/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/docker/README.md b/contrib/docker/README.md index 2797b95ff..443ab6423 100644 --- a/contrib/docker/README.md +++ b/contrib/docker/README.md @@ -119,6 +119,7 @@ You can also `docker exec` or `docker run` into a container and run `/` * `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. * `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. * `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. +* `//docker/entrypoint.d/15-storage-permissions.sh` (optionally) ensures permissions for files are corrected (see [fixing ownership on startup](#fixing-ownership-on-startup)) * `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured * `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed From 99e2a045a6d2d9d0740a19a7b0580775e5c0405f Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 00:11:20 +0000 Subject: [PATCH 189/977] more renaming for clarity --- .../shared/root/docker/entrypoint.d/04-defaults.envsh | 2 +- .../shared/root/docker/entrypoint.d/05-templating.sh | 2 +- .../docker/shared/root/docker/entrypoint.d/10-storage.sh | 2 +- .../root/docker/entrypoint.d/15-storage-permissions.sh | 2 +- .../docker/shared/root/docker/entrypoint.d/20-horizon.sh | 2 +- .../docker/shared/root/docker/entrypoint.d/30-cache.sh | 2 +- contrib/docker/shared/root/docker/entrypoint.sh | 4 ++-- contrib/docker/shared/root/docker/helpers.sh | 8 ++++---- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index 3f1a77843..3507d94ee 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -8,7 +8,7 @@ # # We also don't need to source `helpers.sh` since it's already available -entrypoint-set-name "${BASH_SOURCE[0]}" +entrypoint-set-script-name "${BASH_SOURCE[0]}" load-config-files diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 4e6060e43..618a2d406 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -1,7 +1,7 @@ #!/bin/bash source /docker/helpers.sh -entrypoint-set-name "$0" +entrypoint-set-script-name "$0" # Show [git diff] of templates being rendered (will help verify output) : ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1} diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index add20b5d6..fc952ea3a 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -1,7 +1,7 @@ #!/bin/bash source /docker/helpers.sh -entrypoint-set-name "$0" +entrypoint-set-script-name "$0" # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions run-as-runtime-user cp --recursive storage.skel/ storage/ diff --git a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh index d5844d661..30a58c5a9 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh @@ -1,7 +1,7 @@ #!/bin/bash source /docker/helpers.sh -entrypoint-set-name "$0" +entrypoint-set-script-name "$0" # Optionally fix ownership of configured paths : ${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""} diff --git a/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh index 2d3394746..6b81e7133 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -1,6 +1,6 @@ #!/bin/bash source /docker/helpers.sh -entrypoint-set-name "$0" +entrypoint-set-script-name "$0" run-as-runtime-user php artisan horizon:publish diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh index 5c0f20bb8..e933a179b 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -1,7 +1,7 @@ #!/bin/bash source /docker/helpers.sh -entrypoint-set-name "$0" +entrypoint-set-script-name "$0" run-as-runtime-user php artisan route:cache run-as-runtime-user php artisan view:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index e17a1c42b..173e4dbe3 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -15,7 +15,7 @@ export ENTRYPOINT_ROOT source /docker/helpers.sh # Set the entrypoint name for logging -entrypoint-set-name "entrypoint.sh" +entrypoint-set-script-name "entrypoint.sh" # Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup declare -a skip_scripts @@ -58,7 +58,7 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; # the sourced file will (should) than the log prefix, so this restores our own # "global" log prefix once the file is done being sourced - entrypoint-restore-name + entrypoint-restore-script-name ;; *.sh) diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index d80dc0d24..880027cf4 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -21,15 +21,15 @@ declare -ra dot_env_files=( # environment keys seen when source dot files (so we can [export] them) declare -ga seen_dot_env_variables=() -# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-name] +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] # @arg $1 string The name (or path) of the entrypoint script being run -function entrypoint-set-name() { +function entrypoint-set-script-name() { log_prefix_previous="${log_prefix}" log_prefix="ENTRYPOINT - [$(get-entrypoint-script-name $1)] - " } -# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-name] -function entrypoint-restore-name() { +# @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] +function entrypoint-restore-script-name() { log_prefix="${log_prefix_previous}" } From 5cfd8e15a98f5bfd1a64fb4bc7d337e68224de53 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 00:16:36 +0000 Subject: [PATCH 190/977] quotes --- contrib/docker/Dockerfile | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index cdff0d916..c254b2c4d 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -9,13 +9,13 @@ ARG COMPOSER_VERSION="2.6" # See: https://nginx.org/ -ARG NGINX_VERSION=1.25.3 +ARG NGINX_VERSION="1.25.3" # See: https://github.com/ddollar/forego -ARG FOREGO_VERSION=0.17.2 +ARG FOREGO_VERSION="0.17.2" # See: https://github.com/hairyhenderson/gomplate -ARG GOMPLATE_VERSION=v3.11.6 +ARG GOMPLATE_VERSION="v3.11.6" ### # PHP base configuration @@ -32,22 +32,22 @@ ARG RUNTIME_UID=33 # often called 'www-data' ARG RUNTIME_GID=33 # often called 'www-data' # APT extra packages -ARG APT_PACKAGES_EXTRA= +ARG APT_PACKAGES_EXTRA="" # Extensions installed via [pecl install] ARG PHP_PECL_EXTENSIONS="" -ARG PHP_PECL_EXTENSIONS_EXTRA= +ARG PHP_PECL_EXTENSIONS_EXTRA="" # Extensions installed via [docker-php-ext-install] ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" -ARG PHP_EXTENSIONS_EXTRA= +ARG PHP_EXTENSIONS_EXTRA="" ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite" # GPG key for nginx apt repository -ARG NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 +ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62" # GPP key path for nginx apt repository -ARG NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg +ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg" ####################################################### # Docker "copy from" images @@ -93,15 +93,14 @@ RUN set -ex \ FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base ARG APT_PACKAGES_EXTRA +ARG BUILDKIT_SBOM_SCAN_STAGE="true" ARG PHP_DEBIAN_RELEASE ARG PHP_VERSION ARG RUNTIME_GID ARG RUNTIME_UID - ARG TARGETPLATFORM -ARG BUILDKIT_SBOM_SCAN_STAGE=true -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND="noninteractive" # Ensure we run all scripts through 'bash' rather than 'sh' SHELL ["/bin/bash", "-c"] @@ -160,7 +159,7 @@ ARG RUNTIME_GID ARG TARGETPLATFORM # Make sure composer cache is targeting our cache mount later -ENV COMPOSER_CACHE_DIR=/cache/composer +ENV COMPOSER_CACHE_DIR="/cache/composer" # Don't enforce any memory limits for composer ENV COMPOSER_MEMORY_LIMIT=-1 From f2eb3df85fc209a184443f9e03cc61faa16a7730 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 01:34:46 +0000 Subject: [PATCH 191/977] remove VOLUME and EXPOSE see https://stackoverflow.com/a/52571354/1081818 --- contrib/docker/Dockerfile | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile index c254b2c4d..8ef6a99d4 100644 --- a/contrib/docker/Dockerfile +++ b/contrib/docker/Dockerfile @@ -225,8 +225,6 @@ COPY contrib/docker/shared/root / ENTRYPOINT ["/docker/entrypoint.sh"] -VOLUME /var/www/storage /var/www/bootstrap - ####################################################### # Runtime: apache ####################################################### @@ -241,8 +239,6 @@ RUN set -ex \ CMD ["apache2-foreground"] -EXPOSE 80 - ####################################################### # Runtime: fpm ####################################################### @@ -253,8 +249,6 @@ COPY contrib/docker/fpm/root / CMD ["php-fpm"] -EXPOSE 9000 - ####################################################### # Runtime: nginx ####################################################### @@ -284,8 +278,6 @@ COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ COPY contrib/docker/nginx/root / COPY contrib/docker/nginx/Procfile . -EXPOSE 80 - STOPSIGNAL SIGQUIT CMD ["forego", "start", "-r"] From 10674ac52337b3df7f9bcae0972fcfc44b7e9754 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 16:18:48 +0000 Subject: [PATCH 192/977] iterate on apache example with docker-compose --- .gitignore | 3 + contrib/docker-compose/.env | 916 ++++++++++++++++++ contrib/docker-compose/README.md | 13 + .../docker-compose/docker-compose.apache.yml | 59 ++ .../root/docker/entrypoint.d/10-storage.sh | 2 +- .../templates/usr/local/etc/php/php.ini | 6 +- 6 files changed, 995 insertions(+), 4 deletions(-) create mode 100644 contrib/docker-compose/.env create mode 100644 contrib/docker-compose/README.md create mode 100644 contrib/docker-compose/docker-compose.apache.yml diff --git a/.gitignore b/.gitignore index 0494cee10..4396e4cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ yarn-error.log .git-credentials /.composer/ /nginx.conf +/contrib/docker-compose/data +/contrib/docker-compose/config +!/contrib/docker-compose/.env diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env new file mode 100644 index 000000000..4cfde052b --- /dev/null +++ b/contrib/docker-compose/.env @@ -0,0 +1,916 @@ +# -*- mode: bash -*- +# vi: ft=bash + +############################################################### +# Docker-wide configuration +############################################################### + +# Path (relative) to the docker-compose file where containers will store their data +DOCKER_DATA_ROOT="./data" + +# Path (relative) to the docker-compose file where containers will store their config +DOCKER_CONFIG_ROOT="./config" + +# Pixelfed version (image tag) to pull from the registry +DOCKER_TAG="branch-jippi-fork-apache-8.1" + +# Set timezone used by *all* containers - these should be in sync +# +# See: https://www.php.net/manual/en/timezones.php +TZ="UTC" + +############################################################### +# Pixelfed application configuration +############################################################### + +# A random 32-character string to be used as an encryption key. +# +# No default value; use [php artisan key:generate] to generate. +# +# This key is used by the Illuminate encrypter service and should be set to a random, +# 32 character string, otherwise these encrypted strings will not be safe. +# +# Please do this before deploying an application! +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_key +APP_KEY="" + +# See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +APP_NAME="Pixelfed Prod" + +# Application domains used for routing. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_domain +APP_DOMAIN="your-domain-here-dot-com" + +# This URL is used by the console to properly generate URLs when using the Artisan command line tool. +# You should set this to the root of your application so that it is used when running Artisan tasks. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_url +APP_URL=https://${APP_DOMAIN} + +# Application domains used for routing. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#admin_domain +ADMIN_DOMAIN="${APP_DOMAIN}" + +# This value determines the “environment” your application is currently running in. +# This may determine how you prefer to configure various services your application utilizes. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_env +#APP_ENV="production" + +# When your application is in debug mode, detailed error messages with stack traces will +# be shown on every error that occurs within your application. +# +# If disabled, a simple generic error page is shown. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_debug +#APP_DEBUG="false" + +# Enable/disable new local account registrations. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#open_registration +#OPEN_REGISTRATION=true + +# Require email verification before a new user can do anything. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +#ENFORCE_EMAIL_VERIFICATION="true" + +# Allow a maximum number of user accounts. +# +# Defaults to "1000". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +PF_MAX_USERS="false" + +# See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled +# OAUTH_ENABLED="true" + +# Defaults to "UTC". +# +# Do not edit your timezone or things will break! +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_timezone +# See: https://www.php.net/manual/en/timezones.php +APP_TIMEZONE="${TZ}" + +# The application locale determines the default locale that will be used by the translation service provider. +# You are free to set this value to any of the locales which will be supported by the application. +# +# Defaults to "en". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_locale +#APP_LOCALE="en" + +# The fallback locale determines the locale to use when the current one is not available. +# +# You may change the value to correspond to any of the language folders that are provided through your application. +# +# Defaults to "en". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +APP_FALLBACK_LOCALE="en" + +# See: https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +#LIMIT_ACCOUNT_SIZE="true" + +# Update the max account size, the per user limit of files in kB. +# +# Defaults to "1000000" (1GB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +#MAX_ACCOUNT_SIZE="1000000" + +# Update the max photo size, in kB. +# +# Defaults to "15000" (15MB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +#MAX_PHOTO_SIZE="15000" + +# Update the max avatar size, in kB. +# +# Defaults to "2000" (2MB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +#MAX_AVATAR_SIZE="2000" + +# Change the caption length limit for new local posts. +# +# Defaults to "500". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +#MAX_CAPTION_LENGTH="500" + +# Change the bio length limit for user profiles. +# +# Defaults to "125". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +#MAX_BIO_LENGTH="125" + +# Change the length limit for user names. +# +# Defaults to "30". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_name_length +#MAX_NAME_LENGTH="30" + +# The max number of photos allowed per post. +# +# Defaults to "4". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length +#MAX_ALBUM_LENGTH="4" + +# Set the image optimization quality, must be a value between 1-100. +# +# Defaults to "80". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality +#IMAGE_QUALITY="80" + +# Resize and optimize image uploads. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +#PF_OPTIMIZE_IMAGES="true" + +# Resize and optimize video uploads. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +#PF_OPTIMIZE_VIDEOS="true" + +# Enable account deletion. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#account_deletion +#ACCOUNT_DELETION="true" + +# Set account deletion queue after X days, set to false to delete accounts immediately. +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +#ACCOUNT_DELETE_AFTER="false" + +# Defaults to "Pixelfed - Photo sharing for everyone". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_description +#INSTANCE_DESCRIPTION= + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +#INSTANCE_PUBLIC_HASHTAGS="false" + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +#INSTANCE_PUBLIC_LOCAL_TIMELINE="false" + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#banned_usernames +#BANNED_USERNAMES= + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +#STORIES_ENABLED="false" + +# Defaults to "false". +# +# Level is hardcoded to 1. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +#RESTRICTED_INSTANCE="false" + +############################################################### +# Database configuration +############################################################### + +# Here you may specify which of the database connections below you wish to use as your default connection for all database work. +# +# Of course you may use many connections at once using the database library. +# +# Possible values: +# +# - "sqlite" +# - "mysql" (default) +# - "pgsql" +# - "sqlsrv" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_connection +DB_CONNECTION="mysql" + +# See: https://docs.pixelfed.org/technical-documentation/config/#db_host +DB_HOST="db" + +# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +DB_USERNAME="pixelfed" + +# See: https://docs.pixelfed.org/technical-documentation/config/#db_password +DB_PASSWORD="__CHANGE_ME__" + +# See: https://docs.pixelfed.org/technical-documentation/config/#db_database +DB_DATABASE="pixelfed_prod" + +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_port +DB_PORT="3306" + +############################################################### +# Mail configuration +############################################################### + +# Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. +# You may specify which one you’re using throughout your application here. +# +# Possible values: +# +# "smtp" (default) +# "sendmail" +# "mailgun" +# "mandrill" +# "ses" +# "sparkpost" +# "log" +# "array" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_driver +#MAIL_DRIVER="smtp" + +# The host address of the SMTP server used by your applications. +# +# A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. +# +# Defaults to "smtp.mailgun.org". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_host +#MAIL_HOST="smtp.mailgun.org" + +# This is the SMTP port used by your application to deliver e-mails to users of the application. +# +# Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. +# +# Defaults to 587. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_port +#MAIL_PORT="587" + +# You may wish for all e-mails sent by your application to be sent from the same address. +# +# Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. +# +# Defaults to "hello@example.com". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}" + +# Defaults to "Example". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" + +# If your SMTP server requires a username for authentication, you should set it here. +# +# This will get used to authenticate with your server on connection. +# You may also set the “password” value below this one. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_username +#MAIL_USERNAME= + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_password +#MAIL_PASSWORD= + +# Here you may specify the encryption protocol that should be used when the application send e-mail messages. +# +# A sensible default using the transport layer security protocol should provide great security. +# +# Defaults to "tls". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +#MAIL_ENCRYPTION="tls" + +############################################################### +# Redis configuration +############################################################### + +# Defaults to "phpredis". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +#REDIS_CLIENT="phpredis" + +# Defaults to "tcp". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +#REDIS_SCHEME="tcp" + +# Defaults to "localhost". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_host +REDIS_HOST="cache" + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_password +#REDIS_PASSWORD= + +# Defaults to 6379. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_port +REDIS_PORT="6379" + +# Defaults to 0. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_database +#REDIS_DATABASE="0" + +############################################################### +# Cache settings +############################################################### + +# This option controls the default cache connection that gets used while using this caching library. +# +# This connection is used when another is not explicitly specified when executing a given caching function. +# +# Possible values: +# - "apc" +# - "array" +# - "database" +# - "file" (default) +# - "memcached" +# - "redis" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cache_driver +CACHE_DRIVER="redis" + +# Defaults to ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cache_prefix +# CACHE_PREFIX="{APP_NAME}_cache" + +############################################################### +# Horizon settings +############################################################### + +# 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. +# +# Defaults to "horizon-". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix +#HORIZON_PREFIX="horizon-" + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode +#HORIZON_DARKMODE="false" + +# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker +# may consume before it is terminated and restarted. +# +# You should set this value according to the resources available to your server. +# +# Defaults to "64". +#HORIZON_MEMORY_LIMIT="64" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy +#HORIZON_BALANCE_STRATEGY="auto" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes +#HORIZON_MIN_PROCESSES="1" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes +#HORIZON_MAX_PROCESSES="20" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory +#HORIZON_SUPERVISOR_MEMORY="64" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries +#HORIZON_SUPERVISOR_TRIES="3" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice +#HORIZON_SUPERVISOR_NICE="0" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout +#HORIZON_SUPERVISOR_TIMEOUT="300" + +############################################################### +# Experiments +############################################################### + +# Text only posts (alpha). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_top +#EXP_TOP="false" + +# Poll statuses (alpha). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_polls +#EXP_POLLS="false" + +# Cached public timeline for larger instances (beta). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +#EXP_CPT="false" + +# Enforce Mastoapi Compatibility (alpha). +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc +#EXP_EMC="true" + +############################################################### +# ActivityPub confguration +############################################################### + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#activity_pub +#ACTIVITY_PUB="false" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +#AP_REMOTE_FOLLOW="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +#AP_SHAREDINBOX="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +#AP_INBOX="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +#AP_OUTBOX="true" + +############################################################### +# Federation confguration +############################################################### + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +#ATOM_FEEDS="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +#NODEINFO="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#webfinger +#WEBFINGER="true" + +############################################################### +# Storage (cloud) +############################################################### + +# Store media on object storage like S3, Digital Ocean Spaces, Rackspace +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +#PF_ENABLE_CLOUD="false" + +# Many applications store files both locally and in the cloud. +# +# For this reason, you may specify a default “cloud” driver here. +# This driver will be bound as the Cloud disk implementation in the container. +# +# Defaults to "s3". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +#FILESYSTEM_CLOUD="s3" + +# Defaults to true. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +#MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" + +############################################################### +# Storage (cloud) - S3 +############################################################### + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id +#AWS_ACCESS_KEY_ID= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key +#AWS_SECRET_ACCESS_KEY= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_default_region +#AWS_DEFAULT_REGION= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_bucket +#AWS_BUCKET= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_url +#AWS_URL= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint +#AWS_ENDPOINT= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +#AWS_USE_PATH_STYLE_ENDPOINT="false" + +############################################################### +# COSTAR - Confirm Object Sentiment Transform and Reduce +############################################################### + +# Comma-separated list of domains to block. +# +# Defaults to null +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +#CS_BLOCKED_DOMAINS= + +# Comma-separated list of domains to add warnings. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +#CS_CW_DOMAINS= + +# Comma-separated list of domains to remove from public timelines. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +#CS_UNLISTED_DOMAINS= + +# Comma-separated list of keywords to block. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +#CS_BLOCKED_KEYWORDS= + +# Comma-separated list of keywords to add warnings. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +#CS_CW_KEYWORDS= + +# Comma-separated list of keywords to remove from public timelines. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +#CS_UNLISTED_KEYWORDS= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +#CS_BLOCKED_ACTOR= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +#CS_CW_ACTOR= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +#CS_UNLISTED_ACTOR= + +############################################################### +# Media +############################################################### + +# Defaults to false. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#media_exif_database +MEDIA_EXIF_DATABASE="true" + +# Pixelfed supports GD or ImageMagick to process images. +# +# Defaults to "gd". +# +# Possible values: +# - "gd" (default) +# - "imagick" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#image_driver +#IMAGE_DRIVER="gd" + +############################################################### +# Logging +############################################################### + +# Possible values: +# +# - "stack" (default) +# - "single" +# - "daily" +# - "slack" +# - "stderr" +# - "syslog" +# - "errorlog" +# - "null" +# - "emergency" +# - "media" +LOG_CHANNEL="stderr" + +# Used by single, stderr and syslog. +# +# Defaults to "debug" for all of those. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_level +#LOG_LEVEL="debug" + +# Used by stderr. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +#LOG_STDERR_FORMATTER= + +# Used by slack. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +#LOG_SLACK_WEBHOOK_URL= + +############################################################### +# Broadcasting settings +############################################################### + +# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. +# +# Possible values: +# - "pusher" +# - "redis" +# - "log" +# - "null" (default) +# +# See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver +#BROADCAST_DRIVER= + +############################################################### +# Other settings +############################################################### + +# Defaults to true. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types +#RESTRICT_HTML_TYPES="true" + +############################################################### +# Queue configuration +############################################################### + +# Possible values: +# - "sync" (default) +# - "database" +# - "beanstalkd" +# - "sqs" +# - "redis" +# - "null" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#queue_driver +QUEUE_DRIVER="redis" + +############################################################### +# Queue (SQS) configuration +############################################################### + +# Defaults to "your-public-key". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_key +#SQS_KEY="your-public-key" + +# Defaults to "your-secret-key". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +#SQS_SECRET="your-secret-key" + +# Defaults to "https://sqs.us-east-1.amazonaws.com/your-account-id". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +#SQS_PREFIX= + +# Defaults to "your-queue-name". +# +# https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +#SQS_QUEUE="your-queue-name" + +# Defaults to "us-east-1". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_region +#SQS_REGION="us-east-1" + +############################################################### +# Session configuration +############################################################### + +# This option controls the default session “driver” that will be used on requests. +# +# By default, we will use the lightweight native driver but you may specify any of the other wonderful drivers provided here. +# +# Possible values: +# - "file" +# - "cookie" +# - "database" (default) +# - "apc" +# - "memcached" +# - "redis" +# - "array" +SESSION_DRIVER="redis" + +# Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. +# +# If you want them to immediately expire on the browser closing, set that option. +# +# Defaults to 86400. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +#SESSION_LIFETIME="86400" + +# Here you may change the domain of the cookie used to identify a session in your application. +# +# This will determine which domains the cookie is available to in your application. +# +# A sensible default has been set. +# +# Defaults to the value of APP_DOMAIN, or null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#session_domain +#SESSION_DOMAIN="${APP_DOMAIN}" + +############################################################### +# Proxy configuration +############################################################### + +# Set trusted proxy IP addresses. +# +# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. +# +# The “*” character is syntactic sugar within TrustedProxy to trust any +# proxy that connects directly to your server, a requirement when you cannot +# know the address of your proxy (e.g. if using Rackspace balancers). +# +# The “**” character is syntactic sugar within TrustedProxy to trust not just any +# proxy that connects directly to your server, but also proxies that connect to those proxies, +# and all the way back until you reach the original source IP. It will mean that +# $request->getClientIp() always gets the originating client IP, no matter how many proxies +# that client’s request has subsequently passed through. +# +# Defaults to "*". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#trust_proxies +TRUST_PROXIES="*" + +############################################################### +# Passport configuration +############################################################### +# +# Passport uses encryption keys while generating secure access tokens +# for your application. +# +# By default, the keys are stored as local files but can be set via environment +# variables when that is more convenient. + +# See: https://docs.pixelfed.org/technical-documentation/config/#passport_private_key +#PASSPORT_PRIVATE_KEY= + +# See: https://docs.pixelfed.org/technical-documentation/config/#passport_public_key +#PASSPORT_PUBLIC_KEY= + +############################################################### +# PHP configuration +############################################################### + +# See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit +#PHP_MEMORY_LIMIT="128M" + +############################################################### +# MySQL DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Environment Variables" at https://hub.docker.com/_/mysql + +MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" +MYSQL_USER="${DB_USERNAME}" +MYSQL_PASSWORD="${DB_PASSWORD}" +MYSQL_DATABASE="${DB_DATABASE}" + +############################################################### +# MySQL (MariaDB) DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb + +MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" +MARIADB_USER="${DB_USERNAME}" +MARIADB_PASSWORD="${DB_PASSWORD}" +MARIADB_DATABASE="${DB_DATABASE}" + +############################################################### +# PostgreSQL DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Environment Variables" at https://hub.docker.com/_/postgres + +POSTGRES_USER="${DB_USERNAME}" +POSTGRES_PASSWORD="${DB_PASSWORD}" +POSTGRES_DB="${DB_DATABASE}" + +############################################################### +# Docker Specific configuration +############################################################### + +# Image to pull the Pixelfed Docker images from +# +# Possible values: +# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# - "pixelfed/pixelfed" to pull from DockerHub +# +DOCKER_IMAGE="ghcr.io/jippi/pixelfed" + +# Port that Redis will listen on *outside* the container (e.g. the host machine) +DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" + +# Port that the database will listen on *outside* the container (e.g. the host machine) +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" + +# Port that the web will listen on *outside* the container (e.g. the host machine) +DOCKER_WEB_PORT_EXTERNAL="8080" diff --git a/contrib/docker-compose/README.md b/contrib/docker-compose/README.md new file mode 100644 index 000000000..361c2622b --- /dev/null +++ b/contrib/docker-compose/README.md @@ -0,0 +1,13 @@ +# Pixelfed + Docker + Docker Compose + +## Prerequisites + +* One of the `docker-compose.yml` files in this directory +* A copy of the `example.env` file + +In order to set configuration, please use a .env file in your compose project directory (the same directory as your docker-compose.yml), and set database options, application +name, key, and other settings there. + +A list of available settings is available in .env.example + +The services should scale properly across a swarm cluster if the volumes are properly shared between cluster members. diff --git a/contrib/docker-compose/docker-compose.apache.yml b/contrib/docker-compose/docker-compose.apache.yml new file mode 100644 index 000000000..ff7734797 --- /dev/null +++ b/contrib/docker-compose/docker-compose.apache.yml @@ -0,0 +1,59 @@ +--- +version: "3" + +services: + web: + image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + restart: unless-stopped + env_file: + - "./.env" + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_DATA_ROOT}/pixelfed/bootstrap:/var/www/bootstrap" + - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" + ports: + - "${DOCKER_WEB_PORT_EXTERNAL}:80" + depends_on: + - db + - redis + + worker: + image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + command: gosu www-data php artisan horizon + restart: unless-stopped + env_file: + - "./.env" + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_DATA_ROOT}/pixelfed/bootstrap:/var/www/bootstrap" + - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" + depends_on: + - db + - redis + + db: + image: mariadb:11.2 + command: --default-authentication-plugin=mysql_native_password + restart: unless-stopped + env_file: + - "./.env" + volumes: + - "${DOCKER_DATA_ROOT}/db:/var/lib/mysql" + ports: + - "${DOCKER_DB_PORT_EXTERNAL}:3306" + + redis: + image: redis:7 + restart: unless-stopped + env_file: + - "./.env" + volumes: + - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" + - "${DOCKER_DATA_ROOT}/redis:/data" + ports: + - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" + healthcheck: + interval: 10s + timeout: 5s + retries: 2 + test: ["CMD", "redis-cli", "-p", "6399", "ping"] diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index fc952ea3a..b9809e24c 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -4,7 +4,7 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions -run-as-runtime-user cp --recursive storage.skel/ storage/ +run-as-runtime-user cp --recursive storage.skel/. storage/. # Ensure storage linkk are correctly configured run-as-runtime-user php artisan storage:link diff --git a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 81ba3d207..c34266630 100644 --- a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -376,7 +376,7 @@ zend.exception_ignore_args = On ; threat in any way, but it makes it possible to determine whether you use PHP ; on your server or not. ; http://php.net/expose-php -expose_php = On +expose_php = Off ;;;;;;;;;;;;;;;;;;; ; Resource Limits ; @@ -406,7 +406,7 @@ max_input_time = 60 ; Maximum amount of memory a script may consume (128MB) ; http://php.net/memory-limit -memory_limit = 128M +memory_limit = {{ getenv "PHP_MEMORY_LIMIT" "128M" }} ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Error handling and logging ; @@ -947,7 +947,7 @@ cli_server.color = On [Date] ; Defines the default timezone used by the date functions ; http://php.net/date.timezone -;date.timezone = +date.timezone = {{ getenv "TZ" "UTC" }} ; http://php.net/date.default-latitude ;date.default_latitude = 31.7667 From 215b49ea3d599d4cfb08bb874a8dd94e057b5f0f Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 16:27:11 +0000 Subject: [PATCH 193/977] rename2 --- contrib/docker-compose/.env | 4 ++-- contrib/docker-compose/README.md | 17 ++++++++++++----- ...er-compose.apache.yml => docker-compose.yml} | 0 3 files changed, 14 insertions(+), 7 deletions(-) rename contrib/docker-compose/{docker-compose.apache.yml => docker-compose.yml} (100%) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 4cfde052b..076edf09b 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -113,7 +113,7 @@ APP_TIMEZONE="${TZ}" # Defaults to "en". # # See: https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale -APP_FALLBACK_LOCALE="en" +#APP_FALLBACK_LOCALE="en" # See: https://docs.pixelfed.org/technical-documentation/config/#limit_account_size #LIMIT_ACCOUNT_SIZE="true" @@ -368,7 +368,7 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # Defaults to "localhost". # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_host -REDIS_HOST="cache" +REDIS_HOST="redis" # Defaults to null. # diff --git a/contrib/docker-compose/README.md b/contrib/docker-compose/README.md index 361c2622b..6c57c43d2 100644 --- a/contrib/docker-compose/README.md +++ b/contrib/docker-compose/README.md @@ -3,11 +3,18 @@ ## Prerequisites * One of the `docker-compose.yml` files in this directory -* A copy of the `example.env` file +* A copy of the `example.env` file named `.env` next to `docker-compose.yml` -In order to set configuration, please use a .env file in your compose project directory (the same directory as your docker-compose.yml), and set database options, application -name, key, and other settings there. +Your folder should look like this -A list of available settings is available in .env.example +```plain +. +├── .env +└── docker-compose.yml +``` -The services should scale properly across a swarm cluster if the volumes are properly shared between cluster members. +## Modifying your settings (`.env` file) + +* `APP_NAME` +* `APP_DOMAIN` +* `DB_PASSWORD` diff --git a/contrib/docker-compose/docker-compose.apache.yml b/contrib/docker-compose/docker-compose.yml similarity index 100% rename from contrib/docker-compose/docker-compose.apache.yml rename to contrib/docker-compose/docker-compose.yml From 052c11882c0da9ea4ddd50b6141242bba99d1e01 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 16:33:08 +0000 Subject: [PATCH 194/977] tweak 10-storage.sh --- contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index b9809e24c..6677c313f 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -4,7 +4,7 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions -run-as-runtime-user cp --recursive storage.skel/. storage/. +cp --recursive storage.skel/. storage/ # Ensure storage linkk are correctly configured -run-as-runtime-user php artisan storage:link +php artisan storage:link From c1fbccb07c6419dd809c9a273af33868b6c43a73 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 16:52:00 +0000 Subject: [PATCH 195/977] bootstrapping worked --- .dockerignore | 9 ++++++--- contrib/docker-compose/.env | 2 +- contrib/docker-compose/docker-compose.yml | 12 ++++++++++-- .../shared/root/docker/entrypoint.d/10-storage.sh | 7 +++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.dockerignore b/.dockerignore index bc5af5a21..a4f4ff035 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,9 @@ -data -docker-compose*.yml .dockerignore +.env .git .gitignore -.env +contrib/docker-compose/.env +contrib/docker-compose/config +contrib/docker-compose/data +data +docker-compose*.yml diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 076edf09b..9ad25eeff 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -41,7 +41,7 @@ APP_NAME="Pixelfed Prod" # Application domains used for routing. # # See: https://docs.pixelfed.org/technical-documentation/config/#app_domain -APP_DOMAIN="your-domain-here-dot-com" +APP_DOMAIN="pixelfed-test.jippi.dev" # This URL is used by the console to properly generate URLs when using the Artisan command line tool. # You should set this to the root of your application so that it is used when running Artisan tasks. diff --git a/contrib/docker-compose/docker-compose.yml b/contrib/docker-compose/docker-compose.yml index ff7734797..8df0777b3 100644 --- a/contrib/docker-compose/docker-compose.yml +++ b/contrib/docker-compose/docker-compose.yml @@ -4,12 +4,16 @@ version: "3" services: web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + build: + context: ../.. + dockerfile: contrib/docker/Dockerfile + target: apache-runtime restart: unless-stopped env_file: - "./.env" volumes: - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/bootstrap:/var/www/bootstrap" + - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" ports: - "${DOCKER_WEB_PORT_EXTERNAL}:80" @@ -19,13 +23,17 @@ services: worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + build: + context: ../.. + dockerfile: contrib/docker/Dockerfile + target: apache-runtime command: gosu www-data php artisan horizon restart: unless-stopped env_file: - "./.env" volumes: - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/bootstrap:/var/www/bootstrap" + - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" depends_on: - db diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index 6677c313f..f0e467973 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -3,8 +3,11 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./bootstrap/cache" +run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./storage" + # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions -cp --recursive storage.skel/. storage/ +run-as-runtime-user cp --recursive storage.skel/. ./storage/ # Ensure storage linkk are correctly configured -php artisan storage:link +run-as-runtime-user php artisan storage:link From c4404590f233866da4d3bef6b4635bc8d5baac26 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 17:29:45 +0000 Subject: [PATCH 196/977] add first time setup logic --- contrib/docker-compose/.env | 4 +- ...orage-permissions.sh => 01-permissions.sh} | 5 +++ .../root/docker/entrypoint.d/10-storage.sh | 3 -- .../entrypoint.d/11-first-time-setup.sh | 45 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) rename contrib/docker/shared/root/docker/entrypoint.d/{15-storage-permissions.sh => 01-permissions.sh} (68%) create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 9ad25eeff..f371e21ef 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -33,7 +33,7 @@ TZ="UTC" # Please do this before deploying an application! # # See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY="" +APP_KEY=base64:IvvWCuLmAAcyPBzDI+IH6OxnU9w2kTSYZrcg6F4m7Uk= # See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 APP_NAME="Pixelfed Prod" @@ -264,7 +264,7 @@ DB_HOST="db" DB_USERNAME="pixelfed" # See: https://docs.pixelfed.org/technical-documentation/config/#db_password -DB_PASSWORD="__CHANGE_ME__" +DB_PASSWORD="helloworld" # See: https://docs.pixelfed.org/technical-documentation/config/#db_database DB_DATABASE="pixelfed_prod" diff --git a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh b/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh similarity index 68% rename from contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh rename to contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 30a58c5a9..81d422ecd 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/15-storage-permissions.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -3,6 +3,11 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +# Ensure the two Docker volumes are owned by the runtime user +run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./.env" +run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./bootstrap/cache" +run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./storage" + # Optionally fix ownership of configured paths : ${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""} diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh index f0e467973..bb2f61f0a 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -3,9 +3,6 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" -run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./bootstrap/cache" -run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./storage" - # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions run-as-runtime-user cp --recursive storage.skel/. ./storage/ diff --git a/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh new file mode 100755 index 000000000..0e30e74ac --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +source /docker/helpers.sh + +entrypoint-set-script-name "$0" + +# if the script is running in another container, wait for it to complete +while [ -e "./storage/docker-first-time-is-running" ]; do + sleep 1 +done + +# We got the lock! +touch "./storage/docker-first-time-is-running" + +# Make sure to clean up on exit +trap "rm -f ./storage/docker-first-time-is-running" EXIT + +if [ ! -e "./storage/docker-storage-link-has-run" ]; then + run-as-runtime-user php artisan storage:link + touch "./storage/docker-storage-link-has-run" +fi + +if [ ! -e "./storage/docker-key-generate-has-run" ]; then + run-as-runtime-user php artisan key:generate + touch "./storage/docker-key-generate-has-run" +fi + +if [ ! -e "./storage/docker-migrate-has-run" ]; then + run-as-runtime-user php artisan migrate --force + touch "./storage/docker-migrate-has-run" +fi + +if [ ! -e "./storage/docker-import-cities-has-run" ]; then + run-as-runtime-user php artisan import:cities + touch "./storage/docker-import-cities-has-run" +fi + +# if [ ! -e "./storage/docker-instance-actor-has-run" ]; then +# run-as-runtime-user php artisan instance:actor +# touch "./storage/docker-instance-actor-has-run" +# fi + +# if [ ! -e "./storage/docker-passport-keys-has-run" ]; then +# run-as-runtime-user php artisan instance:actor +# touch "./storage/docker-passport-keys-has-run" +# fi From d8765339910f00e43a77a06a2ca9e03f4ec55b44 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 17:30:30 +0000 Subject: [PATCH 197/977] remove tmp token --- contrib/docker-compose/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index f371e21ef..7c70f3cf6 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -33,7 +33,7 @@ TZ="UTC" # Please do this before deploying an application! # # See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY=base64:IvvWCuLmAAcyPBzDI+IH6OxnU9w2kTSYZrcg6F4m7Uk= +APP_KEY= # See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 APP_NAME="Pixelfed Prod" From 2e2ffc5519c195fd319df8e76c51adffb7e43dbb Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 17:31:34 +0000 Subject: [PATCH 198/977] comment build steps out to use remote image --- contrib/docker-compose/docker-compose.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/docker-compose/docker-compose.yml b/contrib/docker-compose/docker-compose.yml index 8df0777b3..e43e36c78 100644 --- a/contrib/docker-compose/docker-compose.yml +++ b/contrib/docker-compose/docker-compose.yml @@ -4,10 +4,10 @@ version: "3" services: web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - build: - context: ../.. - dockerfile: contrib/docker/Dockerfile - target: apache-runtime + # build: + # context: ../.. + # dockerfile: contrib/docker/Dockerfile + # target: apache-runtime restart: unless-stopped env_file: - "./.env" @@ -23,10 +23,10 @@ services: worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - build: - context: ../.. - dockerfile: contrib/docker/Dockerfile - target: apache-runtime + # build: + # context: ../.. + # dockerfile: contrib/docker/Dockerfile + # target: apache-runtime command: gosu www-data php artisan horizon restart: unless-stopped env_file: From 76e1199dc784d40927ff0101e51873a91f20170a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 17:35:07 +0000 Subject: [PATCH 199/977] sync --- contrib/docker-compose/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contrib/docker-compose/README.md b/contrib/docker-compose/README.md index 6c57c43d2..137773f08 100644 --- a/contrib/docker-compose/README.md +++ b/contrib/docker-compose/README.md @@ -15,6 +15,20 @@ Your folder should look like this ## Modifying your settings (`.env` file) +Minimum required settings to change is: + * `APP_NAME` * `APP_DOMAIN` * `DB_PASSWORD` + +See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! + +You need to mainly focus on following sections + +* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) +* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) + +Since the following things are configured for you out of the box: + +* `Redis` +* `Database` (except for `DB_PASSWORD`) From 7db513b366dfaa123637ea28cf1392a35c32450d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 18:16:38 +0000 Subject: [PATCH 200/977] sync --- contrib/docker-compose/.env | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 7c70f3cf6..54f2bb10d 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -88,7 +88,7 @@ ADMIN_DOMAIN="${APP_DOMAIN}" PF_MAX_USERS="false" # See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled -# OAUTH_ENABLED="true" +OAUTH_ENABLED="true" # Defaults to "UTC". # @@ -167,13 +167,6 @@ APP_TIMEZONE="${TZ}" # See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length #MAX_ALBUM_LENGTH="4" -# Set the image optimization quality, must be a value between 1-100. -# -# Defaults to "80". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality -#IMAGE_QUALITY="80" - # Resize and optimize image uploads. # # Defaults to "true". @@ -181,6 +174,13 @@ APP_TIMEZONE="${TZ}" # See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images #PF_OPTIMIZE_IMAGES="true" +# Set the image optimization quality, must be a value between 1-100. +# +# Defaults to "80". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality +#IMAGE_QUALITY="80" + # Resize and optimize video uploads. # # Defaults to "true". @@ -487,7 +487,7 @@ CACHE_DRIVER="redis" # Defaults to "true". # # See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc -#EXP_EMC="true" +EXP_EMC="true" ############################################################### # ActivityPub confguration @@ -496,7 +496,7 @@ CACHE_DRIVER="redis" # Defaults to "false". # # See: https://docs.pixelfed.org/technical-documentation/config/#activity_pub -#ACTIVITY_PUB="false" +ACTIVITY_PUB="true" # Defaults to "true". # @@ -721,7 +721,7 @@ LOG_CHANNEL="stderr" # - "null" (default) # # See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver -#BROADCAST_DRIVER= +BROADCAST_DRIVER=redis ############################################################### # Other settings From a25b7910b2691f60ff1e8fd447ea3d7b14d9cb01 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 23:16:26 +0000 Subject: [PATCH 201/977] first time setup and more refinements --- contrib/docker-compose/.env | 2 + contrib/docker-compose/docker-compose.yml | 16 +- .../docker/entrypoint.d/01-permissions.sh | 5 +- .../root/docker/entrypoint.d/05-templating.sh | 4 +- .../entrypoint.d/11-first-time-setup.sh | 34 +-- .../root/docker/entrypoint.d/12-migrations.sh | 9 + .../root/docker/entrypoint.d/30-cache.sh | 2 +- .../docker/shared/root/docker/entrypoint.sh | 16 +- contrib/docker/shared/root/docker/helpers.sh | 278 +++++++++++++++++- .../docker/shared/root/docker/install/base.sh | 3 + 10 files changed, 309 insertions(+), 60 deletions(-) create mode 100755 contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 54f2bb10d..ee934ef7e 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -274,6 +274,8 @@ DB_DATABASE="pixelfed_prod" # See: https://docs.pixelfed.org/technical-documentation/config/#db_port DB_PORT="3306" +ENTRYPOINT_DEBUG=0 + ############################################################### # Mail configuration ############################################################### diff --git a/contrib/docker-compose/docker-compose.yml b/contrib/docker-compose/docker-compose.yml index e43e36c78..8df0777b3 100644 --- a/contrib/docker-compose/docker-compose.yml +++ b/contrib/docker-compose/docker-compose.yml @@ -4,10 +4,10 @@ version: "3" services: web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # context: ../.. - # dockerfile: contrib/docker/Dockerfile - # target: apache-runtime + build: + context: ../.. + dockerfile: contrib/docker/Dockerfile + target: apache-runtime restart: unless-stopped env_file: - "./.env" @@ -23,10 +23,10 @@ services: worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # context: ../.. - # dockerfile: contrib/docker/Dockerfile - # target: apache-runtime + build: + context: ../.. + dockerfile: contrib/docker/Dockerfile + target: apache-runtime command: gosu www-data php artisan horizon restart: unless-stopped env_file: diff --git a/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 81d422ecd..25a831531 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -3,7 +3,8 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" -# Ensure the two Docker volumes are owned by the runtime user +# Ensure the two Docker volumes and dot-env files are owned by the runtime user as other scripts +# will be writing to these run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./.env" run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./bootstrap/cache" run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./storage" @@ -22,5 +23,5 @@ fi for path in "${ensure_ownership_paths[@]}"; do log-info "Ensure ownership of [${path}] is correct" - run-as-current-user chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" + stream-prefix-command-output run-as-current-user chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" done diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh index 618a2d406..cafb9d133 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -49,7 +49,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat cat "${template_file}" | gomplate >"${output_file_path}" # Show the diff from the envsubst command - if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF} = 1 ]]; then - git --no-pager diff "${template_file}" "${output_file_path}" || : + if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:-1} = 1 ]]; then + git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : fi done diff --git a/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 0e30e74ac..1a0cbb51c 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -3,36 +3,12 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" -# if the script is running in another container, wait for it to complete -while [ -e "./storage/docker-first-time-is-running" ]; do - sleep 1 -done +await-database-ready -# We got the lock! -touch "./storage/docker-first-time-is-running" - -# Make sure to clean up on exit -trap "rm -f ./storage/docker-first-time-is-running" EXIT - -if [ ! -e "./storage/docker-storage-link-has-run" ]; then - run-as-runtime-user php artisan storage:link - touch "./storage/docker-storage-link-has-run" -fi - -if [ ! -e "./storage/docker-key-generate-has-run" ]; then - run-as-runtime-user php artisan key:generate - touch "./storage/docker-key-generate-has-run" -fi - -if [ ! -e "./storage/docker-migrate-has-run" ]; then - run-as-runtime-user php artisan migrate --force - touch "./storage/docker-migrate-has-run" -fi - -if [ ! -e "./storage/docker-import-cities-has-run" ]; then - run-as-runtime-user php artisan import:cities - touch "./storage/docker-import-cities-has-run" -fi +only-once "storage:link" run-as-runtime-user php artisan storage:link +only-once "key:generate" run-as-runtime-user php artisan key:generate +only-once "initial:migrate" run-as-runtime-user php artisan migrate --force +only-once "import:cities" run-as-runtime-user php artisan import:cities # if [ ! -e "./storage/docker-instance-actor-has-run" ]; then # run-as-runtime-user php artisan instance:actor diff --git a/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh new file mode 100755 index 000000000..2df379ac1 --- /dev/null +++ b/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -0,0 +1,9 @@ +#!/bin/bash +source /docker/helpers.sh + +entrypoint-set-script-name "$0" + +await-database-ready + +declare new_migrations=0 +run-as-runtime-user php artisan migrate:status | grep No && migrations=yes || migrations=no diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh index e933a179b..c8791e65b 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -3,6 +3,6 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +run-as-runtime-user php artisan config:cache run-as-runtime-user php artisan route:cache run-as-runtime-user php artisan view:cache -run-as-runtime-user php artisan config:cache diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/contrib/docker/shared/root/docker/entrypoint.sh index 173e4dbe3..0e8b1089c 100755 --- a/contrib/docker/shared/root/docker/entrypoint.sh +++ b/contrib/docker/shared/root/docker/entrypoint.sh @@ -31,6 +31,8 @@ if is-directory-empty "${ENTRYPOINT_ROOT}"; then exec "$@" fi +acquire-lock + # Start scanning for entrypoint.d files to source or run log-info "looking for shell scripts in [${ENTRYPOINT_ROOT}]" @@ -50,9 +52,9 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info - log-info "Sourcing [${file}]" - log-info + log-info "" + log-info "${notice_message_color}Sourcing [${file}]${color_clear}" + log-info "" source "${file}" @@ -67,9 +69,9 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info - log-info "Running [${file}]" - log-info + log-info "" + log-info "${notice_message_color}Executing [${file}]${color_clear}" + log-info "" "${file}" ;; @@ -80,6 +82,8 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; esac done +release-lock + log-info "Configuration complete; ready for start up" exec "$@" diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index 880027cf4..453af902d 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -6,14 +6,16 @@ set -e -o errexit -o nounset -o pipefail # Some splash of color for important messages declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" +declare -g notice_message_color="\033[1;34m" declare -g color_clear="\033[1;0m" # Current and previous log prefix +declare -g script_name= +declare -g script_name_previous= declare -g log_prefix= -declare -g log_prefix_previous= # dot-env files to source when reading config -declare -ra dot_env_files=( +declare -a dot_env_files=( /var/www/.env.docker /var/www/.env ) @@ -21,16 +23,24 @@ declare -ra dot_env_files=( # environment keys seen when source dot files (so we can [export] them) declare -ga seen_dot_env_variables=() +declare -g docker_state_path="$(readlink -f ./storage/docker)" +declare -g docker_locks_path="${docker_state_path}/lock" +declare -g docker_once_path="${docker_state_path}/once" + +declare -g runtime_username=$(id -un ${RUNTIME_UID}) + # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] # @arg $1 string The name (or path) of the entrypoint script being run function entrypoint-set-script-name() { - log_prefix_previous="${log_prefix}" - log_prefix="ENTRYPOINT - [$(get-entrypoint-script-name $1)] - " + script_name_previous="${script_name}" + script_name="${1}" + + log_prefix="[entrypoint / $(get-entrypoint-script-name $1)] - " } # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] function entrypoint-restore-script-name() { - log_prefix="${log_prefix_previous}" + entrypoint-set-script-name "${script_name_previous}" } # @description Run a command as the [runtime user] @@ -38,7 +48,7 @@ function entrypoint-restore-script-name() { # @exitcode 0 if the command succeeeds # @exitcode 1 if the command fails function run-as-runtime-user() { - run-command-as "$(id -un ${RUNTIME_UID})" "${@}" + run-command-as "${runtime_username}" "${@}" } # @description Run a command as the [runtime user] @@ -64,9 +74,9 @@ function run-command-as() { log-info-stderr "👷 Running [${*}] as [${target_user}]" if [[ ${target_user} != "root" ]]; then - su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" + stream-prefix-command-output su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" else - "${@}" + stream-prefix-command-output "${@}" fi exit_code=$? @@ -80,11 +90,62 @@ function run-command-as() { return $exit_code } +# @description Streams stdout from the command and echo it +# with log prefixing. +# @see stream-prefix-command-output +function stream-stdout-handler() { + local prefix="${1:-}" + + while read line; do + log-info "(stdout) ${line}" + done +} + +# @description Streams stderr from the command and echo it +# with a bit of color and log prefixing. +# @see stream-prefix-command-output +function stream-stderr-handler() { + while read line; do + log-info-stderr "(${error_message_color}stderr${color_clear}) ${line}" + done +} + +# @description Steam stdout and stderr from a command with log prefix +# and stdout/stderr prefix. If stdout or stderr is being piped/redirected +# it will automatically fall back to non-prefixed output. +# @arg $@ string The command to run +function stream-prefix-command-output() { + local stdout=stream-stdout-handler + local stderr=stream-stderr-handler + + # if stdout is being piped, print it like normal with echo + if [ ! -t 1 ]; then + stdout= echo >&1 -ne + fi + + # if stderr is being piped, print it like normal with echo + if [ ! -t 2 ]; then + stderr= echo >&2 -ne + fi + + "$@" > >($stdout) 2> >($stderr) +} + # @description Print the given error message to stderr # @arg $message string A error message. # @stderr The error message provided with log prefix function log-error() { - echo -e "${error_message_color}${log_prefix}ERROR - ${*}${color_clear}" >/dev/stderr + local msg + + if [[ $# -gt 0 ]]; then + msg="$@" + elif [[ ! -t 0 ]]; then + read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${error_message_color}${log_prefix}ERROR - ${msg}${color_clear}" >/dev/stderr } # @description Print the given error message to stderr and exit 1 @@ -94,6 +155,8 @@ function log-error() { function log-error-and-exit() { log-error "$@" + show-call-stack + exit 1 } @@ -101,15 +164,35 @@ function log-error-and-exit() { # @arg $@ string A warning message. # @stderr The warning message provided with log prefix function log-warning() { - echo -e "${warn_message_color}${log_prefix}WARNING - ${*}${color_clear}" >/dev/stderr + local msg + + if [[ $# -gt 0 ]]; then + msg="$@" + elif [[ ! -t 0 ]]; then + read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + fi + + echo -e "${warn_message_color}${log_prefix}WARNING - ${msg}${color_clear}" >/dev/stderr } # @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set # @arg $@ string A info message. # @stdout The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS function log-info() { + local msg + + if [[ $# -gt 0 ]]; then + msg="$@" + elif [[ ! -t 0 ]]; then + read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + fi + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "${log_prefix}$*" + echo -e "${log_prefix}${msg}" fi } @@ -117,8 +200,18 @@ function log-info() { # @arg $@ string A info message. # @stderr The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS function log-info-stderr() { + local msg + + if [[ $# -gt 0 ]]; then + msg="$@" + elif [[ ! -t 0 ]]; then + read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + else + log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + fi + if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "${log_prefix}$*" + echo -e "${log_prefix}$msg" >/dev/stderr fi } @@ -196,3 +289,164 @@ function ensure-directory-exists() { function get-entrypoint-script-name() { echo "${1#"$ENTRYPOINT_ROOT"}" } + +# @description Ensure a command is only run once (via a 'lock' file) in the storage directory. +# The 'lock' is only written if the passed in command ($2) successfully ran. +# @arg $1 string The name of the lock file +# @arg $@ string The command to run +function only-once() { + local name="${1:-$script_name}" + local file="${docker_once_path}/${name}" + shift + + if [[ -e "${file}" ]]; then + log-info "Command [${*}] has already run once before (remove file [${file}] to run it again)" + + return 0 + fi + + ensure-directory-exists "$(dirname "${file}")" + + if ! "$@"; then + return 1 + fi + + touch "${file}" + return 0 +} + +# @description Best effort file lock to ensure *something* is not running in multiple containers. +# The script uses "trap" to clean up after itself if the script crashes +# @arg $1 string The lock identifier +function acquire-lock() { + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + + ensure-directory-exists "$(dirname "${file}")" + + log-info "🔑 Trying to acquire lock: ${file}: " + while [[ -e "${file}" ]]; do + log-info "🔒 Waiting on lock ${file}" + + staggered-sleep + done + + touch "${file}" + + log-info "🔐 Lock acquired [${file}]" + + on-trap "release-lock ${name}" EXIT INT QUIT TERM +} + +# @description Release a lock aquired by [acquire-lock] +# @arg $1 string The lock identifier +function release-lock() { + local name="${1:-$script_name}" + local file="${docker_locks_path}/${name}" + + log-info "🔓 Releasing lock [${file}]" + + rm -f "${file}" +} + +# @description Helper function to append multiple actions onto +# the bash [trap] logic +# @arg $1 string The command to run +# @arg $@ string The list of trap signals to register +function on-trap() { + local trap_add_cmd=$1 + shift || log-error-and-exit "${FUNCNAME} usage error" + + for trap_add_name in "$@"; do + trap -- "$( + # helper fn to get existing trap command from output + # of trap -p + extract_trap_cmd() { printf '%s\n' "${3:-}"; } + # print existing trap command with newline + eval "extract_trap_cmd $(trap -p "${trap_add_name}")" + # print the new trap command + printf '%s\n' "${trap_add_cmd}" + )" "${trap_add_name}" || + log-error-and-exit "unable to add to trap ${trap_add_name}" + done +} + +# Set the trace attribute for the above function. +# +# This is required to modify DEBUG or RETURN traps because functions don't +# inherit them unless the trace attribute is set +declare -f -t on-trap + +# @description Waits for the database to be healthy and responsive +function await-database-ready() { + log-info "❓ Waiting for database to be ready" + + case "${DB_CONNECTION:-}" in + mysql) + while ! echo "SELECT 1" | mysql --user="$DB_USERNAME" --password="$DB_PASSWORD" --host="$DB_HOST" "$DB_DATABASE" --silent >/dev/null; do + staggered-sleep + done + ;; + + pgsql) + while ! echo "SELECT 1" | psql --user="$DB_USERNAME" --password="$DB_PASSWORD" --host="$DB_HOST" "$DB_DATABASE" >/dev/null; do + staggered-sleep + done + ;; + + sqlsrv) + log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" + + while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do + staggered-sleep + done + ;; + + sqlite) + log-info "sqlite are always ready" + ;; + + *) + log-error-and-exit "Unknown database type: [${DB_CONNECT}]" + ;; + esac + + log-info "✅ Successfully connected to database" +} + +# @description sleeps between 1 and 3 seconds to ensure a bit of randomness +# in multiple scripts/containers doing work almost at the same time. +function staggered-sleep() { + sleep $(get-random-number-between 1 3) +} + +# @description Helper function to get a random number between $1 and $2 +# @arg $1 int Minimum number in the range (inclusive) +# @arg $2 int Maximum number in the range (inclusive) +function get-random-number-between() { + local -i from=${1:-1} + local -i to="${2:-10}" + + shuf -i "${from}-${to}" -n 1 +} + +# @description Helper function to show the bask call stack when something +# goes wrong. Is super useful when needing to debug an issue +function show-call-stack() { + local stack_size=${#FUNCNAME[@]} + local func + local lineno + local src + + # to avoid noise we start with 1 to skip the get_stack function + for ((i = 1; i < $stack_size; i++)); do + func="${FUNCNAME[$i]}" + [ x$func = x ] && func=MAIN + + lineno="${BASH_LINENO[$((i - 1))]}" + src="${BASH_SOURCE[$i]}" + [ x"$src" = x ] && src=non_file_source + + log-error " at: ${func} ${src}:${lineno}" + done +} diff --git a/contrib/docker/shared/root/docker/install/base.sh b/contrib/docker/shared/root/docker/install/base.sh index b9b37b031..d3da207e5 100755 --- a/contrib/docker/shared/root/docker/install/base.sh +++ b/contrib/docker/shared/root/docker/install/base.sh @@ -23,6 +23,7 @@ declare -ra standardPackages=( libzip-dev locales locales-all + moreutils nano procps software-properties-common @@ -63,6 +64,8 @@ declare -ra videoProcessing=( declare -ra databaseDependencies=( libpq-dev libsqlite3-dev + mariadb-client + postgresql-client ) apt-get update From a8c5585e19f39567a7565aceed466d71b7e0482b Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 23:41:33 +0000 Subject: [PATCH 202/977] use upstream Docker images over self-built --- contrib/docker-compose/docker-compose.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/docker-compose/docker-compose.yml b/contrib/docker-compose/docker-compose.yml index 8df0777b3..e43e36c78 100644 --- a/contrib/docker-compose/docker-compose.yml +++ b/contrib/docker-compose/docker-compose.yml @@ -4,10 +4,10 @@ version: "3" services: web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - build: - context: ../.. - dockerfile: contrib/docker/Dockerfile - target: apache-runtime + # build: + # context: ../.. + # dockerfile: contrib/docker/Dockerfile + # target: apache-runtime restart: unless-stopped env_file: - "./.env" @@ -23,10 +23,10 @@ services: worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - build: - context: ../.. - dockerfile: contrib/docker/Dockerfile - target: apache-runtime + # build: + # context: ../.. + # dockerfile: contrib/docker/Dockerfile + # target: apache-runtime command: gosu www-data php artisan horizon restart: unless-stopped env_file: From 6edf266a143ea15cf90239c147bba21d47fd06b1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 5 Jan 2024 23:54:17 +0000 Subject: [PATCH 203/977] quick take on applying migrations automatically --- contrib/docker-compose/.env | 3 +++ .../root/docker/entrypoint.d/12-migrations.sh | 26 +++++++++++++++++-- contrib/docker/shared/root/docker/helpers.sh | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index ee934ef7e..c9e235533 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -19,6 +19,9 @@ DOCKER_TAG="branch-jippi-fork-apache-8.1" # See: https://www.php.net/manual/en/timezones.php TZ="UTC" +# Automatically run [artisan migrate --force] if new migrations are detected +DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" + ############################################################### # Pixelfed application configuration ############################################################### diff --git a/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh index 2df379ac1..68008c596 100755 --- a/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -3,7 +3,29 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +# Allow automatic applying of outstanding/new migrations on startup +: ${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0} + +if [[ $DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY -eq 0 ]]; then + log-info "Automatic applying of new database migrations is disabled" + log-info "Please set [DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." + + exit 0 +fi + +# Wait for the database to be ready await-database-ready -declare new_migrations=0 -run-as-runtime-user php artisan migrate:status | grep No && migrations=yes || migrations=no +# Detect if we have new migrations +declare -i new_migrations=0 +run-as-runtime-user php artisan migrate:status | grep No && new_migrations=1 + +if [[ $new_migrations -eq 0 ]]; then + log-info "No outstanding migrations detected" + + exit 0 +fi + +log-warning "New migrations available, will automatically apply them now" + +run-as-runtime-user php artisan migrate --force diff --git a/contrib/docker/shared/root/docker/helpers.sh b/contrib/docker/shared/root/docker/helpers.sh index 453af902d..4217b56a6 100644 --- a/contrib/docker/shared/root/docker/helpers.sh +++ b/contrib/docker/shared/root/docker/helpers.sh @@ -29,6 +29,9 @@ declare -g docker_once_path="${docker_state_path}/once" declare -g runtime_username=$(id -un ${RUNTIME_UID}) +# We should already be in /var/www, but just to be explicit +cd /var/www || log-error-and-exit "could not change to /var/www" + # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] # @arg $1 string The name (or path) of the entrypoint script being run function entrypoint-set-script-name() { From 092f7f704cd2881b27bcab6cd0864044f45d37ca Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 00:01:51 +0000 Subject: [PATCH 204/977] fix nginx? --- contrib/docker-compose/.env | 2 +- .../root/docker/templates/{ => etc/nginx}/conf.d/default.conf | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename contrib/docker/nginx/root/docker/templates/{ => etc/nginx}/conf.d/default.conf (100%) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index c9e235533..0fce42611 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -36,7 +36,7 @@ DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" # Please do this before deploying an application! # # See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY= +APP_KEY=base64:jL9zmd7/HBCssyNf9HpbuyqQB/KN/8Ew7A7GkogF1uc= # See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 APP_NAME="Pixelfed Prod" diff --git a/contrib/docker/nginx/root/docker/templates/conf.d/default.conf b/contrib/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf similarity index 100% rename from contrib/docker/nginx/root/docker/templates/conf.d/default.conf rename to contrib/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf From c9b11a4a29359c99b97e1a15db1c1e64009e2f83 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 14:13:16 +0000 Subject: [PATCH 205/977] remove testing key --- contrib/docker-compose/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env index 0fce42611..c9e235533 100644 --- a/contrib/docker-compose/.env +++ b/contrib/docker-compose/.env @@ -36,7 +36,7 @@ DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" # Please do this before deploying an application! # # See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY=base64:jL9zmd7/HBCssyNf9HpbuyqQB/KN/8Ew7A7GkogF1uc= +APP_KEY= # See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 APP_NAME="Pixelfed Prod" From e228a1622dd6b50480d8b8ca4ed5c6bb980bb3b3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 14:19:36 +0000 Subject: [PATCH 206/977] refactor layout --- .env.docker | 992 ++++++++++++++++-- .github/workflows/docker.yml | 2 +- contrib/docker/Dockerfile => Dockerfile | 14 +- contrib/docker-compose/.env | 921 ---------------- contrib/docker-compose/README.md | 34 - contrib/docker-compose/docker-compose.yml | 67 -- docker-compose.yml | 93 +- {contrib/docker => docker}/README.md | 35 +- .../etc/apache2/conf-available/remoteip.conf | 0 {contrib/docker => docker}/fpm/root/.gitkeep | 0 {contrib/docker => docker}/nginx/Procfile | 0 .../templates/etc/nginx/conf.d/default.conf | 0 .../docker/entrypoint.d/01-permissions.sh | 0 .../docker/entrypoint.d/04-defaults.envsh | 0 .../root/docker/entrypoint.d/05-templating.sh | 0 .../root/docker/entrypoint.d/10-storage.sh | 0 .../entrypoint.d/11-first-time-setup.sh | 0 .../root/docker/entrypoint.d/12-migrations.sh | 0 .../root/docker/entrypoint.d/20-horizon.sh | 0 .../root/docker/entrypoint.d/30-cache.sh | 0 .../shared/root/docker/entrypoint.sh | 0 .../shared/root/docker/helpers.sh | 0 .../shared/root/docker/install/base.sh | 0 .../root/docker/install/php-extensions.sh | 0 .../templates/usr/local/etc/php/php.ini | 0 25 files changed, 959 insertions(+), 1199 deletions(-) rename contrib/docker/Dockerfile => Dockerfile (96%) delete mode 100644 contrib/docker-compose/.env delete mode 100644 contrib/docker-compose/README.md delete mode 100644 contrib/docker-compose/docker-compose.yml rename {contrib/docker => docker}/README.md (91%) rename {contrib/docker => docker}/apache/root/etc/apache2/conf-available/remoteip.conf (100%) rename {contrib/docker => docker}/fpm/root/.gitkeep (100%) rename {contrib/docker => docker}/nginx/Procfile (100%) rename {contrib/docker => docker}/nginx/root/docker/templates/etc/nginx/conf.d/default.conf (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/01-permissions.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/04-defaults.envsh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/05-templating.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/10-storage.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/11-first-time-setup.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/12-migrations.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/20-horizon.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.d/30-cache.sh (100%) rename {contrib/docker => docker}/shared/root/docker/entrypoint.sh (100%) rename {contrib/docker => docker}/shared/root/docker/helpers.sh (100%) rename {contrib/docker => docker}/shared/root/docker/install/base.sh (100%) rename {contrib/docker => docker}/shared/root/docker/install/php-extensions.sh (100%) rename {contrib/docker => docker}/shared/root/docker/templates/usr/local/etc/php/php.ini (100%) diff --git a/.env.docker b/.env.docker index ce4cfe87c..10e88b8b5 100644 --- a/.env.docker +++ b/.env.docker @@ -1,149 +1,921 @@ -## Crypto +# -*- mode: bash -*- +# vi: ft=bash + +############################################################### +# Docker-wide configuration +############################################################### + +# Path (relative) to the docker-compose file where containers will store their data +DOCKER_DATA_ROOT="./data" + +# Path (relative) to the docker-compose file where containers will store their config +DOCKER_CONFIG_ROOT="./config" + +# Pixelfed version (image tag) to pull from the registry +DOCKER_TAG="branch-jippi-fork-apache-8.1" + +# Set timezone used by *all* containers - these should be in sync +# +# See: https://www.php.net/manual/en/timezones.php +TZ="UTC" + +# Automatically run [artisan migrate --force] if new migrations are detected +DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" + +############################################################### +# Pixelfed application configuration +############################################################### + +# A random 32-character string to be used as an encryption key. +# +# No default value; use [php artisan key:generate] to generate. +# +# This key is used by the Illuminate encrypter service and should be set to a random, +# 32 character string, otherwise these encrypted strings will not be safe. +# +# Please do this before deploying an application! +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_key APP_KEY= -## General Settings +# See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 APP_NAME="Pixelfed Prod" -APP_ENV=production -APP_DEBUG=false -APP_URL=https://real.domain -APP_DOMAIN="real.domain" -ADMIN_DOMAIN="real.domain" -SESSION_DOMAIN="real.domain" -OPEN_REGISTRATION=true -ENFORCE_EMAIL_VERIFICATION=false -PF_MAX_USERS=1000 -OAUTH_ENABLED=true +# Application domains used for routing. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_domain +APP_DOMAIN="__CHANGE_ME__" -APP_TIMEZONE=UTC -APP_LOCALE=en +# This URL is used by the console to properly generate URLs when using the Artisan command line tool. +# You should set this to the root of your application so that it is used when running Artisan tasks. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_url +APP_URL=https://${APP_DOMAIN} -## Pixelfed Tweaks -LIMIT_ACCOUNT_SIZE=true -MAX_ACCOUNT_SIZE=1000000 -MAX_PHOTO_SIZE=15000 -MAX_AVATAR_SIZE=2000 -MAX_CAPTION_LENGTH=500 -MAX_BIO_LENGTH=125 -MAX_NAME_LENGTH=30 -MAX_ALBUM_LENGTH=4 -IMAGE_QUALITY=80 -PF_OPTIMIZE_IMAGES=true -PF_OPTIMIZE_VIDEOS=true -ADMIN_ENV_EDITOR=false -ACCOUNT_DELETION=true -ACCOUNT_DELETE_AFTER=false -MAX_LINKS_PER_POST=0 +# Application domains used for routing. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#admin_domain +ADMIN_DOMAIN="${APP_DOMAIN}" -## Instance +# This value determines the “environment” your application is currently running in. +# This may determine how you prefer to configure various services your application utilizes. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_env +#APP_ENV="production" + +# When your application is in debug mode, detailed error messages with stack traces will +# be shown on every error that occurs within your application. +# +# If disabled, a simple generic error page is shown. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_debug +#APP_DEBUG="false" + +# Enable/disable new local account registrations. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#open_registration +#OPEN_REGISTRATION=true + +# Require email verification before a new user can do anything. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +#ENFORCE_EMAIL_VERIFICATION="true" + +# Allow a maximum number of user accounts. +# +# Defaults to "1000". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +PF_MAX_USERS="false" + +# See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled +OAUTH_ENABLED="true" + +# Defaults to "UTC". +# +# Do not edit your timezone or things will break! +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_timezone +# See: https://www.php.net/manual/en/timezones.php +APP_TIMEZONE="${TZ}" + +# The application locale determines the default locale that will be used by the translation service provider. +# You are free to set this value to any of the locales which will be supported by the application. +# +# Defaults to "en". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_locale +#APP_LOCALE="en" + +# The fallback locale determines the locale to use when the current one is not available. +# +# You may change the value to correspond to any of the language folders that are provided through your application. +# +# Defaults to "en". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +#APP_FALLBACK_LOCALE="en" + +# See: https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +#LIMIT_ACCOUNT_SIZE="true" + +# Update the max account size, the per user limit of files in kB. +# +# Defaults to "1000000" (1GB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +#MAX_ACCOUNT_SIZE="1000000" + +# Update the max photo size, in kB. +# +# Defaults to "15000" (15MB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +#MAX_PHOTO_SIZE="15000" + +# Update the max avatar size, in kB. +# +# Defaults to "2000" (2MB). +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +#MAX_AVATAR_SIZE="2000" + +# Change the caption length limit for new local posts. +# +# Defaults to "500". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +#MAX_CAPTION_LENGTH="500" + +# Change the bio length limit for user profiles. +# +# Defaults to "125". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +#MAX_BIO_LENGTH="125" + +# Change the length limit for user names. +# +# Defaults to "30". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_name_length +#MAX_NAME_LENGTH="30" + +# The max number of photos allowed per post. +# +# Defaults to "4". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length +#MAX_ALBUM_LENGTH="4" + +# Resize and optimize image uploads. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +#PF_OPTIMIZE_IMAGES="true" + +# Set the image optimization quality, must be a value between 1-100. +# +# Defaults to "80". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality +#IMAGE_QUALITY="80" + +# Resize and optimize video uploads. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +#PF_OPTIMIZE_VIDEOS="true" + +# Enable account deletion. +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#account_deletion +#ACCOUNT_DELETION="true" + +# Set account deletion queue after X days, set to false to delete accounts immediately. +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +#ACCOUNT_DELETE_AFTER="false" + +# Defaults to "Pixelfed - Photo sharing for everyone". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_description #INSTANCE_DESCRIPTION= -INSTANCE_PUBLIC_HASHTAGS=false -#INSTANCE_CONTACT_EMAIL= -INSTANCE_PUBLIC_LOCAL_TIMELINE=false + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +#INSTANCE_PUBLIC_HASHTAGS="false" + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +#INSTANCE_PUBLIC_LOCAL_TIMELINE="false" + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#banned_usernames #BANNED_USERNAMES= -STORIES_ENABLED=false -RESTRICTED_INSTANCE=false -## Mail -MAIL_DRIVER=log -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_FROM_ADDRESS="pixelfed@example.com" -MAIL_FROM_NAME="Pixelfed" -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +#STORIES_ENABLED="false" -## Databases (MySQL) -DB_CONNECTION=mysql -DB_DATABASE=pixelfed_prod -DB_HOST=db -DB_PASSWORD=pixelfed_db_pass -DB_PORT=3306 -DB_USERNAME=pixelfed -# pass the same values to the db itself -MYSQL_DATABASE=pixelfed_prod -MYSQL_PASSWORD=pixelfed_db_pass -MYSQL_RANDOM_ROOT_PASSWORD=true -MYSQL_USER=pixelfed +# Defaults to "false". +# +# Level is hardcoded to 1. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +#RESTRICTED_INSTANCE="false" -## Databases (Postgres) -#DB_CONNECTION=pgsql -#DB_HOST=postgres -#DB_PORT=5432 -#DB_DATABASE=pixelfed -#DB_USERNAME=postgres -#DB_PASSWORD=postgres +############################################################### +# Database configuration +############################################################### -## Cache (Redis) -REDIS_CLIENT=phpredis -REDIS_SCHEME=tcp -REDIS_HOST=redis -REDIS_PASSWORD=redis_password -REDIS_PORT=6379 -REDIS_DATABASE=0 +# Here you may specify which of the database connections below you wish to use as your default connection for all database work. +# +# Of course you may use many connections at once using the database library. +# +# Possible values: +# +# - "sqlite" +# - "mysql" (default) +# - "pgsql" +# - "sqlsrv" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_connection +DB_CONNECTION="mysql" -HORIZON_PREFIX="horizon-" +# See: https://docs.pixelfed.org/technical-documentation/config/#db_host +DB_HOST="db" -## EXPERIMENTS -EXP_LC=false -EXP_REC=false -EXP_LOOPS=false +# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +DB_USERNAME="pixelfed" -## ActivityPub Federation -ACTIVITY_PUB=false -AP_REMOTE_FOLLOW=false -AP_SHAREDINBOX=false -AP_INBOX=false -AP_OUTBOX=false -ATOM_FEEDS=true -NODEINFO=true -WEBFINGER=true +# See: https://docs.pixelfed.org/technical-documentation/config/#db_password +DB_PASSWORD="helloworld" -## S3 -FILESYSTEM_CLOUD=s3 -PF_ENABLE_CLOUD=false +# See: https://docs.pixelfed.org/technical-documentation/config/#db_database +DB_DATABASE="pixelfed_prod" + +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_port +DB_PORT="3306" + +ENTRYPOINT_DEBUG=0 + +############################################################### +# Mail configuration +############################################################### + +# Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. +# You may specify which one you’re using throughout your application here. +# +# Possible values: +# +# "smtp" (default) +# "sendmail" +# "mailgun" +# "mandrill" +# "ses" +# "sparkpost" +# "log" +# "array" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_driver +#MAIL_DRIVER="smtp" + +# The host address of the SMTP server used by your applications. +# +# A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. +# +# Defaults to "smtp.mailgun.org". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_host +#MAIL_HOST="smtp.mailgun.org" + +# This is the SMTP port used by your application to deliver e-mails to users of the application. +# +# Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. +# +# Defaults to 587. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_port +#MAIL_PORT="587" + +# You may wish for all e-mails sent by your application to be sent from the same address. +# +# Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. +# +# Defaults to "hello@example.com". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}" + +# Defaults to "Example". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" + +# If your SMTP server requires a username for authentication, you should set it here. +# +# This will get used to authenticate with your server on connection. +# You may also set the “password” value below this one. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_username +#MAIL_USERNAME= + +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_password +#MAIL_PASSWORD= + +# Here you may specify the encryption protocol that should be used when the application send e-mail messages. +# +# A sensible default using the transport layer security protocol should provide great security. +# +# Defaults to "tls". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +#MAIL_ENCRYPTION="tls" + +############################################################### +# Redis configuration +############################################################### + +# Defaults to "phpredis". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +#REDIS_CLIENT="phpredis" + +# Defaults to "tcp". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +#REDIS_SCHEME="tcp" + +# Defaults to "localhost". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_host +REDIS_HOST="redis" + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_password +#REDIS_PASSWORD= + +# Defaults to 6379. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_port +REDIS_PORT="6379" + +# Defaults to 0. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_database +#REDIS_DATABASE="0" + +############################################################### +# Cache settings +############################################################### + +# This option controls the default cache connection that gets used while using this caching library. +# +# This connection is used when another is not explicitly specified when executing a given caching function. +# +# Possible values: +# - "apc" +# - "array" +# - "database" +# - "file" (default) +# - "memcached" +# - "redis" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cache_driver +CACHE_DRIVER="redis" + +# Defaults to ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cache_prefix +# CACHE_PREFIX="{APP_NAME}_cache" + +############################################################### +# Horizon settings +############################################################### + +# 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. +# +# Defaults to "horizon-". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix +#HORIZON_PREFIX="horizon-" + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode +#HORIZON_DARKMODE="false" + +# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker +# may consume before it is terminated and restarted. +# +# You should set this value according to the resources available to your server. +# +# Defaults to "64". +#HORIZON_MEMORY_LIMIT="64" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy +#HORIZON_BALANCE_STRATEGY="auto" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes +#HORIZON_MIN_PROCESSES="1" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes +#HORIZON_MAX_PROCESSES="20" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory +#HORIZON_SUPERVISOR_MEMORY="64" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries +#HORIZON_SUPERVISOR_TRIES="3" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice +#HORIZON_SUPERVISOR_NICE="0" + +# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout +#HORIZON_SUPERVISOR_TIMEOUT="300" + +############################################################### +# Experiments +############################################################### + +# Text only posts (alpha). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_top +#EXP_TOP="false" + +# Poll statuses (alpha). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_polls +#EXP_POLLS="false" + +# Cached public timeline for larger instances (beta). +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +#EXP_CPT="false" + +# Enforce Mastoapi Compatibility (alpha). +# +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc +EXP_EMC="true" + +############################################################### +# ActivityPub confguration +############################################################### + +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#activity_pub +ACTIVITY_PUB="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +#AP_REMOTE_FOLLOW="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +#AP_SHAREDINBOX="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +#AP_INBOX="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +#AP_OUTBOX="true" + +############################################################### +# Federation confguration +############################################################### + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +#ATOM_FEEDS="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +#NODEINFO="true" + +# Defaults to "true". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#webfinger +#WEBFINGER="true" + +############################################################### +# Storage (cloud) +############################################################### + +# Store media on object storage like S3, Digital Ocean Spaces, Rackspace +# +# Defaults to "false". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +#PF_ENABLE_CLOUD="false" + +# Many applications store files both locally and in the cloud. +# +# For this reason, you may specify a default “cloud” driver here. +# This driver will be bound as the Cloud disk implementation in the container. +# +# Defaults to "s3". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +#FILESYSTEM_CLOUD="s3" + +# Defaults to true. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +#MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" + +############################################################### +# Storage (cloud) - S3 +############################################################### + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id #AWS_ACCESS_KEY_ID= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key #AWS_SECRET_ACCESS_KEY= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_default_region #AWS_DEFAULT_REGION= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_bucket #AWS_BUCKET= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_url #AWS_URL= + +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint #AWS_ENDPOINT= -#AWS_USE_PATH_STYLE_ENDPOINT=false -## Horizon -HORIZON_DARKMODE=false +# See: https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +#AWS_USE_PATH_STYLE_ENDPOINT="false" -## COSTAR - Confirm Object Sentiment Transform and Reduce -PF_COSTAR_ENABLED=false +############################################################### +# COSTAR - Confirm Object Sentiment Transform and Reduce +############################################################### +# Comma-separated list of domains to block. +# +# Defaults to null +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +#CS_BLOCKED_DOMAINS= + +# Comma-separated list of domains to add warnings. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +#CS_CW_DOMAINS= + +# Comma-separated list of domains to remove from public timelines. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +#CS_UNLISTED_DOMAINS= + +# Comma-separated list of keywords to block. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +#CS_BLOCKED_KEYWORDS= + +# Comma-separated list of keywords to add warnings. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +#CS_CW_KEYWORDS= + +# Comma-separated list of keywords to remove from public timelines. +# +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +#CS_UNLISTED_KEYWORDS= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +#CS_BLOCKED_ACTOR= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +#CS_CW_ACTOR= + +# Defaults to null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +#CS_UNLISTED_ACTOR= + +############################################################### # Media -MEDIA_EXIF_DATABASE=false +############################################################### -## Logging -LOG_CHANNEL=stderr +# Defaults to false. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#media_exif_database +MEDIA_EXIF_DATABASE="true" -## Image -IMAGE_DRIVER=imagick +# Pixelfed supports GD or ImageMagick to process images. +# +# Defaults to "gd". +# +# Possible values: +# - "gd" (default) +# - "imagick" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#image_driver +#IMAGE_DRIVER="gd" -## Broadcasting: log driver for local development -BROADCAST_DRIVER=log +############################################################### +# Logging +############################################################### -## Cache -CACHE_DRIVER=redis +# Possible values: +# +# - "stack" (default) +# - "single" +# - "daily" +# - "slack" +# - "stderr" +# - "syslog" +# - "errorlog" +# - "null" +# - "emergency" +# - "media" +LOG_CHANNEL="stderr" -## Purify -RESTRICT_HTML_TYPES=true +# Used by single, stderr and syslog. +# +# Defaults to "debug" for all of those. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_level +#LOG_LEVEL="debug" -## Queue -QUEUE_DRIVER=redis +# Used by stderr. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +#LOG_STDERR_FORMATTER= -## Session -SESSION_DRIVER=redis +# Used by slack. +# +# Defaults to "". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +#LOG_SLACK_WEBHOOK_URL= -## Trusted Proxy +############################################################### +# Broadcasting settings +############################################################### + +# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. +# +# Possible values: +# - "pusher" +# - "redis" +# - "log" +# - "null" (default) +# +# See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver +BROADCAST_DRIVER=redis + +############################################################### +# Other settings +############################################################### + +# Defaults to true. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types +#RESTRICT_HTML_TYPES="true" + +############################################################### +# Queue configuration +############################################################### + +# Possible values: +# - "sync" (default) +# - "database" +# - "beanstalkd" +# - "sqs" +# - "redis" +# - "null" +# +# See: https://docs.pixelfed.org/technical-documentation/config/#queue_driver +QUEUE_DRIVER="redis" + +############################################################### +# Queue (SQS) configuration +############################################################### + +# Defaults to "your-public-key". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_key +#SQS_KEY="your-public-key" + +# Defaults to "your-secret-key". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +#SQS_SECRET="your-secret-key" + +# Defaults to "https://sqs.us-east-1.amazonaws.com/your-account-id". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +#SQS_PREFIX= + +# Defaults to "your-queue-name". +# +# https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +#SQS_QUEUE="your-queue-name" + +# Defaults to "us-east-1". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_region +#SQS_REGION="us-east-1" + +############################################################### +# Session configuration +############################################################### + +# This option controls the default session “driver” that will be used on requests. +# +# By default, we will use the lightweight native driver but you may specify any of the other wonderful drivers provided here. +# +# Possible values: +# - "file" +# - "cookie" +# - "database" (default) +# - "apc" +# - "memcached" +# - "redis" +# - "array" +SESSION_DRIVER="redis" + +# Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. +# +# If you want them to immediately expire on the browser closing, set that option. +# +# Defaults to 86400. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +#SESSION_LIFETIME="86400" + +# Here you may change the domain of the cookie used to identify a session in your application. +# +# This will determine which domains the cookie is available to in your application. +# +# A sensible default has been set. +# +# Defaults to the value of APP_DOMAIN, or null. +# +# See: https://docs.pixelfed.org/technical-documentation/config/#session_domain +#SESSION_DOMAIN="${APP_DOMAIN}" + +############################################################### +# Proxy configuration +############################################################### + +# Set trusted proxy IP addresses. +# +# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. +# +# The “*” character is syntactic sugar within TrustedProxy to trust any +# proxy that connects directly to your server, a requirement when you cannot +# know the address of your proxy (e.g. if using Rackspace balancers). +# +# The “**” character is syntactic sugar within TrustedProxy to trust not just any +# proxy that connects directly to your server, but also proxies that connect to those proxies, +# and all the way back until you reach the original source IP. It will mean that +# $request->getClientIp() always gets the originating client IP, no matter how many proxies +# that client’s request has subsequently passed through. +# +# Defaults to "*". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#trust_proxies TRUST_PROXIES="*" -## Passport +############################################################### +# Passport configuration +############################################################### +# +# Passport uses encryption keys while generating secure access tokens +# for your application. +# +# By default, the keys are stored as local files but can be set via environment +# variables when that is more convenient. + +# See: https://docs.pixelfed.org/technical-documentation/config/#passport_private_key #PASSPORT_PRIVATE_KEY= + +# See: https://docs.pixelfed.org/technical-documentation/config/#passport_public_key #PASSPORT_PUBLIC_KEY= + +############################################################### +# PHP configuration +############################################################### + +# See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit +#PHP_MEMORY_LIMIT="128M" + +############################################################### +# MySQL DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Environment Variables" at https://hub.docker.com/_/mysql + +MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" +MYSQL_USER="${DB_USERNAME}" +MYSQL_PASSWORD="${DB_PASSWORD}" +MYSQL_DATABASE="${DB_DATABASE}" + +############################################################### +# MySQL (MariaDB) DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb + +MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" +MARIADB_USER="${DB_USERNAME}" +MARIADB_PASSWORD="${DB_PASSWORD}" +MARIADB_DATABASE="${DB_DATABASE}" + +############################################################### +# PostgreSQL DB container configuration (DO NOT CHANGE) +############################################################### +# +# See "Environment Variables" at https://hub.docker.com/_/postgres + +POSTGRES_USER="${DB_USERNAME}" +POSTGRES_PASSWORD="${DB_PASSWORD}" +POSTGRES_DB="${DB_DATABASE}" + +############################################################### +# Docker Specific configuration +############################################################### + +# Image to pull the Pixelfed Docker images from +# +# Possible values: +# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# - "pixelfed/pixelfed" to pull from DockerHub +# +DOCKER_IMAGE="ghcr.io/jippi/pixelfed" + +# Port that Redis will listen on *outside* the container (e.g. the host machine) +DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" + +# Port that the database will listen on *outside* the container (e.g. the host machine) +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" + +# Port that the web will listen on *outside* the container (e.g. the host machine) +DOCKER_WEB_PORT_EXTERNAL="8080" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b71c08c87..f53559b37 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -160,7 +160,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: contrib/docker/Dockerfile + file: Dockerfile target: ${{ matrix.target_runtime }}-runtime platforms: linux/amd64,linux/arm64 builder: ${{ steps.buildx.outputs.name }} diff --git a/contrib/docker/Dockerfile b/Dockerfile similarity index 96% rename from contrib/docker/Dockerfile rename to Dockerfile index 8ef6a99d4..116631f60 100644 --- a/contrib/docker/Dockerfile +++ b/Dockerfile @@ -114,7 +114,7 @@ WORKDIR /var/www/ ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} # Install and configure base layer -COPY contrib/docker/shared/root/docker/install/base.sh /docker/install/base.sh +COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/base.sh @@ -140,7 +140,7 @@ ENV PHP_EXTENSIONS=${PHP_EXTENSIONS} ENV PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} -COPY contrib/docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh +COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ @@ -221,7 +221,7 @@ RUN set -ex \ && cp --recursive --link --preserve=all storage storage.skel \ && rm -rf html && ln -s public html -COPY contrib/docker/shared/root / +COPY docker/shared/root / ENTRYPOINT ["/docker/entrypoint.sh"] @@ -231,7 +231,7 @@ ENTRYPOINT ["/docker/entrypoint.sh"] FROM shared-runtime AS apache-runtime -COPY contrib/docker/apache/root / +COPY docker/apache/root / RUN set -ex \ && a2enmod rewrite remoteip proxy proxy_http \ @@ -245,7 +245,7 @@ CMD ["apache2-foreground"] FROM shared-runtime AS fpm-runtime -COPY contrib/docker/fpm/root / +COPY docker/fpm/root / CMD ["php-fpm"] @@ -275,8 +275,8 @@ RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE # copy docker entrypoints from the *real* nginx image directly COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ -COPY contrib/docker/nginx/root / -COPY contrib/docker/nginx/Procfile . +COPY docker/nginx/root / +COPY docker/nginx/Procfile . STOPSIGNAL SIGQUIT diff --git a/contrib/docker-compose/.env b/contrib/docker-compose/.env deleted file mode 100644 index c9e235533..000000000 --- a/contrib/docker-compose/.env +++ /dev/null @@ -1,921 +0,0 @@ -# -*- mode: bash -*- -# vi: ft=bash - -############################################################### -# Docker-wide configuration -############################################################### - -# Path (relative) to the docker-compose file where containers will store their data -DOCKER_DATA_ROOT="./data" - -# Path (relative) to the docker-compose file where containers will store their config -DOCKER_CONFIG_ROOT="./config" - -# Pixelfed version (image tag) to pull from the registry -DOCKER_TAG="branch-jippi-fork-apache-8.1" - -# Set timezone used by *all* containers - these should be in sync -# -# See: https://www.php.net/manual/en/timezones.php -TZ="UTC" - -# Automatically run [artisan migrate --force] if new migrations are detected -DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" - -############################################################### -# Pixelfed application configuration -############################################################### - -# A random 32-character string to be used as an encryption key. -# -# No default value; use [php artisan key:generate] to generate. -# -# This key is used by the Illuminate encrypter service and should be set to a random, -# 32 character string, otherwise these encrypted strings will not be safe. -# -# Please do this before deploying an application! -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY= - -# See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 -APP_NAME="Pixelfed Prod" - -# Application domains used for routing. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_domain -APP_DOMAIN="pixelfed-test.jippi.dev" - -# This URL is used by the console to properly generate URLs when using the Artisan command line tool. -# You should set this to the root of your application so that it is used when running Artisan tasks. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_url -APP_URL=https://${APP_DOMAIN} - -# Application domains used for routing. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#admin_domain -ADMIN_DOMAIN="${APP_DOMAIN}" - -# This value determines the “environment” your application is currently running in. -# This may determine how you prefer to configure various services your application utilizes. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_env -#APP_ENV="production" - -# When your application is in debug mode, detailed error messages with stack traces will -# be shown on every error that occurs within your application. -# -# If disabled, a simple generic error page is shown. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_debug -#APP_DEBUG="false" - -# Enable/disable new local account registrations. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#open_registration -#OPEN_REGISTRATION=true - -# Require email verification before a new user can do anything. -# -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification -#ENFORCE_EMAIL_VERIFICATION="true" - -# Allow a maximum number of user accounts. -# -# Defaults to "1000". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users -PF_MAX_USERS="false" - -# See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled -OAUTH_ENABLED="true" - -# Defaults to "UTC". -# -# Do not edit your timezone or things will break! -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_timezone -# See: https://www.php.net/manual/en/timezones.php -APP_TIMEZONE="${TZ}" - -# The application locale determines the default locale that will be used by the translation service provider. -# You are free to set this value to any of the locales which will be supported by the application. -# -# Defaults to "en". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_locale -#APP_LOCALE="en" - -# The fallback locale determines the locale to use when the current one is not available. -# -# You may change the value to correspond to any of the language folders that are provided through your application. -# -# Defaults to "en". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale -#APP_FALLBACK_LOCALE="en" - -# See: https://docs.pixelfed.org/technical-documentation/config/#limit_account_size -#LIMIT_ACCOUNT_SIZE="true" - -# Update the max account size, the per user limit of files in kB. -# -# Defaults to "1000000" (1GB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb -#MAX_ACCOUNT_SIZE="1000000" - -# Update the max photo size, in kB. -# -# Defaults to "15000" (15MB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb -#MAX_PHOTO_SIZE="15000" - -# Update the max avatar size, in kB. -# -# Defaults to "2000" (2MB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb -#MAX_AVATAR_SIZE="2000" - -# Change the caption length limit for new local posts. -# -# Defaults to "500". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_caption_length -#MAX_CAPTION_LENGTH="500" - -# Change the bio length limit for user profiles. -# -# Defaults to "125". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_bio_length -#MAX_BIO_LENGTH="125" - -# Change the length limit for user names. -# -# Defaults to "30". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_name_length -#MAX_NAME_LENGTH="30" - -# The max number of photos allowed per post. -# -# Defaults to "4". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length -#MAX_ALBUM_LENGTH="4" - -# Resize and optimize image uploads. -# -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images -#PF_OPTIMIZE_IMAGES="true" - -# Set the image optimization quality, must be a value between 1-100. -# -# Defaults to "80". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality -#IMAGE_QUALITY="80" - -# Resize and optimize video uploads. -# -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos -#PF_OPTIMIZE_VIDEOS="true" - -# Enable account deletion. -# -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#account_deletion -#ACCOUNT_DELETION="true" - -# Set account deletion queue after X days, set to false to delete accounts immediately. -# -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#account_delete_after -#ACCOUNT_DELETE_AFTER="false" - -# Defaults to "Pixelfed - Photo sharing for everyone". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_description -#INSTANCE_DESCRIPTION= - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags -#INSTANCE_PUBLIC_HASHTAGS="false" - -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email -INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline -#INSTANCE_PUBLIC_LOCAL_TIMELINE="false" - -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#banned_usernames -#BANNED_USERNAMES= - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#stories_enabled -#STORIES_ENABLED="false" - -# Defaults to "false". -# -# Level is hardcoded to 1. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#restricted_instance -#RESTRICTED_INSTANCE="false" - -############################################################### -# Database configuration -############################################################### - -# Here you may specify which of the database connections below you wish to use as your default connection for all database work. -# -# Of course you may use many connections at once using the database library. -# -# Possible values: -# -# - "sqlite" -# - "mysql" (default) -# - "pgsql" -# - "sqlsrv" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#db_connection -DB_CONNECTION="mysql" - -# See: https://docs.pixelfed.org/technical-documentation/config/#db_host -DB_HOST="db" - -# See: https://docs.pixelfed.org/technical-documentation/config/#db_username -DB_USERNAME="pixelfed" - -# See: https://docs.pixelfed.org/technical-documentation/config/#db_password -DB_PASSWORD="helloworld" - -# See: https://docs.pixelfed.org/technical-documentation/config/#db_database -DB_DATABASE="pixelfed_prod" - -# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL -# -# See: https://docs.pixelfed.org/technical-documentation/config/#db_port -DB_PORT="3306" - -ENTRYPOINT_DEBUG=0 - -############################################################### -# Mail configuration -############################################################### - -# Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. -# You may specify which one you’re using throughout your application here. -# -# Possible values: -# -# "smtp" (default) -# "sendmail" -# "mailgun" -# "mandrill" -# "ses" -# "sparkpost" -# "log" -# "array" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_driver -#MAIL_DRIVER="smtp" - -# The host address of the SMTP server used by your applications. -# -# A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. -# -# Defaults to "smtp.mailgun.org". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_host -#MAIL_HOST="smtp.mailgun.org" - -# This is the SMTP port used by your application to deliver e-mails to users of the application. -# -# Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. -# -# Defaults to 587. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_port -#MAIL_PORT="587" - -# You may wish for all e-mails sent by your application to be sent from the same address. -# -# Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. -# -# Defaults to "hello@example.com". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_address -MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}" - -# Defaults to "Example". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_name -MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" - -# If your SMTP server requires a username for authentication, you should set it here. -# -# This will get used to authenticate with your server on connection. -# You may also set the “password” value below this one. -# -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_username -#MAIL_USERNAME= - -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_password -#MAIL_PASSWORD= - -# Here you may specify the encryption protocol that should be used when the application send e-mail messages. -# -# A sensible default using the transport layer security protocol should provide great security. -# -# Defaults to "tls". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_encryption -#MAIL_ENCRYPTION="tls" - -############################################################### -# Redis configuration -############################################################### - -# Defaults to "phpredis". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#db_username -#REDIS_CLIENT="phpredis" - -# Defaults to "tcp". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_scheme -#REDIS_SCHEME="tcp" - -# Defaults to "localhost". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_host -REDIS_HOST="redis" - -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_password -#REDIS_PASSWORD= - -# Defaults to 6379. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_port -REDIS_PORT="6379" - -# Defaults to 0. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_database -#REDIS_DATABASE="0" - -############################################################### -# Cache settings -############################################################### - -# This option controls the default cache connection that gets used while using this caching library. -# -# This connection is used when another is not explicitly specified when executing a given caching function. -# -# Possible values: -# - "apc" -# - "array" -# - "database" -# - "file" (default) -# - "memcached" -# - "redis" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cache_driver -CACHE_DRIVER="redis" - -# Defaults to ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cache_prefix -# CACHE_PREFIX="{APP_NAME}_cache" - -############################################################### -# Horizon settings -############################################################### - -# 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. -# -# Defaults to "horizon-". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix -#HORIZON_PREFIX="horizon-" - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode -#HORIZON_DARKMODE="false" - -# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker -# may consume before it is terminated and restarted. -# -# You should set this value according to the resources available to your server. -# -# Defaults to "64". -#HORIZON_MEMORY_LIMIT="64" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy -#HORIZON_BALANCE_STRATEGY="auto" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes -#HORIZON_MIN_PROCESSES="1" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes -#HORIZON_MAX_PROCESSES="20" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory -#HORIZON_SUPERVISOR_MEMORY="64" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries -#HORIZON_SUPERVISOR_TRIES="3" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice -#HORIZON_SUPERVISOR_NICE="0" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout -#HORIZON_SUPERVISOR_TIMEOUT="300" - -############################################################### -# Experiments -############################################################### - -# Text only posts (alpha). -# -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_top -#EXP_TOP="false" - -# Poll statuses (alpha). -# -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_polls -#EXP_POLLS="false" - -# Cached public timeline for larger instances (beta). -# -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_cpt -#EXP_CPT="false" - -# Enforce Mastoapi Compatibility (alpha). -# -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc -EXP_EMC="true" - -############################################################### -# ActivityPub confguration -############################################################### - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#activity_pub -ACTIVITY_PUB="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow -#AP_REMOTE_FOLLOW="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox -#AP_SHAREDINBOX="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_inbox -#AP_INBOX="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_outbox -#AP_OUTBOX="true" - -############################################################### -# Federation confguration -############################################################### - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#atom_feeds -#ATOM_FEEDS="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#nodeinfo -#NODEINFO="true" - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#webfinger -#WEBFINGER="true" - -############################################################### -# Storage (cloud) -############################################################### - -# Store media on object storage like S3, Digital Ocean Spaces, Rackspace -# -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud -#PF_ENABLE_CLOUD="false" - -# Many applications store files both locally and in the cloud. -# -# For this reason, you may specify a default “cloud” driver here. -# This driver will be bound as the Cloud disk implementation in the container. -# -# Defaults to "s3". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud -#FILESYSTEM_CLOUD="s3" - -# Defaults to true. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud -#MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" - -############################################################### -# Storage (cloud) - S3 -############################################################### - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id -#AWS_ACCESS_KEY_ID= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key -#AWS_SECRET_ACCESS_KEY= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_default_region -#AWS_DEFAULT_REGION= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_bucket -#AWS_BUCKET= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_url -#AWS_URL= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint -#AWS_ENDPOINT= - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint -#AWS_USE_PATH_STYLE_ENDPOINT="false" - -############################################################### -# COSTAR - Confirm Object Sentiment Transform and Reduce -############################################################### - -# Comma-separated list of domains to block. -# -# Defaults to null -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains -#CS_BLOCKED_DOMAINS= - -# Comma-separated list of domains to add warnings. -# -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains -#CS_CW_DOMAINS= - -# Comma-separated list of domains to remove from public timelines. -# -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains -#CS_UNLISTED_DOMAINS= - -# Comma-separated list of keywords to block. -# -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords -#CS_BLOCKED_KEYWORDS= - -# Comma-separated list of keywords to add warnings. -# -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords -#CS_CW_KEYWORDS= - -# Comma-separated list of keywords to remove from public timelines. -# -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords -#CS_UNLISTED_KEYWORDS= - -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor -#CS_BLOCKED_ACTOR= - -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor -#CS_CW_ACTOR= - -# Defaults to null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor -#CS_UNLISTED_ACTOR= - -############################################################### -# Media -############################################################### - -# Defaults to false. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#media_exif_database -MEDIA_EXIF_DATABASE="true" - -# Pixelfed supports GD or ImageMagick to process images. -# -# Defaults to "gd". -# -# Possible values: -# - "gd" (default) -# - "imagick" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#image_driver -#IMAGE_DRIVER="gd" - -############################################################### -# Logging -############################################################### - -# Possible values: -# -# - "stack" (default) -# - "single" -# - "daily" -# - "slack" -# - "stderr" -# - "syslog" -# - "errorlog" -# - "null" -# - "emergency" -# - "media" -LOG_CHANNEL="stderr" - -# Used by single, stderr and syslog. -# -# Defaults to "debug" for all of those. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_level -#LOG_LEVEL="debug" - -# Used by stderr. -# -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter -#LOG_STDERR_FORMATTER= - -# Used by slack. -# -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url -#LOG_SLACK_WEBHOOK_URL= - -############################################################### -# Broadcasting settings -############################################################### - -# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. -# -# Possible values: -# - "pusher" -# - "redis" -# - "log" -# - "null" (default) -# -# See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver -BROADCAST_DRIVER=redis - -############################################################### -# Other settings -############################################################### - -# Defaults to true. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types -#RESTRICT_HTML_TYPES="true" - -############################################################### -# Queue configuration -############################################################### - -# Possible values: -# - "sync" (default) -# - "database" -# - "beanstalkd" -# - "sqs" -# - "redis" -# - "null" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#queue_driver -QUEUE_DRIVER="redis" - -############################################################### -# Queue (SQS) configuration -############################################################### - -# Defaults to "your-public-key". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_key -#SQS_KEY="your-public-key" - -# Defaults to "your-secret-key". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_secret -#SQS_SECRET="your-secret-key" - -# Defaults to "https://sqs.us-east-1.amazonaws.com/your-account-id". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix -#SQS_PREFIX= - -# Defaults to "your-queue-name". -# -# https://docs.pixelfed.org/technical-documentation/config/#sqs_queue -#SQS_QUEUE="your-queue-name" - -# Defaults to "us-east-1". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_region -#SQS_REGION="us-east-1" - -############################################################### -# Session configuration -############################################################### - -# This option controls the default session “driver” that will be used on requests. -# -# By default, we will use the lightweight native driver but you may specify any of the other wonderful drivers provided here. -# -# Possible values: -# - "file" -# - "cookie" -# - "database" (default) -# - "apc" -# - "memcached" -# - "redis" -# - "array" -SESSION_DRIVER="redis" - -# Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. -# -# If you want them to immediately expire on the browser closing, set that option. -# -# Defaults to 86400. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#session_lifetime -#SESSION_LIFETIME="86400" - -# Here you may change the domain of the cookie used to identify a session in your application. -# -# This will determine which domains the cookie is available to in your application. -# -# A sensible default has been set. -# -# Defaults to the value of APP_DOMAIN, or null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#session_domain -#SESSION_DOMAIN="${APP_DOMAIN}" - -############################################################### -# Proxy configuration -############################################################### - -# Set trusted proxy IP addresses. -# -# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. -# -# The “*” character is syntactic sugar within TrustedProxy to trust any -# proxy that connects directly to your server, a requirement when you cannot -# know the address of your proxy (e.g. if using Rackspace balancers). -# -# The “**” character is syntactic sugar within TrustedProxy to trust not just any -# proxy that connects directly to your server, but also proxies that connect to those proxies, -# and all the way back until you reach the original source IP. It will mean that -# $request->getClientIp() always gets the originating client IP, no matter how many proxies -# that client’s request has subsequently passed through. -# -# Defaults to "*". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#trust_proxies -TRUST_PROXIES="*" - -############################################################### -# Passport configuration -############################################################### -# -# Passport uses encryption keys while generating secure access tokens -# for your application. -# -# By default, the keys are stored as local files but can be set via environment -# variables when that is more convenient. - -# See: https://docs.pixelfed.org/technical-documentation/config/#passport_private_key -#PASSPORT_PRIVATE_KEY= - -# See: https://docs.pixelfed.org/technical-documentation/config/#passport_public_key -#PASSPORT_PUBLIC_KEY= - -############################################################### -# PHP configuration -############################################################### - -# See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit -#PHP_MEMORY_LIMIT="128M" - -############################################################### -# MySQL DB container configuration (DO NOT CHANGE) -############################################################### -# -# See "Environment Variables" at https://hub.docker.com/_/mysql - -MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" -MYSQL_USER="${DB_USERNAME}" -MYSQL_PASSWORD="${DB_PASSWORD}" -MYSQL_DATABASE="${DB_DATABASE}" - -############################################################### -# MySQL (MariaDB) DB container configuration (DO NOT CHANGE) -############################################################### -# -# See "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb - -MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" -MARIADB_USER="${DB_USERNAME}" -MARIADB_PASSWORD="${DB_PASSWORD}" -MARIADB_DATABASE="${DB_DATABASE}" - -############################################################### -# PostgreSQL DB container configuration (DO NOT CHANGE) -############################################################### -# -# See "Environment Variables" at https://hub.docker.com/_/postgres - -POSTGRES_USER="${DB_USERNAME}" -POSTGRES_PASSWORD="${DB_PASSWORD}" -POSTGRES_DB="${DB_DATABASE}" - -############################################################### -# Docker Specific configuration -############################################################### - -# Image to pull the Pixelfed Docker images from -# -# Possible values: -# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub -# - "pixelfed/pixelfed" to pull from DockerHub -# -DOCKER_IMAGE="ghcr.io/jippi/pixelfed" - -# Port that Redis will listen on *outside* the container (e.g. the host machine) -DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" - -# Port that the database will listen on *outside* the container (e.g. the host machine) -# -# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL -DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" - -# Port that the web will listen on *outside* the container (e.g. the host machine) -DOCKER_WEB_PORT_EXTERNAL="8080" diff --git a/contrib/docker-compose/README.md b/contrib/docker-compose/README.md deleted file mode 100644 index 137773f08..000000000 --- a/contrib/docker-compose/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Pixelfed + Docker + Docker Compose - -## Prerequisites - -* One of the `docker-compose.yml` files in this directory -* A copy of the `example.env` file named `.env` next to `docker-compose.yml` - -Your folder should look like this - -```plain -. -├── .env -└── docker-compose.yml -``` - -## Modifying your settings (`.env` file) - -Minimum required settings to change is: - -* `APP_NAME` -* `APP_DOMAIN` -* `DB_PASSWORD` - -See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! - -You need to mainly focus on following sections - -* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) -* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) - -Since the following things are configured for you out of the box: - -* `Redis` -* `Database` (except for `DB_PASSWORD`) diff --git a/contrib/docker-compose/docker-compose.yml b/contrib/docker-compose/docker-compose.yml deleted file mode 100644 index e43e36c78..000000000 --- a/contrib/docker-compose/docker-compose.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- -version: "3" - -services: - web: - image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # context: ../.. - # dockerfile: contrib/docker/Dockerfile - # target: apache-runtime - restart: unless-stopped - env_file: - - "./.env" - volumes: - - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" - ports: - - "${DOCKER_WEB_PORT_EXTERNAL}:80" - depends_on: - - db - - redis - - worker: - image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # context: ../.. - # dockerfile: contrib/docker/Dockerfile - # target: apache-runtime - command: gosu www-data php artisan horizon - restart: unless-stopped - env_file: - - "./.env" - volumes: - - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" - depends_on: - - db - - redis - - db: - image: mariadb:11.2 - command: --default-authentication-plugin=mysql_native_password - restart: unless-stopped - env_file: - - "./.env" - volumes: - - "${DOCKER_DATA_ROOT}/db:/var/lib/mysql" - ports: - - "${DOCKER_DB_PORT_EXTERNAL}:3306" - - redis: - image: redis:7 - restart: unless-stopped - env_file: - - "./.env" - volumes: - - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - - "${DOCKER_DATA_ROOT}/redis:/data" - ports: - - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" - healthcheck: - interval: 10s - timeout: 5s - retries: 2 - test: ["CMD", "redis-cli", "-p", "6399", "ping"] diff --git a/docker-compose.yml b/docker-compose.yml index 6fca2eeb3..4b93c75e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,82 +1,59 @@ --- -version: '3' +version: "3" -# In order to set configuration, please use a .env file in -# your compose project directory (the same directory as your -# docker-compose.yml), and set database options, application -# name, key, and other settings there. -# A list of available settings is available in .env.example -# -# The services should scale properly across a swarm cluster -# if the volumes are properly shared between cluster members. +############################################################### +# Please see docker/README.md for usage information +############################################################### services: -## App and Worker - app: - # Comment to use dockerhub image - image: pixelfed/pixelfed:latest + web: + image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + # build: + # target: apache-runtime restart: unless-stopped - env_file: - - .env.docker volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - - "./.env.docker:/var/www/.env" - networks: - - external - - internal + - "./.env:/var/www/.env" + - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" + - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" ports: - - "8080:80" + - "${DOCKER_WEB_PORT_EXTERNAL}:80" depends_on: - db - redis worker: - image: pixelfed/pixelfed:latest - restart: unless-stopped - env_file: - - .env.docker - volumes: - - app-storage:/var/www/storage - - app-bootstrap:/var/www/bootstrap - networks: - - external - - internal + image: "${DOCKER_IMAGE}:${DOCKER_TAG}" + # build: + # target: apache-runtime command: gosu www-data php artisan horizon + restart: unless-stopped + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" + - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" depends_on: - db - redis -## DB and Cache db: - image: mariadb:jammy - restart: unless-stopped - networks: - - internal + image: mariadb:11.2 command: --default-authentication-plugin=mysql_native_password - env_file: - - .env.docker + restart: unless-stopped volumes: - - "db-data:/var/lib/mysql" + - "${DOCKER_DATA_ROOT}/db:/var/lib/mysql" + ports: + - "${DOCKER_DB_PORT_EXTERNAL}:3306" redis: - image: redis:5-alpine + image: redis:7 restart: unless-stopped - env_file: - - .env.docker volumes: - - "redis-data:/data" - networks: - - internal - -volumes: - db-data: - redis-data: - app-storage: - app-bootstrap: - -networks: - internal: - internal: true - external: - driver: bridge + - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" + - "${DOCKER_DATA_ROOT}/redis:/data" + ports: + - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" + healthcheck: + interval: 10s + timeout: 5s + retries: 2 + test: ["CMD", "redis-cli", "-p", "6399", "ping"] diff --git a/contrib/docker/README.md b/docker/README.md similarity index 91% rename from contrib/docker/README.md rename to docker/README.md index 443ab6423..03bc31246 100644 --- a/contrib/docker/README.md +++ b/docker/README.md @@ -1,4 +1,37 @@ -# Pixelfed Docker images +# Pixelfed + Docker + Docker Compose + +## Prerequisites + +* One of the `docker-compose.yml` files in this directory +* A copy of the `example.env` file named `.env` next to `docker-compose.yml` + +Your folder should look like this + +```plain +. +├── .env +└── docker-compose.yml +``` + +## Modifying your settings (`.env` file) + +Minimum required settings to change is: + +* `APP_NAME` +* `APP_DOMAIN` +* `DB_PASSWORD` + +See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! + +You need to mainly focus on following sections + +* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) +* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) + +Since the following things are configured for you out of the box: + +* `Redis` +* `Database` (except for `DB_PASSWORD`) ## Runtimes diff --git a/contrib/docker/apache/root/etc/apache2/conf-available/remoteip.conf b/docker/apache/root/etc/apache2/conf-available/remoteip.conf similarity index 100% rename from contrib/docker/apache/root/etc/apache2/conf-available/remoteip.conf rename to docker/apache/root/etc/apache2/conf-available/remoteip.conf diff --git a/contrib/docker/fpm/root/.gitkeep b/docker/fpm/root/.gitkeep similarity index 100% rename from contrib/docker/fpm/root/.gitkeep rename to docker/fpm/root/.gitkeep diff --git a/contrib/docker/nginx/Procfile b/docker/nginx/Procfile similarity index 100% rename from contrib/docker/nginx/Procfile rename to docker/nginx/Procfile diff --git a/contrib/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf similarity index 100% rename from contrib/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf rename to docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf diff --git a/contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/01-permissions.sh rename to docker/shared/root/docker/entrypoint.d/01-permissions.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/04-defaults.envsh rename to docker/shared/root/docker/entrypoint.d/04-defaults.envsh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/05-templating.sh rename to docker/shared/root/docker/entrypoint.d/05-templating.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh b/docker/shared/root/docker/entrypoint.d/10-storage.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/10-storage.sh rename to docker/shared/root/docker/entrypoint.d/10-storage.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh rename to docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/12-migrations.sh rename to docker/shared/root/docker/entrypoint.d/12-migrations.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/docker/shared/root/docker/entrypoint.d/20-horizon.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/20-horizon.sh rename to docker/shared/root/docker/entrypoint.d/20-horizon.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh b/docker/shared/root/docker/entrypoint.d/30-cache.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.d/30-cache.sh rename to docker/shared/root/docker/entrypoint.d/30-cache.sh diff --git a/contrib/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh similarity index 100% rename from contrib/docker/shared/root/docker/entrypoint.sh rename to docker/shared/root/docker/entrypoint.sh diff --git a/contrib/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh similarity index 100% rename from contrib/docker/shared/root/docker/helpers.sh rename to docker/shared/root/docker/helpers.sh diff --git a/contrib/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh similarity index 100% rename from contrib/docker/shared/root/docker/install/base.sh rename to docker/shared/root/docker/install/base.sh diff --git a/contrib/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh similarity index 100% rename from contrib/docker/shared/root/docker/install/php-extensions.sh rename to docker/shared/root/docker/install/php-extensions.sh diff --git a/contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini similarity index 100% rename from contrib/docker/shared/root/docker/templates/usr/local/etc/php/php.ini rename to docker/shared/root/docker/templates/usr/local/etc/php/php.ini From bd1cd9c4fc44bba07ad7dae02a9d9d33cf1ea11c Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 15:39:30 +0000 Subject: [PATCH 207/977] more docs --- .env.docker | 22 ++++----- docker/README.md | 117 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 24 deletions(-) diff --git a/.env.docker b/.env.docker index 10e88b8b5..0508e939a 100644 --- a/.env.docker +++ b/.env.docker @@ -5,12 +5,20 @@ # Docker-wide configuration ############################################################### -# Path (relative) to the docker-compose file where containers will store their data +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their data DOCKER_DATA_ROOT="./data" -# Path (relative) to the docker-compose file where containers will store their config +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their confguration DOCKER_CONFIG_ROOT="./config" +# Image to pull the Pixelfed Docker images from +# +# Possible values: +# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# - "pixelfed/pixelfed" to pull from DockerHub +# +DOCKER_IMAGE="ghcr.io/jippi/pixelfed" + # Pixelfed version (image tag) to pull from the registry DOCKER_TAG="branch-jippi-fork-apache-8.1" @@ -569,7 +577,7 @@ ACTIVITY_PUB="true" #MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" ############################################################### -# Storage (cloud) - S3 +# Storage (cloud) - S3 andS S3 *compatible* providers (most of them) ############################################################### # See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id @@ -901,14 +909,6 @@ POSTGRES_DB="${DB_DATABASE}" # Docker Specific configuration ############################################################### -# Image to pull the Pixelfed Docker images from -# -# Possible values: -# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub -# - "pixelfed/pixelfed" to pull from DockerHub -# -DOCKER_IMAGE="ghcr.io/jippi/pixelfed" - # Port that Redis will listen on *outside* the container (e.g. the host machine) DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" diff --git a/docker/README.md b/docker/README.md index 03bc31246..837c1f646 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,25 +1,103 @@ # Pixelfed + Docker + Docker Compose +This guide will help you install and run Pixelfed on **your** server using [Docker Compose](https://docs.docker.com/compose/). + ## Prerequisites -* One of the `docker-compose.yml` files in this directory -* A copy of the `example.env` file named `.env` next to `docker-compose.yml` +Recommendations and requirements for hardware and software needed to run Pixelfed using Docker Compose. -Your folder should look like this +It's highly recommended that you have *some* experience with Linux (e.g. Ubuntu or Debian), SSH, and lightweight server administration. -```plain -. -├── .env -└── docker-compose.yml -``` +### Server + +A VPS or dedicated server you can SSH into, for example + +* [linode.com VPS](https://www.linode.com/) +* [DigitalOcean VPS](https://digitalocean.com/) +* [Hetzner](https://www.hetzner.com/) + +### Hardware + +Hardware requirements depends on the amount of users you have (or plan to have), and how active they are. + +A safe starter/small instance hardware for 25 users and blow are: + +* **CPU/vCPU** `2` cores. +* **RAM** `2-4 GB` as your instance grow, memory requirements will increase for the database. +* **Storage** `20-50 GB` HDD is fine, but ideally SSD or NVMe, *especially* for the database. +* **Network** `100 Mbit/s` or faster. + +### Other + +* A **Domain** you need a domain (or subdomain) where your Pixelfed server will be running (for example, `pixelfed.social`) +* (Optional) An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. +* (Optional) An **Object Storage** provider for storing all images remotely, rather than locally on your server. + +#### E-mail / SMTP provider + +**NOTE**: If you don't plan to use en e-mail/SMTP provider, then make sure to set `ENFORCE_EMAIL_VERIFICATION="false"` in your `.env` file! + +There are *many* providers out there, with wildly different pricing structures, features, and reliability. + +It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: + +* [Simple Email Service (SES)](https://aws.amazon.com/ses/) by Amazon Web Services (AWS) is pay-as-you-go with a cost of $0.10/1000 emails. +* [Brevo](https://www.brevo.com/) (formerly SendInBlue) has a Free Tier with 300 emails/day. +* [Postmark](https://postmarkapp.com/) has a Free Tier with 100 emails/month. +* [Forward Email](https://forwardemail.net/en/private-business-email?pricing=true) has a $3/mo/domain plan with both sending and receiving included. +* [Mailtrap](https://mailtrap.io/email-sending/) has a 1000 emails/month free-tier (their `Email Sending` product, *not* the `Email Testing` one). + +#### Object Storage + +**NOTE**: This is *entirely* optional - by default Pixelfed will store all uploads (videos, images, etc.) directly on your servers storage. + +> Object storage is a technology that stores and manages data in an unstructured format called objects. Modern organizations create and analyze large volumes of unstructured data such as photos, videos, email, web pages, sensor data, and audio files +> +> -- [*What is object storage?*](https://aws.amazon.com/what-is/object-storage/) by Amazon Web Services + +It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: + +* [R2](https://www.cloudflare.com/developer-platform/r2/) by CloudFlare has cheap storage, free *egress* (e.g. people downloading images) and included (and free) Content Delivery Network (CDN). +* [B2 cloud storage](https://www.backblaze.com/cloud-storage) by Backblaze. +* [Simple Storage Service (S3)](https://aws.amazon.com/s3/) by Amazon Web Services. + +### Software + +Required software to be installed on your server + +* `git` can be installed with `apt-get install git` on Debian/Ubuntu +* `docker` can be installed by [following the official Docker documentation](https://docs.docker.com/engine/install/) + +## Getting things ready + +Connect via SSH to your server and decide where you want to install Pixelfed. + +In this guide I'm going to assume it will be installed at `/data/pixelfed`. + +1. **Install required software** as mentioned in the [Software Prerequisites section above](#software) +1. **Create the parent directory** by running `mkdir -p /data` +1. **Clone the Pixelfed repository** by running `git clone https://github.com/pixelfed/pixelfed.git /data/pixelfed` +1. **Change to the Pixelfed directory** by running `cd /data/pixelfed` ## Modifying your settings (`.env` file) -Minimum required settings to change is: +### Copy the example configuration file -* `APP_NAME` -* `APP_DOMAIN` -* `DB_PASSWORD` +Pixelfed contains a default configuration file (`.env.docker`) you should use as a starter, however, before editing anything, make a copy of it and put it in the *right* place (`.env`). + +Run the following command to copy the file: `cp .env.docker .env` + +### Modifying the configuration file + +The configuration file is *quite* long, but the good news is that you can ignore *most* of it, all of the *server* specific settings are configured for you out of the box. + +The minimum required settings you **must** change is: + +* (required) `APP_DOMAIN` which is the hostname you plan to run your Pixelfed server on (e.g. `pixelfed.social`) - must **not** include `http://` or a trailing slash (`/`)! +* (required) `DB_PASSWORD` which is the database password, you can use a service like [pwgen.io](https://pwgen.io/en/) to generate a secure one. +* (optional) `ENFORCE_EMAIL_VERIFICATION` should be set to `"false"` if you don't plan to send emails. +* (optional) `MAIL_DRIVER` and related `MAIL_*` settings if you plan to use an [email/SMTP provider](#e-mail--smtp-provider) - See [Email variables documentation](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables). +* (optional) `PF_ENABLE_CLOUD` / `FILESYSTEM_CLOUD` if you plan to use an [Object Storage provider](#object-storage). See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! @@ -28,10 +106,23 @@ You need to mainly focus on following sections * [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) * [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) -Since the following things are configured for you out of the box: +You can skip the following sections, since they are already configured/automated for you: * `Redis` * `Database` (except for `DB_PASSWORD`) +* `One-time setup tasks` + +### Starting the service + +With everything in place and (hopefully) well-configured, we can now go ahead and start our services by running + +```shell +docker compose up -d +``` + +This will download all the required Docker images, start the containers, and being the automatic setup. + +You can follow the logs by running `docker compose logs --tail=100 --follow`. ## Runtimes From 9445980e0433b04579efa89bc4c2e061b7f242ba Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 15:57:20 +0000 Subject: [PATCH 208/977] expose both http and https ports --- .env.docker | 7 +++++-- docker-compose.yml | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.env.docker b/.env.docker index 0508e939a..ba4f62556 100644 --- a/.env.docker +++ b/.env.docker @@ -917,5 +917,8 @@ DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" -# Port that the web will listen on *outside* the container (e.g. the host machine) -DOCKER_WEB_PORT_EXTERNAL="8080" +# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTP traffic +DOCKER_WEB_HTTP_PORT_EXTERNAL="8080" + +# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +DOCKER_WEB_HTTPS_PORT_EXTERNAL="444" diff --git a/docker-compose.yml b/docker-compose.yml index 4b93c75e9..4b1b6ac4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,8 @@ services: - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" ports: - - "${DOCKER_WEB_PORT_EXTERNAL}:80" + - "${DOCKER_WEB_HTTP_PORT_EXTERNAL}:80" + - "${DOCKER_WEB_HTTPS_PORT_EXTERNAL}:443" depends_on: - db - redis From 284bb26d92376b3d66d8c0805d977237d92ed0b7 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 16:43:48 +0000 Subject: [PATCH 209/977] sync --- .dockerignore | 6 ++-- .env.docker | 16 +++++++-- .gitignore | 3 +- docker-compose.yml | 51 ++++++++++++++++++++++++++-- docker/shared/root/docker/helpers.sh | 2 +- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index a4f4ff035..b7a6691d9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,6 @@ .env .git .gitignore -contrib/docker-compose/.env -contrib/docker-compose/config -contrib/docker-compose/data -data docker-compose*.yml + +/docker-compose/ diff --git a/.env.docker b/.env.docker index ba4f62556..72994a23a 100644 --- a/.env.docker +++ b/.env.docker @@ -6,10 +6,10 @@ ############################################################### # Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their data -DOCKER_DATA_ROOT="./data" +DOCKER_DATA_ROOT="./docker-compose/data" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their confguration -DOCKER_CONFIG_ROOT="./config" +DOCKER_CONFIG_ROOT="./docker-compose/config" # Image to pull the Pixelfed Docker images from # @@ -30,6 +30,12 @@ TZ="UTC" # Automatically run [artisan migrate --force] if new migrations are detected DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" +# The e-mail to use for Lets Encrypt certificate requests +LETSENCRYPT_EMAIL="__CHANGE_ME__" + +# Lets Encrypt staging/test servers for certificate requests +LETSENCRYPT_TEST="true" + ############################################################### # Pixelfed application configuration ############################################################### @@ -922,3 +928,9 @@ DOCKER_WEB_HTTP_PORT_EXTERNAL="8080" # Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic DOCKER_WEB_HTTPS_PORT_EXTERNAL="444" + +# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTP traffic +DOCKER_PROXY_PORT_EXTERNAL_HTTP="8080" + +# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" diff --git a/.gitignore b/.gitignore index 4396e4cdd..abb42ef7c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,5 @@ yarn-error.log .git-credentials /.composer/ /nginx.conf -/contrib/docker-compose/data -/contrib/docker-compose/config +/docker-compose/ !/contrib/docker-compose/.env diff --git a/docker-compose.yml b/docker-compose.yml index 4b1b6ac4b..5960f3484 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,43 @@ version: "3" ############################################################### services: + # HTTP/HTTPS proxy + # + # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs + proxy: + image: nginxproxy/nginx-proxy:1.4 + container_name: "${APP_DOMAIN}-proxy" + #restart: unless-stopped + volumes: + - "/var/run/docker.sock:/tmp/docker.sock:ro" + - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_CONFIG_ROOT}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" + ports: + - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" + - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" + + # Proxy companion for managing letsencrypt SSL certificates + # + # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs + proxy-acme: + image: nginxproxy/acme-companion + container_name: "${APP_DOMAIN}-proxy-acme" + #restart: unless-stopped + environment: + DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" + LETSENCRYPT_TEST: "${LETSENCRYPT_TEST}" + NGINX_PROXY_CONTAINER: "${APP_DOMAIN}-proxy" + depends_on: + - proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_CONFIG_ROOT}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" + web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" # build: @@ -15,9 +52,17 @@ services: - "./.env:/var/www/.env" - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" - ports: - - "${DOCKER_WEB_HTTP_PORT_EXTERNAL}:80" - - "${DOCKER_WEB_HTTPS_PORT_EXTERNAL}:443" + environment: + LETSENCRYPT_HOST: "${APP_DOMAIN},*.${APP_DOMAIN}" + VIRTUAL_HOST: "${APP_DOMAIN},*.${APP_DOMAIN}" + VIRTUAL_PORT: "80" + labels: + com.github.nginx-proxy.nginx-proxy.keepalive: 30 + com.github.nginx-proxy.nginx-proxy.http2.enable: true + com.github.nginx-proxy.nginx-proxy.http3.enable: true + # ports: + # - "${DOCKER_WEB_HTTP_PORT_EXTERNAL}:80" + # - "${DOCKER_WEB_HTTPS_PORT_EXTERNAL}:443" depends_on: - db - redis diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 4217b56a6..c9b5bb531 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -410,7 +410,7 @@ function await-database-ready() { ;; *) - log-error-and-exit "Unknown database type: [${DB_CONNECT}]" + log-error-and-exit "Unknown database type: [${DB_CONNECTION}]" ;; esac From 2e3c7e862c307f23a654bdf3b105051d9d18b00e Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 6 Jan 2024 18:01:48 +0000 Subject: [PATCH 210/977] iterating on proxy + letsencrypt setup --- .dockerignore | 5 +- .env.docker | 21 ++++++-- .gitignore | 27 +++++----- docker-compose.yml | 49 +++++++++++++------ .../entrypoint.d/11-first-time-setup.sh | 1 + docker/shared/root/docker/helpers.sh | 10 ++-- 6 files changed, 68 insertions(+), 45 deletions(-) diff --git a/.dockerignore b/.dockerignore index b7a6691d9..c8ae49a4e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,4 @@ -.dockerignore .env .git .gitignore -docker-compose*.yml - -/docker-compose/ +/docker-compose-state/ diff --git a/.env.docker b/.env.docker index 72994a23a..2b4eba197 100644 --- a/.env.docker +++ b/.env.docker @@ -34,7 +34,7 @@ DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" LETSENCRYPT_EMAIL="__CHANGE_ME__" # Lets Encrypt staging/test servers for certificate requests -LETSENCRYPT_TEST="true" +LETSENCRYPT_TEST= ############################################################### # Pixelfed application configuration @@ -147,7 +147,7 @@ APP_TIMEZONE="${TZ}" # Defaults to "15000" (15MB). # # See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb -#MAX_PHOTO_SIZE="15000" +MAX_PHOTO_SIZE="15000" # Update the max avatar size, in kB. # @@ -182,7 +182,7 @@ APP_TIMEZONE="${TZ}" # Defaults to "4". # # See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length -#MAX_ALBUM_LENGTH="4" +MAX_ALBUM_LENGTH="4" # Resize and optimize image uploads. # @@ -912,9 +912,14 @@ POSTGRES_PASSWORD="${DB_PASSWORD}" POSTGRES_DB="${DB_DATABASE}" ############################################################### -# Docker Specific configuration +# Lets Encrypt configuration ############################################################### +LETSENCRYPT_HOST="${APP_DOMAIN}" + +############################################################### +# Docker Specific configuration +############################################################### # Port that Redis will listen on *outside* the container (e.g. the host machine) DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" @@ -933,4 +938,10 @@ DOCKER_WEB_HTTPS_PORT_EXTERNAL="444" DOCKER_PROXY_PORT_EXTERNAL_HTTP="8080" # Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic -DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" +DOCKER_PROXY_PORT_EXTERNAL_HTTPS="444" + +# Path to the Docker socket on the *host* +DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" + +# Prefix for container names (without any dash at the end) +DOCKER_CONTAINER_NAME_PREFIX="${APP_DOMAIN}-" diff --git a/.gitignore b/.gitignore index abb42ef7c..a5cdf3af1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,21 @@ +.bash_history +.bash_profile +.bashrc +.DS_Store +.env +.git-credentials +.gitconfig +/.composer/ +/.idea +/.vagrant +/.vscode +/docker-compose-state/ /node_modules /public/hot /public/storage /storage/*.key /vendor -/.idea -/.vscode -/.vagrant -/docker-volumes Homestead.json Homestead.yaml npm-debug.log yarn-error.log -.env -.DS_Store -.bash_profile -.bash_history -.bashrc -.gitconfig -.git-credentials -/.composer/ -/nginx.conf -/docker-compose/ -!/contrib/docker-compose/.env diff --git a/docker-compose.yml b/docker-compose.yml index 5960f3484..3b131635f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,14 +11,14 @@ services: # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs proxy: image: nginxproxy/nginx-proxy:1.4 - container_name: "${APP_DOMAIN}-proxy" + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" #restart: unless-stopped volumes: - - "/var/run/docker.sock:/tmp/docker.sock:ro" - - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_HOST_SOCKET_PATH}:/tmp/docker.sock:ro" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" - - "${DOCKER_CONFIG_ROOT}/proxy/html:/usr/share/nginx/html" - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_DATA_ROOT}/proxy/html:/usr/share/nginx/html" ports: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" @@ -28,33 +28,40 @@ services: # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs proxy-acme: image: nginxproxy/acme-companion - container_name: "${APP_DOMAIN}-proxy-acme" + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" #restart: unless-stopped environment: + DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" - LETSENCRYPT_TEST: "${LETSENCRYPT_TEST}" - NGINX_PROXY_CONTAINER: "${APP_DOMAIN}-proxy" + NGINX_PROXY_CONTAINER: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" depends_on: - proxy volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_HOST_SOCKET_PATH}:/var/run/docker.sock:ro" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" - - "${DOCKER_CONFIG_ROOT}/proxy/html:/usr/share/nginx/html" - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_DATA_ROOT}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_DATA_ROOT}/proxy-acme:/etc/acme.sh" web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # target: apache-runtime + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-web" restart: unless-stopped + build: + target: apache-runtime + deploy: + mode: replicated + replicas: 1 volumes: - "./.env:/var/www/.env" - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" environment: - LETSENCRYPT_HOST: "${APP_DOMAIN},*.${APP_DOMAIN}" - VIRTUAL_HOST: "${APP_DOMAIN},*.${APP_DOMAIN}" + LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" + LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" + LETSENCRYPT_TEST: "${LETSENCRYPT_TEST}" + VIRTUAL_HOST: "${APP_DOMAIN}" VIRTUAL_PORT: "80" labels: com.github.nginx-proxy.nginx-proxy.keepalive: 30 @@ -69,10 +76,14 @@ services: worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - # build: - # target: apache-runtime + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + build: + target: apache-runtime volumes: - "./.env:/var/www/.env" - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" @@ -83,8 +94,11 @@ services: db: image: mariadb:11.2 + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-db" command: --default-authentication-plugin=mysql_native_password restart: unless-stopped + env_file: + - ".env" volumes: - "${DOCKER_DATA_ROOT}/db:/var/lib/mysql" ports: @@ -92,7 +106,10 @@ services: redis: image: redis:7 + container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped + env_file: + - ".env" volumes: - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - "${DOCKER_DATA_ROOT}/redis:/data" diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 1a0cbb51c..529c1d0cf 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -3,6 +3,7 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +load-config-files await-database-ready only-once "storage:link" run-as-runtime-user php artisan storage:link diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index c9b5bb531..24bd7f1f8 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -283,7 +283,7 @@ function is-directory-empty() { # @exitcode 0 If $1 If the path exists *or* was created # @exitcode 1 If $1 If the path does *NOT* exists and could *NOT* be created function ensure-directory-exists() { - mkdir -pv "$@" + stream-prefix-command-output mkdir -pv "$@" } # @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_ROOT prefix @@ -314,7 +314,7 @@ function only-once() { return 1 fi - touch "${file}" + stream-prefix-command-output touch "${file}" return 0 } @@ -334,7 +334,7 @@ function acquire-lock() { staggered-sleep done - touch "${file}" + stream-prefix-command-output touch "${file}" log-info "🔐 Lock acquired [${file}]" @@ -349,7 +349,7 @@ function release-lock() { log-info "🔓 Releasing lock [${file}]" - rm -f "${file}" + stream-prefix-command-output rm -fv "${file}" } # @description Helper function to append multiple actions onto @@ -410,7 +410,7 @@ function await-database-ready() { ;; *) - log-error-and-exit "Unknown database type: [${DB_CONNECTION}]" + log-error-and-exit "Unknown database type: [${DB_CONNECTION:-}]" ;; esac From d25209f74aecdfd5ada482fd99c89d9599eb04a7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 6 Jan 2024 11:43:56 -0700 Subject: [PATCH 211/977] Update ApiV1Controller, update favourites max limit. Fixes #4854 --- app/Http/Controllers/Api/ApiV1Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index c1dd8cbf4..993df3555 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1188,7 +1188,7 @@ class ApiV1Controller extends Controller { abort_if(!$request->user(), 403); $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:20' + 'limit' => 'sometimes|integer|min:1|max:40' ]); $user = $request->user(); From 19e8037c85d10fcc259321d6fa000176c1ed586e Mon Sep 17 00:00:00 2001 From: nexryai <61890205+nexryai@users.noreply.github.com> Date: Sun, 7 Jan 2024 21:05:04 +0900 Subject: [PATCH 212/977] Fix lang/vendor/backup/ja/notifications.php --- resources/lang/vendor/backup/ja/notifications.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/vendor/backup/ja/notifications.php b/resources/lang/vendor/backup/ja/notifications.php index 911fa4ab2..b8fff4a2b 100644 --- a/resources/lang/vendor/backup/ja/notifications.php +++ b/resources/lang/vendor/backup/ja/notifications.php @@ -31,5 +31,5 @@ return [ 'unhealthy_backup_found_empty' => 'このアプリケーションのバックアップはありません。', 'unhealthy_backup_found_old' => ':date に作成されたバックアップは古すぎます。', 'unhealthy_backup_found_unknown' => '正確な原因が特定できませんでした。', - 'unhealthy_backup_found_full' => 'バックアップが使用できる容量(:disk_limit)を超えています。(現在の使用量 :disk_usage), + 'unhealthy_backup_found_full' => 'バックアップが使用できる容量(:disk_limit)を超えています。(現在の使用量 :disk_usage)', ]; From 6f0a6aeb3de31ed8dfd5df86ed2fc131969e45ee Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 7 Jan 2024 14:54:28 +0000 Subject: [PATCH 213/977] fix hadolint path --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f53559b37..d08ab8e73 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,7 +36,7 @@ jobs: - name: Docker Lint uses: hadolint/hadolint-action@v3.1.0 with: - dockerfile: contrib/docker/Dockerfile + dockerfile: Dockerfile failure-threshold: error build: From 4e567e34115e018fb96eb9ab199ff157de5a4981 Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Tue, 9 Jan 2024 04:49:01 +0000 Subject: [PATCH 214/977] Provide an informative error message when account size limit is reached --- resources/assets/js/components/ComposeModal.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 4ffd84666..2c4e3ba42 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -1204,12 +1204,19 @@ export default { }, 300); }).catch(function(e) { switch(e.response.status) { + case 403: + self.uploading = false; + io.value = null; + swal('Account size limit reached', 'Contact your admin for assistance.', 'error'); + self.page = 2; + break; + case 413: self.uploading = false; io.value = null; swal('File is too large', 'The file you uploaded has the size of ' + self.formatBytes(io.size) + '. Unfortunately, only images up to ' + self.formatBytes(self.config.uploader.max_photo_size * 1024) + ' are supported.\nPlease resize the file and try again.', 'error'); self.page = 2; - break; + break; case 451: self.uploading = false; From c53894fe16a6a147d4d6710f3a04c0780d350b4a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 01:35:15 -0700 Subject: [PATCH 215/977] Add Parental Controls feature --- .../Controllers/Auth/RegisterController.php | 4 +- .../ParentalControlsController.php | 207 ++++++++++++++++++ .../DispatchChildInvitePipeline.php | 38 ++++ app/Mail/ParentChildInvite.php | 49 +++++ app/Models/ParentalControls.php | 55 +++++ app/Services/UserRoleService.php | 72 ++++++ config/instance.php | 12 +- ..._052419_create_parental_controls_table.php | 45 ++++ resources/views/components/collapse.blade.php | 12 + .../emails/parental-controls/invite.blade.php | 18 ++ .../settings/parental-controls/add.blade.php | 59 +++++ .../parental-controls/checkbox.blade.php | 7 + .../parental-controls/child-status.blade.php | 44 ++++ .../parental-controls/delete-invite.blade.php | 32 +++ .../parental-controls/index.blade.php | 62 ++++++ .../invite-register-form.blade.php | 115 ++++++++++ .../parental-controls/manage.blade.php | 119 ++++++++++ .../parental-controls/stop-managing.blade.php | 32 +++ .../site/help/parental-controls.blade.php | 47 ++++ routes/web.php | 13 ++ 20 files changed, 1039 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/ParentalControlsController.php create mode 100644 app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php create mode 100644 app/Mail/ParentChildInvite.php create mode 100644 app/Models/ParentalControls.php create mode 100644 database/migrations/2024_01_09_052419_create_parental_controls_table.php create mode 100644 resources/views/components/collapse.blade.php create mode 100644 resources/views/emails/parental-controls/invite.blade.php create mode 100644 resources/views/settings/parental-controls/add.blade.php create mode 100644 resources/views/settings/parental-controls/checkbox.blade.php create mode 100644 resources/views/settings/parental-controls/child-status.blade.php create mode 100644 resources/views/settings/parental-controls/delete-invite.blade.php create mode 100644 resources/views/settings/parental-controls/index.blade.php create mode 100644 resources/views/settings/parental-controls/invite-register-form.blade.php create mode 100644 resources/views/settings/parental-controls/manage.blade.php create mode 100644 resources/views/settings/parental-controls/stop-managing.blade.php create mode 100644 resources/views/site/help/parental-controls.blade.php diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 5eb1159fe..8c10e5d0c 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -60,7 +60,7 @@ class RegisterController extends Controller * * @return \Illuminate\Contracts\Validation\Validator */ - protected function validator(array $data) + public function validator(array $data) { if(config('database.default') == 'pgsql') { $data['username'] = strtolower($data['username']); @@ -151,7 +151,7 @@ class RegisterController extends Controller * * @return \App\User */ - protected function create(array $data) + public function create(array $data) { if(config('database.default') == 'pgsql') { $data['username'] = strtolower($data['username']); diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php new file mode 100644 index 000000000..5c60cfae2 --- /dev/null +++ b/app/Http/Controllers/ParentalControlsController.php @@ -0,0 +1,207 @@ +user(), 404); + } + abort_unless(config('instance.parental_controls.enabled'), 404); + if(config_cache('pixelfed.open_registration') == false) { + abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404); + } + if($maxUserCheck == true) { + $hasLimit = config('pixelfed.enforce_max_users'); + if($hasLimit) { + $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count(); + $limit = (int) config('pixelfed.max_users'); + + abort_if($limit && $limit <= $count, 404); + } + } + } + + public function index(Request $request) + { + $this->authPreflight($request); + $children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5); + return view('settings.parental-controls.index', compact('children')); + } + + public function add(Request $request) + { + $this->authPreflight($request, true); + return view('settings.parental-controls.add'); + } + + public function view(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + return view('settings.parental-controls.manage', compact('pc')); + } + + public function update(Request $request, $id) + { + $this->authPreflight($request); + $uid = $request->user()->id; + $pc = ParentalControls::whereParentId($uid)->findOrFail($id); + $pc->permissions = $this->requestFormFields($request); + $pc->save(); + return redirect($pc->manageUrl() . '?permissions'); + } + + public function store(Request $request) + { + $this->authPreflight($request, true); + $this->validate($request, [ + 'email' => 'required|email|unique:parental_controls,email|unique:users,email', + ]); + + $state = $this->requestFormFields($request); + + $pc = new ParentalControls; + $pc->parent_id = $request->user()->id; + $pc->email = $request->input('email'); + $pc->verify_code = str_random(32); + $pc->permissions = $state; + $pc->save(); + + DispatchChildInvitePipeline::dispatch($pc); + return redirect($pc->manageUrl()); + } + + public function inviteRegister(Request $request, $id, $code) + { + $this->authPreflight($request, true, false); + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id); + abort_unless(User::whereId($pc->parent_id)->exists(), 404); + return view('settings.parental-controls.invite-register-form', compact('pc')); + } + + public function inviteRegisterStore(Request $request, $id, $code) + { + $this->authPreflight($request, true, false); + + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id); + + $fields = $request->all(); + $fields['email'] = $pc->email; + $defaults = UserRoleService::defaultRoles(); + $validator = (new RegisterController)->validator($fields); + $valid = $validator->validate(); + abort_if(!$valid, 404); + event(new Registered($user = (new RegisterController)->create($fields))); + sleep(5); + $user->has_roles = true; + $user->parent_id = $pc->parent_id; + if(config('instance.parental_controls.limits.auto_verify_email')) { + $user->email_verified_at = now(); + $user->save(); + sleep(3); + } else { + $user->save(); + sleep(3); + } + $ur = UserRoles::updateOrCreate([ + 'user_id' => $user->id, + ],[ + 'roles' => UserRoleService::mapInvite($user->id, $pc->permissions) + ]); + $pc->email_verified_at = now(); + $pc->child_id = $user->id; + $pc->save(); + sleep(2); + Auth::guard()->login($user); + + return redirect('/i/web'); + } + + public function cancelInvite(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.delete-invite', compact('pc')); + } + + public function cancelInviteHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + public function stopManaging(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + + return view('settings.parental-controls.stop-managing', compact('pc')); + } + + public function stopManagingHandle(Request $request, $id) + { + $this->authPreflight($request); + $pc = ParentalControls::whereParentId($request->user()->id) + ->whereNotNull(['email_verified_at', 'child_id']) + ->findOrFail($id); + $pc->child()->update([ + 'has_roles' => false, + 'parent_id' => null, + ]); + $pc->delete(); + + return redirect('/settings/parental-controls'); + } + + protected function requestFormFields($request) + { + $state = []; + $fields = [ + 'post', + 'comment', + 'like', + 'share', + 'follow', + 'bookmark', + 'story', + 'collection', + 'discovery_feeds', + 'dms', + 'federation', + 'hide_network', + 'private', + 'hide_cw' + ]; + + foreach ($fields as $field) { + $state[$field] = $request->input($field) == 'on'; + } + + return $state; + } +} diff --git a/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php new file mode 100644 index 000000000..a67f4e444 --- /dev/null +++ b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php @@ -0,0 +1,38 @@ +pc = $pc; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $pc = $this->pc; + + Mail::to($pc->email)->send(new ParentChildInvite($pc)); + } +} diff --git a/app/Mail/ParentChildInvite.php b/app/Mail/ParentChildInvite.php new file mode 100644 index 000000000..843ea472d --- /dev/null +++ b/app/Mail/ParentChildInvite.php @@ -0,0 +1,49 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/ParentalControls.php b/app/Models/ParentalControls.php new file mode 100644 index 000000000..83d47c18a --- /dev/null +++ b/app/Models/ParentalControls.php @@ -0,0 +1,55 @@ + 'array', + 'email_sent_at' => 'datetime', + 'email_verified_at' => 'datetime' + ]; + + protected $guarded = []; + + public function parent() + { + return $this->belongsTo(User::class, 'parent_id'); + } + + public function child() + { + return $this->belongsTo(User::class, 'child_id'); + } + + public function childAccount() + { + if($u = $this->child) { + if($u->profile_id) { + return AccountService::get($u->profile_id, true); + } else { + return []; + } + } else { + return []; + } + } + + public function manageUrl() + { + return url('/settings/parental-controls/manage/' . $this->id); + } + + public function inviteUrl() + { + return url('/auth/pci/' . $this->id . '/' . $this->verify_code); + } +} diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php index 500a4666e..a18810bf0 100644 --- a/app/Services/UserRoleService.php +++ b/app/Services/UserRoleService.php @@ -52,6 +52,13 @@ class UserRoleService 'can-follow' => false, 'can-make-public' => false, + + 'can-direct-message' => false, + 'can-use-stories' => false, + 'can-view-sensitive' => false, + 'can-bookmark' => false, + 'can-collections' => false, + 'can-federation' => false, ]; } @@ -114,6 +121,71 @@ class UserRoleService 'title' => 'Can make account public', 'action' => 'Allows the ability to make account public' ], + + 'can-direct-message' => [ + 'title' => '', + 'action' => '' + ], + 'can-use-stories' => [ + 'title' => '', + 'action' => '' + ], + 'can-view-sensitive' => [ + 'title' => '', + 'action' => '' + ], + 'can-bookmark' => [ + 'title' => '', + 'action' => '' + ], + 'can-collections' => [ + 'title' => '', + 'action' => '' + ], + 'can-federation' => [ + 'title' => '', + 'action' => '' + ], ]; } + + public static function mapInvite($id, $data = []) + { + $roles = self::get($id); + + $map = [ + 'account-force-private' => 'private', + 'account-ignore-follow-requests' => 'private', + + 'can-view-public-feed' => 'discovery_feeds', + 'can-view-network-feed' => 'discovery_feeds', + 'can-view-discover' => 'discovery_feeds', + 'can-view-hashtag-feed' => 'discovery_feeds', + + 'can-post' => 'post', + 'can-comment' => 'comment', + 'can-like' => 'like', + 'can-share' => 'share', + + 'can-follow' => 'follow', + 'can-make-public' => '!private', + + 'can-direct-message' => 'dms', + 'can-use-stories' => 'story', + 'can-view-sensitive' => '!hide_cw', + 'can-bookmark' => 'bookmark', + 'can-collections' => 'collection', + 'can-federation' => 'federation', + ]; + + foreach ($map as $key => $value) { + if(!isset($data[$value], $data[substr($value, 1)])) { + $map[$key] = false; + continue; + } + $map[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value]; + } + + return $map; + } } diff --git a/config/instance.php b/config/instance.php index 6357afe63..5e173684c 100644 --- a/config/instance.php +++ b/config/instance.php @@ -129,5 +129,15 @@ return [ 'banner' => [ 'blurhash' => env('INSTANCE_BANNER_BLURHASH', 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt') - ] + ], + + 'parental_controls' => [ + 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', true), + + 'limits' => [ + 'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true), + 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 10), + 'auto_verify_email' => true, + ], + ] ]; diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php new file mode 100644 index 000000000..4ef7fd2c7 --- /dev/null +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -0,0 +1,45 @@ +id(); + $table->unsignedInteger('parent_id')->index(); + $table->unsignedInteger('child_id')->unique()->index()->nullable(); + $table->string('email')->unique()->nullable(); + $table->string('verify_code')->nullable(); + $table->timestamp('email_sent_at')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->json('permissions')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::table('user_roles', function (Blueprint $table) { + $table->dropIndex('user_roles_profile_id_unique'); + $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('parental_controls'); + + Schema::table('user_roles', function (Blueprint $table) { + $table->dropIndex('user_roles_profile_id_unique'); + $table->unsignedBigInteger('profile_id')->unique()->index()->change(); + }); + } +}; diff --git a/resources/views/components/collapse.blade.php b/resources/views/components/collapse.blade.php new file mode 100644 index 000000000..144579366 --- /dev/null +++ b/resources/views/components/collapse.blade.php @@ -0,0 +1,12 @@ +@php +$cid = 'col' . str_random(6); +@endphp +

+ +

+ {{ $slot }} +
+

diff --git a/resources/views/emails/parental-controls/invite.blade.php b/resources/views/emails/parental-controls/invite.blade.php new file mode 100644 index 000000000..bece5b1bb --- /dev/null +++ b/resources/views/emails/parental-controls/invite.blade.php @@ -0,0 +1,18 @@ + +# You've been invited to join Pixelfed! + + +A parent account with the username **{{ $verify->parent->username }}** has invited you to join Pixelfed with a special youth account managed by them. + +If you do not recognize this account as your parents or a trusted guardian, please check with them first. + + + +Accept Invite + + +Thanks,
+Pixelfed + +This email is automatically generated. Please do not reply to this message. +
diff --git a/resources/views/settings/parental-controls/add.blade.php b/resources/views/settings/parental-controls/add.blade.php new file mode 100644 index 000000000..b7ca4c7ab --- /dev/null +++ b/resources/views/settings/parental-controls/add.blade.php @@ -0,0 +1,59 @@ +@extends('settings.template-vue') + +@section('section') + + @csrf +
+
+
+

+

Add child

+
+
+ +
+ +
+

Choose your child's policies

+ +
+

Allowed Actions

+ + @include('settings.parental-controls.checkbox', ['name' => 'post', 'title' => 'Post', 'checked' => true]) + @include('settings.parental-controls.checkbox', ['name' => 'comment', 'title' => 'Comment', 'checked' => true]) + @include('settings.parental-controls.checkbox', ['name' => 'like', 'title' => 'Like', 'checked' => true]) + @include('settings.parental-controls.checkbox', ['name' => 'share', 'title' => 'Share', 'checked' => true]) + @include('settings.parental-controls.checkbox', ['name' => 'follow', 'title' => 'Follow']) + @include('settings.parental-controls.checkbox', ['name' => 'bookmark', 'title' => 'Bookmark']) + @include('settings.parental-controls.checkbox', ['name' => 'story', 'title' => 'Add to story']) + @include('settings.parental-controls.checkbox', ['name' => 'collection', 'title' => 'Add to collection']) +
+
+

Enabled features

+ + @include('settings.parental-controls.checkbox', ['name' => 'discovery_feeds', 'title' => 'Discovery Feeds']) + @include('settings.parental-controls.checkbox', ['name' => 'dms', 'title' => 'Direct Messages']) + @include('settings.parental-controls.checkbox', ['name' => 'federation', 'title' => 'Federation']) +
+
+

Preferences

+ + @include('settings.parental-controls.checkbox', ['name' => 'hide_network', 'title' => 'Hide my child\'s connections']) + @include('settings.parental-controls.checkbox', ['name' => 'private', 'title' => 'Make my child\'s account private']) + @include('settings.parental-controls.checkbox', ['name' => 'hide_cw', 'title' => 'Hide sensitive media']) +
+
+ +
+
+ +

Where should we send this invite?

+ +
+ + +
+
+ +@endsection + diff --git a/resources/views/settings/parental-controls/checkbox.blade.php b/resources/views/settings/parental-controls/checkbox.blade.php new file mode 100644 index 000000000..b6cedbe92 --- /dev/null +++ b/resources/views/settings/parental-controls/checkbox.blade.php @@ -0,0 +1,7 @@ +@php +$id = str_random(6) . '_' . str_slug($name); +$defaultChecked = isset($checked) && $checked ? 'checked=""' : ''; +@endphp
+ + +
diff --git a/resources/views/settings/parental-controls/child-status.blade.php b/resources/views/settings/parental-controls/child-status.blade.php new file mode 100644 index 000000000..9852dacd7 --- /dev/null +++ b/resources/views/settings/parental-controls/child-status.blade.php @@ -0,0 +1,44 @@ +@if($state) +
+ @if($state === 'sent_invite') +
+ +

Child Invite Sent!

+ +
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+
+ @elseif($state === 'awaiting_email_confirmation') +
+ +

Child Invite Sent!

+ +
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+
+ @elseif($state === 'active') +
+ +

Child Account Active

+ +
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+ + View Account +
+ @endif +
+@else +@endif diff --git a/resources/views/settings/parental-controls/delete-invite.blade.php b/resources/views/settings/parental-controls/delete-invite.blade.php new file mode 100644 index 000000000..36b66ab15 --- /dev/null +++ b/resources/views/settings/parental-controls/delete-invite.blade.php @@ -0,0 +1,32 @@ +@extends('settings.template-vue') + +@section('section') +
+ @csrf +
+
+
+

+
+

Cancel child invite

+

Last updated: {{ $pc->updated_at->diffForHumans() }}

+
+
+
+
+
+
+ +
+

+ +

+

Are you sure you want to cancel this invite?

+

The child you invited will not be able to join if you cancel the invite.

+
+ + +
+
+ +@endsection diff --git a/resources/views/settings/parental-controls/index.blade.php b/resources/views/settings/parental-controls/index.blade.php new file mode 100644 index 000000000..a99093f33 --- /dev/null +++ b/resources/views/settings/parental-controls/index.blade.php @@ -0,0 +1,62 @@ +@extends('settings.template-vue') + +@section('section') +
+
+
+

+

Parental Controls

+
+
+ +
+ + @if($children->count()) + + @else +
+

You are not managing any children accounts.

+
+ @endif + +
+ + Add Child + + +
+ {{ $children->total() }}/{{ config('instance.parental_controls.limits.max_children') }} + children added +
+
+ +
+@endsection + diff --git a/resources/views/settings/parental-controls/invite-register-form.blade.php b/resources/views/settings/parental-controls/invite-register-form.blade.php new file mode 100644 index 000000000..5b894e8d2 --- /dev/null +++ b/resources/views/settings/parental-controls/invite-register-form.blade.php @@ -0,0 +1,115 @@ +@extends('layouts.app') + +@section('content') +
+
+
+ +
+
Create your Account
+ +
+
+ @csrf + + +
+
+ + + + @if ($errors->has('name')) + + {{ $errors->first('name') }} + + @endif +
+
+ +
+
+ + + + @if ($errors->has('username')) + + {{ $errors->first('username') }} + + @endif +
+
+ +
+
+ + + + @if ($errors->has('password')) + + {{ $errors->first('password') }} + + @endif +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + @if(config('captcha.enabled') || config('captcha.active.register')) +
+ {!! Captcha::display() !!} +
+ @endif + +

By signing up, you agree to our Terms of Use and Privacy Policy, in addition, you understand that your account is managed by {{ $pc->parent->username }} and they can limit your account without your permission. For more details, view the Parental Controls help center page.

+ +
+
+ +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/settings/parental-controls/manage.blade.php b/resources/views/settings/parental-controls/manage.blade.php new file mode 100644 index 000000000..a6cc62119 --- /dev/null +++ b/resources/views/settings/parental-controls/manage.blade.php @@ -0,0 +1,119 @@ +@extends('settings.template-vue') + +@section('section') +
+ @csrf +
+
+
+

+
+

Manage child

+

Last updated: {{ $pc->updated_at->diffForHumans() }}

+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+ @if(!$pc->child_id && !$pc->email_verified_at) + @include('settings.parental-controls.child-status', ['state' => 'sent_invite']) + @elseif($pc->child_id && !$pc->email_verified_at) + @include('settings.parental-controls.child-status', ['state' => 'awaiting_email_confirmation']) + @elseif($pc->child_id && $pc->email_verified_at) + @include('settings.parental-controls.child-status', ['state' => 'active']) + @else + @include('settings.parental-controls.child-status', ['state' => 'sent_invite']) + @endif +
+
+
+

Allowed Actions

+ + @include('settings.parental-controls.checkbox', ['name' => 'post', 'title' => 'Post', 'checked' => $pc->permissions['post']]) + @include('settings.parental-controls.checkbox', ['name' => 'comment', 'title' => 'Comment', 'checked' => $pc->permissions['comment']]) + @include('settings.parental-controls.checkbox', ['name' => 'like', 'title' => 'Like', 'checked' => $pc->permissions['like']]) + @include('settings.parental-controls.checkbox', ['name' => 'share', 'title' => 'Share', 'checked' => $pc->permissions['share']]) + @include('settings.parental-controls.checkbox', ['name' => 'follow', 'title' => 'Follow', 'checked' => $pc->permissions['follow']]) + @include('settings.parental-controls.checkbox', ['name' => 'bookmark', 'title' => 'Bookmark', 'checked' => $pc->permissions['bookmark']]) + @include('settings.parental-controls.checkbox', ['name' => 'story', 'title' => 'Add to story', 'checked' => $pc->permissions['story']]) + @include('settings.parental-controls.checkbox', ['name' => 'collection', 'title' => 'Add to collection', 'checked' => $pc->permissions['collection']]) +
+
+

Enabled features

+ + @include('settings.parental-controls.checkbox', ['name' => 'discovery_feeds', 'title' => 'Discovery Feeds', 'checked' => $pc->permissions['discovery_feeds']]) + @include('settings.parental-controls.checkbox', ['name' => 'dms', 'title' => 'Direct Messages', 'checked' => $pc->permissions['dms']]) + @include('settings.parental-controls.checkbox', ['name' => 'federation', 'title' => 'Federation', 'checked' => $pc->permissions['federation']]) +
+
+

Preferences

+ + @include('settings.parental-controls.checkbox', ['name' => 'hide_network', 'title' => 'Hide my child\'s connections', 'checked' => $pc->permissions['hide_network']]) + @include('settings.parental-controls.checkbox', ['name' => 'private', 'title' => 'Make my child\'s account private', 'checked' => $pc->permissions['private']]) + @include('settings.parental-controls.checkbox', ['name' => 'hide_cw', 'title' => 'Hide sensitive media', 'checked' => $pc->permissions['hide_cw']]) +
+
+
+
+
+ + +
+
+
+
+
+ @if(!$pc->child_id && !$pc->email_verified_at) +
+

Cancel Invite

+

Cancel the child invite and prevent it from being used.

+ Cancel Invite +
+ @else +
+

Stop Managing

+

Transition account to a regular account without parental controls.

+ Stop Managing Child +
+ @endif +
+
+
+
+ +
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/settings/parental-controls/stop-managing.blade.php b/resources/views/settings/parental-controls/stop-managing.blade.php new file mode 100644 index 000000000..747339fd8 --- /dev/null +++ b/resources/views/settings/parental-controls/stop-managing.blade.php @@ -0,0 +1,32 @@ +@extends('settings.template-vue') + +@section('section') +
+ @csrf +
+
+
+

+
+

Stop Managing Child

+

Last updated: {{ $pc->updated_at->diffForHumans() }}

+
+
+
+
+
+
+ +
+

+ +

+

Confirm Stop Managing this Account?

+

This child account will be transitioned to a regular account without any limitations.

+
+ + +
+
+ +@endsection diff --git a/resources/views/site/help/parental-controls.blade.php b/resources/views/site/help/parental-controls.blade.php new file mode 100644 index 000000000..d7b9710dd --- /dev/null +++ b/resources/views/site/help/parental-controls.blade.php @@ -0,0 +1,47 @@ +@extends('site.help.partial.template', ['breadcrumb'=>'Parental Controls']) + +@section('section') +
+

Parental Controls

+
+
+ +

In the digital age, ensuring your children's online safety is paramount. Designed with both fun and safety in mind, this feature allows parents to create child accounts, tailor-made for a worry-free social media experience.

+ +

Key Features:

+ +
    +
  • Child Account Creation: Easily set up a child account with just a few clicks. This account is linked to your own, giving you complete oversight.
  • +
  • Post Control: Decide if your child can post content. This allows you to ensure they're only sharing what's appropriate and safe.
  • +
  • Comment Management: Control whether your child can comment on posts. This helps in safeguarding them from unwanted interactions and maintaining a positive online environment.
  • +
  • Like & Share Restrictions: You have the power to enable or disable the ability to like and share posts. This feature helps in controlling the extent of your child's social media engagement.
  • +
  • Disable Federation: For added safety, you can choose to disable federation for your child's account, limiting their interaction to a more controlled environment.
  • +
+
+ + +
+ @if(config('instance.parental_controls.enabled')) +
    +
  1. Click here and tap on the Add Child button in the bottom left corner
  2. +
  3. Select the Allowed Actions, Enabled features and Preferences
  4. +
  5. Enter your childs email address
  6. +
  7. Press the Add Child buttton
  8. +
  9. Open your childs email and tap on the Accept Invite button in the email, ensure your parent username is present in the email
  10. +
  11. Fill out the child display name, username and password
  12. +
  13. Press Register and your child account will be active!
  14. +
+ @else +

This feature has been disabled by server admins.

+ @endif +
+
+ +@if(config('instance.parental_controls.enabled')) + +
+ You can create and manage up to {{ config('instance.parental_controls.limits.max_children') }} child accounts. +
+
+@endif +@endsection diff --git a/routes/web.php b/routes/web.php index b8149a605..dffd5e271 100644 --- a/routes/web.php +++ b/routes/web.php @@ -200,6 +200,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('auth/raw/mastodon/s/account-to-id', 'RemoteAuthController@accountToId'); Route::post('auth/raw/mastodon/s/finish-up', 'RemoteAuthController@finishUp'); Route::post('auth/raw/mastodon/s/login', 'RemoteAuthController@handleLogin'); + Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); + Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); Route::get('discover', 'DiscoverController@home')->name('discover'); @@ -534,6 +536,16 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact }); + Route::get('parental-controls', 'ParentalControlsController@index'); + Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add'); + Route::post('parental-controls/add', 'ParentalControlsController@store'); + Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view'); + Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update'); + Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite'); + Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle'); + Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing'); + Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle'); + Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone'); Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone'); Route::post('data-export/following', 'SettingsController@exportFollowing')->middleware('dangerzone'); @@ -618,6 +630,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::view('licenses', 'site.help.licenses')->name('help.licenses'); Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit'); Route::view('import', 'site.help.import')->name('help.import'); + Route::view('parental-controls', 'site.help.parental-controls'); }); Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); Route::get('newsroom/archive', 'NewsroomController@archive'); From ef57d471e56264090bdaa0e019db02912b002c87 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 01:50:51 -0700 Subject: [PATCH 216/977] Update migration --- ...3_12_27_082024_add_has_roles_to_users_table.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php index 09246e37b..f32fe599c 100644 --- a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -24,9 +24,17 @@ return new class extends Migration public function down(): void { Schema::table('users', function (Blueprint $table) { - $table->dropColumn('has_roles'); - $table->dropColumn('parent_id'); - $table->dropColumn('role_id'); + if (Schema::hasColumn('users', 'has_roles')) { + $table->dropColumn('has_roles'); + } + + if (Schema::hasColumn('users', 'role_id')) { + $table->dropColumn('role_id'); + } + + if (Schema::hasColumn('users', 'parent_id')) { + $table->dropColumn('parent_id'); + } }); } }; From 319a20b473eff60b00fe6e30a99c2bb9fa38a20e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 02:12:54 -0700 Subject: [PATCH 217/977] Update ParentalControlsController, redirect to new custom error page on active session when attempting to use child invite link so as to not overwrite parent active session with child session --- app/Http/Controllers/ParentalControlsController.php | 13 +++++++++++++ resources/views/errors/custom.blade.php | 10 ++++++++++ 2 files changed, 23 insertions(+) create mode 100644 resources/views/errors/custom.blade.php diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php index 5c60cfae2..1dc2f578f 100644 --- a/app/Http/Controllers/ParentalControlsController.php +++ b/app/Http/Controllers/ParentalControlsController.php @@ -87,7 +87,14 @@ class ParentalControlsController extends Controller public function inviteRegister(Request $request, $id, $code) { + if($request->user()) { + $title = 'You cannot complete this action on this device.'; + $body = 'Please log out or use a different device or browser to complete the invitation registration.'; + return view('errors.custom', compact('title', 'body')); + } + $this->authPreflight($request, true, false); + $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id); abort_unless(User::whereId($pc->parent_id)->exists(), 404); return view('settings.parental-controls.invite-register-form', compact('pc')); @@ -95,6 +102,12 @@ class ParentalControlsController extends Controller public function inviteRegisterStore(Request $request, $id, $code) { + if($request->user()) { + $title = 'You cannot complete this action on this device.'; + $body = 'Please log out or use a different device or browser to complete the invitation registration.'; + return view('errors.custom', compact('title', 'body')); + } + $this->authPreflight($request, true, false); $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id); diff --git a/resources/views/errors/custom.blade.php b/resources/views/errors/custom.blade.php new file mode 100644 index 000000000..932292b6b --- /dev/null +++ b/resources/views/errors/custom.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') +
+
+

{!! $title ?? config('instance.page.404.header')!!}

+

{!! $body ?? config('instance.page.404.body')!!}

+
+
+@endsection From 5f6ed85770a51591cf15d503afbf865fbac30c5f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 02:34:43 -0700 Subject: [PATCH 218/977] Update settings sidebar --- .../Controllers/Settings/HomeSettings.php | 312 +++++++++--------- resources/views/settings/email.blade.php | 85 ++--- .../views/settings/partial/sidebar.blade.php | 153 ++++----- 3 files changed, 262 insertions(+), 288 deletions(-) diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php index 082a72af0..99326c097 100644 --- a/app/Http/Controllers/Settings/HomeSettings.php +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -22,189 +22,189 @@ use App\Services\PronounService; trait HomeSettings { - public function home() - { - $id = Auth::user()->profile->id; - $storage = []; - $used = Media::whereProfileId($id)->sum('size'); - $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024; - $storage['used'] = $used; - $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100); - $storage['limitPretty'] = PrettyNumber::size($storage['limit']); - $storage['usedPretty'] = PrettyNumber::size($storage['used']); - $pronouns = PronounService::get($id); + public function home() + { + $id = Auth::user()->profile->id; + $storage = []; + $used = Media::whereProfileId($id)->sum('size'); + $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024; + $storage['used'] = $used; + $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100); + $storage['limitPretty'] = PrettyNumber::size($storage['limit']); + $storage['usedPretty'] = PrettyNumber::size($storage['used']); + $pronouns = PronounService::get($id); - return view('settings.home', compact('storage', 'pronouns')); - } + return view('settings.home', compact('storage', 'pronouns')); + } - public function homeUpdate(Request $request) - { - $this->validate($request, [ - 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), - 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'), - 'website' => 'nullable|url', - 'language' => 'nullable|string|min:2|max:5', - 'pronouns' => 'nullable|array|max:4' - ]); + public function homeUpdate(Request $request) + { + $this->validate($request, [ + 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), + 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'), + 'website' => 'nullable|url', + 'language' => 'nullable|string|min:2|max:5', + 'pronouns' => 'nullable|array|max:4' + ]); - $changes = false; - $name = strip_tags(Purify::clean($request->input('name'))); - $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null; - $website = $request->input('website'); - $language = $request->input('language'); - $user = Auth::user(); - $profile = $user->profile; - $pronouns = $request->input('pronouns'); - $existingPronouns = PronounService::get($profile->id); - $layout = $request->input('profile_layout'); - if($layout) { - $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; - } + $changes = false; + $name = strip_tags(Purify::clean($request->input('name'))); + $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null; + $website = $request->input('website'); + $language = $request->input('language'); + $user = Auth::user(); + $profile = $user->profile; + $pronouns = $request->input('pronouns'); + $existingPronouns = PronounService::get($profile->id); + $layout = $request->input('profile_layout'); + if($layout) { + $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; + } - $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification'); + $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification'); - // Only allow email to be updated if not yet verified - if (!$enforceEmailVerification || !$changes && $user->email_verified_at) { - if ($profile->name != $name) { - $changes = true; - $user->name = $name; - $profile->name = $name; - } + // Only allow email to be updated if not yet verified + if (!$enforceEmailVerification || !$changes && $user->email_verified_at) { + if ($profile->name != $name) { + $changes = true; + $user->name = $name; + $profile->name = $name; + } - if ($profile->website != $website) { - $changes = true; - $profile->website = $website; - } + if ($profile->website != $website) { + $changes = true; + $profile->website = $website; + } - if (strip_tags($profile->bio) != $bio) { - $changes = true; - $profile->bio = Autolink::create()->autolink($bio); - } + if (strip_tags($profile->bio) != $bio) { + $changes = true; + $profile->bio = Autolink::create()->autolink($bio); + } - if($user->language != $language && - in_array($language, \App\Util\Localization\Localization::languages()) - ) { - $changes = true; - $user->language = $language; - session()->put('locale', $language); - } + if($user->language != $language && + in_array($language, \App\Util\Localization\Localization::languages()) + ) { + $changes = true; + $user->language = $language; + session()->put('locale', $language); + } - if($existingPronouns != $pronouns) { - if($pronouns && in_array('Select Pronoun(s)', $pronouns)) { - PronounService::clear($profile->id); - } else { - PronounService::put($profile->id, $pronouns); - } - } - } + if($existingPronouns != $pronouns) { + if($pronouns && in_array('Select Pronoun(s)', $pronouns)) { + PronounService::clear($profile->id); + } else { + PronounService::put($profile->id, $pronouns); + } + } + } - if ($changes === true) { - $user->save(); - $profile->save(); - Cache::forget('user:account:id:'.$user->id); - AccountService::del($profile->id); - return redirect('/settings/home')->with('status', 'Profile successfully updated!'); - } + if ($changes === true) { + $user->save(); + $profile->save(); + Cache::forget('user:account:id:'.$user->id); + AccountService::del($profile->id); + return redirect('/settings/home')->with('status', 'Profile successfully updated!'); + } - return redirect('/settings/home'); - } + return redirect('/settings/home'); + } - public function password() - { - return view('settings.password'); - } + public function password() + { + return view('settings.password'); + } - public function passwordUpdate(Request $request) - { - $this->validate($request, [ - 'current' => 'required|string', - 'password' => 'required|string', - 'password_confirmation' => 'required|string', - ]); + public function passwordUpdate(Request $request) + { + $this->validate($request, [ + 'current' => 'required|string', + 'password' => 'required|string', + 'password_confirmation' => 'required|string', + ]); - $current = $request->input('current'); - $new = $request->input('password'); - $confirm = $request->input('password_confirmation'); + $current = $request->input('current'); + $new = $request->input('password'); + $confirm = $request->input('password_confirmation'); - $user = Auth::user(); + $user = Auth::user(); - if (password_verify($current, $user->password) && $new === $confirm) { - $user->password = bcrypt($new); - $user->save(); + if (password_verify($current, $user->password) && $new === $confirm) { + $user->password = bcrypt($new); + $user->save(); - $log = new AccountLog(); - $log->user_id = $user->id; - $log->item_id = $user->id; - $log->item_type = 'App\User'; - $log->action = 'account.edit.password'; - $log->message = 'Password changed'; - $log->link = null; - $log->ip_address = $request->ip(); - $log->user_agent = $request->userAgent(); - $log->save(); + $log = new AccountLog(); + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = 'App\User'; + $log->action = 'account.edit.password'; + $log->message = 'Password changed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); - Mail::to($request->user())->send(new PasswordChange($user)); - return redirect('/settings/home')->with('status', 'Password successfully updated!'); - } else { - return redirect()->back()->with('error', 'There was an error with your request! Please try again.'); - } + Mail::to($request->user())->send(new PasswordChange($user)); + return redirect('/settings/home')->with('status', 'Password successfully updated!'); + } else { + return redirect()->back()->with('error', 'There was an error with your request! Please try again.'); + } - } + } - public function email() - { - return view('settings.email'); - } + public function email() + { + return view('settings.email'); + } - public function emailUpdate(Request $request) - { - $this->validate($request, [ - 'email' => 'required|email|unique:users,email', - ]); - $changes = false; - $email = $request->input('email'); - $user = Auth::user(); - $profile = $user->profile; + public function emailUpdate(Request $request) + { + $this->validate($request, [ + 'email' => 'required|email|unique:users,email', + ]); + $changes = false; + $email = $request->input('email'); + $user = Auth::user(); + $profile = $user->profile; - $validate = config_cache('pixelfed.enforce_email_verification'); + $validate = config_cache('pixelfed.enforce_email_verification'); - if ($user->email != $email) { - $changes = true; - $user->email = $email; + if ($user->email != $email) { + $changes = true; + $user->email = $email; - if ($validate) { - $user->email_verified_at = null; - // Prevent old verifications from working - EmailVerification::whereUserId($user->id)->delete(); - } + if ($validate) { + // auto verify admin email addresses + $user->email_verified_at = $user->is_admin == true ? now() : null; + // Prevent old verifications from working + EmailVerification::whereUserId($user->id)->delete(); + } - $log = new AccountLog(); - $log->user_id = $user->id; - $log->item_id = $user->id; - $log->item_type = 'App\User'; - $log->action = 'account.edit.email'; - $log->message = 'Email changed'; - $log->link = null; - $log->ip_address = $request->ip(); - $log->user_agent = $request->userAgent(); - $log->save(); - } + $log = new AccountLog(); + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = 'App\User'; + $log->action = 'account.edit.email'; + $log->message = 'Email changed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); + } - if ($changes === true) { - Cache::forget('user:account:id:'.$user->id); - $user->save(); - $profile->save(); + if ($changes === true) { + Cache::forget('user:account:id:'.$user->id); + $user->save(); + $profile->save(); - return redirect('/settings/home')->with('status', 'Email successfully updated!'); - } else { - return redirect('/settings/email'); - } + return redirect('/settings/email')->with('status', 'Email successfully updated!'); + } else { + return redirect('/settings/email'); + } - } - - public function avatar() - { - return view('settings.avatar'); - } + } + public function avatar() + { + return view('settings.avatar'); + } } diff --git a/resources/views/settings/email.blade.php b/resources/views/settings/email.blade.php index 7286896b1..4b7ddd677 100644 --- a/resources/views/settings/email.blade.php +++ b/resources/views/settings/email.blade.php @@ -1,63 +1,36 @@ -@extends('layouts.app') +@extends('settings.template') -@section('content') -@if (session('status')) -
- {{ session('status') }} -
-@endif -@if ($errors->any()) -
- @foreach($errors->all() as $error) -

{{ $error }}

- @endforeach -
-@endif -@if (session('error')) -
- {{ session('error') }} -
-@endif +@section('section') -
-
-
-
-
-
-
-

Email Settings

-
-
-
- @csrf - - - - -
- - -

- @if(Auth::user()->email_verified_at) - Verified {{Auth::user()->email_verified_at->diffForHumans()}} - @else - Unverified You need to verify your email. - @endif -

-
-
-
- -
-
-
-
-
-
+
+
+

+

Email Settings

-
+
+
+ @csrf + + + +
+ + +

+ @if(Auth::user()->email_verified_at) + Verified {{Auth::user()->email_verified_at->diffForHumans()}} + @else + Unverified You need to verify your email. + @endif +

+
+
+
+ +
+
+
@endsection diff --git a/resources/views/settings/partial/sidebar.blade.php b/resources/views/settings/partial/sidebar.blade.php index a3837066a..2d5913550 100644 --- a/resources/views/settings/partial/sidebar.blade.php +++ b/resources/views/settings/partial/sidebar.blade.php @@ -1,79 +1,80 @@ -
- +
+ + @push('styles') + + @endpush From 58745a88081fae04d9057492e8fb5e04a645cdb1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 02:37:13 -0700 Subject: [PATCH 219/977] Update settings sidebar --- resources/views/settings/partial/sidebar.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/settings/partial/sidebar.blade.php b/resources/views/settings/partial/sidebar.blade.php index 2d5913550..b971e1f5d 100644 --- a/resources/views/settings/partial/sidebar.blade.php +++ b/resources/views/settings/partial/sidebar.blade.php @@ -17,9 +17,9 @@ - + --}} From 42298a2e9ccf14679cf18de0c757daa85f282e5f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 02:40:52 -0700 Subject: [PATCH 220/977] Apply dangerZone middleware to parental controls routes --- routes/web.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/routes/web.php b/routes/web.php index dffd5e271..e71e6fd9e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -536,15 +536,15 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact }); - Route::get('parental-controls', 'ParentalControlsController@index'); - Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add'); - Route::post('parental-controls/add', 'ParentalControlsController@store'); - Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view'); - Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update'); - Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite'); - Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle'); - Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing'); - Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle'); + Route::get('parental-controls', 'ParentalControlsController@index')->name('settings.parental-controls')->middleware('dangerzone'); + Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add')->middleware('dangerzone'); + Route::post('parental-controls/add', 'ParentalControlsController@store')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle')->middleware('dangerzone'); Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone'); Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone'); From 1a16ec2078db594d19dea77f59371af65600498c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 03:22:35 -0700 Subject: [PATCH 221/977] Update BookmarkController, add parental control support --- app/Http/Controllers/Api/ApiV1Controller.php | 2 + app/Http/Controllers/BookmarkController.php | 86 ++++++++++---------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 993df3555..8f41b38fb 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3438,6 +3438,7 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $pid = $request->user()->profile_id; + abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); @@ -3477,6 +3478,7 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $pid = $request->user()->profile_id; + abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php index a24520d64..d1d793dd2 100644 --- a/app/Http/Controllers/BookmarkController.php +++ b/app/Http/Controllers/BookmarkController.php @@ -8,60 +8,56 @@ use Auth; use Illuminate\Http\Request; use App\Services\BookmarkService; use App\Services\FollowerService; +use App\Services\UserRoleService; class BookmarkController extends Controller { - public function __construct() - { - $this->middleware('auth'); - } + public function __construct() + { + $this->middleware('auth'); + } - public function store(Request $request) - { - $this->validate($request, [ - 'item' => 'required|integer|min:1', - ]); + public function store(Request $request) + { + $this->validate($request, [ + 'item' => 'required|integer|min:1', + ]); - $profile = Auth::user()->profile; - $status = Status::findOrFail($request->input('item')); + $user = $request->user(); + $status = Status::findOrFail($request->input('item')); - abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); - abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); - abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); + abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); + abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); + abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404); + abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404); - if($status->scope == 'private') { - if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) { - if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) { - BookmarkService::del($profile->id, $status->id); - $exists->delete(); + if($status->scope == 'private') { + if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) { + if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) { + BookmarkService::del($user->profile_id, $status->id); + $exists->delete(); - if ($request->ajax()) { - return ['code' => 200, 'msg' => 'Bookmark removed!']; - } else { - return redirect()->back(); - } - } - abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.'); - } - } + if ($request->ajax()) { + return ['code' => 200, 'msg' => 'Bookmark removed!']; + } else { + return redirect()->back(); + } + } + abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.'); + } + } - $bookmark = Bookmark::firstOrCreate( - ['status_id' => $status->id], ['profile_id' => $profile->id] - ); + $bookmark = Bookmark::firstOrCreate( + ['status_id' => $status->id], ['profile_id' => $user->profile_id] + ); - if (!$bookmark->wasRecentlyCreated) { - BookmarkService::del($profile->id, $status->id); - $bookmark->delete(); - } else { - BookmarkService::add($profile->id, $status->id); - } + if (!$bookmark->wasRecentlyCreated) { + BookmarkService::del($user->profile_id, $status->id); + $bookmark->delete(); + } else { + BookmarkService::add($user->profile_id, $status->id); + } - if ($request->ajax()) { - $response = ['code' => 200, 'msg' => 'Bookmark saved!']; - } else { - $response = redirect()->back(); - } - - return $response; - } + return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back(); + } } From 2dcfc814958c9ebb623f876338bea35e0e689c5e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 04:40:25 -0700 Subject: [PATCH 222/977] Update ComposeController, add parental controls support --- app/Http/Controllers/Api/ApiV1Controller.php | 11 + app/Http/Controllers/ComposeController.php | 1280 +++++++++--------- 2 files changed, 661 insertions(+), 630 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 8f41b38fb..dde416064 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1750,6 +1750,8 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); $media = Media::whereUserId($user->id) @@ -2983,6 +2985,15 @@ class ApiV1Controller extends Controller $in_reply_to_id = $request->input('in_reply_to_id'); $user = $request->user(); + + if($user->has_roles) { + if($in_reply_to_id != null) { + abort_if(!UserRoleService::can('can-comment', $user->id), 403, 'Invalid permissions for this action'); + } else { + abort_if(!UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + } + } + $profile = $user->profile; $limitKey = 'compose:rate-limit:store:' . $user->id; diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index e79625861..e17a37fd7 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -6,26 +6,26 @@ use Illuminate\Http\Request; use Auth, Cache, DB, Storage, URL; use Carbon\Carbon; use App\{ - Avatar, - Collection, - CollectionItem, - Hashtag, - Like, - Media, - MediaTag, - Notification, - Profile, - Place, - Status, - UserFilter, - UserSetting + Avatar, + Collection, + CollectionItem, + Hashtag, + Like, + Media, + MediaTag, + Notification, + Profile, + Place, + Status, + UserFilter, + UserSetting }; use App\Models\Poll; use App\Transformer\Api\{ - MediaTransformer, - MediaDraftTransformer, - StatusTransformer, - StatusStatelessTransformer + MediaTransformer, + MediaDraftTransformer, + StatusTransformer, + StatusStatelessTransformer }; use League\Fractal; use App\Util\Media\Filter; @@ -36,9 +36,9 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize; use App\Jobs\ImageOptimizePipeline\ImageThumbnail; use App\Jobs\StatusPipeline\NewStatusPipeline; use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail + VideoOptimize, + VideoPostProcess, + VideoThumbnail }; use App\Services\AccountService; use App\Services\CollectionService; @@ -58,230 +58,234 @@ use App\Services\UserRoleService; class ComposeController extends Controller { - protected $fractal; + protected $fractal; - public function __construct() - { - $this->middleware('auth'); - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); - } + public function __construct() + { + $this->middleware('auth'); + $this->fractal = new Fractal\Manager(); + $this->fractal->setSerializer(new ArraySerializer()); + } - public function show(Request $request) - { - return view('status.compose'); - } + public function show(Request $request) + { + return view('status.compose'); + } - public function mediaUpload(Request $request) - { - abort_if(!$request->user(), 403); + public function mediaUpload(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24' - ]); + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24' + ]); - $user = Auth::user(); - $profile = $user->profile; - abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + $user = Auth::user(); + $profile = $user->profile; + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - return $dailyLimit >= 1250; - }); + return $dailyLimit >= 1250; + }); - abort_if($limitReached == true, 429); + abort_if($limitReached == true, 429); - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } + if(config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + return Media::whereUserId($user->id)->sum('size') / 1000; + }); + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($size >= $limit) { + abort(403, 'Account size limit reached.'); + } + } - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - $photo = $request->file('file'); + $photo = $request->file('file'); - $mimes = explode(',', config_cache('pixelfed.media_types')); + $mimes = explode(',', config_cache('pixelfed.media_types')); - abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); + abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $mime = $photo->getMimeType(); + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $mime = $photo->getMimeType(); - abort_if(MediaBlocklistService::exists($hash) == true, 451); + abort_if(MediaBlocklistService::exists($hash) == true, 451); - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - $media->version = 3; - $media->save(); + $media = new Media(); + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + $media->version = 3; + $media->save(); - $preview_url = $media->url() . '?v=' . time(); - $url = $media->url() . '?v=' . time(); + $preview_url = $media->url() . '?v=' . time(); + $url = $media->url() . '?v=' . time(); - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - case 'image/webp': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + case 'image/webp': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; - default: - break; - } + default: + break; + } - Cache::forget($limitKey); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $this->fractal->createData($resource)->toArray(); - $res['preview_url'] = $preview_url; - $res['url'] = $url; - return response()->json($res); - } + Cache::forget($limitKey); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + $res['preview_url'] = $preview_url; + $res['url'] = $url; + return response()->json($res); + } - public function mediaUpdate(Request $request) - { - $this->validate($request, [ - 'id' => 'required', - 'file' => function() { - return [ - 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - ]); + public function mediaUpdate(Request $request) + { + $this->validate($request, [ + 'id' => 'required', + 'file' => function() { + return [ + 'required', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ]; + }, + ]); - $user = Auth::user(); - abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + $user = Auth::user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:media-updates:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + $limitKey = 'compose:rate-limit:media-updates:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - return $dailyLimit >= 1500; - }); + return $dailyLimit >= 1500; + }); - abort_if($limitReached == true, 429); + abort_if($limitReached == true, 429); - $photo = $request->file('file'); - $id = $request->input('id'); + $photo = $request->file('file'); + $id = $request->input('id'); - $media = Media::whereUserId($user->id) - ->whereProfileId($user->profile_id) - ->whereNull('status_id') - ->findOrFail($id); + $media = Media::whereUserId($user->id) + ->whereProfileId($user->profile_id) + ->whereNull('status_id') + ->findOrFail($id); - $media->save(); + $media->save(); - $fragments = explode('/', $media->media_path); - $name = last($fragments); - array_pop($fragments); - $dir = implode('/', $fragments); - $path = $photo->storePubliclyAs($dir, $name); - $res = [ - 'url' => $media->url() . '?v=' . time() - ]; - ImageOptimize::dispatch($media)->onQueue('mmo'); - Cache::forget($limitKey); - return $res; - } + $fragments = explode('/', $media->media_path); + $name = last($fragments); + array_pop($fragments); + $dir = implode('/', $fragments); + $path = $photo->storePubliclyAs($dir, $name); + $res = [ + 'url' => $media->url() . '?v=' . time() + ]; + ImageOptimize::dispatch($media)->onQueue('mmo'); + Cache::forget($limitKey); + return $res; + } - public function mediaDelete(Request $request) - { - abort_if(!$request->user(), 403); + public function mediaDelete(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'id' => 'required|integer|min:1|exists:media,id' - ]); + $this->validate($request, [ + 'id' => 'required|integer|min:1|exists:media,id' + ]); - $media = Media::whereNull('status_id') - ->whereUserId(Auth::id()) - ->findOrFail($request->input('id')); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - MediaStorageService::delete($media, true); + $media = Media::whereNull('status_id') + ->whereUserId(Auth::id()) + ->findOrFail($request->input('id')); - return response()->json([ - 'msg' => 'Successfully deleted', - 'code' => 200 - ]); - } + MediaStorageService::delete($media, true); - public function searchTag(Request $request) - { - abort_if(!$request->user(), 403); + return response()->json([ + 'msg' => 'Successfully deleted', + 'code' => 200 + ]); + } - $this->validate($request, [ - 'q' => 'required|string|min:1|max:50' - ]); + public function searchTag(Request $request) + { + abort_if(!$request->user(), 403); - $q = $request->input('q'); + $this->validate($request, [ + 'q' => 'required|string|min:1|max:50' + ]); - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - $q = mb_substr($q, 1); - } + $q = $request->input('q'); - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); + if(Str::of($q)->startsWith('@')) { + if(strlen($q) < 3) { + return []; + } + $q = mb_substr($q, 1); + } - $blocked->push($request->user()->profile_id); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->whereNull('domain') - ->where('username','like','%'.$q.'%') - ->limit(15) - ->get() - ->map(function($r) { - return [ - 'id' => (string) $r->id, - 'name' => $r->username, - 'privacy' => true, - 'avatar' => $r->avatarUrl() - ]; - }); + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); - return $results; - } + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id','domain','username') + ->whereNotIn('id', $blocked) + ->whereNull('domain') + ->where('username','like','%'.$q.'%') + ->limit(15) + ->get() + ->map(function($r) { + return [ + 'id' => (string) $r->id, + 'name' => $r->username, + 'privacy' => true, + 'avatar' => $r->avatarUrl() + ]; + }); + + return $results; + } public function searchUntag(Request $request) { @@ -292,6 +296,8 @@ class ComposeController extends Controller 'profile_id' => 'required' ]); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + $user = $request->user(); $status_id = $request->input('status_id'); $profile_id = (int) $request->input('profile_id'); @@ -316,506 +322,520 @@ class ComposeController extends Controller return [200]; } - public function searchLocation(Request $request) - { - abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|max:100' - ]); - $pid = $request->user()->profile_id; - abort_if(!$pid, 400); - $q = e($request->input('q')); + public function searchLocation(Request $request) + { + abort_if(!$request->user(), 403); + $this->validate($request, [ + 'q' => 'required|string|max:100' + ]); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; + abort_if(!$pid, 400); + $q = e($request->input('q')); - $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() { - $minId = SnowflakeService::byDate(now()->subDays(290)); - if(config('database.default') == 'pgsql') { - return Status::selectRaw('id, place_id, count(place_id) as pc') - ->whereNotNull('place_id') - ->where('id', '>', $minId) - ->orderByDesc('pc') - ->groupBy(['place_id', 'id']) - ->limit(400) - ->get() - ->filter(function($post) { - return $post; - }) - ->map(function($place) { - return [ - 'id' => $place->place_id, - 'count' => $place->pc - ]; - }) - ->unique('id') - ->values(); - } - return Status::selectRaw('id, place_id, count(place_id) as pc') - ->whereNotNull('place_id') - ->where('id', '>', $minId) - ->groupBy('place_id') - ->orderByDesc('pc') - ->limit(400) - ->get() - ->filter(function($post) { - return $post; - }) - ->map(function($place) { - return [ - 'id' => $place->place_id, - 'count' => $place->pc - ]; - }); - }); - $q = '%' . $q . '%'; - $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; + $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() { + $minId = SnowflakeService::byDate(now()->subDays(290)); + if(config('database.default') == 'pgsql') { + return Status::selectRaw('id, place_id, count(place_id) as pc') + ->whereNotNull('place_id') + ->where('id', '>', $minId) + ->orderByDesc('pc') + ->groupBy(['place_id', 'id']) + ->limit(400) + ->get() + ->filter(function($post) { + return $post; + }) + ->map(function($place) { + return [ + 'id' => $place->place_id, + 'count' => $place->pc + ]; + }) + ->unique('id') + ->values(); + } + return Status::selectRaw('id, place_id, count(place_id) as pc') + ->whereNotNull('place_id') + ->where('id', '>', $minId) + ->groupBy('place_id') + ->orderByDesc('pc') + ->limit(400) + ->get() + ->filter(function($post) { + return $post; + }) + ->map(function($place) { + return [ + 'id' => $place->place_id, + 'count' => $place->pc + ]; + }); + }); + $q = '%' . $q . '%'; + $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; - $places = DB::table('places') - ->where('name', $wildcard, $q) - ->limit((strlen($q) > 5 ? 360 : 30)) - ->get() - ->sortByDesc(function($place, $key) use($popular) { - return $popular->filter(function($p) use($place) { - return $p['id'] == $place->id; - })->map(function($p) use($place) { - return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; - })->values(); - }) - ->map(function($r) { - return [ - 'id' => $r->id, - 'name' => $r->name, - 'country' => $r->country, - 'url' => url('/discover/places/' . $r->id . '/' . $r->slug) - ]; - }) - ->values() - ->all(); - return $places; - } + $places = DB::table('places') + ->where('name', $wildcard, $q) + ->limit((strlen($q) > 5 ? 360 : 30)) + ->get() + ->sortByDesc(function($place, $key) use($popular) { + return $popular->filter(function($p) use($place) { + return $p['id'] == $place->id; + })->map(function($p) use($place) { + return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; + })->values(); + }) + ->map(function($r) { + return [ + 'id' => $r->id, + 'name' => $r->name, + 'country' => $r->country, + 'url' => url('/discover/places/' . $r->id . '/' . $r->slug) + ]; + }) + ->values() + ->all(); + return $places; + } - public function searchMentionAutocomplete(Request $request) - { - abort_if(!$request->user(), 403); + public function searchMentionAutocomplete(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50' + ]); - $q = $request->input('q'); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - } + $q = $request->input('q'); - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); + if(Str::of($q)->startsWith('@')) { + if(strlen($q) < 3) { + return []; + } + } - $blocked->push($request->user()->profile_id); + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->where('username','like','%'.$q.'%') - ->groupBy('id', 'domain') - ->limit(15) - ->get() - ->map(function($profile) { - $username = $profile->domain ? substr($profile->username, 1) : $profile->username; + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id','domain','username') + ->whereNotIn('id', $blocked) + ->where('username','like','%'.$q.'%') + ->groupBy('id', 'domain') + ->limit(15) + ->get() + ->map(function($profile) { + $username = $profile->domain ? substr($profile->username, 1) : $profile->username; return [ 'key' => '@' . str_limit($username, 30), 'value' => $username, ]; - }); + }); - return $results; - } + return $results; + } - public function searchHashtagAutocomplete(Request $request) - { - abort_if(!$request->user(), 403); + public function searchHashtagAutocomplete(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50' + ]); - $q = $request->input('q'); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $results = Hashtag::select('slug') - ->where('slug', 'like', '%'.$q.'%') - ->whereIsNsfw(false) - ->whereIsBanned(false) - ->limit(5) - ->get() - ->map(function($tag) { - return [ - 'key' => '#' . $tag->slug, - 'value' => $tag->slug - ]; - }); + $q = $request->input('q'); - return $results; - } + $results = Hashtag::select('slug') + ->where('slug', 'like', '%'.$q.'%') + ->whereIsNsfw(false) + ->whereIsBanned(false) + ->limit(5) + ->get() + ->map(function($tag) { + return [ + 'key' => '#' . $tag->slug, + 'value' => $tag->slug + ]; + }); - public function store(Request $request) - { - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'media.*' => 'required', - 'media.*.id' => 'required|integer|min:1', - 'media.*.filter_class' => 'nullable|alpha_dash|max:30', - 'media.*.license' => 'nullable|string|max:140', - 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', - 'place' => 'nullable', - 'comments_disabled' => 'nullable', - 'tagged' => 'nullable', - 'license' => 'nullable|integer|min:1|max:16', - 'collections' => 'sometimes|array|min:1|max:5', - 'spoiler_text' => 'nullable|string|max:140', - // 'optimize_media' => 'nullable' - ]); + return $results; + } - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } + public function store(Request $request) + { + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), + 'media.*' => 'required', + 'media.*.id' => 'required|integer|min:1', + 'media.*.filter_class' => 'nullable|alpha_dash|max:30', + 'media.*.license' => 'nullable|string|max:140', + 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', + 'place' => 'nullable', + 'comments_disabled' => 'nullable', + 'tagged' => 'nullable', + 'license' => 'nullable|integer|min:1|max:16', + 'collections' => 'sometimes|array|min:1|max:5', + 'spoiler_text' => 'nullable|string|max:140', + // 'optimize_media' => 'nullable' + ]); - $user = Auth::user(); - $profile = $user->profile; + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $limitKey = 'compose:rate-limit:store:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Status::whereProfileId($user->profile_id) - ->whereNull('in_reply_to_id') - ->whereNull('reblog_of_id') - ->where('created_at', '>', now()->subDays(1)) - ->count(); + if(config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if($blockedKeywords !== null && $request->caption) { + $keywords = config('costar.keyword.block'); + foreach($keywords as $kw) { + if(Str::contains($request->caption, $kw) == true) { + abort(400, 'Invalid object'); + } + } + } + } - return $dailyLimit >= 1000; - }); + $user = $request->user(); + $profile = $user->profile; - abort_if($limitReached == true, 429); + $limitKey = 'compose:rate-limit:store:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Status::whereProfileId($user->profile_id) + ->whereNull('in_reply_to_id') + ->whereNull('reblog_of_id') + ->where('created_at', '>', now()->subDays(1)) + ->count(); - $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null; + return $dailyLimit >= 1000; + }); - $visibility = $request->input('visibility'); - $medias = $request->input('media'); - $attachments = []; - $status = new Status; - $mimes = []; - $place = $request->input('place'); - $cw = $request->input('cw'); - $tagged = $request->input('tagged'); - $optimize_media = (bool) $request->input('optimize_media'); + abort_if($limitReached == true, 429); - foreach($medias as $k => $media) { - if($k + 1 > config_cache('pixelfed.max_album_length')) { - continue; - } - $m = Media::findOrFail($media['id']); - if($m->profile_id !== $profile->id || $m->status_id) { - abort(403, 'Invalid media id'); - } - $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; - $m->license = $license; - $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; - $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; + $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null; - if($cw == true || $profile->cw == true) { - $m->is_nsfw = $cw; - $status->is_nsfw = $cw; - } - $m->save(); - $attachments[] = $m; - array_push($mimes, $m->mime); - } + $visibility = $request->input('visibility'); + $medias = $request->input('media'); + $attachments = []; + $status = new Status; + $mimes = []; + $place = $request->input('place'); + $cw = $request->input('cw'); + $tagged = $request->input('tagged'); + $optimize_media = (bool) $request->input('optimize_media'); - abort_if(empty($attachments), 422); + foreach($medias as $k => $media) { + if($k + 1 > config_cache('pixelfed.max_album_length')) { + continue; + } + $m = Media::findOrFail($media['id']); + if($m->profile_id !== $profile->id || $m->status_id) { + abort(403, 'Invalid media id'); + } + $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; + $m->license = $license; + $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; + $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; - $mediaType = StatusController::mimeTypeCheck($mimes); + if($cw == true || $profile->cw == true) { + $m->is_nsfw = $cw; + $status->is_nsfw = $cw; + } + $m->save(); + $attachments[] = $m; + array_push($mimes, $m->mime); + } - if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { - abort(400, __('exception.compose.invalid.album')); - } + abort_if(empty($attachments), 422); - if($place && is_array($place)) { - $status->place_id = $place['id']; - } + $mediaType = StatusController::mimeTypeCheck($mimes); - if($request->filled('comments_disabled')) { - $status->comments_disabled = (bool) $request->input('comments_disabled'); - } + if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { + abort(400, __('exception.compose.invalid.album')); + } - if($request->filled('spoiler_text') && $cw) { - $status->cw_summary = $request->input('spoiler_text'); - } + if($place && is_array($place)) { + $status->place_id = $place['id']; + } - $status->caption = strip_tags($request->caption); - $status->rendered = Autolink::create()->autolink($status->caption); - $status->scope = 'draft'; - $status->visibility = 'draft'; - $status->profile_id = $profile->id; - $status->save(); + if($request->filled('comments_disabled')) { + $status->comments_disabled = (bool) $request->input('comments_disabled'); + } - foreach($attachments as $media) { - $media->status_id = $status->id; - $media->save(); - } + if($request->filled('spoiler_text') && $cw) { + $status->cw_summary = $request->input('spoiler_text'); + } - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - $visibility = $profile->is_private ? 'private' : $visibility; - $cw = $profile->cw == true ? true : $cw; - $status->is_nsfw = $cw; - $status->visibility = $visibility; - $status->scope = $visibility; - $status->type = $mediaType; - $status->save(); + $status->caption = strip_tags($request->caption); + $status->rendered = Autolink::create()->autolink($status->caption); + $status->scope = 'draft'; + $status->visibility = 'draft'; + $status->profile_id = $profile->id; + $status->save(); - foreach($tagged as $tg) { - $mt = new MediaTag; - $mt->status_id = $status->id; - $mt->media_id = $status->media->first()->id; - $mt->profile_id = $tg['id']; - $mt->tagged_username = $tg['name']; - $mt->is_public = true; - $mt->metadata = json_encode([ - '_v' => 1, - ]); - $mt->save(); - MediaTagService::set($mt->status_id, $mt->profile_id); - MediaTagService::sendNotification($mt); - } + foreach($attachments as $media) { + $media->status_id = $status->id; + $media->save(); + } - if($request->filled('collections')) { - $collections = Collection::whereProfileId($profile->id) - ->find($request->input('collections')) - ->each(function($collection) use($status) { - $count = $collection->items()->count(); - CollectionItem::firstOrCreate([ - 'collection_id' => $collection->id, - 'object_type' => 'App\Status', - 'object_id' => $status->id - ], [ - 'order' => $count - ]); + $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; + $visibility = $profile->is_private ? 'private' : $visibility; + $cw = $profile->cw == true ? true : $cw; + $status->is_nsfw = $cw; + $status->visibility = $visibility; + $status->scope = $visibility; + $status->type = $mediaType; + $status->save(); - CollectionService::addItem( - $collection->id, - $status->id, - $count - ); + foreach($tagged as $tg) { + $mt = new MediaTag; + $mt->status_id = $status->id; + $mt->media_id = $status->media->first()->id; + $mt->profile_id = $tg['id']; + $mt->tagged_username = $tg['name']; + $mt->is_public = true; + $mt->metadata = json_encode([ + '_v' => 1, + ]); + $mt->save(); + MediaTagService::set($mt->status_id, $mt->profile_id); + MediaTagService::sendNotification($mt); + } - $collection->updated_at = now(); + if($request->filled('collections')) { + $collections = Collection::whereProfileId($profile->id) + ->find($request->input('collections')) + ->each(function($collection) use($status) { + $count = $collection->items()->count(); + CollectionItem::firstOrCreate([ + 'collection_id' => $collection->id, + 'object_type' => 'App\Status', + 'object_id' => $status->id + ], [ + 'order' => $count + ]); + + CollectionService::addItem( + $collection->id, + $status->id, + $count + ); + + $collection->updated_at = now(); $collection->save(); CollectionService::setCollection($collection->id, $collection); - }); - } + }); + } - NewStatusPipeline::dispatch($status); - Cache::forget('user:account:id:'.$profile->user_id); - Cache::forget('_api:statuses:recent_9:'.$profile->id); - Cache::forget('profile:status_count:'.$profile->id); - Cache::forget('status:transformer:media:attachments:'.$status->id); - Cache::forget($user->storageUsedKey()); - Cache::forget('profile:embed:' . $status->profile_id); - Cache::forget($limitKey); + NewStatusPipeline::dispatch($status); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('_api:statuses:recent_9:'.$profile->id); + Cache::forget('profile:status_count:'.$profile->id); + Cache::forget('status:transformer:media:attachments:'.$status->id); + Cache::forget($user->storageUsedKey()); + Cache::forget('profile:embed:' . $status->profile_id); + Cache::forget($limitKey); - return $status->url(); - } + return $status->url(); + } - public function storeText(Request $request) - { - abort_unless(config('exp.top'), 404); - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', - 'place' => 'nullable', - 'comments_disabled' => 'nullable', - 'tagged' => 'nullable', - ]); + public function storeText(Request $request) + { + abort_unless(config('exp.top'), 404); + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', + 'place' => 'nullable', + 'comments_disabled' => 'nullable', + 'tagged' => 'nullable', + ]); - if(config('costar.enabled') == true) { - $blockedKeywords = config('costar.keyword.block'); - if($blockedKeywords !== null && $request->caption) { - $keywords = config('costar.keyword.block'); - foreach($keywords as $kw) { - if(Str::contains($request->caption, $kw) == true) { - abort(400, 'Invalid object'); - } - } - } - } + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $user = Auth::user(); - $profile = $user->profile; - $visibility = $request->input('visibility'); - $status = new Status; - $place = $request->input('place'); - $cw = $request->input('cw'); - $tagged = $request->input('tagged'); + if(config('costar.enabled') == true) { + $blockedKeywords = config('costar.keyword.block'); + if($blockedKeywords !== null && $request->caption) { + $keywords = config('costar.keyword.block'); + foreach($keywords as $kw) { + if(Str::contains($request->caption, $kw) == true) { + abort(400, 'Invalid object'); + } + } + } + } - if($place && is_array($place)) { - $status->place_id = $place['id']; - } + $user = $request->user(); + $profile = $user->profile; + $visibility = $request->input('visibility'); + $status = new Status; + $place = $request->input('place'); + $cw = $request->input('cw'); + $tagged = $request->input('tagged'); - if($request->filled('comments_disabled')) { - $status->comments_disabled = (bool) $request->input('comments_disabled'); - } + if($place && is_array($place)) { + $status->place_id = $place['id']; + } - $status->caption = strip_tags($request->caption); - $status->profile_id = $profile->id; - $entities = []; - $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; - $cw = $profile->cw == true ? true : $cw; - $status->is_nsfw = $cw; - $status->visibility = $visibility; - $status->scope = $visibility; - $status->type = 'text'; - $status->rendered = Autolink::create()->autolink($status->caption); - $status->entities = json_encode(array_merge([ - 'timg' => [ - 'version' => 0, - 'bg_id' => 1, - 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', - 'length' => strlen($status->caption), - ] - ], $entities), JSON_UNESCAPED_SLASHES); - $status->save(); + if($request->filled('comments_disabled')) { + $status->comments_disabled = (bool) $request->input('comments_disabled'); + } - foreach($tagged as $tg) { - $mt = new MediaTag; - $mt->status_id = $status->id; - $mt->media_id = $status->media->first()->id; - $mt->profile_id = $tg['id']; - $mt->tagged_username = $tg['name']; - $mt->is_public = true; - $mt->metadata = json_encode([ - '_v' => 1, - ]); - $mt->save(); - MediaTagService::set($mt->status_id, $mt->profile_id); - MediaTagService::sendNotification($mt); - } + $status->caption = strip_tags($request->caption); + $status->profile_id = $profile->id; + $entities = []; + $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; + $cw = $profile->cw == true ? true : $cw; + $status->is_nsfw = $cw; + $status->visibility = $visibility; + $status->scope = $visibility; + $status->type = 'text'; + $status->rendered = Autolink::create()->autolink($status->caption); + $status->entities = json_encode(array_merge([ + 'timg' => [ + 'version' => 0, + 'bg_id' => 1, + 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', + 'length' => strlen($status->caption), + ] + ], $entities), JSON_UNESCAPED_SLASHES); + $status->save(); + + foreach($tagged as $tg) { + $mt = new MediaTag; + $mt->status_id = $status->id; + $mt->media_id = $status->media->first()->id; + $mt->profile_id = $tg['id']; + $mt->tagged_username = $tg['name']; + $mt->is_public = true; + $mt->metadata = json_encode([ + '_v' => 1, + ]); + $mt->save(); + MediaTagService::set($mt->status_id, $mt->profile_id); + MediaTagService::sendNotification($mt); + } - Cache::forget('user:account:id:'.$profile->user_id); - Cache::forget('_api:statuses:recent_9:'.$profile->id); - Cache::forget('profile:status_count:'.$profile->id); + Cache::forget('user:account:id:'.$profile->user_id); + Cache::forget('_api:statuses:recent_9:'.$profile->id); + Cache::forget('profile:status_count:'.$profile->id); - return $status->url(); - } + return $status->url(); + } - public function mediaProcessingCheck(Request $request) - { - $this->validate($request, [ - 'id' => 'required|integer|min:1' - ]); + public function mediaProcessingCheck(Request $request) + { + $this->validate($request, [ + 'id' => 'required|integer|min:1' + ]); - $media = Media::whereUserId($request->user()->id) - ->whereNull('status_id') - ->findOrFail($request->input('id')); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - if(config('pixelfed.media_fast_process')) { - return [ - 'finished' => true - ]; - } + $media = Media::whereUserId($request->user()->id) + ->whereNull('status_id') + ->findOrFail($request->input('id')); - $finished = false; + if(config('pixelfed.media_fast_process')) { + return [ + 'finished' => true + ]; + } - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - case 'video/mp4': - $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; - break; + $finished = false; - default: - # code... - break; - } + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + case 'video/mp4': + $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; + break; - return [ - 'finished' => $finished - ]; - } + default: + # code... + break; + } - public function composeSettings(Request $request) - { - $uid = $request->user()->id; - $default = [ - 'default_license' => 1, - 'media_descriptions' => false, - 'max_altext_length' => config_cache('pixelfed.max_altext_length') - ]; - $settings = AccountService::settings($uid); - if(isset($settings['other']) && isset($settings['other']['scope'])) { - $s = $settings['compose_settings']; - $s['default_scope'] = $settings['other']['scope']; - $settings['compose_settings'] = $s; - } + return [ + 'finished' => $finished + ]; + } - return array_merge($default, $settings['compose_settings']); - } + public function composeSettings(Request $request) + { + $uid = $request->user()->id; + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - public function createPoll(Request $request) - { - $this->validate($request, [ - 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), - 'cw' => 'nullable|boolean', - 'visibility' => 'required|string|in:public,private', - 'comments_disabled' => 'nullable', - 'expiry' => 'required|in:60,360,1440,10080', - 'pollOptions' => 'required|array|min:1|max:4' - ]); + $default = [ + 'default_license' => 1, + 'media_descriptions' => false, + 'max_altext_length' => config_cache('pixelfed.max_altext_length') + ]; + $settings = AccountService::settings($uid); + if(isset($settings['other']) && isset($settings['other']['scope'])) { + $s = $settings['compose_settings']; + $s['default_scope'] = $settings['other']['scope']; + $settings['compose_settings'] = $s; + } - abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); + return array_merge($default, $settings['compose_settings']); + } - abort_if(Status::whereType('poll') - ->whereProfileId($request->user()->profile_id) - ->whereCaption($request->input('caption')) - ->where('created_at', '>', now()->subDays(2)) - ->exists() - , 422, 'Duplicate detected.'); + public function createPoll(Request $request) + { + $this->validate($request, [ + 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), + 'cw' => 'nullable|boolean', + 'visibility' => 'required|string|in:public,private', + 'comments_disabled' => 'nullable', + 'expiry' => 'required|in:60,360,1440,10080', + 'pollOptions' => 'required|array|min:1|max:4' + ]); + abort(404); + abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); + abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); - $status = new Status; - $status->profile_id = $request->user()->profile_id; - $status->caption = $request->input('caption'); - $status->rendered = Autolink::create()->autolink($status->caption); - $status->visibility = 'draft'; - $status->scope = 'draft'; - $status->type = 'poll'; - $status->local = true; - $status->save(); + abort_if(Status::whereType('poll') + ->whereProfileId($request->user()->profile_id) + ->whereCaption($request->input('caption')) + ->where('created_at', '>', now()->subDays(2)) + ->exists() + , 422, 'Duplicate detected.'); - $poll = new Poll; - $poll->status_id = $status->id; - $poll->profile_id = $status->profile_id; - $poll->poll_options = $request->input('pollOptions'); - $poll->expires_at = now()->addMinutes($request->input('expiry')); - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { - return 0; - })->toArray(); - $poll->save(); + $status = new Status; + $status->profile_id = $request->user()->profile_id; + $status->caption = $request->input('caption'); + $status->rendered = Autolink::create()->autolink($status->caption); + $status->visibility = 'draft'; + $status->scope = 'draft'; + $status->type = 'poll'; + $status->local = true; + $status->save(); - $status->visibility = $request->input('visibility'); - $status->scope = $request->input('visibility'); - $status->save(); + $poll = new Poll; + $poll->status_id = $status->id; + $poll->profile_id = $status->profile_id; + $poll->poll_options = $request->input('pollOptions'); + $poll->expires_at = now()->addMinutes($request->input('expiry')); + $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { + return 0; + })->toArray(); + $poll->save(); - NewStatusPipeline::dispatch($status); + $status->visibility = $request->input('visibility'); + $status->scope = $request->input('visibility'); + $status->save(); - return ['url' => $status->url()]; - } + NewStatusPipeline::dispatch($status); + + return ['url' => $status->url()]; + } } From 9d365d07f9223cacfa22932982e58a817f78f921 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 04:41:38 -0700 Subject: [PATCH 223/977] Update ParentalControls, map updated saved permissions/roles --- .../ParentalControlsController.php | 6 ++- app/Services/UserRoleService.php | 41 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php index 1dc2f578f..373021ab9 100644 --- a/app/Http/Controllers/ParentalControlsController.php +++ b/app/Http/Controllers/ParentalControlsController.php @@ -59,9 +59,13 @@ class ParentalControlsController extends Controller { $this->authPreflight($request); $uid = $request->user()->id; + $ff = $this->requestFormFields($request); $pc = ParentalControls::whereParentId($uid)->findOrFail($id); - $pc->permissions = $this->requestFormFields($request); + $pc->permissions = $ff; $pc->save(); + + $roles = UserRoleService::mapActions($pc->child_id, $ff); + UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]); return redirect($pc->manageUrl() . '?permissions'); } diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php index a18810bf0..ed765a930 100644 --- a/app/Services/UserRoleService.php +++ b/app/Services/UserRoleService.php @@ -179,7 +179,7 @@ class UserRoleService ]; foreach ($map as $key => $value) { - if(!isset($data[$value], $data[substr($value, 1)])) { + if(!isset($data[$value]) && !isset($data[substr($value, 1)])) { $map[$key] = false; continue; } @@ -188,4 +188,43 @@ class UserRoleService return $map; } + + public static function mapActions($id, $data = []) + { + $res = []; + $map = [ + 'account-force-private' => 'private', + 'account-ignore-follow-requests' => 'private', + + 'can-view-public-feed' => 'discovery_feeds', + 'can-view-network-feed' => 'discovery_feeds', + 'can-view-discover' => 'discovery_feeds', + 'can-view-hashtag-feed' => 'discovery_feeds', + + 'can-post' => 'post', + 'can-comment' => 'comment', + 'can-like' => 'like', + 'can-share' => 'share', + + 'can-follow' => 'follow', + 'can-make-public' => '!private', + + 'can-direct-message' => 'dms', + 'can-use-stories' => 'story', + 'can-view-sensitive' => '!hide_cw', + 'can-bookmark' => 'bookmark', + 'can-collections' => 'collection', + 'can-federation' => 'federation', + ]; + + foreach ($map as $key => $value) { + if(!isset($data[$value]) && !isset($data[substr($value, 1)])) { + $res[$key] = false; + continue; + } + $res[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value]; + } + + return $res; + } } From fd9b5ad443dbad8056ed2f99297d2618e32c6fbe Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 04:50:11 -0700 Subject: [PATCH 224/977] Update api controllers, add parental control support --- app/Http/Controllers/Api/ApiV1Controller.php | 5 + app/Http/Controllers/Api/ApiV2Controller.php | 515 ++++++++++--------- 2 files changed, 267 insertions(+), 253 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index dde416064..fe1196916 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -758,6 +758,8 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-follow', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); $target = Profile::where('id', '!=', $user->profile_id) @@ -843,6 +845,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + AccountService::setLastActive($user->id); $target = Profile::where('id', '!=', $user->profile_id) @@ -947,6 +950,8 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-view-discover', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); $query = $request->input('q'); $limit = $request->input('limit') ?? 20; diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 2ca5b96c5..93f930cd5 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -17,304 +17,313 @@ use App\Services\SearchApiV2Service; use App\Util\Media\Filter; use App\Jobs\MediaPipeline\MediaDeletePipeline; use App\Jobs\VideoPipeline\{ - VideoOptimize, - VideoPostProcess, - VideoThumbnail + VideoOptimize, + VideoPostProcess, + VideoThumbnail }; use App\Jobs\ImageOptimizePipeline\ImageOptimize; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Transformer\Api\Mastodon\v1\{ - AccountTransformer, - MediaTransformer, - NotificationTransformer, - StatusTransformer, + AccountTransformer, + MediaTransformer, + NotificationTransformer, + StatusTransformer, }; use App\Transformer\Api\{ - RelationshipTransformer, + RelationshipTransformer, }; use App\Util\Site\Nodeinfo; +use App\Services\UserRoleService; class ApiV2Controller extends Controller { - const PF_API_ENTITY_KEY = "_pe"; + const PF_API_ENTITY_KEY = "_pe"; - public function json($res, $code = 200, $headers = []) - { - return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); - } + public function json($res, $code = 200, $headers = []) + { + return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); + } public function instance(Request $request) { - $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { - if(config_cache('instance.admin.pid')) { - return AccountService::getMastodon(config_cache('instance.admin.pid'), true); - } - $admin = User::whereIsAdmin(true)->first(); - return $admin && isset($admin->profile_id) ? - AccountService::getMastodon($admin->profile_id, true) : - null; - }); + $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () { + if(config_cache('instance.admin.pid')) { + return AccountService::getMastodon(config_cache('instance.admin.pid'), true); + } + $admin = User::whereIsAdmin(true)->first(); + return $admin && isset($admin->profile_id) ? + AccountService::getMastodon($admin->profile_id, true) : + null; + }); - $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { - return config_cache('app.rules') ? - collect(json_decode(config_cache('app.rules'), true)) - ->map(function($rule, $key) { - $id = $key + 1; - return [ - 'id' => "{$id}", - 'text' => $rule - ]; - }) - ->toArray() : []; - }); + $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { + return config_cache('app.rules') ? + collect(json_decode(config_cache('app.rules'), true)) + ->map(function($rule, $key) { + $id = $key + 1; + return [ + 'id' => "{$id}", + 'text' => $rule + ]; + }) + ->toArray() : []; + }); - $res = [ - 'domain' => config('pixelfed.domain.app'), - 'title' => config_cache('app.name'), - 'version' => config('pixelfed.version'), - 'source_url' => 'https://github.com/pixelfed/pixelfed', - 'description' => config_cache('app.short_description'), - 'usage' => [ - 'users' => [ - 'active_month' => (int) Nodeinfo::activeUsersMonthly() - ] - ], - 'thumbnail' => [ - 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - 'blurhash' => InstanceService::headerBlurhash(), - 'versions' => [ - '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')) - ] - ], - 'languages' => [config('app.locale')], - 'configuration' => [ - 'urls' => [ - 'streaming' => 'wss://' . config('pixelfed.domain.app'), - 'status' => null - ], - 'accounts' => [ - 'max_featured_tags' => 0, - ], - 'statuses' => [ - 'max_characters' => (int) config('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), - 'characters_reserved_per_url' => 23 - ], - 'media_attachments' => [ - 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), - 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'image_matrix_limit' => 3686400, - 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'video_frame_rate_limit' => 240, - 'video_matrix_limit' => 3686400 - ], - 'polls' => [ - 'max_options' => 4, - 'max_characters_per_option' => 50, - 'min_expiration' => 300, - 'max_expiration' => 2629746, - ], - 'translation' => [ - 'enabled' => false, - ], - ], - 'registrations' => [ - 'enabled' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, - 'message' => null - ], - 'contact' => [ - 'email' => config('instance.email'), - 'account' => $contact - ], - 'rules' => $rules - ]; + $res = [ + 'domain' => config('pixelfed.domain.app'), + 'title' => config_cache('app.name'), + 'version' => config('pixelfed.version'), + 'source_url' => 'https://github.com/pixelfed/pixelfed', + 'description' => config_cache('app.short_description'), + 'usage' => [ + 'users' => [ + 'active_month' => (int) Nodeinfo::activeUsersMonthly() + ] + ], + 'thumbnail' => [ + 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + 'blurhash' => InstanceService::headerBlurhash(), + 'versions' => [ + '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')) + ] + ], + 'languages' => [config('app.locale')], + 'configuration' => [ + 'urls' => [ + 'streaming' => 'wss://' . config('pixelfed.domain.app'), + 'status' => null + ], + 'accounts' => [ + 'max_featured_tags' => 0, + ], + 'statuses' => [ + 'max_characters' => (int) config('pixelfed.max_caption_length'), + 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), + 'characters_reserved_per_url' => 23 + ], + 'media_attachments' => [ + 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), + 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'image_matrix_limit' => 3686400, + 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'video_frame_rate_limit' => 240, + 'video_matrix_limit' => 3686400 + ], + 'polls' => [ + 'max_options' => 4, + 'max_characters_per_option' => 50, + 'min_expiration' => 300, + 'max_expiration' => 2629746, + ], + 'translation' => [ + 'enabled' => false, + ], + ], + 'registrations' => [ + 'enabled' => (bool) config_cache('pixelfed.open_registration'), + 'approval_required' => false, + 'message' => null + ], + 'contact' => [ + 'email' => config('instance.email'), + 'account' => $contact + ], + 'rules' => $rules + ]; - return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); + return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } - /** - * GET /api/v2/search - * - * - * @return array - */ - public function search(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v2/search + * + * + * @return array + */ + public function search(Request $request) + { + abort_if(!$request->user(), 403); - $this->validate($request, [ - 'q' => 'required|string|min:1|max:100', - 'account_id' => 'nullable|string', - 'max_id' => 'nullable|string', - 'min_id' => 'nullable|string', - 'type' => 'nullable|in:accounts,hashtags,statuses', - 'exclude_unreviewed' => 'nullable', - 'resolve' => 'nullable', - 'limit' => 'nullable|integer|max:40', - 'offset' => 'nullable|integer', - 'following' => 'nullable' - ]); + $this->validate($request, [ + 'q' => 'required|string|min:1|max:100', + 'account_id' => 'nullable|string', + 'max_id' => 'nullable|string', + 'min_id' => 'nullable|string', + 'type' => 'nullable|in:accounts,hashtags,statuses', + 'exclude_unreviewed' => 'nullable', + 'resolve' => 'nullable', + 'limit' => 'nullable|integer|max:40', + 'offset' => 'nullable|integer', + 'following' => 'nullable' + ]); - $mastodonMode = !$request->has('_pe'); - return $this->json(SearchApiV2Service::query($request, $mastodonMode)); - } + if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) { + return [ + 'accounts' => [], + 'hashtags' => [], + 'statuses' => [] + ]; + } - /** - * GET /api/v2/streaming/config - * - * - * @return object - */ - public function getWebsocketConfig() - { - return config('broadcasting.default') === 'pusher' ? [ - 'host' => config('broadcasting.connections.pusher.options.host'), - 'port' => config('broadcasting.connections.pusher.options.port'), - 'key' => config('broadcasting.connections.pusher.key'), - 'cluster' => config('broadcasting.connections.pusher.options.cluster') - ] : []; - } + $mastodonMode = !$request->has('_pe'); + return $this->json(SearchApiV2Service::query($request, $mastodonMode)); + } - /** - * POST /api/v2/media - * - * - * @return MediaTransformer - */ - public function mediaUploadV2(Request $request) - { - abort_if(!$request->user(), 403); + /** + * GET /api/v2/streaming/config + * + * + * @return object + */ + public function getWebsocketConfig() + { + return config('broadcasting.default') === 'pusher' ? [ + 'host' => config('broadcasting.connections.pusher.options.host'), + 'port' => config('broadcasting.connections.pusher.options.port'), + 'key' => config('broadcasting.connections.pusher.key'), + 'cluster' => config('broadcasting.connections.pusher.options.cluster') + ] : []; + } - $this->validate($request, [ - 'file.*' => [ - 'required_without:file', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'file' => [ - 'required_without:file.*', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ], - 'filter_name' => 'nullable|string|max:24', - 'filter_class' => 'nullable|alpha_dash|max:24', - 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), - 'replace_id' => 'sometimes' - ]); + /** + * POST /api/v2/media + * + * + * @return MediaTransformer + */ + public function mediaUploadV2(Request $request) + { + abort_if(!$request->user(), 403); - $user = $request->user(); + $this->validate($request, [ + 'file.*' => [ + 'required_without:file', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'file' => [ + 'required_without:file.*', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ], + 'filter_name' => 'nullable|string|max:24', + 'filter_class' => 'nullable|alpha_dash|max:24', + 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'), + 'replace_id' => 'sometimes' + ]); - if($user->last_active_at == null) { - return []; - } + $user = $request->user(); - if(empty($request->file('file'))) { - return response('', 422); - } + if($user->last_active_at == null) { + return []; + } - $limitKey = 'compose:rate-limit:media-upload:' . $user->id; - $limitTtl = now()->addMinutes(15); - $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { - $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); + if(empty($request->file('file'))) { + return response('', 422); + } - return $dailyLimit >= 1250; - }); - abort_if($limitReached == true, 429); + $limitKey = 'compose:rate-limit:media-upload:' . $user->id; + $limitTtl = now()->addMinutes(15); + $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { + $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); - $profile = $user->profile; + return $dailyLimit >= 1250; + }); + abort_if($limitReached == true, 429); - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } + $profile = $user->profile; - $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; - $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; + if(config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + return Media::whereUserId($user->id)->sum('size') / 1000; + }); + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($size >= $limit) { + abort(403, 'Account size limit reached.'); + } + } - $photo = $request->file('file'); + $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null; + $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null; - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } + $photo = $request->file('file'); - $storagePath = MediaPathService::get($user, 2); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - $license = null; - $mime = $photo->getMimeType(); + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } - $settings = UserSetting::whereUserId($user->id)->first(); + $storagePath = MediaPathService::get($user, 2); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + $license = null; + $mime = $photo->getMimeType(); - if($settings && !empty($settings->compose_settings)) { - $compose = $settings->compose_settings; + $settings = UserSetting::whereUserId($user->id)->first(); - if(isset($compose['default_license']) && $compose['default_license'] != 1) { - $license = $compose['default_license']; - } - } + if($settings && !empty($settings->compose_settings)) { + $compose = $settings->compose_settings; - abort_if(MediaBlocklistService::exists($hash) == true, 451); + if(isset($compose['default_license']) && $compose['default_license'] != 1) { + $license = $compose['default_license']; + } + } - if($request->has('replace_id')) { - $rpid = $request->input('replace_id'); - $removeMedia = Media::whereNull('status_id') - ->whereUserId($user->id) - ->whereProfileId($profile->id) - ->where('created_at', '>', now()->subHours(2)) - ->find($rpid); - if($removeMedia) { - MediaDeletePipeline::dispatch($removeMedia) - ->onQueue('mmo') - ->delay(now()->addMinutes(15)); - } - } + abort_if(MediaBlocklistService::exists($hash) == true, 451); - $media = new Media(); - $media->status_id = null; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $mime; - $media->caption = $request->input('description'); - $media->filter_class = $filterClass; - $media->filter_name = $filterName; - if($license) { - $media->license = $license; - } - $media->save(); + if($request->has('replace_id')) { + $rpid = $request->input('replace_id'); + $removeMedia = Media::whereNull('status_id') + ->whereUserId($user->id) + ->whereProfileId($profile->id) + ->where('created_at', '>', now()->subHours(2)) + ->find($rpid); + if($removeMedia) { + MediaDeletePipeline::dispatch($removeMedia) + ->onQueue('mmo') + ->delay(now()->addMinutes(15)); + } + } - switch ($media->mime) { - case 'image/jpeg': - case 'image/png': - ImageOptimize::dispatch($media)->onQueue('mmo'); - break; + $media = new Media(); + $media->status_id = null; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $mime; + $media->caption = $request->input('description'); + $media->filter_class = $filterClass; + $media->filter_name = $filterName; + if($license) { + $media->license = $license; + } + $media->save(); - case 'video/mp4': - VideoThumbnail::dispatch($media)->onQueue('mmo'); - $preview_url = '/storage/no-preview.png'; - $url = '/storage/no-preview.png'; - break; - } + switch ($media->mime) { + case 'image/jpeg': + case 'image/png': + ImageOptimize::dispatch($media)->onQueue('mmo'); + break; - Cache::forget($limitKey); - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($media, new MediaTransformer()); - $res = $fractal->createData($resource)->toArray(); - $res['preview_url'] = $media->url(). '?v=' . time(); - $res['url'] = null; - return $this->json($res, 202); - } + case 'video/mp4': + VideoThumbnail::dispatch($media)->onQueue('mmo'); + $preview_url = '/storage/no-preview.png'; + $url = '/storage/no-preview.png'; + break; + } + + Cache::forget($limitKey); + $fractal = new Fractal\Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Fractal\Resource\Item($media, new MediaTransformer()); + $res = $fractal->createData($resource)->toArray(); + $res['preview_url'] = $media->url(). '?v=' . time(); + $res['url'] = null; + return $this->json($res, 202); + } } From fe30cd25d1038341666279df075d4bb7194496b0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 05:25:23 -0700 Subject: [PATCH 225/977] Update DirectMessageController, add parental controls support --- app/Http/Controllers/Api/ApiV1Controller.php | 6 +- .../Controllers/DirectMessageController.php | 1698 +++++++++-------- 2 files changed, 867 insertions(+), 837 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index fe1196916..b342d68ce 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2575,7 +2575,11 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 20); $scope = $request->input('scope', 'inbox'); - $pid = $request->user()->profile_id; + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + $pid = $user->profile_id; if(config('database.default') == 'pgsql') { $dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) { diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php index df76d2ab9..0d91d4f17 100644 --- a/app/Http/Controllers/DirectMessageController.php +++ b/app/Http/Controllers/DirectMessageController.php @@ -5,14 +5,14 @@ namespace App\Http\Controllers; use Auth, Cache; use Illuminate\Http\Request; use App\{ - DirectMessage, - Media, - Notification, - Profile, - Status, - User, - UserFilter, - UserSetting + DirectMessage, + Media, + Notification, + Profile, + Status, + User, + UserFilter, + UserSetting }; use App\Services\MediaPathService; use App\Services\MediaBlocklistService; @@ -26,835 +26,861 @@ use App\Services\WebfingerService; use App\Models\Conversation; use App\Jobs\DirectPipeline\DirectDeletePipeline; use App\Jobs\DirectPipeline\DirectDeliverPipeline; +use App\Services\UserRoleService; class DirectMessageController extends Controller { - public function __construct() - { - $this->middleware('auth'); - } - - public function browse(Request $request) - { - $this->validate($request, [ - 'a' => 'nullable|string|in:inbox,sent,filtered', - 'page' => 'nullable|integer|min:1|max:99' - ]); - - $profile = $request->user()->profile_id; - $action = $request->input('a', 'inbox'); - $page = $request->input('page'); - - if(config('database.default') == 'pgsql') { - if($action == 'inbox') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(false) - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->latest() - ->get() - ->unique('from_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - })->values(); - } - - if($action == 'sent') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereFromId($profile) - ->with(['author','status']) - ->orderBy('id', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->get() - ->unique('to_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'filtered') { - $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(true) - ->orderBy('id', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->get() - ->unique('from_id') - ->take(8) - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - } elseif(config('database.default') == 'mysql') { - if($action == 'inbox') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(false) - ->groupBy('from_id') - ->latest() - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'sent') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereFromId($profile) - ->with(['author','status']) - ->groupBy('to_id') - ->orderBy('createdAt', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - - if($action == 'filtered') { - $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') - ->whereToId($profile) - ->with(['author','status']) - ->whereIsHidden(true) - ->groupBy('from_id') - ->orderBy('createdAt', 'desc') - ->when($page, function($q, $page) { - if($page > 1) { - return $q->offset($page * 8 - 8); - } - }) - ->limit(8) - ->get() - ->map(function($r) use($profile) { - return $r->from_id !== $profile ? [ - 'id' => (string) $r->from_id, - 'name' => $r->author->name, - 'username' => $r->author->username, - 'avatar' => $r->author->avatarUrl(), - 'url' => $r->author->url(), - 'isLocal' => (bool) !$r->author->domain, - 'domain' => $r->author->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ] : [ - 'id' => (string) $r->to_id, - 'name' => $r->recipient->name, - 'username' => $r->recipient->username, - 'avatar' => $r->recipient->avatarUrl(), - 'url' => $r->recipient->url(), - 'isLocal' => (bool) !$r->recipient->domain, - 'domain' => $r->recipient->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => $r->status->caption, - 'messages' => [] - ]; - }); - } - } - - return response()->json($dms->all()); - } - - public function create(Request $request) - { - $this->validate($request, [ - 'to_id' => 'required', - 'message' => 'required|string|min:1|max:500', - 'type' => 'required|in:text,emoji' - ]); - - $profile = $request->user()->profile; - $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); - - abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); - $msg = $request->input('message'); - - if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { - if($recipient->follows($profile) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - $status = new Status; - $status->profile_id = $profile->id; - $status->caption = $msg; - $status->rendered = $msg; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->in_reply_to_profile_id = $recipient->id; - $status->save(); - - $dm = new DirectMessage; - $dm->to_id = $recipient->id; - $dm->from_id = $profile->id; - $dm->status_id = $status->id; - $dm->is_hidden = $hidden; - $dm->type = $request->input('type'); - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $recipient->id, - 'from_id' => $profile->id - ], - [ - 'type' => $dm->type, - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if(filter_var($msg, FILTER_VALIDATE_URL)) { - if(Helpers::validateUrl($msg)) { - $dm->type = 'link'; - $dm->meta = [ - 'domain' => parse_url($msg, PHP_URL_HOST), - 'local' => parse_url($msg, PHP_URL_HOST) == - parse_url(config('app.url'), PHP_URL_HOST) - ]; - $dm->save(); - } - } - - $nf = UserFilter::whereUserId($recipient->id) - ->whereFilterableId($profile->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->exists(); - - if($recipient->domain == null && $hidden == false && !$nf) { - $notification = new Notification(); - $notification->profile_id = $recipient->id; - $notification->actor_id = $profile->id; - $notification->action = 'dm'; - $notification->item_id = $dm->id; - $notification->item_type = "App\DirectMessage"; - $notification->save(); - } - - if($recipient->domain) { - $this->remoteDeliver($dm); - } - - $res = [ - 'id' => (string) $dm->id, - 'isAuthor' => $profile->id == $dm->from_id, - 'reportId' => (string) $dm->status_id, - 'hidden' => (bool) $dm->is_hidden, - 'type' => $dm->type, - 'text' => $dm->status->caption, - 'media' => null, - 'timeAgo' => $dm->created_at->diffForHumans(null,null,true), - 'seen' => $dm->read_at != null, - 'meta' => $dm->meta - ]; - - return response()->json($res); - } - - public function thread(Request $request) - { - $this->validate($request, [ - 'pid' => 'required' - ]); - $uid = $request->user()->profile_id; - $pid = $request->input('pid'); - $max_id = $request->input('max_id'); - $min_id = $request->input('min_id'); - - $r = Profile::findOrFail($pid); - - if($min_id) { - $res = DirectMessage::select('*') - ->where('id', '>', $min_id) - ->where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } else if ($max_id) { - $res = DirectMessage::select('*') - ->where('id', '<', $max_id) - ->where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } else { - $res = DirectMessage::where(function($q) use($pid,$uid) { - return $q->where([['from_id',$pid],['to_id',$uid] - ])->orWhere([['from_id',$uid],['to_id',$pid]]); - }) - ->latest() - ->take(8) - ->get(); - } - - $res = $res->filter(function($s) { - return $s && $s->status; - }) - ->map(function($s) use ($uid) { - return [ - 'id' => (string) $s->id, - 'hidden' => (bool) $s->is_hidden, - 'isAuthor' => $uid == $s->from_id, - 'type' => $s->type, - 'text' => $s->status->caption, - 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null, - 'timeAgo' => $s->created_at->diffForHumans(null,null,true), - 'seen' => $s->read_at != null, - 'reportId' => (string) $s->status_id, - 'meta' => json_decode($s->meta,true) - ]; - }) - ->values(); - - $w = [ - 'id' => (string) $r->id, - 'name' => $r->name, - 'username' => $r->username, - 'avatar' => $r->avatarUrl(), - 'url' => $r->url(), - 'muted' => UserFilter::whereUserId($uid) - ->whereFilterableId($r->id) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->first() ? true : false, - 'isLocal' => (bool) !$r->domain, - 'domain' => $r->domain, - 'timeAgo' => $r->created_at->diffForHumans(null, true, true), - 'lastMessage' => '', - 'messages' => $res - ]; - - return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function delete(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $sid = $request->input('id'); - $pid = $request->user()->profile_id; - - $dm = DirectMessage::whereFromId($pid) - ->whereStatusId($sid) - ->firstOrFail(); - - $status = Status::whereProfileId($pid) - ->findOrFail($dm->status_id); - - $recipient = AccountService::get($dm->to_id); - - if(!$recipient) { - return response('', 422); - } - - if($recipient['local'] == false) { - $dmc = $dm; - $this->remoteDelete($dmc); - } else { - StatusDelete::dispatch($status)->onQueue('high'); - } - - if(Conversation::whereStatusId($sid)->count()) { - $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id]) - ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) - ->latest() - ->first(); - - if($latest->status_id == $sid) { - Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) - ->update([ - 'updated_at' => $latest->updated_at, - 'status_id' => $latest->status_id, - 'type' => $latest->type, - 'is_hidden' => false - ]); - - Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id]) - ->update([ - 'updated_at' => $latest->updated_at, - 'status_id' => $latest->status_id, - 'type' => $latest->type, - 'is_hidden' => false - ]); - } else { - Conversation::where([ - 'status_id' => $sid, - 'to_id' => $dm->from_id, - 'from_id' => $dm->to_id - ])->delete(); - - Conversation::where([ - 'status_id' => $sid, - 'from_id' => $dm->from_id, - 'to_id' => $dm->to_id - ])->delete(); - } - } - - StatusService::del($status->id, true); - - $status->forceDeleteQuietly(); - return [200]; - } - - public function get(Request $request, $id) - { - $pid = $request->user()->profile_id; - $dm = DirectMessage::whereStatusId($id)->firstOrFail(); - abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404); - return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function mediaUpload(Request $request) - { - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:' . config_cache('pixelfed.media_types'), - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - 'to_id' => 'required' - ]); - - $user = $request->user(); - $profile = $user->profile; - $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); - abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); - - if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { - if($recipient->follows($profile) == true) { - $hidden = false; - } else { - $hidden = true; - } - } else { - $hidden = false; - } - - if(config_cache('pixelfed.enforce_account_limit') == true) { - $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { - return Media::whereUserId($user->id)->sum('size') / 1000; - }); - $limit = (int) config_cache('pixelfed.max_account_size'); - if ($size >= $limit) { - abort(403, 'Account size limit reached.'); - } - } - $photo = $request->file('file'); - - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), $mimes) == false) { - abort(403, 'Invalid or unsupported mime type.'); - } - - $storagePath = MediaPathService::get($user, 2) . Str::random(8); - $path = $photo->storePublicly($storagePath); - $hash = \hash_file('sha256', $photo); - - abort_if(MediaBlocklistService::exists($hash) == true, 451); - - $status = new Status; - $status->profile_id = $profile->id; - $status->caption = null; - $status->rendered = null; - $status->visibility = 'direct'; - $status->scope = 'direct'; - $status->in_reply_to_profile_id = $recipient->id; - $status->save(); - - $media = new Media(); - $media->status_id = $status->id; - $media->profile_id = $profile->id; - $media->user_id = $user->id; - $media->media_path = $path; - $media->original_sha256 = $hash; - $media->size = $photo->getSize(); - $media->mime = $photo->getMimeType(); - $media->caption = null; - $media->filter_class = null; - $media->filter_name = null; - $media->save(); - - $dm = new DirectMessage; - $dm->to_id = $recipient->id; - $dm->from_id = $profile->id; - $dm->status_id = $status->id; - $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo'; - $dm->is_hidden = $hidden; - $dm->save(); - - Conversation::updateOrInsert( - [ - 'to_id' => $recipient->id, - 'from_id' => $profile->id - ], - [ - 'type' => $dm->type, - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => $hidden - ] - ); - - if($recipient->domain) { - $this->remoteDeliver($dm); - } - - return [ - 'id' => $dm->id, - 'reportId' => (string) $dm->status_id, - 'type' => $dm->type, - 'url' => $media->url() - ]; - } - - public function composeLookup(Request $request) - { - $this->validate($request, [ - 'q' => 'required|string|min:2|max:50', - 'remote' => 'nullable', - ]); - - $q = $request->input('q'); - $r = $request->input('remote', false); - - if($r && !Str::of($q)->contains('.')) { - return []; - } - - if($r && Helpers::validateUrl($q)) { - Helpers::profileFetch($q); - } - - if(Str::of($q)->startsWith('@')) { - if(strlen($q) < 3) { - return []; - } - if(substr_count($q, '@') == 2) { - WebfingerService::lookup($q); - } - $q = mb_substr($q, 1); - } - - $blocked = UserFilter::whereFilterableType('App\Profile') - ->whereFilterType('block') - ->whereFilterableId($request->user()->profile_id) - ->pluck('user_id'); - - $blocked->push($request->user()->profile_id); - - $results = Profile::select('id','domain','username') - ->whereNotIn('id', $blocked) - ->where('username','like','%'.$q.'%') - ->orderBy('domain') - ->limit(8) - ->get() - ->map(function($r) { - $acct = AccountService::get($r->id); - return [ - 'local' => (bool) !$r->domain, - 'id' => (string) $r->id, - 'name' => $r->username, - 'privacy' => true, - 'avatar' => $r->avatarUrl(), - 'account' => $acct - ]; - }); - - return $results; - } - - public function read(Request $request) - { - $this->validate($request, [ - 'pid' => 'required', - 'sid' => 'required' - ]); - - $pid = $request->input('pid'); - $sid = $request->input('sid'); - - $dms = DirectMessage::whereToId($request->user()->profile_id) - ->whereFromId($pid) - ->where('status_id', '>=', $sid) - ->get(); - - $now = now(); - foreach($dms as $dm) { - $dm->read_at = $now; - $dm->save(); - } - - return response()->json($dms->pluck('id')); - } - - public function mute(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $fid = $request->input('id'); - $pid = $request->user()->profile_id; - - UserFilter::firstOrCreate( - [ - 'user_id' => $pid, - 'filterable_id' => $fid, - 'filterable_type' => 'App\Profile', - 'filter_type' => 'dm.mute' - ] - ); - - return [200]; - } - - public function unmute(Request $request) - { - $this->validate($request, [ - 'id' => 'required' - ]); - - $fid = $request->input('id'); - $pid = $request->user()->profile_id; - - $f = UserFilter::whereUserId($pid) - ->whereFilterableId($fid) - ->whereFilterableType('App\Profile') - ->whereFilterType('dm.mute') - ->firstOrFail(); - - $f->delete(); - - return [200]; - } - - public function remoteDeliver($dm) - { - $profile = $dm->author; - $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; - - $tags = [ - [ - 'type' => 'Mention', - 'href' => $dm->recipient->permalink(), - 'name' => $dm->recipient->emailUrl(), - ] - ]; - - $body = [ - '@context' => [ - 'https://w3id.org/security/v1', - 'https://www.w3.org/ns/activitystreams', - ], - 'id' => $dm->status->permalink(), - 'type' => 'Create', - 'actor' => $dm->status->profile->permalink(), - 'published' => $dm->status->created_at->toAtomString(), - 'to' => [$dm->recipient->permalink()], - 'cc' => [], - 'object' => [ - 'id' => $dm->status->url(), - 'type' => 'Note', - 'summary' => null, - 'content' => $dm->status->rendered ?? $dm->status->caption, - 'inReplyTo' => null, - 'published' => $dm->status->created_at->toAtomString(), - 'url' => $dm->status->url(), - 'attributedTo' => $dm->status->profile->permalink(), - 'to' => [$dm->recipient->permalink()], - 'cc' => [], - 'sensitive' => (bool) $dm->status->is_nsfw, - 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) { - return [ - 'type' => $media->activityVerb(), - 'mediaType' => $media->mime, - 'url' => $media->url(), - 'name' => $media->caption, - ]; - })->toArray(), - 'tag' => $tags, - ] - ]; - - DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high'); - } - - public function remoteDelete($dm) - { - $profile = $dm->author; - $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; - - $body = [ - '@context' => [ - 'https://www.w3.org/ns/activitystreams', - ], - 'id' => $dm->status->permalink('#delete'), - 'to' => [ - 'https://www.w3.org/ns/activitystreams#Public' - ], - 'type' => 'Delete', - 'actor' => $dm->status->profile->permalink(), - 'object' => [ - 'id' => $dm->status->url(), - 'type' => 'Tombstone' - ] - ]; - DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high'); - } + public function __construct() + { + $this->middleware('auth'); + } + + public function browse(Request $request) + { + $this->validate($request, [ + 'a' => 'nullable|string|in:inbox,sent,filtered', + 'page' => 'nullable|integer|min:1|max:99' + ]); + + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + $profile = $user->profile_id; + $action = $request->input('a', 'inbox'); + $page = $request->input('page'); + + if(config('database.default') == 'pgsql') { + if($action == 'inbox') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereToId($profile) + ->with(['author','status']) + ->whereIsHidden(false) + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->latest() + ->get() + ->unique('from_id') + ->take(8) + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + })->values(); + } + + if($action == 'sent') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereFromId($profile) + ->with(['author','status']) + ->orderBy('id', 'desc') + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->get() + ->unique('to_id') + ->take(8) + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + }); + } + + if($action == 'filtered') { + $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at') + ->whereToId($profile) + ->with(['author','status']) + ->whereIsHidden(true) + ->orderBy('id', 'desc') + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->get() + ->unique('from_id') + ->take(8) + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + }); + } + } elseif(config('database.default') == 'mysql') { + if($action == 'inbox') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereToId($profile) + ->with(['author','status']) + ->whereIsHidden(false) + ->groupBy('from_id') + ->latest() + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + }); + } + + if($action == 'sent') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereFromId($profile) + ->with(['author','status']) + ->groupBy('to_id') + ->orderBy('createdAt', 'desc') + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + }); + } + + if($action == 'filtered') { + $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') + ->whereToId($profile) + ->with(['author','status']) + ->whereIsHidden(true) + ->groupBy('from_id') + ->orderBy('createdAt', 'desc') + ->when($page, function($q, $page) { + if($page > 1) { + return $q->offset($page * 8 - 8); + } + }) + ->limit(8) + ->get() + ->map(function($r) use($profile) { + return $r->from_id !== $profile ? [ + 'id' => (string) $r->from_id, + 'name' => $r->author->name, + 'username' => $r->author->username, + 'avatar' => $r->author->avatarUrl(), + 'url' => $r->author->url(), + 'isLocal' => (bool) !$r->author->domain, + 'domain' => $r->author->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ] : [ + 'id' => (string) $r->to_id, + 'name' => $r->recipient->name, + 'username' => $r->recipient->username, + 'avatar' => $r->recipient->avatarUrl(), + 'url' => $r->recipient->url(), + 'isLocal' => (bool) !$r->recipient->domain, + 'domain' => $r->recipient->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => $r->status->caption, + 'messages' => [] + ]; + }); + } + } + + return response()->json($dms->all()); + } + + public function create(Request $request) + { + $this->validate($request, [ + 'to_id' => 'required', + 'message' => 'required|string|min:1|max:500', + 'type' => 'required|in:text,emoji' + ]); + + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + $profile = $user->profile; + $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); + + abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); + $msg = $request->input('message'); + + if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if($recipient->follows($profile) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + $status = new Status; + $status->profile_id = $profile->id; + $status->caption = $msg; + $status->rendered = $msg; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->in_reply_to_profile_id = $recipient->id; + $status->save(); + + $dm = new DirectMessage; + $dm->to_id = $recipient->id; + $dm->from_id = $profile->id; + $dm->status_id = $status->id; + $dm->is_hidden = $hidden; + $dm->type = $request->input('type'); + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $recipient->id, + 'from_id' => $profile->id + ], + [ + 'type' => $dm->type, + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden + ] + ); + + if(filter_var($msg, FILTER_VALIDATE_URL)) { + if(Helpers::validateUrl($msg)) { + $dm->type = 'link'; + $dm->meta = [ + 'domain' => parse_url($msg, PHP_URL_HOST), + 'local' => parse_url($msg, PHP_URL_HOST) == + parse_url(config('app.url'), PHP_URL_HOST) + ]; + $dm->save(); + } + } + + $nf = UserFilter::whereUserId($recipient->id) + ->whereFilterableId($profile->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->exists(); + + if($recipient->domain == null && $hidden == false && !$nf) { + $notification = new Notification(); + $notification->profile_id = $recipient->id; + $notification->actor_id = $profile->id; + $notification->action = 'dm'; + $notification->item_id = $dm->id; + $notification->item_type = "App\DirectMessage"; + $notification->save(); + } + + if($recipient->domain) { + $this->remoteDeliver($dm); + } + + $res = [ + 'id' => (string) $dm->id, + 'isAuthor' => $profile->id == $dm->from_id, + 'reportId' => (string) $dm->status_id, + 'hidden' => (bool) $dm->is_hidden, + 'type' => $dm->type, + 'text' => $dm->status->caption, + 'media' => null, + 'timeAgo' => $dm->created_at->diffForHumans(null,null,true), + 'seen' => $dm->read_at != null, + 'meta' => $dm->meta + ]; + + return response()->json($res); + } + + public function thread(Request $request) + { + $this->validate($request, [ + 'pid' => 'required' + ]); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $uid = $user->profile_id; + $pid = $request->input('pid'); + $max_id = $request->input('max_id'); + $min_id = $request->input('min_id'); + + $r = Profile::findOrFail($pid); + + if($min_id) { + $res = DirectMessage::select('*') + ->where('id', '>', $min_id) + ->where(function($q) use($pid,$uid) { + return $q->where([['from_id',$pid],['to_id',$uid] + ])->orWhere([['from_id',$uid],['to_id',$pid]]); + }) + ->latest() + ->take(8) + ->get(); + } else if ($max_id) { + $res = DirectMessage::select('*') + ->where('id', '<', $max_id) + ->where(function($q) use($pid,$uid) { + return $q->where([['from_id',$pid],['to_id',$uid] + ])->orWhere([['from_id',$uid],['to_id',$pid]]); + }) + ->latest() + ->take(8) + ->get(); + } else { + $res = DirectMessage::where(function($q) use($pid,$uid) { + return $q->where([['from_id',$pid],['to_id',$uid] + ])->orWhere([['from_id',$uid],['to_id',$pid]]); + }) + ->latest() + ->take(8) + ->get(); + } + + $res = $res->filter(function($s) { + return $s && $s->status; + }) + ->map(function($s) use ($uid) { + return [ + 'id' => (string) $s->id, + 'hidden' => (bool) $s->is_hidden, + 'isAuthor' => $uid == $s->from_id, + 'type' => $s->type, + 'text' => $s->status->caption, + 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null, + 'timeAgo' => $s->created_at->diffForHumans(null,null,true), + 'seen' => $s->read_at != null, + 'reportId' => (string) $s->status_id, + 'meta' => json_decode($s->meta,true) + ]; + }) + ->values(); + + $w = [ + 'id' => (string) $r->id, + 'name' => $r->name, + 'username' => $r->username, + 'avatar' => $r->avatarUrl(), + 'url' => $r->url(), + 'muted' => UserFilter::whereUserId($uid) + ->whereFilterableId($r->id) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->first() ? true : false, + 'isLocal' => (bool) !$r->domain, + 'domain' => $r->domain, + 'timeAgo' => $r->created_at->diffForHumans(null, true, true), + 'lastMessage' => '', + 'messages' => $res + ]; + + return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function delete(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + $sid = $request->input('id'); + $pid = $request->user()->profile_id; + + $dm = DirectMessage::whereFromId($pid) + ->whereStatusId($sid) + ->firstOrFail(); + + $status = Status::whereProfileId($pid) + ->findOrFail($dm->status_id); + + $recipient = AccountService::get($dm->to_id); + + if(!$recipient) { + return response('', 422); + } + + if($recipient['local'] == false) { + $dmc = $dm; + $this->remoteDelete($dmc); + } else { + StatusDelete::dispatch($status)->onQueue('high'); + } + + if(Conversation::whereStatusId($sid)->count()) { + $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id]) + ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) + ->latest() + ->first(); + + if($latest->status_id == $sid) { + Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id]) + ->update([ + 'updated_at' => $latest->updated_at, + 'status_id' => $latest->status_id, + 'type' => $latest->type, + 'is_hidden' => false + ]); + + Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id]) + ->update([ + 'updated_at' => $latest->updated_at, + 'status_id' => $latest->status_id, + 'type' => $latest->type, + 'is_hidden' => false + ]); + } else { + Conversation::where([ + 'status_id' => $sid, + 'to_id' => $dm->from_id, + 'from_id' => $dm->to_id + ])->delete(); + + Conversation::where([ + 'status_id' => $sid, + 'from_id' => $dm->from_id, + 'to_id' => $dm->to_id + ])->delete(); + } + } + + StatusService::del($status->id, true); + + $status->forceDeleteQuietly(); + return [200]; + } + + public function get(Request $request, $id) + { + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $pid = $request->user()->profile_id; + $dm = DirectMessage::whereStatusId($id)->firstOrFail(); + abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404); + return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function mediaUpload(Request $request) + { + $this->validate($request, [ + 'file' => function() { + return [ + 'required', + 'mimetypes:' . config_cache('pixelfed.media_types'), + 'max:' . config_cache('pixelfed.max_photo_size'), + ]; + }, + 'to_id' => 'required' + ]); + + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + $profile = $user->profile; + $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); + abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403); + + if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) { + if($recipient->follows($profile) == true) { + $hidden = false; + } else { + $hidden = true; + } + } else { + $hidden = false; + } + + if(config_cache('pixelfed.enforce_account_limit') == true) { + $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { + return Media::whereUserId($user->id)->sum('size') / 1000; + }); + $limit = (int) config_cache('pixelfed.max_account_size'); + if ($size >= $limit) { + abort(403, 'Account size limit reached.'); + } + } + $photo = $request->file('file'); + + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), $mimes) == false) { + abort(403, 'Invalid or unsupported mime type.'); + } + + $storagePath = MediaPathService::get($user, 2) . Str::random(8); + $path = $photo->storePublicly($storagePath); + $hash = \hash_file('sha256', $photo); + + abort_if(MediaBlocklistService::exists($hash) == true, 451); + + $status = new Status; + $status->profile_id = $profile->id; + $status->caption = null; + $status->rendered = null; + $status->visibility = 'direct'; + $status->scope = 'direct'; + $status->in_reply_to_profile_id = $recipient->id; + $status->save(); + + $media = new Media(); + $media->status_id = $status->id; + $media->profile_id = $profile->id; + $media->user_id = $user->id; + $media->media_path = $path; + $media->original_sha256 = $hash; + $media->size = $photo->getSize(); + $media->mime = $photo->getMimeType(); + $media->caption = null; + $media->filter_class = null; + $media->filter_name = null; + $media->save(); + + $dm = new DirectMessage; + $dm->to_id = $recipient->id; + $dm->from_id = $profile->id; + $dm->status_id = $status->id; + $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo'; + $dm->is_hidden = $hidden; + $dm->save(); + + Conversation::updateOrInsert( + [ + 'to_id' => $recipient->id, + 'from_id' => $profile->id + ], + [ + 'type' => $dm->type, + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => $hidden + ] + ); + + if($recipient->domain) { + $this->remoteDeliver($dm); + } + + return [ + 'id' => $dm->id, + 'reportId' => (string) $dm->status_id, + 'type' => $dm->type, + 'url' => $media->url() + ]; + } + + public function composeLookup(Request $request) + { + $this->validate($request, [ + 'q' => 'required|string|min:2|max:50', + 'remote' => 'nullable', + ]); + + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) { + return []; + } + + $q = $request->input('q'); + $r = $request->input('remote', false); + + if($r && !Str::of($q)->contains('.')) { + return []; + } + + if($r && Helpers::validateUrl($q)) { + Helpers::profileFetch($q); + } + + if(Str::of($q)->startsWith('@')) { + if(strlen($q) < 3) { + return []; + } + if(substr_count($q, '@') == 2) { + WebfingerService::lookup($q); + } + $q = mb_substr($q, 1); + } + + $blocked = UserFilter::whereFilterableType('App\Profile') + ->whereFilterType('block') + ->whereFilterableId($request->user()->profile_id) + ->pluck('user_id'); + + $blocked->push($request->user()->profile_id); + + $results = Profile::select('id','domain','username') + ->whereNotIn('id', $blocked) + ->where('username','like','%'.$q.'%') + ->orderBy('domain') + ->limit(8) + ->get() + ->map(function($r) { + $acct = AccountService::get($r->id); + return [ + 'local' => (bool) !$r->domain, + 'id' => (string) $r->id, + 'name' => $r->username, + 'privacy' => true, + 'avatar' => $r->avatarUrl(), + 'account' => $acct + ]; + }); + + return $results; + } + + public function read(Request $request) + { + $this->validate($request, [ + 'pid' => 'required', + 'sid' => 'required' + ]); + + $pid = $request->input('pid'); + $sid = $request->input('sid'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $dms = DirectMessage::whereToId($request->user()->profile_id) + ->whereFromId($pid) + ->where('status_id', '>=', $sid) + ->get(); + + $now = now(); + foreach($dms as $dm) { + $dm->read_at = $now; + $dm->save(); + } + + return response()->json($dms->pluck('id')); + } + + public function mute(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + $fid = $request->input('id'); + $pid = $request->user()->profile_id; + + UserFilter::firstOrCreate( + [ + 'user_id' => $pid, + 'filterable_id' => $fid, + 'filterable_type' => 'App\Profile', + 'filter_type' => 'dm.mute' + ] + ); + + return [200]; + } + + public function unmute(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); + + $fid = $request->input('id'); + $pid = $request->user()->profile_id; + + $f = UserFilter::whereUserId($pid) + ->whereFilterableId($fid) + ->whereFilterableType('App\Profile') + ->whereFilterType('dm.mute') + ->firstOrFail(); + + $f->delete(); + + return [200]; + } + + public function remoteDeliver($dm) + { + $profile = $dm->author; + $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; + + $tags = [ + [ + 'type' => 'Mention', + 'href' => $dm->recipient->permalink(), + 'name' => $dm->recipient->emailUrl(), + ] + ]; + + $body = [ + '@context' => [ + 'https://w3id.org/security/v1', + 'https://www.w3.org/ns/activitystreams', + ], + 'id' => $dm->status->permalink(), + 'type' => 'Create', + 'actor' => $dm->status->profile->permalink(), + 'published' => $dm->status->created_at->toAtomString(), + 'to' => [$dm->recipient->permalink()], + 'cc' => [], + 'object' => [ + 'id' => $dm->status->url(), + 'type' => 'Note', + 'summary' => null, + 'content' => $dm->status->rendered ?? $dm->status->caption, + 'inReplyTo' => null, + 'published' => $dm->status->created_at->toAtomString(), + 'url' => $dm->status->url(), + 'attributedTo' => $dm->status->profile->permalink(), + 'to' => [$dm->recipient->permalink()], + 'cc' => [], + 'sensitive' => (bool) $dm->status->is_nsfw, + 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) { + return [ + 'type' => $media->activityVerb(), + 'mediaType' => $media->mime, + 'url' => $media->url(), + 'name' => $media->caption, + ]; + })->toArray(), + 'tag' => $tags, + ] + ]; + + DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high'); + } + + public function remoteDelete($dm) + { + $profile = $dm->author; + $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; + + $body = [ + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + ], + 'id' => $dm->status->permalink('#delete'), + 'to' => [ + 'https://www.w3.org/ns/activitystreams#Public' + ], + 'type' => 'Delete', + 'actor' => $dm->status->profile->permalink(), + 'object' => [ + 'id' => $dm->status->url(), + 'type' => 'Tombstone' + ] + ]; + DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high'); + } } From 71c148c61ea399993e4738ad30b600aa04da4544 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 05:46:02 -0700 Subject: [PATCH 226/977] Update StoryController, add parental controls support --- .../Controllers/StoryComposeController.php | 767 +++++++++--------- app/Http/Controllers/StoryController.php | 492 +++++------ 2 files changed, 645 insertions(+), 614 deletions(-) diff --git a/app/Http/Controllers/StoryComposeController.php b/app/Http/Controllers/StoryComposeController.php index 8f9358b74..eb2d859c0 100644 --- a/app/Http/Controllers/StoryComposeController.php +++ b/app/Http/Controllers/StoryComposeController.php @@ -29,306 +29,315 @@ use App\Jobs\StoryPipeline\StoryFanout; use App\Jobs\StoryPipeline\StoryDelete; use ImageOptimizer; use App\Models\Conversation; +use App\Services\UserRoleService; class StoryComposeController extends Controller { public function apiV1Add(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - ]); + $this->validate($request, [ + 'file' => function() { + return [ + 'required', + 'mimetypes:image/jpeg,image/png,video/mp4', + 'max:' . config_cache('pixelfed.max_photo_size'), + ]; + }, + ]); - $user = $request->user(); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $count = Story::whereProfileId($user->profile_id) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - $count = Story::whereProfileId($user->profile_id) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + if($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + $photo = $request->file('file'); + $path = $this->storePhoto($photo, $user); - $photo = $request->file('file'); - $path = $this->storePhoto($photo, $user); + $story = new Story(); + $story->duration = 3; + $story->profile_id = $user->profile_id; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; + $story->mime = $photo->getMimeType(); + $story->path = $path; + $story->local = true; + $story->size = $photo->getSize(); + $story->bearcap_token = str_random(64); + $story->expires_at = now()->addMinutes(1440); + $story->save(); - $story = new Story(); - $story->duration = 3; - $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; - $story->mime = $photo->getMimeType(); - $story->path = $path; - $story->local = true; - $story->size = $photo->getSize(); - $story->bearcap_token = str_random(64); - $story->expires_at = now()->addMinutes(1440); - $story->save(); + $url = $story->path; - $url = $story->path; + $res = [ + 'code' => 200, + 'msg' => 'Successfully added', + 'media_id' => (string) $story->id, + 'media_url' => url(Storage::url($url)) . '?v=' . time(), + 'media_type' => $story->type + ]; - $res = [ - 'code' => 200, - 'msg' => 'Successfully added', - 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type - ]; + if($story->type === 'video') { + $video = FFMpeg::open($path); + $duration = $video->getDurationInSeconds(); + $res['media_duration'] = $duration; + if($duration > 500) { + Storage::delete($story->path); + $story->delete(); + return response()->json([ + 'message' => 'Video duration cannot exceed 60 seconds' + ], 422); + } + } - if($story->type === 'video') { - $video = FFMpeg::open($path); - $duration = $video->getDurationInSeconds(); - $res['media_duration'] = $duration; - if($duration > 500) { - Storage::delete($story->path); - $story->delete(); - return response()->json([ - 'message' => 'Video duration cannot exceed 60 seconds' - ], 422); - } - } + return $res; + } - return $res; - } + protected function storePhoto($photo, $user) + { + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), [ + 'image/jpeg', + 'image/png', + 'video/mp4' + ]) == false) { + abort(400, 'Invalid media type'); + return; + } - protected function storePhoto($photo, $user) - { - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]) == false) { - abort(400, 'Invalid media type'); - return; - } + $storagePath = MediaPathService::story($user->profile); + $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); + if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) { + $fpath = storage_path('app/' . $path); + $img = Intervention::make($fpath); + $img->orientate(); + $img->save($fpath, config_cache('pixelfed.image_quality')); + $img->destroy(); + } + return $path; + } - $storagePath = MediaPathService::story($user->profile); - $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); - if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) { - $fpath = storage_path('app/' . $path); - $img = Intervention::make($fpath); - $img->orientate(); - $img->save($fpath, config_cache('pixelfed.image_quality')); - $img->destroy(); - } - return $path; - } + public function cropPhoto(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function cropPhoto(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'media_id' => 'required|integer|min:1', + 'width' => 'required', + 'height' => 'required', + 'x' => 'required', + 'y' => 'required' + ]); - $this->validate($request, [ - 'media_id' => 'required|integer|min:1', - 'width' => 'required', - 'height' => 'required', - 'x' => 'required', - 'y' => 'required' - ]); + $user = $request->user(); + $id = $request->input('media_id'); + $width = round($request->input('width')); + $height = round($request->input('height')); + $x = round($request->input('x')); + $y = round($request->input('y')); - $user = $request->user(); - $id = $request->input('media_id'); - $width = round($request->input('width')); - $height = round($request->input('height')); - $x = round($request->input('x')); - $y = round($request->input('y')); + $story = Story::whereProfileId($user->profile_id)->findOrFail($id); - $story = Story::whereProfileId($user->profile_id)->findOrFail($id); + $path = storage_path('app/' . $story->path); - $path = storage_path('app/' . $story->path); + if(!is_file($path)) { + abort(400, 'Invalid or missing media.'); + } - if(!is_file($path)) { - abort(400, 'Invalid or missing media.'); - } + if($story->type === 'photo') { + $img = Intervention::make($path); + $img->crop($width, $height, $x, $y); + $img->resize(1080, 1920, function ($constraint) { + $constraint->aspectRatio(); + }); + $img->save($path, config_cache('pixelfed.image_quality')); + } - if($story->type === 'photo') { - $img = Intervention::make($path); - $img->crop($width, $height, $x, $y); - $img->resize(1080, 1920, function ($constraint) { - $constraint->aspectRatio(); - }); - $img->save($path, config_cache('pixelfed.image_quality')); - } + return [ + 'code' => 200, + 'msg' => 'Successfully cropped', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully cropped', - ]; - } + public function publishStory(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function publishStory(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'media_id' => 'required', + 'duration' => 'required|integer|min:3|max:120', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean' + ]); - $this->validate($request, [ - 'media_id' => 'required', - 'duration' => 'required|integer|min:3|max:120', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $id = $request->input('media_id'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); - $id = $request->input('media_id'); - $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); + $story->active = true; + $story->duration = $request->input('duration', 10); + $story->can_reply = $request->input('can_reply'); + $story->can_react = $request->input('can_react'); + $story->save(); - $story->active = true; - $story->duration = $request->input('duration', 10); - $story->can_reply = $request->input('can_reply'); - $story->can_react = $request->input('can_react'); - $story->save(); + StoryService::delLatest($story->profile_id); + StoryFanout::dispatch($story)->onQueue('story'); + StoryService::addRotateQueue($story->id); - StoryService::delLatest($story->profile_id); - StoryFanout::dispatch($story)->onQueue('story'); - StoryService::addRotateQueue($story->id); + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + public function apiV1Delete(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function apiV1Delete(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); - $user = $request->user(); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + $story->active = false; + $story->save(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - $story->active = false; - $story->save(); + StoryDelete::dispatch($story)->onQueue('story'); - StoryDelete::dispatch($story)->onQueue('story'); + return [ + 'code' => 200, + 'msg' => 'Successfully deleted' + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully deleted' - ]; - } + public function compose(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); - public function compose(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + return view('stories.compose'); + } - return view('stories.compose'); - } + public function createPoll(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(!config_cache('instance.polls.enabled'), 404); - public function createPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - abort_if(!config_cache('instance.polls.enabled'), 404); + return $request->all(); + } - return $request->all(); - } + public function publishStoryPoll(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function publishStoryPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'question' => 'required|string|min:6|max:140', + 'options' => 'required|array|min:2|max:4', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean' + ]); - $this->validate($request, [ - 'question' => 'required|string|min:6|max:140', - 'options' => 'required|array|min:2|max:4', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; - $pid = $request->user()->profile_id; + $count = Story::whereProfileId($pid) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - $count = Story::whereProfileId($pid) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + if($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + $story = new Story; + $story->type = 'poll'; + $story->story = json_encode([ + 'question' => $request->input('question'), + 'options' => $request->input('options') + ]); + $story->public = false; + $story->local = true; + $story->profile_id = $pid; + $story->expires_at = now()->addMinutes(1440); + $story->duration = 30; + $story->can_reply = false; + $story->can_react = false; + $story->save(); - $story = new Story; - $story->type = 'poll'; - $story->story = json_encode([ - 'question' => $request->input('question'), - 'options' => $request->input('options') - ]); - $story->public = false; - $story->local = true; - $story->profile_id = $pid; - $story->expires_at = now()->addMinutes(1440); - $story->duration = 30; - $story->can_reply = false; - $story->can_react = false; - $story->save(); + $poll = new Poll; + $poll->story_id = $story->id; + $poll->profile_id = $pid; + $poll->poll_options = $request->input('options'); + $poll->expires_at = $story->expires_at; + $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { + return 0; + })->toArray(); + $poll->save(); - $poll = new Poll; - $poll->story_id = $story->id; - $poll->profile_id = $pid; - $poll->poll_options = $request->input('options'); - $poll->expires_at = $story->expires_at; - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { - return 0; - })->toArray(); - $poll->save(); + $story->active = true; + $story->save(); - $story->active = true; - $story->save(); + StoryService::delLatest($story->profile_id); - StoryService::delLatest($story->profile_id); + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + public function storyPollVote(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function storyPollVote(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'ci' => 'required|integer|min:0|max:3' + ]); - $this->validate($request, [ - 'sid' => 'required', - 'ci' => 'required|integer|min:0|max:3' - ]); + $pid = $request->user()->profile_id; + $ci = $request->input('ci'); + $story = Story::findOrFail($request->input('sid')); + abort_if(!FollowerService::follows($pid, $story->profile_id), 403); + $poll = Poll::whereStoryId($story->id)->firstOrFail(); - $pid = $request->user()->profile_id; - $ci = $request->input('ci'); - $story = Story::findOrFail($request->input('sid')); - abort_if(!FollowerService::follows($pid, $story->profile_id), 403); - $poll = Poll::whereStoryId($story->id)->firstOrFail(); + $vote = new PollVote; + $vote->profile_id = $pid; + $vote->poll_id = $poll->id; + $vote->story_id = $story->id; + $vote->status_id = null; + $vote->choice = $ci; + $vote->save(); - $vote = new PollVote; - $vote->profile_id = $pid; - $vote->poll_id = $poll->id; - $vote->story_id = $story->id; - $vote->status_id = null; - $vote->choice = $ci; - $vote->save(); + $poll->votes_count = $poll->votes_count + 1; + $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { + return $ci == $key ? $tally + 1 : $tally; + })->toArray(); + $poll->save(); - $poll->votes_count = $poll->votes_count + 1; - $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { - return $ci == $key ? $tally + 1 : $tally; - })->toArray(); - $poll->save(); + return 200; + } - return 200; - } + public function storeReport(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function storeReport(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ + $this->validate($request, [ 'type' => 'required|alpha_dash', 'id' => 'required|integer|min:1', ]); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; $sid = $request->input('id'); $type = $request->input('type'); @@ -355,17 +364,17 @@ class StoryComposeController extends Controller abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow'); if( Report::whereProfileId($pid) - ->whereObjectType('App\Story') - ->whereObjectId($story->id) - ->exists() + ->whereObjectType('App\Story') + ->whereObjectId($story->id) + ->exists() ) { - return response()->json(['error' => [ - 'code' => 409, - 'message' => 'Cannot report the same story again' - ]], 409); + return response()->json(['error' => [ + 'code' => 409, + 'message' => 'Cannot report the same story again' + ]], 409); } - $report = new Report; + $report = new Report; $report->profile_id = $pid; $report->user_id = $request->user()->id; $report->object_id = $story->id; @@ -376,149 +385,151 @@ class StoryComposeController extends Controller $report->save(); return [200]; - } + } - public function react(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'reaction' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('reaction'); + public function react(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'reaction' => 'required|string' + ]); + $pid = $request->user()->profile_id; + $text = $request->input('reaction'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - $story = Story::findOrFail($request->input('sid')); + abort_if(!$story->can_react, 422); + abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); - abort_if(!$story->can_react, 422); - abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); + $status = new Status; + $status->profile_id = $pid; + $status->type = 'story:reaction'; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'reaction' => $text + ]); + $status->save(); - $status = new Status; - $status->profile_id = $pid; - $status->type = 'story:reaction'; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'reaction' => $text - ]); - $status->save(); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:react'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'reaction' => $text + ]); + $dm->save(); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:react'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text - ]); - $dm->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid + ], + [ + 'type' => 'story:react', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:react', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); + if($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:react'; + $n->save(); + } else { + StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); + } - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:react'; - $n->save(); - } else { - StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); - } + StoryService::reactIncrement($story->id, $pid); - StoryService::reactIncrement($story->id, $pid); + return 200; + } - return 200; - } + public function comment(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'caption' => 'required|string' + ]); + $pid = $request->user()->profile_id; + $text = $request->input('caption'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - public function comment(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'caption' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('caption'); + abort_if(!$story->can_reply, 422); - $story = Story::findOrFail($request->input('sid')); + $status = new Status; + $status->type = 'story:reply'; + $status->profile_id = $pid; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id + ]); + $status->save(); - abort_if(!$story->can_reply, 422); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text + ]); + $dm->save(); - $status = new Status; - $status->type = 'story:reply'; - $status->profile_id = $pid; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id - ]); - $status->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); + if($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + } else { + StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); + } - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - } else { - StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); - } - - return 200; - } + return 200; + } } diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index 5a9fb5530..692e27961 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -28,288 +28,308 @@ use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Resource\Item; use App\Transformer\ActivityPub\Verb\StoryVerb; use App\Jobs\StoryPipeline\StoryViewDeliver; +use App\Services\UserRoleService; class StoryController extends StoryComposeController { - public function recent(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $pid = $request->user()->profile_id; + public function recent(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $pid = $user->profile_id; - if(config('database.default') == 'pgsql') { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->get() - ->map(function($s) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $s->profile_id; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }) - ->unique('profile_id'); - }); + if(config('database.default') == 'pgsql') { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->get() + ->map(function($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }) + ->unique('profile_id'); + }); - } else { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->groupBy('followers.following_id') - ->orderByDesc('id') - ->get(); - }); - } + } else { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->groupBy('followers.following_id') + ->orderByDesc('id') + ->get(); + }); + } - $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { - return Story::whereProfileId($pid) - ->whereActive(true) - ->orderByDesc('id') - ->limit(1) - ->get() - ->map(function($s) use($pid) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $pid; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }); - }); + $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { + return Story::whereProfileId($pid) + ->whereActive(true) + ->orderByDesc('id') + ->limit(1) + ->get() + ->map(function($s) use($pid) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $pid; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }); + }); - if($self->count()) { - $s->prepend($self->first()); - } + if($self->count()) { + $s->prepend($self->first()); + } - $res = $s->map(function($s) use($pid) { - $profile = AccountService::get($s->profile_id); - $url = $profile['local'] ? url("/stories/{$profile['username']}") : - url("/i/rs/{$profile['id']}"); - return [ - 'pid' => $profile['id'], - 'avatar' => $profile['avatar'], - 'local' => $profile['local'], - 'username' => $profile['acct'], - 'latest' => [ - 'id' => $s->id, - 'type' => $s->type, - 'preview_url' => url(Storage::url($s->path)) - ], - 'url' => $url, - 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), - 'sid' => $s->id - ]; - }) - ->sortBy('seen') - ->values(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + $res = $s->map(function($s) use($pid) { + $profile = AccountService::get($s->profile_id); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); + return [ + 'pid' => $profile['id'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'username' => $profile['acct'], + 'latest' => [ + 'id' => $s->id, + 'type' => $s->type, + 'preview_url' => url(Storage::url($s->path)) + ], + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), + 'sid' => $s->id + ]; + }) + ->sortBy('seen') + ->values(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function profile(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function profile(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $authed = $request->user()->profile_id; - $profile = Profile::findOrFail($id); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile_id; + $profile = Profile::findOrFail($id); - if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { - return abort([], 403); - } + if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { + return abort([], 403); + } - $stories = Story::whereProfileId($profile->id) - ->whereActive(true) - ->orderBy('expires_at') - ->get() - ->map(function($s, $k) use($authed) { - $seen = StoryService::hasSeen($authed, $s->id); - $res = [ - 'id' => (string) $s->id, - 'type' => $s->type, - 'duration' => $s->duration, - 'src' => url(Storage::url($s->path)), - 'created_at' => $s->created_at->toAtomString(), - 'expires_at' => $s->expires_at->toAtomString(), - 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, - 'seen' => $seen, - 'progress' => $seen ? 100 : 0, - 'can_reply' => (bool) $s->can_reply, - 'can_react' => (bool) $s->can_react - ]; + $stories = Story::whereProfileId($profile->id) + ->whereActive(true) + ->orderBy('expires_at') + ->get() + ->map(function($s, $k) use($authed) { + $seen = StoryService::hasSeen($authed, $s->id); + $res = [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'duration' => $s->duration, + 'src' => url(Storage::url($s->path)), + 'created_at' => $s->created_at->toAtomString(), + 'expires_at' => $s->expires_at->toAtomString(), + 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, + 'seen' => $seen, + 'progress' => $seen ? 100 : 0, + 'can_reply' => (bool) $s->can_reply, + 'can_react' => (bool) $s->can_react + ]; - if($s->type == 'poll') { - $res['question'] = json_decode($s->story, true)['question']; - $res['options'] = json_decode($s->story, true)['options']; - $res['voted'] = PollService::votedStory($s->id, $authed); - if($res['voted']) { - $res['voted_index'] = PollService::storyChoice($s->id, $authed); - } - } + if($s->type == 'poll') { + $res['question'] = json_decode($s->story, true)['question']; + $res['options'] = json_decode($s->story, true)['options']; + $res['voted'] = PollService::votedStory($s->id, $authed); + if($res['voted']) { + $res['voted_index'] = PollService::storyChoice($s->id, $authed); + } + } - return $res; - })->toArray(); - if(count($stories) == 0) { - return []; - } - $cursor = count($stories) - 1; - $stories = [[ - 'id' => (string) $stories[$cursor]['id'], - 'nodes' => $stories, - 'account' => AccountService::get($profile->id), - 'pid' => (string) $profile->id - ]]; - return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + return $res; + })->toArray(); + if(count($stories) == 0) { + return []; + } + $cursor = count($stories) - 1; + $stories = [[ + 'id' => (string) $stories[$cursor]['id'], + 'nodes' => $stories, + 'account' => AccountService::get($profile->id), + 'pid' => (string) $profile->id + ]]; + return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function viewed(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewed(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'id' => 'required|min:1', - ]); - $id = $request->input('id'); + $this->validate($request, [ + 'id' => 'required|min:1', + ]); + $id = $request->input('id'); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile; - $authed = $request->user()->profile; + $story = Story::with('profile') + ->findOrFail($id); + $exp = $story->expires_at; - $story = Story::with('profile') - ->findOrFail($id); - $exp = $story->expires_at; + $profile = $story->profile; - $profile = $story->profile; + if($story->profile_id == $authed->id) { + return []; + } - if($story->profile_id == $authed->id) { - return []; - } + $publicOnly = (bool) $profile->followedBy($authed); + abort_if(!$publicOnly, 403); - $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + $v = StoryView::firstOrCreate([ + 'story_id' => $id, + 'profile_id' => $authed->id + ]); - $v = StoryView::firstOrCreate([ - 'story_id' => $id, - 'profile_id' => $authed->id - ]); + if($v->wasRecentlyCreated) { + Story::findOrFail($story->id)->increment('view_count'); - if($v->wasRecentlyCreated) { - Story::findOrFail($story->id)->increment('view_count'); + if($story->local == false) { + StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); + } + } - if($story->local == false) { - StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); - } - } + Cache::forget('stories:recent:by_id:' . $authed->id); + StoryService::addSeen($authed->id, $story->id); + return ['code' => 200]; + } - Cache::forget('stories:recent:by_id:' . $authed->id); - StoryService::addSeen($authed->id, $story->id); - return ['code' => 200]; - } + public function exists(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json(false); + } + return response()->json(Story::whereProfileId($id) + ->whereActive(true) + ->exists()); + } - public function exists(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function iRedirect(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - return response()->json(Story::whereProfileId($id) - ->whereActive(true) - ->exists()); - } + $user = $request->user(); + abort_if(!$user, 404); + $username = $user->username; + return redirect("/stories/{$username}"); + } - public function iRedirect(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewers(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $user = $request->user(); - abort_if(!$user, 404); - $username = $user->username; - return redirect("/stories/{$username}"); - } + $this->validate($request, [ + 'sid' => 'required|string' + ]); - public function viewers(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json([]); + } - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + $viewers = StoryView::whereStoryId($story->id) + ->latest() + ->simplePaginate(10) + ->map(function($view) { + return AccountService::get($view->profile_id); + }) + ->values(); - $viewers = StoryView::whereStoryId($story->id) - ->latest() - ->simplePaginate(10) - ->map(function($view) { - return AccountService::get($view->profile_id); - }) - ->values(); + return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + public function remoteStory(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function remoteStory(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $profile = Profile::findOrFail($id); + if($profile->user_id != null || $profile->domain == null) { + return redirect('/stories/' . $profile->username); + } + $pid = $profile->id; + return view('stories.show_remote', compact('pid')); + } - $profile = Profile::findOrFail($id); - if($profile->user_id != null || $profile->domain == null) { - return redirect('/stories/' . $profile->username); - } - $pid = $profile->id; - return view('stories.show_remote', compact('pid')); - } + public function pollResults(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function pollResults(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required|string' + ]); - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + return PollService::storyResults($sid); + } - return PollService::storyResults($sid); - } + public function getActivityObject(Request $request, $username, $id) + { + abort_if(!config_cache('instance.stories.enabled'), 404); - public function getActivityObject(Request $request, $username, $id) - { - abort_if(!config_cache('instance.stories.enabled'), 404); + if(!$request->wantsJson()) { + return redirect('/stories/' . $username); + } - if(!$request->wantsJson()) { - return redirect('/stories/' . $username); - } + abort_if(!$request->hasHeader('Authorization'), 404); - abort_if(!$request->hasHeader('Authorization'), 404); + $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); + $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); - $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); - $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); + abort_if($story->bearcap_token == null, 404); + abort_if(now()->gt($story->expires_at), 404); + $token = substr($request->header('Authorization'), 7); + abort_if(hash_equals($story->bearcap_token, $token) === false, 404); + abort_if($story->created_at->lt(now()->subMinutes(20)), 404); - abort_if($story->bearcap_token == null, 404); - abort_if(now()->gt($story->expires_at), 404); - $token = substr($request->header('Authorization'), 7); - abort_if(hash_equals($story->bearcap_token, $token) === false, 404); - abort_if($story->created_at->lt(now()->subMinutes(20)), 404); + $fractal = new Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Item($story, new StoryVerb()); + $res = $fractal->createData($resource)->toArray(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - $fractal = new Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Item($story, new StoryVerb()); - $res = $fractal->createData($resource)->toArray(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function showSystemStory() - { - // return view('stories.system'); - } + public function showSystemStory() + { + // return view('stories.system'); + } } From c7ed684a5c0488d3501a23e9c5101a35baa17af0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 06:31:19 -0700 Subject: [PATCH 227/977] Update ParentalControlsController --- app/Http/Controllers/ParentalControlsController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php index 373021ab9..24f7747ac 100644 --- a/app/Http/Controllers/ParentalControlsController.php +++ b/app/Http/Controllers/ParentalControlsController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\ParentalControls; use App\Models\UserRoles; +use App\Profile; use App\User; use App\Http\Controllers\Auth\RegisterController; use Illuminate\Auth\Events\Registered; @@ -65,6 +66,11 @@ class ParentalControlsController extends Controller $pc->save(); $roles = UserRoleService::mapActions($pc->child_id, $ff); + if(isset($roles['account-force-private'])) { + $c = Profile::whereUserId($pc->child_id)->first(); + $c->is_private = $roles['account-force-private']; + $c->save(); + } UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]); return redirect($pc->manageUrl() . '?permissions'); } From db1b466792e2b6229b2632bf7ff8edba0989d8c9 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 06:45:32 -0700 Subject: [PATCH 228/977] Update instance config --- config/instance.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/instance.php b/config/instance.php index 5e173684c..03f666a79 100644 --- a/config/instance.php +++ b/config/instance.php @@ -132,11 +132,11 @@ return [ ], 'parental_controls' => [ - 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', true), + 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', false), 'limits' => [ 'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true), - 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 10), + 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 1), 'auto_verify_email' => true, ], ] From c91f1c595aff9cccba6c58485f6f531fdec3d18b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 06:52:12 -0700 Subject: [PATCH 229/977] Update ParentalControlsController, prevent children from adding accounts --- app/Http/Controllers/ParentalControlsController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php index 24f7747ac..7d8625863 100644 --- a/app/Http/Controllers/ParentalControlsController.php +++ b/app/Http/Controllers/ParentalControlsController.php @@ -19,6 +19,7 @@ class ParentalControlsController extends Controller { if($authCheck) { abort_unless($request->user(), 404); + abort_unless($request->user()->has_roles === 0, 404); } abort_unless(config('instance.parental_controls.enabled'), 404); if(config_cache('pixelfed.open_registration') == false) { From 85a612742d3c9f6994b9740adf545f4f2a28104c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 06:54:20 -0700 Subject: [PATCH 230/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13baca035..06c1a9720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7)) - Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46)) - Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) +- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) From 84c9aeb514cc05f3d46ec004f49f90b0ddd17841 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 14:16:54 +0000 Subject: [PATCH 231/977] fixing postgresql and some more utility help --- Dockerfile | 7 ++ .../docker/entrypoint.d/00-check-config.sh | 18 ++++ .../root/docker/entrypoint.d/05-templating.sh | 4 +- .../entrypoint.d/11-first-time-setup.sh | 10 --- .../root/docker/entrypoint.d/12-migrations.sh | 16 ++-- docker/shared/root/docker/helpers.sh | 82 +++++++++++++++++-- docker/shared/root/docker/install/base.sh | 3 + 7 files changed, 111 insertions(+), 29 deletions(-) create mode 100755 docker/shared/root/docker/entrypoint.d/00-check-config.sh diff --git a/Dockerfile b/Dockerfile index 116631f60..33d0eeee3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,11 @@ ARG FOREGO_VERSION="0.17.2" # See: https://github.com/hairyhenderson/gomplate ARG GOMPLATE_VERSION="v3.11.6" +# See: https://github.com/dotenv-linter/dotenv-linter/releases +# +# WARN: v3.3.0 and above requires newer libc version than Ubuntu ships with +ARG DOTENV_LINTER_VERSION="v3.2.0" + ### # PHP base configuration ### @@ -99,8 +104,10 @@ ARG PHP_VERSION ARG RUNTIME_GID ARG RUNTIME_UID ARG TARGETPLATFORM +ARG DOTENV_LINTER_VERSION ENV DEBIAN_FRONTEND="noninteractive" +ENV DOTENV_LINTER_VERSION="${DOTENV_LINTER_VERSION}" # Ensure we run all scripts through 'bash' rather than 'sh' SHELL ["/bin/bash", "-c"] diff --git a/docker/shared/root/docker/entrypoint.d/00-check-config.sh b/docker/shared/root/docker/entrypoint.d/00-check-config.sh new file mode 100755 index 000000000..36244f6ca --- /dev/null +++ b/docker/shared/root/docker/entrypoint.d/00-check-config.sh @@ -0,0 +1,18 @@ +#!/bin/bash +source /docker/helpers.sh + +entrypoint-set-script-name "$0" + +# Validating dot-env files for any issues +for file in "${dot_env_files[@]}"; do + if file-exists "$file"; then + log-warning "Could not source file [${file}]: does not exists" + continue + fi + + log-info "Linting dotenv file ${file}" + dotenv-linter --skip=QuoteCharacter --skip=UnorderedKey "${file}" +done + +# Write the config cache +run-as-runtime-user php artisan config:cache diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh index cafb9d133..1bb62aae6 100755 --- a/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -49,7 +49,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat cat "${template_file}" | gomplate >"${output_file_path}" # Show the diff from the envsubst command - if [[ ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:-1} = 1 ]]; then - git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : + if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then + git --no-pager diff --color=always "${template_file}" "${output_file_path}" || : # ignore diff exit code fi done diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 529c1d0cf..f9fd6a61b 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -10,13 +10,3 @@ only-once "storage:link" run-as-runtime-user php artisan storage:link only-once "key:generate" run-as-runtime-user php artisan key:generate only-once "initial:migrate" run-as-runtime-user php artisan migrate --force only-once "import:cities" run-as-runtime-user php artisan import:cities - -# if [ ! -e "./storage/docker-instance-actor-has-run" ]; then -# run-as-runtime-user php artisan instance:actor -# touch "./storage/docker-instance-actor-has-run" -# fi - -# if [ ! -e "./storage/docker-passport-keys-has-run" ]; then -# run-as-runtime-user php artisan instance:actor -# touch "./storage/docker-passport-keys-has-run" -# fi diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index 68008c596..20cbe3a31 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -6,13 +6,6 @@ entrypoint-set-script-name "$0" # Allow automatic applying of outstanding/new migrations on startup : ${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0} -if [[ $DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY -eq 0 ]]; then - log-info "Automatic applying of new database migrations is disabled" - log-info "Please set [DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." - - exit 0 -fi - # Wait for the database to be ready await-database-ready @@ -20,7 +13,7 @@ await-database-ready declare -i new_migrations=0 run-as-runtime-user php artisan migrate:status | grep No && new_migrations=1 -if [[ $new_migrations -eq 0 ]]; then +if is-true "${new_migrations}"; then log-info "No outstanding migrations detected" exit 0 @@ -28,4 +21,11 @@ fi log-warning "New migrations available, will automatically apply them now" +if is-false "${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then + log-info "Automatic applying of new database migrations is disabled" + log-info "Please set [DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." + + exit 0 +fi + run-as-runtime-user php artisan migrate --force diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 24bd7f1f8..e71b4f295 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -224,17 +224,17 @@ function load-config-files() { # Associative array (aka map/dictionary) holding the unique keys found in dot-env files local -A _tmp_dot_env_keys - for f in "${dot_env_files[@]}"; do - if [ ! -e "$f" ]; then - log-warning "Could not source file [${f}]: does not exists" + for file in "${dot_env_files[@]}"; do + if ! file-exists "${file}"; then + log-warning "Could not source file [${file}]: does not exists" continue fi - log-info "Sourcing ${f}" - source "${f}" + log-info "Sourcing ${file}" + source "${file}" # find all keys in the dot-env file and store them in our temp associative array - for k in "$(grep -v '^#' "${f}" | sed -E 's/(.*)=.*/\1/' | xargs)"; do + for k in "$(grep -v '^#' "${file}" | cut -d"=" -f1 | xargs)"; do _tmp_dot_env_keys[$k]=1 done done @@ -270,6 +270,21 @@ function is-writable() { [[ -w "$1" ]] } +# @description Checks if $1 exists (directory or file) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function path-exists() { + [[ -e "$1" ]] +} + +# @description Checks if $1 exists (file only) +# @arg $1 string The path to check +# @exitcode 0 If $1 exists +# @exitcode 1 If $1 does *NOT* exists +function file-exists() { + [[ -f "$1" ]] +} # @description Checks if $1 contains any files or not # @arg $1 string The path to check # @exitcode 0 If $1 contains files @@ -328,7 +343,7 @@ function acquire-lock() { ensure-directory-exists "$(dirname "${file}")" log-info "🔑 Trying to acquire lock: ${file}: " - while [[ -e "${file}" ]]; do + while file-exists "${file}"; do log-info "🔒 Waiting on lock ${file}" staggered-sleep @@ -384,15 +399,17 @@ declare -f -t on-trap function await-database-ready() { log-info "❓ Waiting for database to be ready" + load-config-files + case "${DB_CONNECTION:-}" in mysql) - while ! echo "SELECT 1" | mysql --user="$DB_USERNAME" --password="$DB_PASSWORD" --host="$DB_HOST" "$DB_DATABASE" --silent >/dev/null; do + while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do staggered-sleep done ;; pgsql) - while ! echo "SELECT 1" | psql --user="$DB_USERNAME" --password="$DB_PASSWORD" --host="$DB_HOST" "$DB_DATABASE" >/dev/null; do + while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" >/dev/null; do staggered-sleep done ;; @@ -453,3 +470,50 @@ function show-call-stack() { log-error " at: ${func} ${src}:${lineno}" done } + +# @description Helper function see if $1 could be considered truthy +# @arg $1 string The string to evaluate +# @see as-boolean +function is-true() { + as-boolean "${1:-}" && return 0 +} + +# @description Helper function see if $1 could be considered falsey +# @arg $1 string The string to evaluate +# @see as-boolean +function is-false() { + as-boolean "${1:-}" || return 0 +} + +# @description Helper function see if $1 could be truethy or falsey. +# since this is a bash context, returning 0 is true and 1 is false +# so it works with [if is-false $input; then .... fi] +# +# This is a bit confusing, *especially* in a PHP world where [1] would be truthy and +# [0] would be falsely as return values +# @arg $1 string The string to evaluate +function as-boolean() { + local input="${1:-}" + local var="${input,,}" # convert input to lower-case + + case "$var" in + 1 | true) + log-info "[as-boolean] variable [${var}] was detected as truthy/true, returning [0]" + + return 0 + ;; + + 0 | false) + log-info "[as-boolean] variable [${var}] was detected as falsey/false, returning [1]" + + return 1 + ;; + + *) + log-warning "[as-boolean] variable [${var}] could not be detected as true or false, returning [1] (false) as default" + + return 1 + ;; + + esac +} diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index d3da207e5..5856836f9 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -83,3 +83,6 @@ apt-get install -y \ locale-gen update-locale + +# Install dotenv linter (https://github.com/dotenv-linter/dotenv-linter) +curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin ${DOTENV_LINTER_VERSION} From c258a15761c1a8c22214119fa74571cb98824385 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 14:42:54 +0000 Subject: [PATCH 232/977] cleanup a bit --- .env.docker | 10 ++-------- docker-compose.yml | 7 ++----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.env.docker b/.env.docker index 2b4eba197..b7094fb49 100644 --- a/.env.docker +++ b/.env.docker @@ -929,16 +929,10 @@ DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" # Port that the web will listen on *outside* the container (e.g. the host machine) for HTTP traffic -DOCKER_WEB_HTTP_PORT_EXTERNAL="8080" +DOCKER_PROXY_PORT_EXTERNAL_HTTP="80" # Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic -DOCKER_WEB_HTTPS_PORT_EXTERNAL="444" - -# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTP traffic -DOCKER_PROXY_PORT_EXTERNAL_HTTP="8080" - -# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic -DOCKER_PROXY_PORT_EXTERNAL_HTTPS="444" +DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" # Path to the Docker socket on the *host* DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" diff --git a/docker-compose.yml b/docker-compose.yml index 3b131635f..888a65847 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: proxy: image: nginxproxy/nginx-proxy:1.4 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" - #restart: unless-stopped + restart: unless-stopped volumes: - "${DOCKER_HOST_SOCKET_PATH}:/tmp/docker.sock:ro" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" @@ -29,7 +29,7 @@ services: proxy-acme: image: nginxproxy/acme-companion container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" - #restart: unless-stopped + restart: unless-stopped environment: DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -67,9 +67,6 @@ services: com.github.nginx-proxy.nginx-proxy.keepalive: 30 com.github.nginx-proxy.nginx-proxy.http2.enable: true com.github.nginx-proxy.nginx-proxy.http3.enable: true - # ports: - # - "${DOCKER_WEB_HTTP_PORT_EXTERNAL}:80" - # - "${DOCKER_WEB_HTTPS_PORT_EXTERNAL}:443" depends_on: - db - redis From edbc1e4d60be0490dc3c79a449d9eb4d188805df Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 14:44:47 +0000 Subject: [PATCH 233/977] expand docs --- .env.docker | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.docker b/.env.docker index b7094fb49..00bdb4570 100644 --- a/.env.docker +++ b/.env.docker @@ -102,7 +102,12 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # Defaults to "1000". # # See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users -PF_MAX_USERS="false" +PF_MAX_USERS="1000" + +# Enforce the maximum number of user accounts +# +# Defaults to "true". +#PF_ENFORCE_MAX_USERS="true" # See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled OAUTH_ENABLED="true" From 543dac34f6445829596c30f9d8fb1f942ae353f7 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 14:48:12 +0000 Subject: [PATCH 234/977] update path --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a5cdf3af1..ebfef1ace 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /.idea /.vagrant /.vscode -/docker-compose-state/ +/docker-compose/ /node_modules /public/hot /public/storage From 519704cbe84907d3a349eb068f413eb829ef146e Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 14:57:40 +0000 Subject: [PATCH 235/977] more tuning --- .env.docker | 34 ++++++++++++++++++++-------------- .github/workflows/docker.yml | 3 ++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.env.docker b/.env.docker index 00bdb4570..559c06553 100644 --- a/.env.docker +++ b/.env.docker @@ -19,22 +19,30 @@ DOCKER_CONFIG_ROOT="./docker-compose/config" # DOCKER_IMAGE="ghcr.io/jippi/pixelfed" -# Pixelfed version (image tag) to pull from the registry +# Pixelfed version (image tag) to pull from the registry. +# +# See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed DOCKER_TAG="branch-jippi-fork-apache-8.1" -# Set timezone used by *all* containers - these should be in sync +# Set timezone used by *all* containers - these should be in sync. # # See: https://www.php.net/manual/en/timezones.php TZ="UTC" -# Automatically run [artisan migrate --force] if new migrations are detected +# Automatically run [artisan migrate --force] if new migrations are detected. DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" -# The e-mail to use for Lets Encrypt certificate requests +# The e-mail to use for Lets Encrypt certificate requests. LETSENCRYPT_EMAIL="__CHANGE_ME__" -# Lets Encrypt staging/test servers for certificate requests -LETSENCRYPT_TEST= +# Lets Encrypt staging/test servers for certificate requests. +# +# Setting this to any value will change to letsencrypt test servers. +#LETSENCRYPT_TEST="1" + +# Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) +# by setting this to "1". +#ENTRYPOINT_DEBUG="0" ############################################################### # Pixelfed application configuration @@ -42,13 +50,13 @@ LETSENCRYPT_TEST= # A random 32-character string to be used as an encryption key. # -# No default value; use [php artisan key:generate] to generate. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# ! NOTE: This will be auto-generated by Docker during bootstrap +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # # This key is used by the Illuminate encrypter service and should be set to a random, # 32 character string, otherwise these encrypted strings will not be safe. # -# Please do this before deploying an application! -# # See: https://docs.pixelfed.org/technical-documentation/config/#app_key APP_KEY= @@ -102,7 +110,7 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # Defaults to "1000". # # See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users -PF_MAX_USERS="1000" +#PF_MAX_USERS="1000" # Enforce the maximum number of user accounts # @@ -152,7 +160,7 @@ APP_TIMEZONE="${TZ}" # Defaults to "15000" (15MB). # # See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb -MAX_PHOTO_SIZE="15000" +#MAX_PHOTO_SIZE="15000" # Update the max avatar size, in kB. # @@ -187,7 +195,7 @@ MAX_PHOTO_SIZE="15000" # Defaults to "4". # # See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length -MAX_ALBUM_LENGTH="4" +#MAX_ALBUM_LENGTH="4" # Resize and optimize image uploads. # @@ -296,8 +304,6 @@ DB_DATABASE="pixelfed_prod" # See: https://docs.pixelfed.org/technical-documentation/config/#db_port DB_PORT="3306" -ENTRYPOINT_DEBUG=0 - ############################################################### # Mail configuration ############################################################### diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d08ab8e73..a2e73a61a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -147,7 +147,8 @@ jobs: latest=auto suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} tags: | - type=edge,branch=dev + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} type=ref,event=branch,prefix=branch- From 9814a39fd8c44cfde6feb854233ea75c3e6315fa Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 15:14:44 +0000 Subject: [PATCH 236/977] more docs --- docker-compose.yml | 3 +++ docker/README.md | 34 ++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 888a65847..567998411 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,9 @@ version: "3" services: # HTTP/HTTPS proxy # + # Sits in front of the *real* webserver and manages SSL and (optionally) + # load-balancing between multiple web servers + # # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs proxy: image: nginxproxy/nginx-proxy:1.4 diff --git a/docker/README.md b/docker/README.md index 837c1f646..c47b1331a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -27,11 +27,23 @@ A safe starter/small instance hardware for 25 users and blow are: * **Storage** `20-50 GB` HDD is fine, but ideally SSD or NVMe, *especially* for the database. * **Network** `100 Mbit/s` or faster. -### Other +### Domain and DNS -* A **Domain** you need a domain (or subdomain) where your Pixelfed server will be running (for example, `pixelfed.social`) -* (Optional) An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. -* (Optional) An **Object Storage** provider for storing all images remotely, rather than locally on your server. +* A **Domain** (or subdomain) is needed for the Pixelfed server (for example, `pixelfed.social` or `pixelfed.mydomain.com`) +* Having the required `A`/`CNAME` DNS records for your domain (above) pointing to your server. + * Typically an `A` record for the root (sometimes shown as `@`) record for `mydomain.com`. + * Possibly an `A` record for `www.` subdomain as well. + +### Network + +* Port `80` (HTTP) and `443` (HTTPS) ports forwarded to the server. + * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `80`: `ufw allow 80` + * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `443`: `ufw allow 443` + +### Optional + +* An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. +* An **Object Storage** provider for storing all images remotely, rather than locally on your server. #### E-mail / SMTP provider @@ -89,7 +101,7 @@ Run the following command to copy the file: `cp .env.docker .env` ### Modifying the configuration file -The configuration file is *quite* long, but the good news is that you can ignore *most* of it, all of the *server* specific settings are configured for you out of the box. +The configuration file is *quite* long, but the good news is that you can ignore *most* of it, most of the *server-specific* settings are configured for you out of the box. The minimum required settings you **must** change is: @@ -122,7 +134,15 @@ docker compose up -d This will download all the required Docker images, start the containers, and being the automatic setup. -You can follow the logs by running `docker compose logs --tail=100 --follow`. +You can follow the logs by running `docker compose logs` - you might want to scroll to the top to logs from the start. + +You can use the CLI flag `--tail=100` to only see the most recent (`100` in this example) log lines for each container. + +You can use the CLI flag `--follow` to continue to see log output from the containers. + +You can combine `--tail=100` and `--follow` like this `docker compose logs --tail=100 --follow`. + +If you only care about specific contaieners, you can add them to the end of the command like this `docker compose logs web worker proxy`. ## Runtimes @@ -130,6 +150,8 @@ The Pixelfed Dockerfile support multiple target *runtimes* ([Apache](#apache), [ You can consider a *runtime* target as individual Dockerfiles, but instead, all of them are build from the same optimized Dockerfile, sharing +90% of their configuration and packages. +**If you are unsure of which runtime to choose, please use the [Apache runtime](#apache) it's the most straightforward one and also the default** + ### Apache Building a custom Pixelfed Docker image using Apache + mod_php can be achieved the following way. From 01ecde15925a27f8ea1341b052e1804ecc8801f4 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 15:32:29 +0000 Subject: [PATCH 237/977] allow skipping one-time setup tasks --- .../root/docker/entrypoint.d/11-first-time-setup.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index f9fd6a61b..6778bd90e 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -3,6 +3,16 @@ source /docker/helpers.sh entrypoint-set-script-name "$0" +# Allow automatic applying of outstanding/new migrations on startup +: ${DOCKER_RUN_ONE_TIME_SETUP_TASKS:=1} + +if is-false "${DOCKER_RUN_ONE_TIME_SETUP_TASKS}"; then + log-warning "Automatic run of the 'One-time setup tasks' is disabled." + log-warning "Please set [DOCKER_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this." + + exit 0 +fi + load-config-files await-database-ready From 20a15c2b659ffe655ab9384198b8483bea7e6544 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 16:09:07 +0000 Subject: [PATCH 238/977] split up docs into smaller docs --- docker/README.md | 443 +----------------------------------------- docker/customizing.md | 201 +++++++++++++++++++ docker/new-server.md | 145 ++++++++++++++ docker/runtimes.md | 94 +++++++++ 4 files changed, 443 insertions(+), 440 deletions(-) create mode 100644 docker/customizing.md create mode 100644 docker/new-server.md create mode 100644 docker/runtimes.md diff --git a/docker/README.md b/docker/README.md index c47b1331a..cbadcbf90 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,442 +1,5 @@ # Pixelfed + Docker + Docker Compose -This guide will help you install and run Pixelfed on **your** server using [Docker Compose](https://docs.docker.com/compose/). - -## Prerequisites - -Recommendations and requirements for hardware and software needed to run Pixelfed using Docker Compose. - -It's highly recommended that you have *some* experience with Linux (e.g. Ubuntu or Debian), SSH, and lightweight server administration. - -### Server - -A VPS or dedicated server you can SSH into, for example - -* [linode.com VPS](https://www.linode.com/) -* [DigitalOcean VPS](https://digitalocean.com/) -* [Hetzner](https://www.hetzner.com/) - -### Hardware - -Hardware requirements depends on the amount of users you have (or plan to have), and how active they are. - -A safe starter/small instance hardware for 25 users and blow are: - -* **CPU/vCPU** `2` cores. -* **RAM** `2-4 GB` as your instance grow, memory requirements will increase for the database. -* **Storage** `20-50 GB` HDD is fine, but ideally SSD or NVMe, *especially* for the database. -* **Network** `100 Mbit/s` or faster. - -### Domain and DNS - -* A **Domain** (or subdomain) is needed for the Pixelfed server (for example, `pixelfed.social` or `pixelfed.mydomain.com`) -* Having the required `A`/`CNAME` DNS records for your domain (above) pointing to your server. - * Typically an `A` record for the root (sometimes shown as `@`) record for `mydomain.com`. - * Possibly an `A` record for `www.` subdomain as well. - -### Network - -* Port `80` (HTTP) and `443` (HTTPS) ports forwarded to the server. - * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `80`: `ufw allow 80` - * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `443`: `ufw allow 443` - -### Optional - -* An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. -* An **Object Storage** provider for storing all images remotely, rather than locally on your server. - -#### E-mail / SMTP provider - -**NOTE**: If you don't plan to use en e-mail/SMTP provider, then make sure to set `ENFORCE_EMAIL_VERIFICATION="false"` in your `.env` file! - -There are *many* providers out there, with wildly different pricing structures, features, and reliability. - -It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: - -* [Simple Email Service (SES)](https://aws.amazon.com/ses/) by Amazon Web Services (AWS) is pay-as-you-go with a cost of $0.10/1000 emails. -* [Brevo](https://www.brevo.com/) (formerly SendInBlue) has a Free Tier with 300 emails/day. -* [Postmark](https://postmarkapp.com/) has a Free Tier with 100 emails/month. -* [Forward Email](https://forwardemail.net/en/private-business-email?pricing=true) has a $3/mo/domain plan with both sending and receiving included. -* [Mailtrap](https://mailtrap.io/email-sending/) has a 1000 emails/month free-tier (their `Email Sending` product, *not* the `Email Testing` one). - -#### Object Storage - -**NOTE**: This is *entirely* optional - by default Pixelfed will store all uploads (videos, images, etc.) directly on your servers storage. - -> Object storage is a technology that stores and manages data in an unstructured format called objects. Modern organizations create and analyze large volumes of unstructured data such as photos, videos, email, web pages, sensor data, and audio files -> -> -- [*What is object storage?*](https://aws.amazon.com/what-is/object-storage/) by Amazon Web Services - -It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: - -* [R2](https://www.cloudflare.com/developer-platform/r2/) by CloudFlare has cheap storage, free *egress* (e.g. people downloading images) and included (and free) Content Delivery Network (CDN). -* [B2 cloud storage](https://www.backblaze.com/cloud-storage) by Backblaze. -* [Simple Storage Service (S3)](https://aws.amazon.com/s3/) by Amazon Web Services. - -### Software - -Required software to be installed on your server - -* `git` can be installed with `apt-get install git` on Debian/Ubuntu -* `docker` can be installed by [following the official Docker documentation](https://docs.docker.com/engine/install/) - -## Getting things ready - -Connect via SSH to your server and decide where you want to install Pixelfed. - -In this guide I'm going to assume it will be installed at `/data/pixelfed`. - -1. **Install required software** as mentioned in the [Software Prerequisites section above](#software) -1. **Create the parent directory** by running `mkdir -p /data` -1. **Clone the Pixelfed repository** by running `git clone https://github.com/pixelfed/pixelfed.git /data/pixelfed` -1. **Change to the Pixelfed directory** by running `cd /data/pixelfed` - -## Modifying your settings (`.env` file) - -### Copy the example configuration file - -Pixelfed contains a default configuration file (`.env.docker`) you should use as a starter, however, before editing anything, make a copy of it and put it in the *right* place (`.env`). - -Run the following command to copy the file: `cp .env.docker .env` - -### Modifying the configuration file - -The configuration file is *quite* long, but the good news is that you can ignore *most* of it, most of the *server-specific* settings are configured for you out of the box. - -The minimum required settings you **must** change is: - -* (required) `APP_DOMAIN` which is the hostname you plan to run your Pixelfed server on (e.g. `pixelfed.social`) - must **not** include `http://` or a trailing slash (`/`)! -* (required) `DB_PASSWORD` which is the database password, you can use a service like [pwgen.io](https://pwgen.io/en/) to generate a secure one. -* (optional) `ENFORCE_EMAIL_VERIFICATION` should be set to `"false"` if you don't plan to send emails. -* (optional) `MAIL_DRIVER` and related `MAIL_*` settings if you plan to use an [email/SMTP provider](#e-mail--smtp-provider) - See [Email variables documentation](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables). -* (optional) `PF_ENABLE_CLOUD` / `FILESYSTEM_CLOUD` if you plan to use an [Object Storage provider](#object-storage). - -See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! - -You need to mainly focus on following sections - -* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) -* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) - -You can skip the following sections, since they are already configured/automated for you: - -* `Redis` -* `Database` (except for `DB_PASSWORD`) -* `One-time setup tasks` - -### Starting the service - -With everything in place and (hopefully) well-configured, we can now go ahead and start our services by running - -```shell -docker compose up -d -``` - -This will download all the required Docker images, start the containers, and being the automatic setup. - -You can follow the logs by running `docker compose logs` - you might want to scroll to the top to logs from the start. - -You can use the CLI flag `--tail=100` to only see the most recent (`100` in this example) log lines for each container. - -You can use the CLI flag `--follow` to continue to see log output from the containers. - -You can combine `--tail=100` and `--follow` like this `docker compose logs --tail=100 --follow`. - -If you only care about specific contaieners, you can add them to the end of the command like this `docker compose logs web worker proxy`. - -## Runtimes - -The Pixelfed Dockerfile support multiple target *runtimes* ([Apache](#apache), [Nginx + FPM](#nginx), and [fpm](#fpm)). - -You can consider a *runtime* target as individual Dockerfiles, but instead, all of them are build from the same optimized Dockerfile, sharing +90% of their configuration and packages. - -**If you are unsure of which runtime to choose, please use the [Apache runtime](#apache) it's the most straightforward one and also the default** - -### Apache - -Building a custom Pixelfed Docker image using Apache + mod_php can be achieved the following way. - -#### docker build (Apache) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target apache-runtime \ - --tag / \ - . -``` - -#### docker compose (Apache) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: apache-runtime -``` - -### Nginx - -Building a custom Pixelfed Docker image using nginx + FPM can be achieved the following way. - -#### docker build (nginx) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target nginx-runtime \ - --build-arg 'PHP_BASE_TYPE=fpm' \ - --tag / \ - . -``` - -#### docker compose (nginx) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: nginx-runtime - args: - PHP_BASE_TYPE: fpm -``` - -### FPM - -Building a custom Pixelfed Docker image using FPM (only) can be achieved the following way. - -#### docker build (fpm) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target fpm-runtime \ - --build-arg 'PHP_BASE_TYPE=fpm' \ - --tag / \ - . -``` - -#### docker compose (fpm) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: fpm-runtime - args: - PHP_BASE_TYPE: fpm -``` - -## Customizing your `Dockerfile` - -### Running commands on container start - -#### Description - -When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint) script will - -1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). -1. Check if the file is executable. - 1. If the file is *not* executable, print an error and exit the container. -1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). -1. If the file has the extension `.sh` the file will be run like a normal script. -1. Any other file extension will log a warning and will be ignored. - -#### Debugging - -You can set environment variable `ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. - -You can also `docker exec` or `docker run` into a container and run `/` - -#### Included scripts - -* `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. -* `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. -* `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. -* `//docker/entrypoint.d/15-storage-permissions.sh` (optionally) ensures permissions for files are corrected (see [fixing ownership on startup](#fixing-ownership-on-startup)) -* `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured -* `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed - -#### Disabling entrypoint or individual scripts - -To disable the entire entrypoint you can set the variable `ENTRYPOINT_SKIP=1`. - -To disable individual entrypoint scripts you can add the filename to the space (`" "`) separated variable `ENTRYPOINT_SKIP_SCRIPTS`. (example: `ENTRYPOINT_SKIP_SCRIPTS="10-storage.sh 30-cache.sh"`) - -### Templating - -The Docker container can do some basic templating (more like variable replacement) as part of the entrypoint scripts via [gomplate](https://docs.gomplate.ca/). - -Any file put in the `/docker/templates/` directory will be templated and written to the right directory. - -#### File path examples - -1. To template `/usr/local/etc/php/php.ini` in the container put the source file in `/docker/templates/usr/local/etc/php/php.ini`. -1. To template `/a/fantastic/example.txt` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. -1. To template `/some/path/anywhere` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. - -#### Available variables - -Variables available for templating are sourced (in order, so *last* source takes precedence) like this: - -1. `env:` in your `docker-compose.yml` or `-e` in your `docker run` / `docker compose run` -1. Any exported variables in `.envsh` files loaded *before* `05-templating.sh` (e.g. any file with `04-`, `03-`, `02-`, `01-` or `00-` prefix) -1. All key/value pairs in `/var/www/.env.docker` -1. All key/value pairs in `/var/www/.env` - -#### Template guide 101 - -Please see the [`gomplate` documentation](https://docs.gomplate.ca/) for a more comprehensive overview. - -The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: - -* `{{ getenv "VAR_NAME" }}` print an environment variable and **fail** if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) -* `{{ getenv "VAR_NAME" "default" }}` print an environment variable and print `default` if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) - -The script will *fail* if you reference a variable that does not exist (and don't have a default value) in a template. - -Please see the - -* [`gomplate` syntax documentation](https://docs.gomplate.ca/syntax/) -* [`gomplate` functions documentation](https://docs.gomplate.ca/functions/) - -### Fixing ownership on startup - -You can set the environment variable `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. - -The variable is a space-delimited list shown below and accepts both relative and absolute paths: - -* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` -* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` - -## Build settings (arguments) - -The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). - -Using *build arguments* allow us to create a flexible and more maintainable Dockerfile, supporting [multiple runtimes](#runtimes) ([FPM](#fpm), [Nginx](#nginx), [Apache + mod_php](#apache)) and end-user flexibility without having to fork or copy the Dockerfile. - -*Build arguments* can be configured using `--build-arg 'name=value'` for `docker build`, `docker compose build` and `docker buildx build`. For `docker-compose.yml` the `args` key for [`build`](https://docs.docker.com/compose/compose-file/compose-file-v3/#build) can be used. - -### `PHP_VERSION` - -The `PHP` version to use when building the runtime container. - -Any valid Docker Hub PHP version is acceptable here, as long as it's [published to Docker Hub](https://hub.docker.com/_/php/tags) - -**Example values**: - -* `8` will use the latest version of PHP 8 -* `8.1` will use the latest version of PHP 8.1 -* `8.2.14` will use PHP 8.2.14 -* `latest` will use whatever is the latest PHP version - -**Default value**: `8.1` - -### `PHP_PECL_EXTENSIONS` - -PECL extensions to install via `pecl install` - -Use [PHP_PECL_EXTENSIONS_EXTRA](#php_pecl_extensions_extra) if you want to add *additional* extenstions. - -Only change this setting if you want to change the baseline extensions. - -See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `imagick redis` - -### `PHP_PECL_EXTENSIONS_EXTRA` - -Extra PECL extensions (separated by space) to install via `pecl install` - -See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `""` - -### `PHP_EXTENSIONS` - -PHP Extensions to install via `docker-php-ext-install`. - -**NOTE:** use [`PHP_EXTENSIONS_EXTRA`](#php_extensions_extra) if you want to add *additional* extensions, only override this if you want to change the baseline extensions. - -See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information - -**Default value**: `intl bcmath zip pcntl exif curl gd` - -### `PHP_EXTENSIONS_EXTRA` - -Extra PHP Extensions (separated by space) to install via `docker-php-ext-install`. - -See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `""` - -### `PHP_EXTENSIONS_DATABASE` - -PHP database extensions to install. - -By default we install both `pgsql` and `mysql` since it's more convinient (and adds very little build time! but can be overwritten here if required. - -**Default value**: `pdo_pgsql pdo_mysql pdo_sqlite` - -### `COMPOSER_VERSION` - -The version of Composer to install. - -Please see the [Docker Hub `composer` page](https://hub.docker.com/_/composer) for valid values. - -**Default value**: `2.6` - -### `APT_PACKAGES_EXTRA` - -Extra APT packages (separated by space) that should be installed inside the image by `apt-get install` - -**Default value**: `""` - -### `NGINX_VERSION` - -Version of `nginx` to when targeting [`nginx-runtime`](#nginx). - -Please see the [Docker Hub `nginx` page](https://hub.docker.com/_/nginx) for available versions. - -**Default value**: `1.25.3` - -### `PHP_BASE_TYPE` - -The `PHP` base image layer to use when building the runtime container. - -When targeting - -* [`apache-runtime`](#apache) use `apache` -* [`fpm-runtime`](#fpm) use `fpm` -* [`nginx-runtime`](#nginx) use `fpm` - -**Valid values**: - -* `apache` -* `fpm` -* `cli` - -**Default value**: `apache` - -### `PHP_DEBIAN_RELEASE` - -The `Debian` Operation System version to use. - -**Valid values**: - -* `bullseye` -* `bookworm` - -**Default value**: `bullseye` +* [Setting up a new Pixelfed server with Docker Compose](new-server.md) +* [Understanding Pixelfed Container runtimes (Apache, FPM, Nginx + FPM)](runtimes.md) +* [Customizing Docker image](customizing.md) diff --git a/docker/customizing.md b/docker/customizing.md new file mode 100644 index 000000000..adcc70b28 --- /dev/null +++ b/docker/customizing.md @@ -0,0 +1,201 @@ +# Customizing your `Dockerfile` + +## Running commands on container start + +### Description + +When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint) script will + +1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). +1. Check if the file is executable. + 1. If the file is *not* executable, print an error and exit the container. +1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). +1. If the file has the extension `.sh` the file will be run like a normal script. +1. Any other file extension will log a warning and will be ignored. + +### Debugging + +You can set environment variable `ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. + +You can also `docker exec` or `docker run` into a container and run `/` + +### Included scripts + +* `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. +* `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. +* `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. +* `//docker/entrypoint.d/15-storage-permissions.sh` (optionally) ensures permissions for files are corrected (see [fixing ownership on startup](#fixing-ownership-on-startup)) +* `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured +* `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed + +### Disabling entrypoint or individual scripts + +To disable the entire entrypoint you can set the variable `ENTRYPOINT_SKIP=1`. + +To disable individual entrypoint scripts you can add the filename to the space (`" "`) separated variable `ENTRYPOINT_SKIP_SCRIPTS`. (example: `ENTRYPOINT_SKIP_SCRIPTS="10-storage.sh 30-cache.sh"`) + +## Templating + +The Docker container can do some basic templating (more like variable replacement) as part of the entrypoint scripts via [gomplate](https://docs.gomplate.ca/). + +Any file put in the `/docker/templates/` directory will be templated and written to the right directory. + +### File path examples + +1. To template `/usr/local/etc/php/php.ini` in the container put the source file in `/docker/templates/usr/local/etc/php/php.ini`. +1. To template `/a/fantastic/example.txt` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. +1. To template `/some/path/anywhere` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. + +### Available variables + +Variables available for templating are sourced (in order, so *last* source takes precedence) like this: + +1. `env:` in your `docker-compose.yml` or `-e` in your `docker run` / `docker compose run` +1. Any exported variables in `.envsh` files loaded *before* `05-templating.sh` (e.g. any file with `04-`, `03-`, `02-`, `01-` or `00-` prefix) +1. All key/value pairs in `/var/www/.env.docker` +1. All key/value pairs in `/var/www/.env` + +### Template guide 101 + +Please see the [`gomplate` documentation](https://docs.gomplate.ca/) for a more comprehensive overview. + +The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: + +* `{{ getenv "VAR_NAME" }}` print an environment variable and **fail** if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) +* `{{ getenv "VAR_NAME" "default" }}` print an environment variable and print `default` if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) + +The script will *fail* if you reference a variable that does not exist (and don't have a default value) in a template. + +Please see the + +* [`gomplate` syntax documentation](https://docs.gomplate.ca/syntax/) +* [`gomplate` functions documentation](https://docs.gomplate.ca/functions/) + +## Fixing ownership on startup + +You can set the environment variable `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. + +The variable is a space-delimited list shown below and accepts both relative and absolute paths: + +* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` +* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` + +## Build settings (arguments) + +The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). + +Using *build arguments* allow us to create a flexible and more maintainable Dockerfile, supporting [multiple runtimes](runtimes.md) ([FPM](runtimes.md#fpm), [Nginx](runtimes.md#nginx), [Apache + mod_php](runtimes.md#apache)) and end-user flexibility without having to fork or copy the Dockerfile. + +*Build arguments* can be configured using `--build-arg 'name=value'` for `docker build`, `docker compose build` and `docker buildx build`. For `docker-compose.yml` the `args` key for [`build`](https://docs.docker.com/compose/compose-file/compose-file-v3/#build) can be used. + +### `PHP_VERSION` + +The `PHP` version to use when building the runtime container. + +Any valid Docker Hub PHP version is acceptable here, as long as it's [published to Docker Hub](https://hub.docker.com/_/php/tags) + +**Example values**: + +* `8` will use the latest version of PHP 8 +* `8.1` will use the latest version of PHP 8.1 +* `8.2.14` will use PHP 8.2.14 +* `latest` will use whatever is the latest PHP version + +**Default value**: `8.1` + +### `PHP_PECL_EXTENSIONS` + +PECL extensions to install via `pecl install` + +Use [PHP_PECL_EXTENSIONS_EXTRA](#php_pecl_extensions_extra) if you want to add *additional* extenstions. + +Only change this setting if you want to change the baseline extensions. + +See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `imagick redis` + +### `PHP_PECL_EXTENSIONS_EXTRA` + +Extra PECL extensions (separated by space) to install via `pecl install` + +See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `""` + +### `PHP_EXTENSIONS` + +PHP Extensions to install via `docker-php-ext-install`. + +**NOTE:** use [`PHP_EXTENSIONS_EXTRA`](#php_extensions_extra) if you want to add *additional* extensions, only override this if you want to change the baseline extensions. + +See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information + +**Default value**: `intl bcmath zip pcntl exif curl gd` + +### `PHP_EXTENSIONS_EXTRA` + +Extra PHP Extensions (separated by space) to install via `docker-php-ext-install`. + +See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. + +**Default value**: `""` + +### `PHP_EXTENSIONS_DATABASE` + +PHP database extensions to install. + +By default we install both `pgsql` and `mysql` since it's more convinient (and adds very little build time! but can be overwritten here if required. + +**Default value**: `pdo_pgsql pdo_mysql pdo_sqlite` + +### `COMPOSER_VERSION` + +The version of Composer to install. + +Please see the [Docker Hub `composer` page](https://hub.docker.com/_/composer) for valid values. + +**Default value**: `2.6` + +### `APT_PACKAGES_EXTRA` + +Extra APT packages (separated by space) that should be installed inside the image by `apt-get install` + +**Default value**: `""` + +### `NGINX_VERSION` + +Version of `nginx` to when targeting [`nginx-runtime`](runtimes.md#nginx). + +Please see the [Docker Hub `nginx` page](https://hub.docker.com/_/nginx) for available versions. + +**Default value**: `1.25.3` + +### `PHP_BASE_TYPE` + +The `PHP` base image layer to use when building the runtime container. + +When targeting + +* [`apache-runtime`](runtimes.md#apache) use `apache` +* [`fpm-runtime`](runtimes.md#fpm) use `fpm` +* [`nginx-runtime`](runtimes.md#nginx) use `fpm` + +**Valid values**: + +* `apache` +* `fpm` +* `cli` + +**Default value**: `apache` + +### `PHP_DEBIAN_RELEASE` + +The `Debian` Operation System version to use. + +**Valid values**: + +* `bullseye` +* `bookworm` + +**Default value**: `bullseye` diff --git a/docker/new-server.md b/docker/new-server.md new file mode 100644 index 000000000..dbbf325f4 --- /dev/null +++ b/docker/new-server.md @@ -0,0 +1,145 @@ +# New Pixelfed + Docker + Docker Compose server + +This guide will help you install and run Pixelfed on **your** server using [Docker Compose](https://docs.docker.com/compose/). + +## Prerequisites + +Recommendations and requirements for hardware and software needed to run Pixelfed using Docker Compose. + +It's highly recommended that you have *some* experience with Linux (e.g. Ubuntu or Debian), SSH, and lightweight server administration. + +### Server + +A VPS or dedicated server you can SSH into, for example + +* [linode.com VPS](https://www.linode.com/) +* [DigitalOcean VPS](https://digitalocean.com/) +* [Hetzner](https://www.hetzner.com/) + +### Hardware + +Hardware requirements depends on the amount of users you have (or plan to have), and how active they are. + +A safe starter/small instance hardware for 25 users and blow are: + +* **CPU/vCPU** `2` cores. +* **RAM** `2-4 GB` as your instance grow, memory requirements will increase for the database. +* **Storage** `20-50 GB` HDD is fine, but ideally SSD or NVMe, *especially* for the database. +* **Network** `100 Mbit/s` or faster. + +### Domain and DNS + +* A **Domain** (or subdomain) is needed for the Pixelfed server (for example, `pixelfed.social` or `pixelfed.mydomain.com`) +* Having the required `A`/`CNAME` DNS records for your domain (above) pointing to your server. + * Typically an `A` record for the root (sometimes shown as `@`) record for `mydomain.com`. + * Possibly an `A` record for `www.` subdomain as well. + +### Network + +* Port `80` (HTTP) and `443` (HTTPS) ports forwarded to the server. + * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `80`: `ufw allow 80` + * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `443`: `ufw allow 443` + +### Optional + +* An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. +* An **Object Storage** provider for storing all images remotely, rather than locally on your server. + +#### E-mail / SMTP provider + +**NOTE**: If you don't plan to use en e-mail/SMTP provider, then make sure to set `ENFORCE_EMAIL_VERIFICATION="false"` in your `.env` file! + +There are *many* providers out there, with wildly different pricing structures, features, and reliability. + +It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: + +* [Simple Email Service (SES)](https://aws.amazon.com/ses/) by Amazon Web Services (AWS) is pay-as-you-go with a cost of $0.10/1000 emails. +* [Brevo](https://www.brevo.com/) (formerly SendInBlue) has a Free Tier with 300 emails/day. +* [Postmark](https://postmarkapp.com/) has a Free Tier with 100 emails/month. +* [Forward Email](https://forwardemail.net/en/private-business-email?pricing=true) has a $3/mo/domain plan with both sending and receiving included. +* [Mailtrap](https://mailtrap.io/email-sending/) has a 1000 emails/month free-tier (their `Email Sending` product, *not* the `Email Testing` one). + +#### Object Storage + +**NOTE**: This is *entirely* optional - by default Pixelfed will store all uploads (videos, images, etc.) directly on your servers storage. + +> Object storage is a technology that stores and manages data in an unstructured format called objects. Modern organizations create and analyze large volumes of unstructured data such as photos, videos, email, web pages, sensor data, and audio files +> +> -- [*What is object storage?*](https://aws.amazon.com/what-is/object-storage/) by Amazon Web Services + +It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: + +* [R2](https://www.cloudflare.com/developer-platform/r2/) by CloudFlare has cheap storage, free *egress* (e.g. people downloading images) and included (and free) Content Delivery Network (CDN). +* [B2 cloud storage](https://www.backblaze.com/cloud-storage) by Backblaze. +* [Simple Storage Service (S3)](https://aws.amazon.com/s3/) by Amazon Web Services. + +### Software + +Required software to be installed on your server + +* `git` can be installed with `apt-get install git` on Debian/Ubuntu +* `docker` can be installed by [following the official Docker documentation](https://docs.docker.com/engine/install/) + +## Getting things ready + +Connect via SSH to your server and decide where you want to install Pixelfed. + +In this guide I'm going to assume it will be installed at `/data/pixelfed`. + +1. **Install required software** as mentioned in the [Software Prerequisites section above](#software) +1. **Create the parent directory** by running `mkdir -p /data` +1. **Clone the Pixelfed repository** by running `git clone https://github.com/pixelfed/pixelfed.git /data/pixelfed` +1. **Change to the Pixelfed directory** by running `cd /data/pixelfed` + +## Modifying your settings (`.env` file) + +### Copy the example configuration file + +Pixelfed contains a default configuration file (`.env.docker`) you should use as a starter, however, before editing anything, make a copy of it and put it in the *right* place (`.env`). + +Run the following command to copy the file: `cp .env.docker .env` + +### Modifying the configuration file + +The configuration file is *quite* long, but the good news is that you can ignore *most* of it, most of the *server-specific* settings are configured for you out of the box. + +The minimum required settings you **must** change is: + +* (required) `APP_DOMAIN` which is the hostname you plan to run your Pixelfed server on (e.g. `pixelfed.social`) - must **not** include `http://` or a trailing slash (`/`)! +* (required) `DB_PASSWORD` which is the database password, you can use a service like [pwgen.io](https://pwgen.io/en/) to generate a secure one. +* (optional) `ENFORCE_EMAIL_VERIFICATION` should be set to `"false"` if you don't plan to send emails. +* (optional) `MAIL_DRIVER` and related `MAIL_*` settings if you plan to use an [email/SMTP provider](#e-mail--smtp-provider) - See [Email variables documentation](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables). +* (optional) `PF_ENABLE_CLOUD` / `FILESYSTEM_CLOUD` if you plan to use an [Object Storage provider](#object-storage). + +See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! + +You need to mainly focus on following sections + +* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) +* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) + +You can skip the following sections, since they are already configured/automated for you: + +* `Redis` +* `Database` (except for `DB_PASSWORD`) +* `One-time setup tasks` + +### Starting the service + +With everything in place and (hopefully) well-configured, we can now go ahead and start our services by running + +```shell +docker compose up -d +``` + +This will download all the required Docker images, start the containers, and being the automatic setup. + +You can follow the logs by running `docker compose logs` - you might want to scroll to the top to logs from the start. + +You can use the CLI flag `--tail=100` to only see the most recent (`100` in this example) log lines for each container. + +You can use the CLI flag `--follow` to continue to see log output from the containers. + +You can combine `--tail=100` and `--follow` like this `docker compose logs --tail=100 --follow`. + +If you only care about specific contaieners, you can add them to the end of the command like this `docker compose logs web worker proxy`. diff --git a/docker/runtimes.md b/docker/runtimes.md new file mode 100644 index 000000000..68d7ee943 --- /dev/null +++ b/docker/runtimes.md @@ -0,0 +1,94 @@ +# Pixelfed Docker container runtimes + +The Pixelfed Dockerfile support multiple target *runtimes* ([Apache](#apache), [Nginx + FPM](#nginx), and [fpm](#fpm)). + +You can consider a *runtime* target as individual Dockerfiles, but instead, all of them are build from the same optimized Dockerfile, sharing +90% of their configuration and packages. + +**If you are unsure of which runtime to choose, please use the [Apache runtime](#apache) it's the most straightforward one and also the default** + +## Apache + +Building a custom Pixelfed Docker image using Apache + mod_php can be achieved the following way. + +### docker build (Apache) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target apache-runtime \ + --tag / \ + . +``` + +### docker compose (Apache) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: apache-runtime +``` + +## Nginx + +Building a custom Pixelfed Docker image using nginx + FPM can be achieved the following way. + +### docker build (nginx) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target nginx-runtime \ + --build-arg 'PHP_BASE_TYPE=fpm' \ + --tag / \ + . +``` + +### docker compose (nginx) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: nginx-runtime + args: + PHP_BASE_TYPE: fpm +``` + +## FPM + +Building a custom Pixelfed Docker image using FPM (only) can be achieved the following way. + +### docker build (fpm) + +```shell +docker build \ + -f contrib/docker/Dockerfile \ + --target fpm-runtime \ + --build-arg 'PHP_BASE_TYPE=fpm' \ + --tag / \ + . +``` + +### docker compose (fpm) + +```yaml +version: "3" + +services: + app: + build: + context: . + dockerfile: contrib/docker/Dockerfile + target: fpm-runtime + args: + PHP_BASE_TYPE: fpm +``` From 20ef1c7b940c24f932a05d9b2b4b320761ad2e0a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 16:13:59 +0000 Subject: [PATCH 239/977] backfil docs --- docker/customizing.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docker/customizing.md b/docker/customizing.md index adcc70b28..8dfc534dc 100644 --- a/docker/customizing.md +++ b/docker/customizing.md @@ -171,6 +171,24 @@ Please see the [Docker Hub `nginx` page](https://hub.docker.com/_/nginx) for ava **Default value**: `1.25.3` +### `FOREGO_VERSION` + +Version of [`forego`](https://github.com/ddollar/forego) to install. + +**Default value**: `0.17.2` + +### `GOMPLATE_VERSION` + +Version of [`goplate`](https://github.com/hairyhenderson/gomplate) to install. + +**Default value**: `v3.11.6` + +### `DOTENV_LINTER_VERSION` + +Version of [`dotenv-linter`](https://github.com/dotenv-linter/dotenv-linter) to install. + +**Default value**: `v3.2.0` + ### `PHP_BASE_TYPE` The `PHP` base image layer to use when building the runtime container. From 901d11df6007ac25efce6c1ce5173d03900688d6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 16:16:58 +0000 Subject: [PATCH 240/977] more docs help --- docker/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/README.md b/docker/README.md index cbadcbf90..65fcdf720 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,4 +2,12 @@ * [Setting up a new Pixelfed server with Docker Compose](new-server.md) * [Understanding Pixelfed Container runtimes (Apache, FPM, Nginx + FPM)](runtimes.md) + * [Apache](runtimes.md#apache) + * [FPM](runtimes.md#fpm) + * [Nginx + FPM](runtimes.md#nginx) * [Customizing Docker image](customizing.md) + * [Running commands on container start](customizing.md#running-commands-on-container-start) + * [Disabling entrypoint or individual scripts](customizing.md#disabling-entrypoint-or-individual-scripts) + * [Templating](customizing.md#templating) + * [Fixing ownership on startup](customizing.md#fixing-ownership-on-startup) + * [Build settings (arguments)](customizing.md#build-settings-arguments) From ed0f9d64c8a29860fbca99e7a9167a59e83d12e3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 17:16:00 +0000 Subject: [PATCH 241/977] implement automatic shellcheck linting --- .github/workflows/docker.yml | 12 +++ docker/.shellcheckrc | 12 +++ .../docker/entrypoint.d/00-check-config.sh | 5 +- .../docker/entrypoint.d/01-permissions.sh | 17 ++-- .../docker/entrypoint.d/04-defaults.envsh | 4 +- .../root/docker/entrypoint.d/05-templating.sh | 15 ++-- .../root/docker/entrypoint.d/10-storage.sh | 5 +- .../entrypoint.d/11-first-time-setup.sh | 7 +- .../root/docker/entrypoint.d/12-migrations.sh | 7 +- .../root/docker/entrypoint.d/20-horizon.sh | 5 +- .../root/docker/entrypoint.d/30-cache.sh | 5 +- docker/shared/root/docker/entrypoint.sh | 37 +++++---- docker/shared/root/docker/helpers.sh | 79 +++++++++++-------- docker/shared/root/docker/install/base.sh | 19 +++-- .../root/docker/install/php-extensions.sh | 23 +++++- 15 files changed, 171 insertions(+), 81 deletions(-) create mode 100644 docker/.shellcheckrc diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a2e73a61a..2efa04948 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,6 +39,18 @@ jobs: dockerfile: Dockerfile failure-threshold: error + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: --shell=bash --external-sources + with: + scandir: docker/ + build: runs-on: ubuntu-latest diff --git a/docker/.shellcheckrc b/docker/.shellcheckrc new file mode 100644 index 000000000..be92f81fc --- /dev/null +++ b/docker/.shellcheckrc @@ -0,0 +1,12 @@ +# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files + +source-path=SCRIPTDIR + +# Allow opening any 'source'd file, even if not specified as input +external-sources=true + +# Turn on warnings for unquoted variables with safe values +enable=quote-safe-variables + +# Turn on warnings for unassigned uppercase variables +enable=check-unassigned-uppercase diff --git a/docker/shared/root/docker/entrypoint.d/00-check-config.sh b/docker/shared/root/docker/entrypoint.d/00-check-config.sh index 36244f6ca..290deb3fc 100755 --- a/docker/shared/root/docker/entrypoint.d/00-check-config.sh +++ b/docker/shared/root/docker/entrypoint.d/00-check-config.sh @@ -1,5 +1,8 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 25a831531..58b2f0bb8 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -1,19 +1,22 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Ensure the two Docker volumes and dot-env files are owned by the runtime user as other scripts # will be writing to these -run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./.env" -run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./bootstrap/cache" -run-as-current-user chown --verbose ${RUNTIME_UID}:${RUNTIME_GID} "./storage" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" # Optionally fix ownership of configured paths -: ${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""} +: "${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -a ensure_ownership_paths <<<"${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS}" +IFS=' ' read -ar ensure_ownership_paths <<<"${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS}" if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then log-info "No paths has been configured for ownership fixes via [\$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS]." @@ -23,5 +26,5 @@ fi for path in "${ensure_ownership_paths[@]}"; do log-info "Ensure ownership of [${path}] is correct" - stream-prefix-command-output run-as-current-user chown --recursive ${RUNTIME_UID}:${RUNTIME_GID} "${path}" + stream-prefix-command-output run-as-current-user chown --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "${path}" done diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index 3507d94ee..352ea23b7 100755 --- a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -13,7 +13,7 @@ entrypoint-set-script-name "${BASH_SOURCE[0]}" load-config-files # We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead -: ${POST_MAX_SIZE_BUFFER:=1M} +: "${POST_MAX_SIZE_BUFFER:=1M}" log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" buffer=$(numfmt --invalid=fail --from=auto --to=none --to-unit=K "${POST_MAX_SIZE_BUFFER}") log-info "POST_MAX_SIZE_BUFFER converted to KB is [${buffer}]" @@ -23,7 +23,7 @@ log-info "POST_MAX_SIZE will be calculated by [({MAX_PHOTO_SIZE} * {MAX_ALBUM_LE log-info " MAX_PHOTO_SIZE=${MAX_PHOTO_SIZE}" log-info " MAX_ALBUM_LENGTH=${MAX_ALBUM_LENGTH}" log-info " POST_MAX_SIZE_BUFFER=${buffer}" -: ${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((${MAX_PHOTO_SIZE} * ${MAX_ALBUM_LENGTH}) + ${buffer})))} +: "${POST_MAX_SIZE:=$(numfmt --invalid=fail --from=auto --from-unit=K --to=si $(((MAX_PHOTO_SIZE * MAX_ALBUM_LENGTH) + buffer)))}" log-info "POST_MAX_SIZE was calculated to [${POST_MAX_SIZE}]" # NOTE: must export the value so it's available in other scripts! diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh index 1bb62aae6..4d229b11c 100755 --- a/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -1,14 +1,17 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Show [git diff] of templates being rendered (will help verify output) -: ${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1} +: "${ENTRYPOINT_SHOW_TEMPLATE_DIFF:=1}" # Directory where templates can be found -: ${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/} +: "${ENTRYPOINT_TEMPLATE_DIR:=/docker/templates/}" # Root path to write template template_files to (default is '', meaning it will be written to /) -: ${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=} +: "${ENTRYPOINT_TEMPLATE_OUTPUT_PREFIX:=}" declare template_file relative_template_file_path output_file_dir @@ -16,6 +19,8 @@ declare template_file relative_template_file_path output_file_dir load-config-files # export all dot-env variables so they are available in templating +# +# shellcheck disable=SC2068 export ${seen_dot_env_variables[@]} find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r template_file; do @@ -46,7 +51,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat # Render the template log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" - cat "${template_file}" | gomplate >"${output_file_path}" + gomplate <"${template_file}" >"${output_file_path}" # Show the diff from the envsubst command if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then diff --git a/docker/shared/root/docker/entrypoint.d/10-storage.sh b/docker/shared/root/docker/entrypoint.d/10-storage.sh index bb2f61f0a..f0c28241d 100755 --- a/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -1,5 +1,8 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 6778bd90e..9559eb4de 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -1,10 +1,13 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Allow automatic applying of outstanding/new migrations on startup -: ${DOCKER_RUN_ONE_TIME_SETUP_TASKS:=1} +: "${DOCKER_RUN_ONE_TIME_SETUP_TASKS:=1}" if is-false "${DOCKER_RUN_ONE_TIME_SETUP_TASKS}"; then log-warning "Automatic run of the 'One-time setup tasks' is disabled." diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index 20cbe3a31..b10e34c18 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -1,10 +1,13 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Allow automatic applying of outstanding/new migrations on startup -: ${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0} +: "${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}" # Wait for the database to be ready await-database-ready diff --git a/docker/shared/root/docker/entrypoint.d/20-horizon.sh b/docker/shared/root/docker/entrypoint.d/20-horizon.sh index 6b81e7133..55efd768d 100755 --- a/docker/shared/root/docker/entrypoint.d/20-horizon.sh +++ b/docker/shared/root/docker/entrypoint.d/20-horizon.sh @@ -1,5 +1,8 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" diff --git a/docker/shared/root/docker/entrypoint.d/30-cache.sh b/docker/shared/root/docker/entrypoint.d/30-cache.sh index c8791e65b..c970db60b 100755 --- a/docker/shared/root/docker/entrypoint.d/30-cache.sh +++ b/docker/shared/root/docker/entrypoint.d/30-cache.sh @@ -1,5 +1,8 @@ #!/bin/bash -source /docker/helpers.sh +: "${ENTRYPOINT_ROOT:="/docker"}" + +# shellcheck source=SCRIPTDIR/../helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 0e8b1089c..49d62afd0 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -4,42 +4,48 @@ if [[ ${ENTRYPOINT_SKIP:=0} != 0 ]]; then exec "$@" fi -# Directory where entrypoint scripts lives -: ${ENTRYPOINT_ROOT:="/docker/entrypoint.d/"} +: "${ENTRYPOINT_ROOT:="/docker"}" export ENTRYPOINT_ROOT +# Directory where entrypoint scripts lives +: "${ENTRYPOINT_D_ROOT:="${ENTRYPOINT_ROOT}/entrypoint.d/"}" +export ENTRYPOINT_D_ROOT + # Space separated list of scripts the entrypoint runner should skip -: ${ENTRYPOINT_SKIP_SCRIPTS:=""} +: "${ENTRYPOINT_SKIP_SCRIPTS:=""}" # Load helper scripts -source /docker/helpers.sh +# +# shellcheck source=SCRIPTDIR/helpers.sh +source "${ENTRYPOINT_ROOT}/helpers.sh" # Set the entrypoint name for logging entrypoint-set-script-name "entrypoint.sh" # Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup declare -a skip_scripts -IFS=' ' read -a skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" +# shellcheck disable=SC2034 +IFS=' ' read -ar skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" # Ensure the entrypoint root folder exists -mkdir -p "${ENTRYPOINT_ROOT}" +mkdir -p "${ENTRYPOINT_D_ROOT}" -# If ENTRYPOINT_ROOT directory is empty, warn and run the regular command -if is-directory-empty "${ENTRYPOINT_ROOT}"; then - log-warning "No files found in ${ENTRYPOINT_ROOT}, skipping configuration" +# If ENTRYPOINT_D_ROOT directory is empty, warn and run the regular command +if is-directory-empty "${ENTRYPOINT_D_ROOT}"; then + log-warning "No files found in ${ENTRYPOINT_D_ROOT}, skipping configuration" exec "$@" fi -acquire-lock +acquire-lock "entrypoint.sh" # Start scanning for entrypoint.d files to source or run -log-info "looking for shell scripts in [${ENTRYPOINT_ROOT}]" +log-info "looking for shell scripts in [${ENTRYPOINT_D_ROOT}]" -find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; do +find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r file; do # Skip the script if it's in the skip-script list - if in-array $(get-entrypoint-script-name "${file}") skip_scripts; then - log-warning "Skipping script [${script_name}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" + if in-array "$(get-entrypoint-script-name "${file}")" skip_scripts; then + log-warning "Skipping script [${file}] since it's in the skip list (\$ENTRYPOINT_SKIP_SCRIPTS)" continue fi @@ -56,6 +62,7 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; log-info "${notice_message_color}Sourcing [${file}]${color_clear}" log-info "" + # shellcheck disable=SC1090 source "${file}" # the sourced file will (should) than the log prefix, so this restores our own @@ -82,7 +89,7 @@ find "${ENTRYPOINT_ROOT}" -follow -type f -print | sort -V | while read -r file; esac done -release-lock +release-lock "entrypoint.sh" log-info "Configuration complete; ready for start up" diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index e71b4f295..74aa6c468 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -3,6 +3,9 @@ set -e -o errexit -o nounset -o pipefail [[ ${ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x +: "${RUNTIME_UID:="33"}" +: "${RUNTIME_GID:="33"}" + # Some splash of color for important messages declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" @@ -23,11 +26,14 @@ declare -a dot_env_files=( # environment keys seen when source dot files (so we can [export] them) declare -ga seen_dot_env_variables=() -declare -g docker_state_path="$(readlink -f ./storage/docker)" +declare -g docker_state_path +docker_state_path="$(readlink -f ./storage/docker)" + declare -g docker_locks_path="${docker_state_path}/lock" declare -g docker_once_path="${docker_state_path}/once" -declare -g runtime_username=$(id -un ${RUNTIME_UID}) +declare -g runtime_username +runtime_username=$(id -un "${RUNTIME_UID}") # We should already be in /var/www, but just to be explicit cd /var/www || log-error-and-exit "could not change to /var/www" @@ -38,7 +44,7 @@ function entrypoint-set-script-name() { script_name_previous="${script_name}" script_name="${1}" - log_prefix="[entrypoint / $(get-entrypoint-script-name $1)] - " + log_prefix="[entrypoint / $(get-entrypoint-script-name "$1")] - " } # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] @@ -86,20 +92,18 @@ function run-command-as() { if [[ $exit_code != 0 ]]; then log-error "❌ Error!" - return $exit_code + return "$exit_code" fi log-info-stderr "✅ OK!" - return $exit_code + return "$exit_code" } # @description Streams stdout from the command and echo it # with log prefixing. # @see stream-prefix-command-output function stream-stdout-handler() { - local prefix="${1:-}" - - while read line; do + while read -r line; do log-info "(stdout) ${line}" done } @@ -108,7 +112,7 @@ function stream-stdout-handler() { # with a bit of color and log prefixing. # @see stream-prefix-command-output function stream-stderr-handler() { - while read line; do + while read -r line; do log-info-stderr "(${error_message_color}stderr${color_clear}) ${line}" done } @@ -123,11 +127,13 @@ function stream-prefix-command-output() { # if stdout is being piped, print it like normal with echo if [ ! -t 1 ]; then + # shellcheck disable=SC1007 stdout= echo >&1 -ne fi # if stderr is being piped, print it like normal with echo if [ ! -t 2 ]; then + # shellcheck disable=SC1007 stderr= echo >&2 -ne fi @@ -141,11 +147,11 @@ function log-error() { local msg if [[ $# -gt 0 ]]; then - msg="$@" + msg="$*" elif [[ ! -t 0 ]]; then - read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" else - log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi echo -e "${error_message_color}${log_prefix}ERROR - ${msg}${color_clear}" >/dev/stderr @@ -170,11 +176,11 @@ function log-warning() { local msg if [[ $# -gt 0 ]]; then - msg="$@" + msg="$*" elif [[ ! -t 0 ]]; then - read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" else - log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi echo -e "${warn_message_color}${log_prefix}WARNING - ${msg}${color_clear}" >/dev/stderr @@ -187,15 +193,15 @@ function log-info() { local msg if [[ $# -gt 0 ]]; then - msg="$@" + msg="$*" elif [[ ! -t 0 ]]; then - read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" else - log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${log_prefix}${msg}" + echo -e "${notice_message_color}${log_prefix}${msg}${color_clear}" fi } @@ -206,11 +212,11 @@ function log-info-stderr() { local msg if [[ $# -gt 0 ]]; then - msg="$@" + msg="$*" elif [[ ! -t 0 ]]; then - read msg || log-error-and-exit "[${FUNCNAME}] could not read from stdin" + read -r msg || log-error-and-exit "[${FUNCNAME[0]}] could not read from stdin" else - log-error-and-exit "[${FUNCNAME}] did not receive any input arguments and STDIN is empty" + log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then @@ -231,15 +237,19 @@ function load-config-files() { fi log-info "Sourcing ${file}" + # shellcheck disable=SC1090 source "${file}" # find all keys in the dot-env file and store them in our temp associative array - for k in "$(grep -v '^#' "${file}" | cut -d"=" -f1 | xargs)"; do + for k in $(grep -v '^#' "${file}" | cut -d"=" -f1 | xargs); do _tmp_dot_env_keys[$k]=1 done done - seen_dot_env_variables=(${!_tmp_dot_env_keys[@]}) + # Used in other scripts (like templating) for [export]-ing the values + # + # shellcheck disable=SC2034 + seen_dot_env_variables=("${!_tmp_dot_env_keys[@]}") } # @description Checks if $needle exists in $haystack @@ -290,7 +300,7 @@ function file-exists() { # @exitcode 0 If $1 contains files # @exitcode 1 If $1 does *NOT* contain files function is-directory-empty() { - ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v + ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read -r } # @description Ensures a directory exists (via mkdir) @@ -301,11 +311,11 @@ function ensure-directory-exists() { stream-prefix-command-output mkdir -pv "$@" } -# @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_ROOT prefix +# @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_D_ROOT prefix # @arg $1 string The path to manipulate # @stdout The relative path to the entrypoint script function get-entrypoint-script-name() { - echo "${1#"$ENTRYPOINT_ROOT"}" + echo "${1#"$ENTRYPOINT_D_ROOT"}" } # @description Ensure a command is only run once (via a 'lock' file) in the storage directory. @@ -373,12 +383,14 @@ function release-lock() { # @arg $@ string The list of trap signals to register function on-trap() { local trap_add_cmd=$1 - shift || log-error-and-exit "${FUNCNAME} usage error" + shift || log-error-and-exit "${FUNCNAME[0]} usage error" for trap_add_name in "$@"; do trap -- "$( # helper fn to get existing trap command from output # of trap -p + # + # shellcheck disable=SC2317 extract_trap_cmd() { printf '%s\n' "${3:-}"; } # print existing trap command with newline eval "extract_trap_cmd $(trap -p "${trap_add_name}")" @@ -403,12 +415,14 @@ function await-database-ready() { case "${DB_CONNECTION:-}" in mysql) + # shellcheck disable=SC2154 while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do staggered-sleep done ;; pgsql) + # shellcheck disable=SC2154 while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" >/dev/null; do staggered-sleep done @@ -417,6 +431,7 @@ function await-database-ready() { sqlsrv) log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" + # shellcheck disable=SC2154 while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do staggered-sleep done @@ -437,7 +452,7 @@ function await-database-ready() { # @description sleeps between 1 and 3 seconds to ensure a bit of randomness # in multiple scripts/containers doing work almost at the same time. function staggered-sleep() { - sleep $(get-random-number-between 1 3) + sleep "$(get-random-number-between 1 3)" } # @description Helper function to get a random number between $1 and $2 @@ -459,13 +474,13 @@ function show-call-stack() { local src # to avoid noise we start with 1 to skip the get_stack function - for ((i = 1; i < $stack_size; i++)); do + for ((i = 1; i < stack_size; i++)); do func="${FUNCNAME[$i]}" - [ x$func = x ] && func=MAIN + [ -z "$func" ] && func="MAIN" lineno="${BASH_LINENO[$((i - 1))]}" src="${BASH_SOURCE[$i]}" - [ x"$src" = x ] && src=non_file_source + [ -z "$src" ] && src="non_file_source" log-error " at: ${func} ${src}:${lineno}" done diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index 5856836f9..5c4f38062 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -1,6 +1,9 @@ #!/bin/bash set -ex -o errexit -o nounset -o pipefail +: "${APT_PACKAGES_EXTRA:=""}" +: "${DOTENV_LINTER_VERSION:=""}" + # Ensure we keep apt cache around in a Docker environment rm -f /etc/apt/apt.conf.d/docker-clean echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache @@ -73,16 +76,16 @@ apt-get update apt-get upgrade -y apt-get install -y \ - ${standardPackages[*]} \ - ${imageOptimization[*]} \ - ${imageProcessing[*]} \ - ${gdDependencies[*]} \ - ${videoProcessing[*]} \ - ${databaseDependencies[*]} \ - ${APT_PACKAGES_EXTRA} + "${standardPackages[@]}" \ + "${imageOptimization[@]}" \ + "${imageProcessing[@]}" \ + "${gdDependencies[@]}" \ + "${videoProcessing[@]}" \ + "${databaseDependencies[@]}" \ + "${APT_PACKAGES_EXTRA}" locale-gen update-locale # Install dotenv linter (https://github.com/dotenv-linter/dotenv-linter) -curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin ${DOTENV_LINTER_VERSION} +curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION}" diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh index 1cb86fd77..baf460c80 100755 --- a/docker/shared/root/docker/install/php-extensions.sh +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -1,6 +1,12 @@ #!/bin/bash set -ex -o errexit -o nounset -o pipefail +: "${PHP_PECL_EXTENSIONS:=""}" +: "${PHP_PECL_EXTENSIONS_EXTRA:=""}" +: "${PHP_EXTENSIONS:=""}" +: "${PHP_EXTENSIONS_EXTRA:=""}" +: "${PHP_EXTENSIONS_DATABASE:=""}" + # Grab the PHP source code so we can compile against it docker-php-source extract @@ -14,7 +20,7 @@ docker-php-ext-configure gd \ # Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] # this can also overwirte the [gd] configure above by simply running it again if [[ -f /install/php-extension-configure.sh ]]; then - if [ !-x "$f" ]; then + if [ ! -x "/install/php-extension-configure.sh" ]; then echo >&2 "ERROR: found /install/php-extension-configure.sh but its not executable - please [chmod +x] the file!" exit 1 fi @@ -23,10 +29,19 @@ if [[ -f /install/php-extension-configure.sh ]]; then fi # Install pecl extensions -pecl install ${PHP_PECL_EXTENSIONS} ${PHP_PECL_EXTENSIONS_EXTRA} +pecl install "${PHP_PECL_EXTENSIONS}" "${PHP_PECL_EXTENSIONS_EXTRA}" # PHP extensions (dependencies) -docker-php-ext-install -j$(nproc) ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS_DATABASE} +docker-php-ext-install \ + -j "$(nproc)" \ + "${PHP_EXTENSIONS}" \ + "${PHP_EXTENSIONS_EXTRA}" \ + "${PHP_EXTENSIONS_DATABASE}" # Enable all extensions -docker-php-ext-enable ${PHP_PECL_EXTENSIONS} ${PHP_PECL_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS} ${PHP_EXTENSIONS_EXTRA} ${PHP_EXTENSIONS_DATABASE} +docker-php-ext-enable \ + "${PHP_PECL_EXTENSIONS}" \ + "${PHP_PECL_EXTENSIONS_EXTRA}" \ + "${PHP_EXTENSIONS}" \ + "${PHP_EXTENSIONS_EXTRA}" \ + "${PHP_EXTENSIONS_DATABASE}" From f2f251750360b350f11e7a481aa14837de693bd3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 17:17:48 +0000 Subject: [PATCH 242/977] implement automatic shellcheck linting --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2efa04948..79dae64e7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -49,7 +49,7 @@ jobs: env: SHELLCHECK_OPTS: --shell=bash --external-sources with: - scandir: docker/ + scandir: ./docker/ build: runs-on: ubuntu-latest From b2d6d3dbe7084a1a1b1c424668600dbf673639fa Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 17:23:32 +0000 Subject: [PATCH 243/977] implement automatic shellcheck linting --- .github/workflows/docker.yml | 1 + .../root/docker/install/php-extensions.sh | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 79dae64e7..7127cdcbe 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -48,6 +48,7 @@ jobs: uses: ludeeus/action-shellcheck@master env: SHELLCHECK_OPTS: --shell=bash --external-sources + INPUT_ADDITIONAL_FILE_ARGS: -o -name '*.envsh' with: scandir: ./docker/ diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh index baf460c80..86075ef1d 100755 --- a/docker/shared/root/docker/install/php-extensions.sh +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -32,16 +32,20 @@ fi pecl install "${PHP_PECL_EXTENSIONS}" "${PHP_PECL_EXTENSIONS_EXTRA}" # PHP extensions (dependencies) +# +# shellcheck disable=SC2086 docker-php-ext-install \ -j "$(nproc)" \ - "${PHP_EXTENSIONS}" \ - "${PHP_EXTENSIONS_EXTRA}" \ - "${PHP_EXTENSIONS_DATABASE}" + ${PHP_EXTENSIONS} \ + ${PHP_EXTENSIONS_EXTRA} \ + ${PHP_EXTENSIONS_DATABASE} # Enable all extensions +# +# shellcheck disable=SC2086 docker-php-ext-enable \ - "${PHP_PECL_EXTENSIONS}" \ - "${PHP_PECL_EXTENSIONS_EXTRA}" \ - "${PHP_EXTENSIONS}" \ - "${PHP_EXTENSIONS_EXTRA}" \ - "${PHP_EXTENSIONS_DATABASE}" + ${PHP_PECL_EXTENSIONS} \ + ${PHP_PECL_EXTENSIONS_EXTRA} \ + ${PHP_EXTENSIONS} \ + ${PHP_EXTENSIONS_EXTRA} \ + ${PHP_EXTENSIONS_DATABASE} From fa10fe999e90dd68d7242b300f001a3242b7b2d1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 17:25:42 +0000 Subject: [PATCH 244/977] implement automatic shellcheck linting --- .github/workflows/docker.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7127cdcbe..bf73cc946 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -48,9 +48,10 @@ jobs: uses: ludeeus/action-shellcheck@master env: SHELLCHECK_OPTS: --shell=bash --external-sources - INPUT_ADDITIONAL_FILE_ARGS: -o -name '*.envsh' with: + version: v0.9.0 scandir: ./docker/ + additional_files: "*.envsh" build: runs-on: ubuntu-latest From 7f99bb10242762e553d6c19622156f5106ec66f3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 17:30:14 +0000 Subject: [PATCH 245/977] implement automatic shellcheck linting --- .../root/docker/install/php-extensions.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh index 86075ef1d..8abb3dff8 100755 --- a/docker/shared/root/docker/install/php-extensions.sh +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -1,11 +1,20 @@ #!/bin/bash set -ex -o errexit -o nounset -o pipefail -: "${PHP_PECL_EXTENSIONS:=""}" -: "${PHP_PECL_EXTENSIONS_EXTRA:=""}" -: "${PHP_EXTENSIONS:=""}" -: "${PHP_EXTENSIONS_EXTRA:=""}" -: "${PHP_EXTENSIONS_DATABASE:=""}" +# shellcheck disable=SC2223 +: ${PHP_PECL_EXTENSIONS:=""} + +# shellcheck disable=SC2223 +: ${PHP_PECL_EXTENSIONS_EXTRA:=""} + +# shellcheck disable=SC2223 +: ${PHP_EXTENSIONS:=""} + +# shellcheck disable=SC2223 +: ${PHP_EXTENSIONS_EXTRA:=""} + +# shellcheck disable=SC2223 +: ${PHP_EXTENSIONS_DATABASE:=""} # Grab the PHP source code so we can compile against it docker-php-source extract From 685f62a5d0550f4a64e89a2dac60dbae0c5d2586 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 18:44:43 +0000 Subject: [PATCH 246/977] allow skipping one-time setup tasks --- Dockerfile | 4 +- .../docker/entrypoint.d/04-defaults.envsh | 3 ++ .../root/docker/install/php-extensions.sh | 48 ++++++------------- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 33d0eeee3..6151d8758 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,8 +40,8 @@ ARG RUNTIME_GID=33 # often called 'www-data' ARG APT_PACKAGES_EXTRA="" # Extensions installed via [pecl install] -ARG PHP_PECL_EXTENSIONS="" -ARG PHP_PECL_EXTENSIONS_EXTRA="" +ARG PHP_PECL_EXTENSIONS= +ARG PHP_PECL_EXTENSIONS_EXTRA= # Extensions installed via [docker-php-ext-install] ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd" diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index 352ea23b7..d47f55728 100755 --- a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -10,6 +10,9 @@ entrypoint-set-script-name "${BASH_SOURCE[0]}" +: "${MAX_PHOTO_SIZE:-}" +: "${MAX_ALBUM_LENGTH:-}" + load-config-files # We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh index 8abb3dff8..42c149d76 100755 --- a/docker/shared/root/docker/install/php-extensions.sh +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -1,20 +1,14 @@ #!/bin/bash set -ex -o errexit -o nounset -o pipefail -# shellcheck disable=SC2223 -: ${PHP_PECL_EXTENSIONS:=""} +declare -a pecl_extensions=() +readarray -d ' ' -t pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#pecl_extensions[@]}" pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS_EXTRA:-}") -# shellcheck disable=SC2223 -: ${PHP_PECL_EXTENSIONS_EXTRA:=""} - -# shellcheck disable=SC2223 -: ${PHP_EXTENSIONS:=""} - -# shellcheck disable=SC2223 -: ${PHP_EXTENSIONS_EXTRA:=""} - -# shellcheck disable=SC2223 -: ${PHP_EXTENSIONS_DATABASE:=""} +declare -a php_extensions=() +readarray -d ' ' -t php_extensions < <(echo -n "${PHP_EXTENSIONS:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_EXTRA:-}") +readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_DATABASE:-}") # Grab the PHP source code so we can compile against it docker-php-source extract @@ -28,33 +22,21 @@ docker-php-ext-configure gd \ # Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] # this can also overwirte the [gd] configure above by simply running it again -if [[ -f /install/php-extension-configure.sh ]]; then - if [ ! -x "/install/php-extension-configure.sh" ]; then - echo >&2 "ERROR: found /install/php-extension-configure.sh but its not executable - please [chmod +x] the file!" +declare -r custom_pre_configure_script="" +if [[ -e "${custom_pre_configure_script}" ]]; then + if [ ! -x "${custom_pre_configure_script}" ]; then + echo >&2 "ERROR: found ${custom_pre_configure_script} but its not executable - please [chmod +x] the file!" exit 1 fi - /install/php-extension-configure.sh + "${custom_pre_configure_script}" fi # Install pecl extensions -pecl install "${PHP_PECL_EXTENSIONS}" "${PHP_PECL_EXTENSIONS_EXTRA}" +pecl install "${pecl_extensions[@]}" # PHP extensions (dependencies) -# -# shellcheck disable=SC2086 -docker-php-ext-install \ - -j "$(nproc)" \ - ${PHP_EXTENSIONS} \ - ${PHP_EXTENSIONS_EXTRA} \ - ${PHP_EXTENSIONS_DATABASE} +docker-php-ext-install -j "$(nproc)" "${php_extensions[@]}" # Enable all extensions -# -# shellcheck disable=SC2086 -docker-php-ext-enable \ - ${PHP_PECL_EXTENSIONS} \ - ${PHP_PECL_EXTENSIONS_EXTRA} \ - ${PHP_EXTENSIONS} \ - ${PHP_EXTENSIONS_EXTRA} \ - ${PHP_EXTENSIONS_DATABASE} +docker-php-ext-enable "${pecl_extensions[@]}" "${php_extensions[@]}" From 903aeb7608def2a99b87873d9f22e4fe041154e6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 18:53:54 +0000 Subject: [PATCH 247/977] more cleanup --- .../docker/entrypoint.d/04-defaults.envsh | 6 ++-- docker/shared/root/docker/install/base.sh | 32 +++++++------------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index d47f55728..fe906120a 100755 --- a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -10,11 +10,11 @@ entrypoint-set-script-name "${BASH_SOURCE[0]}" -: "${MAX_PHOTO_SIZE:-}" -: "${MAX_ALBUM_LENGTH:-}" - load-config-files +: "${MAX_PHOTO_SIZE:=}" +: "${MAX_ALBUM_LENGTH:=}" + # We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead : "${POST_MAX_SIZE_BUFFER:=1M}" log-info "POST_MAX_SIZE_BUFFER is set to [${POST_MAX_SIZE_BUFFER}]" diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index 5c4f38062..7fa43b0f9 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -1,9 +1,6 @@ #!/bin/bash set -ex -o errexit -o nounset -o pipefail -: "${APT_PACKAGES_EXTRA:=""}" -: "${DOTENV_LINTER_VERSION:=""}" - # Ensure we keep apt cache around in a Docker environment rm -f /etc/apt/apt.conf.d/docker-clean echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache @@ -14,8 +11,10 @@ echo 'APT::Install-Recommends "false";' >>/etc/apt/apt.conf # Don't install suggested packages by default echo 'APT::Install-Suggests "false";' >>/etc/apt/apt.conf +declare -a packages=() + # Standard packages -declare -ra standardPackages=( +packages+=( apt-utils ca-certificates curl @@ -36,7 +35,7 @@ declare -ra standardPackages=( ) # Image Optimization -declare -ra imageOptimization=( +packages+=( gifsicle jpegoptim optipng @@ -44,14 +43,14 @@ declare -ra imageOptimization=( ) # Image Processing -declare -ra imageProcessing=( +packages+=( libjpeg62-turbo-dev libmagickwand-dev libpng-dev ) # Required for GD -declare -ra gdDependencies=( +packages+=( libwebp-dev libwebp6 libxpm-dev @@ -59,33 +58,26 @@ declare -ra gdDependencies=( ) # Video Processing -declare -ra videoProcessing=( +packages+=( ffmpeg ) # Database -declare -ra databaseDependencies=( +packages+=( libpq-dev libsqlite3-dev mariadb-client postgresql-client ) +readarray -d ' ' -t -O "${#packages[@]}" packages < <(echo -n "${APT_PACKAGES_EXTRA:-}") + apt-get update - apt-get upgrade -y - -apt-get install -y \ - "${standardPackages[@]}" \ - "${imageOptimization[@]}" \ - "${imageProcessing[@]}" \ - "${gdDependencies[@]}" \ - "${videoProcessing[@]}" \ - "${databaseDependencies[@]}" \ - "${APT_PACKAGES_EXTRA}" +apt-get install -y "${packages[@]}" locale-gen update-locale # Install dotenv linter (https://github.com/dotenv-linter/dotenv-linter) -curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION}" +curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION:-}" From 53eb9c11fc2170d72556c35b4c0cc5e18655e0aa Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 19:20:22 +0000 Subject: [PATCH 248/977] add faq --- .env.docker | 7 +++++-- docker-compose.yml | 6 ++++++ docker/README.md | 2 ++ docker/faq.md | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 docker/faq.md diff --git a/.env.docker b/.env.docker index 559c06553..0f5aa6112 100644 --- a/.env.docker +++ b/.env.docker @@ -939,12 +939,15 @@ DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" -# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTP traffic +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic DOCKER_PROXY_PORT_EXTERNAL_HTTP="80" -# Port that the web will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" +# Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only +DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" + # Path to the Docker socket on the *host* DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" diff --git a/docker-compose.yml b/docker-compose.yml index 567998411..b2977e1e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,8 @@ services: image: nginxproxy/nginx-proxy:1.4 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" restart: unless-stopped + profiles: + - ${DOCKER_PROXY_PROFILE} volumes: - "${DOCKER_HOST_SOCKET_PATH}:/tmp/docker.sock:ro" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" @@ -33,6 +35,8 @@ services: image: nginxproxy/acme-companion container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" restart: unless-stopped + profiles: + - ${DOCKER_PROXY_ACME_PROFILE:-$DOCKER_PROXY_PROFILE} environment: DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -70,6 +74,8 @@ services: com.github.nginx-proxy.nginx-proxy.keepalive: 30 com.github.nginx-proxy.nginx-proxy.http2.enable: true com.github.nginx-proxy.nginx-proxy.http3.enable: true + # ports: + # - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" depends_on: - db - redis diff --git a/docker/README.md b/docker/README.md index 65fcdf720..bb3389965 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,8 @@ # Pixelfed + Docker + Docker Compose * [Setting up a new Pixelfed server with Docker Compose](new-server.md) + * [I already have a SSL certificate, how do I use it?](faq.md#i-already-have-a-proxy-how-do-i-disable-the-included-one) + * [I already have an proxy / how do I disable the Nginx proxy](faq.md#i-already-have-a-ssl-certificate-how-do-i-use-it) * [Understanding Pixelfed Container runtimes (Apache, FPM, Nginx + FPM)](runtimes.md) * [Apache](runtimes.md#apache) * [FPM](runtimes.md#fpm) diff --git a/docker/faq.md b/docker/faq.md new file mode 100644 index 000000000..c284940b8 --- /dev/null +++ b/docker/faq.md @@ -0,0 +1,19 @@ +# Pixelfed Docker FAQ + +## I already have a Proxy, how do I disable the included one? + +No problem! All you have to do is + +1. *Comment out* (or delete) the `proxy` and `proxy-acme` services in `docker-compose.yml` +1. *Uncomment* the `ports` block for the `web` servince in `docker-compose.yml` +1. Change the `DOCKER_WEB_PORT_EXTERNAL_HTTP` setting in your `.env` if you want to change the port from the default `8080` +1. Point your proxy upstream to the exposed `web` port. + +## I already have a SSL certificate, how do I use it? + +1. *Comment out* (or delete) the `proxy-acme` service in `docker-compose.yml` +1. Put your certificates in `${DOCKER_CONFIG_ROOT}/proxy/certs/${APP_DOMAIN}/`. The following files are expected to exist in the directory for the proxy to detect and use them automatically (this is the same directory and file names as LetsEncrypt uses) + 1. `cert.pem` + 1. `chain.pem` + 1. `fullchain.pem` + 1. `key.pem` From 98660760c9d4b8cf0ca6293bf8277e78269b7db9 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 19:39:59 +0000 Subject: [PATCH 249/977] improve faq --- .env.docker | 8 +++++++- docker-compose.yml | 9 +++------ docker/faq.md | 29 ++++++++++++++++++----------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/.env.docker b/.env.docker index 0f5aa6112..b512ce5d0 100644 --- a/.env.docker +++ b/.env.docker @@ -952,4 +952,10 @@ DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" # Prefix for container names (without any dash at the end) -DOCKER_CONTAINER_NAME_PREFIX="${APP_DOMAIN}-" +DOCKER_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service +DOCKER_PROXY_PROFILE="" + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service +DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" diff --git a/docker-compose.yml b/docker-compose.yml index b2977e1e2..d56f61aa6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" restart: unless-stopped profiles: - - ${DOCKER_PROXY_ACME_PROFILE:-$DOCKER_PROXY_PROFILE} + - ${DOCKER_PROXY_ACME_PROFILE} environment: DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -57,9 +57,6 @@ services: restart: unless-stopped build: target: apache-runtime - deploy: - mode: replicated - replicas: 1 volumes: - "./.env:/var/www/.env" - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" @@ -74,8 +71,8 @@ services: com.github.nginx-proxy.nginx-proxy.keepalive: 30 com.github.nginx-proxy.nginx-proxy.http2.enable: true com.github.nginx-proxy.nginx-proxy.http3.enable: true - # ports: - # - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" + ports: + - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" depends_on: - db - redis diff --git a/docker/faq.md b/docker/faq.md index c284940b8..f1354f1c3 100644 --- a/docker/faq.md +++ b/docker/faq.md @@ -2,18 +2,25 @@ ## I already have a Proxy, how do I disable the included one? -No problem! All you have to do is +No problem! All you have to do is: -1. *Comment out* (or delete) the `proxy` and `proxy-acme` services in `docker-compose.yml` -1. *Uncomment* the `ports` block for the `web` servince in `docker-compose.yml` -1. Change the `DOCKER_WEB_PORT_EXTERNAL_HTTP` setting in your `.env` if you want to change the port from the default `8080` -1. Point your proxy upstream to the exposed `web` port. +1. Change the `DOCKER_PROXY_PROFILE` key/value pair in your `.env` file to `"disabled"`. + * This disables the `proxy` *and* `proxy-acme` services in `docker-compose.yml`. + * The setting is near the bottom of the file. +1. Point your proxy upstream to the exposed `web` port (**Default**: `8080`). + * The port is controlled by the `DOCKER_WEB_PORT_EXTERNAL_HTTP` key in `.env`. + * The setting is near the bottom of the file. ## I already have a SSL certificate, how do I use it? -1. *Comment out* (or delete) the `proxy-acme` service in `docker-compose.yml` -1. Put your certificates in `${DOCKER_CONFIG_ROOT}/proxy/certs/${APP_DOMAIN}/`. The following files are expected to exist in the directory for the proxy to detect and use them automatically (this is the same directory and file names as LetsEncrypt uses) - 1. `cert.pem` - 1. `chain.pem` - 1. `fullchain.pem` - 1. `key.pem` +1. Change the `DOCKER_PROXY_ACME_PROFILE` key/value pair in your `.env` file to `"disabled"`. + * This disabled the `proxy-acme` service in `docker-compose.yml`. + * It does *not* disable the `proxy` service. +1. Put your certificates in `${DOCKER_CONFIG_ROOT}/proxy/certs` (e.g. `./docker-compose/config/proxy/certs`) + * You may need to create this folder manually if it does not exists. + * The following files are expected to exist in the directory for the proxy to detect and use them automatically (this is the same directory and file names as LetsEncrypt uses) + 1. `${APP_DOMAIN}.cert.pem` + 1. `${APP_DOMAIN}.chain.pem` + 1. `${APP_DOMAIN}.fullchain.pem` + 1. `${APP_DOMAIN}.key.pem` + * See the [`nginx-proxy` configuration file for name patterns](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl#L659-L670) From 48e5d45b3f09e1dfbb714ef96c04ee8c12ba04c8 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 19:43:52 +0000 Subject: [PATCH 250/977] improve faq --- docker/README.md | 5 +++-- docker/faq.md | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docker/README.md b/docker/README.md index bb3389965..ea1870fd3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,8 +1,9 @@ # Pixelfed + Docker + Docker Compose * [Setting up a new Pixelfed server with Docker Compose](new-server.md) - * [I already have a SSL certificate, how do I use it?](faq.md#i-already-have-a-proxy-how-do-i-disable-the-included-one) - * [I already have an proxy / how do I disable the Nginx proxy](faq.md#i-already-have-a-ssl-certificate-how-do-i-use-it) +* [Frequently Asked Question / FAQ](faq.md) + * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) + * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) * [Understanding Pixelfed Container runtimes (Apache, FPM, Nginx + FPM)](runtimes.md) * [Apache](runtimes.md#apache) * [FPM](runtimes.md#fpm) diff --git a/docker/faq.md b/docker/faq.md index f1354f1c3..39a81f99d 100644 --- a/docker/faq.md +++ b/docker/faq.md @@ -1,6 +1,6 @@ # Pixelfed Docker FAQ -## I already have a Proxy, how do I disable the included one? +## How do I use my own Proxy server? No problem! All you have to do is: @@ -11,7 +11,9 @@ No problem! All you have to do is: * The port is controlled by the `DOCKER_WEB_PORT_EXTERNAL_HTTP` key in `.env`. * The setting is near the bottom of the file. -## I already have a SSL certificate, how do I use it? +## How do I use my own SSL certificate? + +No problem! All you have to do is: 1. Change the `DOCKER_PROXY_ACME_PROFILE` key/value pair in your `.env` file to `"disabled"`. * This disabled the `proxy-acme` service in `docker-compose.yml`. From 9c426b48a122d5e30baff73b62e915a4ce393eb2 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 19:56:35 +0000 Subject: [PATCH 251/977] more docs --- docker-compose.yml | 13 +++++++++++-- docker/faq.md | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d56f61aa6..574cb1ec8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,13 +11,19 @@ services: # Sits in front of the *real* webserver and manages SSL and (optionally) # load-balancing between multiple web servers # + # You can disable this service by setting [DOCKER_PROXY_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # + # This also disables the [proxy-acme] service, if this is not desired, change the + # [DOCKER_PROXY_ACME_PROFILE] setting to an empty string [""] + # # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs proxy: image: nginxproxy/nginx-proxy:1.4 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" restart: unless-stopped profiles: - - ${DOCKER_PROXY_PROFILE} + - ${DOCKER_PROXY_PROFILE:-} volumes: - "${DOCKER_HOST_SOCKET_PATH}:/tmp/docker.sock:ro" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" @@ -30,13 +36,16 @@ services: # Proxy companion for managing letsencrypt SSL certificates # + # You can disable this service by setting [DOCKER_PROXY_ACME_PROFILE="disabled"] + # in your [.env] file - the setting is near the bottom of the file. + # # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs proxy-acme: image: nginxproxy/acme-companion container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" restart: unless-stopped profiles: - - ${DOCKER_PROXY_ACME_PROFILE} + - ${DOCKER_PROXY_ACME_PROFILE:-} environment: DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" diff --git a/docker/faq.md b/docker/faq.md index 39a81f99d..3988be578 100644 --- a/docker/faq.md +++ b/docker/faq.md @@ -26,3 +26,7 @@ No problem! All you have to do is: 1. `${APP_DOMAIN}.fullchain.pem` 1. `${APP_DOMAIN}.key.pem` * See the [`nginx-proxy` configuration file for name patterns](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl#L659-L670) + +## How do I change the container name prefix? + +Change the `DOCKER_CONTAINER_NAME_PREFIX` key/value pair in your `.env` file. From 2135199c9787444c3df22fdf2d7afa6524787c25 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 20:19:04 +0000 Subject: [PATCH 252/977] tune the github workflow config --- .env.docker | 15 +++++++++++++-- .github/workflows/docker.yml | 23 +++++++---------------- docker-compose.yml | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.env.docker b/.env.docker index b512ce5d0..dd7a06887 100644 --- a/.env.docker +++ b/.env.docker @@ -1,6 +1,8 @@ # -*- mode: bash -*- # vi: ft=bash +# shellcheck disable=SC2034 + ############################################################### # Docker-wide configuration ############################################################### @@ -32,6 +34,15 @@ TZ="UTC" # Automatically run [artisan migrate --force] if new migrations are detected. DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" +# Automatically run "One-time setup tasks" commands. +# +# If you are migrating to this docker-compose setup or have manually run the "One time seutp" +# tasks (https://docs.pixelfed.org/running-pixelfed/installation/#setting-up-services) +# you can set this to "0" to prevent them from running. +# +# Otherwise, leave it at "1" to have them run *once*. +DOCKER_RUN_ONE_TIME_SETUP_TASKS="1" + # The e-mail to use for Lets Encrypt certificate requests. LETSENCRYPT_EMAIL="__CHANGE_ME__" @@ -294,7 +305,7 @@ DB_HOST="db" DB_USERNAME="pixelfed" # See: https://docs.pixelfed.org/technical-documentation/config/#db_password -DB_PASSWORD="helloworld" +DB_PASSWORD="__CHANGE_ME__" # See: https://docs.pixelfed.org/technical-documentation/config/#db_database DB_DATABASE="pixelfed_prod" @@ -751,7 +762,7 @@ LOG_CHANNEL="stderr" # - "null" (default) # # See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver -BROADCAST_DRIVER=redis +BROADCAST_DRIVER="redis" ############################################################### # Other settings diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf73cc946..bda7004e6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -102,25 +102,14 @@ jobs: packages: write env: - # Set the repo variable [DOCKER_HUB_USERNAME] to override the default at https://github.com///settings/variables/actions - # - # NOTE: no login attempt will happen with Docker Hub until this secret is set + # Set the repo variable [DOCKER_HUB_USERNAME] to override the default + # at https://github.com///settings/variables/actions DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }} - # Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default at https://github.com///settings/variables/actions - # - # NOTE: no login attempt will happen with Docker Hub until this secret is set - DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }} - - # Set the repo variable [DOCKER_HUB_REPO] to override the default at https://github.com///settings/variables/actions - # - # NOTE: no login attempt will happen with Docker Hub until this secret is set - DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }} - # For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN] # set to your Personal Access Token at https://github.com///settings/secrets/actions # - # NOTE: no login attempt will happen with Docker Hub until this secret is set + # ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set! HAS_DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN != '' }} steps: @@ -136,6 +125,7 @@ jobs: with: version: v0.12.0 # *or* newer, needed for annotations to work + # See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry - name: Log in to the GitHub Container registry uses: docker/login-action@v3 with: @@ -143,12 +133,13 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub - name: Login to Docker Hub registry (conditionally) + if: ${{ env.HAS_DOCKER_HUB_TOKEN == true }} uses: docker/login-action@v3 with: username: ${{ env.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - if: ${{ env.HAS_DOCKER_HUB_TOKEN == true }} - name: Docker meta uses: docker/metadata-action@v5 @@ -156,7 +147,7 @@ jobs: with: images: | name=ghcr.io/${{ github.repository }},enable=true - name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_TOKEN }} + name=${{ vars.GITHUB_REPOSITORY }},enable=${{ env.HAS_DOCKER_HUB_TOKEN }} flavor: | latest=auto suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} diff --git a/docker-compose.yml b/docker-compose.yml index 574cb1ec8..0e5d2bb19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,7 +81,7 @@ services: com.github.nginx-proxy.nginx-proxy.http2.enable: true com.github.nginx-proxy.nginx-proxy.http3.enable: true ports: - - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" + - "${DOCKER_WEB_PORT_EXTERNAL_HTTP:-8080}:80" depends_on: - db - redis From af1df5edfdfd73e31b9896e2f7895bae0b187470 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 20:24:02 +0000 Subject: [PATCH 253/977] ooops --- .github/workflows/docker.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bda7004e6..591c9d22a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -106,11 +106,19 @@ jobs: # at https://github.com///settings/variables/actions DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }} + # Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }} + + # Set the repo variable [DOCKER_HUB_REPO] to override the default + # at https://github.com///settings/variables/actions + DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }} + # For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN] # set to your Personal Access Token at https://github.com///settings/secrets/actions # # ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set! - HAS_DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN != '' }} + HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }} steps: - name: Checkout Code @@ -135,7 +143,7 @@ jobs: # See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub - name: Login to Docker Hub registry (conditionally) - if: ${{ env.HAS_DOCKER_HUB_TOKEN == true }} + if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }} uses: docker/login-action@v3 with: username: ${{ env.DOCKER_HUB_USERNAME }} @@ -147,7 +155,7 @@ jobs: with: images: | name=ghcr.io/${{ github.repository }},enable=true - name=${{ vars.GITHUB_REPOSITORY }},enable=${{ env.HAS_DOCKER_HUB_TOKEN }} + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} flavor: | latest=auto suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} From 72b454143b0225aea509f2c38bf59e3ef8276c5a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 20:42:11 +0000 Subject: [PATCH 254/977] tweaking configs --- .env.docker | 38 ++++++++++++++++--- .github/workflows/docker.yml | 3 +- docker/.shellcheckrc => .shellcheckrc | 0 docker-compose.yml | 12 +++--- .../root/docker/entrypoint.d/12-migrations.sh | 6 +-- 5 files changed, 43 insertions(+), 16 deletions(-) rename docker/.shellcheckrc => .shellcheckrc (100%) diff --git a/.env.docker b/.env.docker index dd7a06887..5dfd766bb 100644 --- a/.env.docker +++ b/.env.docker @@ -1,8 +1,8 @@ +# shellcheck disable=SC2034,SC2148 + # -*- mode: bash -*- # vi: ft=bash -# shellcheck disable=SC2034 - ############################################################### # Docker-wide configuration ############################################################### @@ -31,9 +31,6 @@ DOCKER_TAG="branch-jippi-fork-apache-8.1" # See: https://www.php.net/manual/en/timezones.php TZ="UTC" -# Automatically run [artisan migrate --force] if new migrations are detected. -DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" - # Automatically run "One-time setup tasks" commands. # # If you are migrating to this docker-compose setup or have manually run the "One time seutp" @@ -315,6 +312,9 @@ DB_DATABASE="pixelfed_prod" # See: https://docs.pixelfed.org/technical-documentation/config/#db_port DB_PORT="3306" +# Automatically run [artisan migrate --force] if new migrations are detected. +DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" + ############################################################### # Mail configuration ############################################################### @@ -970,3 +970,31 @@ DOCKER_PROXY_PROFILE="" # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data +DOCKER_DATA_ROOT="./docker-compose-state/data" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration +DOCKER_CONFIG_ROOT="./docker-compose-state/config" + +# Path (on host system) where the [db] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DB_DATA_PATH="${DOCKER_DATA_ROOT}/db" + +# Path (on host system) where the [redis] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +REDIS_DATA_PATH="${DOCKER_DATA_ROOT}/redis" + +# Path (on host system) where the [app] + [worker] container will write +# its [storage] data (e.g uploads/images/profile pictures etc.). +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +APP_STORAGE_PATH="${DOCKER_DATA_ROOT}/pixelfed/storage" + +# Path (on host system) where the [app] + [worker] container will write +# its [cache] data. +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 591c9d22a..64978070a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -50,8 +50,7 @@ jobs: SHELLCHECK_OPTS: --shell=bash --external-sources with: version: v0.9.0 - scandir: ./docker/ - additional_files: "*.envsh" + additional_files: "*.envsh .env .env.docker .env.example .env.testing" build: runs-on: ubuntu-latest diff --git a/docker/.shellcheckrc b/.shellcheckrc similarity index 100% rename from docker/.shellcheckrc rename to .shellcheckrc diff --git a/docker-compose.yml b/docker-compose.yml index 0e5d2bb19..614d3954a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,8 +68,8 @@ services: target: apache-runtime volumes: - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" + - "${APP_CACHE_PATH}:/var/www/bootstrap/cache" + - "${APP_STORAGE_PATH}:/var/www/storage" environment: LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -98,8 +98,8 @@ services: target: apache-runtime volumes: - "./.env:/var/www/.env" - - "${DOCKER_DATA_ROOT}/pixelfed/cache:/var/www/bootstrap/cache" - - "${DOCKER_DATA_ROOT}/pixelfed/storage:/var/www/storage" + - "${APP_CACHE_PATH}:/var/www/bootstrap/cache" + - "${APP_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis @@ -112,7 +112,7 @@ services: env_file: - ".env" volumes: - - "${DOCKER_DATA_ROOT}/db:/var/lib/mysql" + - "${DB_DATA_PATH}:/var/lib/mysql" ports: - "${DOCKER_DB_PORT_EXTERNAL}:3306" @@ -124,7 +124,7 @@ services: - ".env" volumes: - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - - "${DOCKER_DATA_ROOT}/redis:/data" + - "${REDIS_DATA_PATH}:/data" ports: - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" healthcheck: diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index b10e34c18..1fb49f393 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -7,7 +7,7 @@ source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Allow automatic applying of outstanding/new migrations on startup -: "${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}" +: "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY:=0}" # Wait for the database to be ready await-database-ready @@ -24,9 +24,9 @@ fi log-warning "New migrations available, will automatically apply them now" -if is-false "${DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then +if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then log-info "Automatic applying of new database migrations is disabled" - log-info "Please set [DOCKER_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." + log-info "Please set [DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY=1] in your [.env] file to enable this." exit 0 fi From de96c5f06d0c92393e7953547e6b1e678105fbe0 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 23:50:16 +0000 Subject: [PATCH 255/977] migration docs --- .env.docker | 360 +++++++++++------- .markdownlint.json | 4 + Dockerfile | 2 +- docker-compose.migrate.yml | 42 ++ docker-compose.yml | 35 +- docker/README.md | 1 + docker/customizing.md | 6 +- docker/faq.md | 2 + docker/migration.md | 326 ++++++++++++++++ .../docker/entrypoint.d/01-permissions.sh | 6 +- 10 files changed, 627 insertions(+), 157 deletions(-) create mode 100644 .markdownlint.json create mode 100644 docker-compose.migrate.yml create mode 100644 docker/migration.md diff --git a/.env.docker b/.env.docker index 5dfd766bb..5f5152968 100644 --- a/.env.docker +++ b/.env.docker @@ -1,60 +1,24 @@ -# shellcheck disable=SC2034,SC2148 - # -*- mode: bash -*- # vi: ft=bash -############################################################### -# Docker-wide configuration -############################################################### +# shellcheck disable=SC2034,SC2148 -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their data -DOCKER_DATA_ROOT="./docker-compose/data" - -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) file where containers will store their confguration -DOCKER_CONFIG_ROOT="./docker-compose/config" - -# Image to pull the Pixelfed Docker images from -# -# Possible values: -# - "ghcr.io/pixelfed/pixelfed" to pull from GitHub -# - "pixelfed/pixelfed" to pull from DockerHub -# -DOCKER_IMAGE="ghcr.io/jippi/pixelfed" - -# Pixelfed version (image tag) to pull from the registry. -# -# See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed -DOCKER_TAG="branch-jippi-fork-apache-8.1" - -# Set timezone used by *all* containers - these should be in sync. -# -# See: https://www.php.net/manual/en/timezones.php -TZ="UTC" - -# Automatically run "One-time setup tasks" commands. -# -# If you are migrating to this docker-compose setup or have manually run the "One time seutp" -# tasks (https://docs.pixelfed.org/running-pixelfed/installation/#setting-up-services) -# you can set this to "0" to prevent them from running. -# -# Otherwise, leave it at "1" to have them run *once*. -DOCKER_RUN_ONE_TIME_SETUP_TASKS="1" - -# The e-mail to use for Lets Encrypt certificate requests. -LETSENCRYPT_EMAIL="__CHANGE_ME__" - -# Lets Encrypt staging/test servers for certificate requests. -# -# Setting this to any value will change to letsencrypt test servers. -#LETSENCRYPT_TEST="1" - -# Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) -# by setting this to "1". -#ENTRYPOINT_DEBUG="0" - -############################################################### +################################################################################ # Pixelfed application configuration -############################################################### +################################################################################ + +# The docker tag prefix to use for pulling images, can be one of +# +# * latest +# * +# * staging +# * edge +# * branch- +# * pr- +# +# Combined with [DOCKER_RUNTIME] and [PHP_VERSION] configured +# elsewhere in this file, the final Docker tag is computed. +PIXELFED_RELEASE="branch-jippi-fork" # A random 32-character string to be used as an encryption key. # @@ -80,7 +44,7 @@ APP_DOMAIN="__CHANGE_ME__" # You should set this to the root of your application so that it is used when running Artisan tasks. # # See: https://docs.pixelfed.org/technical-documentation/config/#app_url -APP_URL=https://${APP_DOMAIN} +APP_URL="https://${APP_DOMAIN}" # Application domains used for routing. # @@ -104,7 +68,7 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # Enable/disable new local account registrations. # # See: https://docs.pixelfed.org/technical-documentation/config/#open_registration -#OPEN_REGISTRATION=true +#OPEN_REGISTRATION="true" # Require email verification before a new user can do anything. # @@ -130,11 +94,11 @@ OAUTH_ENABLED="true" # Defaults to "UTC". # -# Do not edit your timezone or things will break! +# ! Do not edit your timezone once the service is running - or things will break! # # See: https://docs.pixelfed.org/technical-documentation/config/#app_timezone # See: https://www.php.net/manual/en/timezones.php -APP_TIMEZONE="${TZ}" +APP_TIMEZONE="UTC" # The application locale determines the default locale that will be used by the translation service provider. # You are free to set this value to any of the locales which will be supported by the application. @@ -277,11 +241,27 @@ INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" # See: https://docs.pixelfed.org/technical-documentation/config/#restricted_instance #RESTRICTED_INSTANCE="false" -############################################################### -# Database configuration -############################################################### +################################################################################ +# Lets Encrypt configuration +################################################################################ -# Here you may specify which of the database connections below you wish to use as your default connection for all database work. +# The host to request LetsEncrypt certificate for +LETSENCRYPT_HOST="${APP_DOMAIN}" + +# The e-mail to use for Lets Encrypt certificate requests. +LETSENCRYPT_EMAIL="__CHANGE_ME__" + +# Lets Encrypt staging/test servers for certificate requests. +# +# Setting this to any value will change to letsencrypt test servers. +#LETSENCRYPT_TEST="1" + +################################################################################ +# Database configuration +################################################################################ + +# Here you may specify which of the database connections below +# you wish to use as your default connection for all database work. # # Of course you may use many connections at once using the database library. # @@ -313,11 +293,11 @@ DB_DATABASE="pixelfed_prod" DB_PORT="3306" # Automatically run [artisan migrate --force] if new migrations are detected. -DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="0" +DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" -############################################################### +################################################################################ # Mail configuration -############################################################### +################################################################################ # Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. # You may specify which one you’re using throughout your application here. @@ -392,9 +372,9 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # See: https://docs.pixelfed.org/technical-documentation/config/#mail_encryption #MAIL_ENCRYPTION="tls" -############################################################### +################################################################################ # Redis configuration -############################################################### +################################################################################ # Defaults to "phpredis". # @@ -419,16 +399,16 @@ REDIS_HOST="redis" # Defaults to 6379. # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_port -REDIS_PORT="6379" +#REDIS_PORT="6379" # Defaults to 0. # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_database #REDIS_DATABASE="0" -############################################################### +################################################################################ # Cache settings -############################################################### +################################################################################ # This option controls the default cache connection that gets used while using this caching library. # @@ -448,11 +428,11 @@ CACHE_DRIVER="redis" # Defaults to ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. # # See: https://docs.pixelfed.org/technical-documentation/config/#cache_prefix -# CACHE_PREFIX="{APP_NAME}_cache" +#CACHE_PREFIX="{APP_NAME}_cache" -############################################################### +################################################################################ # Horizon settings -############################################################### +################################################################################ # This prefix will be used when storing all Horizon data in Redis. # @@ -498,9 +478,9 @@ CACHE_DRIVER="redis" # See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout #HORIZON_SUPERVISOR_TIMEOUT="300" -############################################################### +################################################################################ # Experiments -############################################################### +################################################################################ # Text only posts (alpha). # @@ -523,16 +503,16 @@ CACHE_DRIVER="redis" # See: https://docs.pixelfed.org/technical-documentation/config/#exp_cpt #EXP_CPT="false" -# Enforce Mastoapi Compatibility (alpha). +# Enforce Mastodon API Compatibility (alpha). # # Defaults to "true". # # See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc -EXP_EMC="true" +#EXP_EMC="true" -############################################################### +################################################################################ # ActivityPub confguration -############################################################### +################################################################################ # Defaults to "false". # @@ -559,9 +539,9 @@ ACTIVITY_PUB="true" # See: https://docs.pixelfed.org/technical-documentation/config/#ap_outbox #AP_OUTBOX="true" -############################################################### +################################################################################ # Federation confguration -############################################################### +################################################################################ # Defaults to "true". # @@ -578,9 +558,9 @@ ACTIVITY_PUB="true" # See: https://docs.pixelfed.org/technical-documentation/config/#webfinger #WEBFINGER="true" -############################################################### +################################################################################ # Storage (cloud) -############################################################### +################################################################################ # Store media on object storage like S3, Digital Ocean Spaces, Rackspace # @@ -604,9 +584,9 @@ ACTIVITY_PUB="true" # See: https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud #MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" -############################################################### +################################################################################ # Storage (cloud) - S3 andS S3 *compatible* providers (most of them) -############################################################### +################################################################################ # See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id #AWS_ACCESS_KEY_ID= @@ -765,10 +745,10 @@ LOG_CHANNEL="stderr" BROADCAST_DRIVER="redis" ############################################################### -# Other settings +# Sanitizing settings ############################################################### -# Defaults to true. +# Defaults to "true". # # See: https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types #RESTRICT_HTML_TYPES="true" @@ -898,52 +878,122 @@ TRUST_PROXIES="*" # PHP configuration ############################################################### +# The PHP version to use for [web] and [worker] container +# +# Any version published on https://hub.docker.com/_/php should work +# +# Example: +# +# * 8.1 +# * 8.2 +# * 8.2.14 +# * latest +# +# Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") +# *only* the version part. The rest of the full tag is derived from +# the [DOCKER_RUNTIME] and [PHP_DEBIAN_RELEASE] settings +PHP_VERSION="8.1" + # See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit #PHP_MEMORY_LIMIT="128M" -############################################################### -# MySQL DB container configuration (DO NOT CHANGE) -############################################################### +# The Debian release variant to use of the [php] Docker image +#PHP_DEBIAN_RELEASE="bullseye" + +# The [php] Docker image base type # -# See "Environment Variables" at https://hub.docker.com/_/mysql +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/runtimes.md +#PHP_BASE_TYPE="apache" -MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" -MYSQL_USER="${DB_USERNAME}" -MYSQL_PASSWORD="${DB_PASSWORD}" -MYSQL_DATABASE="${DB_DATABASE}" - -############################################################### -# MySQL (MariaDB) DB container configuration (DO NOT CHANGE) -############################################################### +# List of extra APT packages (separated by space) to install when building +# locally using [docker compose build]. # -# See "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#APT_PACKAGES_EXTRA="" -MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" -MARIADB_USER="${DB_USERNAME}" -MARIADB_PASSWORD="${DB_PASSWORD}" -MARIADB_DATABASE="${DB_DATABASE}" - -############################################################### -# PostgreSQL DB container configuration (DO NOT CHANGE) -############################################################### +# List of *extra* PECL extensions (separated by space) to install when +# building locally using [docker compose build]. # -# See "Environment Variables" at https://hub.docker.com/_/postgres +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#PHP_PECL_EXTENSIONS_EXTRA="" -POSTGRES_USER="${DB_USERNAME}" -POSTGRES_PASSWORD="${DB_PASSWORD}" -POSTGRES_DB="${DB_DATABASE}" +# List of *extra* PHP extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#PHP_EXTENSIONS_EXTRA="" -############################################################### -# Lets Encrypt configuration -############################################################### +################################################################################ +# Other configuration +################################################################################ -LETSENCRYPT_HOST="${APP_DOMAIN}" +# ? Add your own configuration here -############################################################### +################################################################################ +# Timezone configuration +################################################################################ + +# Set timezone used by *all* containers - these must be in sync. +# +# ! Do not edit your timezone once the service is running - or things will break! +# +# See: https://www.php.net/manual/en/timezones.php +TZ="${APP_TIMEZONE}" + +################################################################################ # Docker Specific configuration -############################################################### +################################################################################ + +# Image to pull the Pixelfed Docker images from. +# +# Example values: +# +# * "ghcr.io/pixelfed/pixelfed" to pull from GitHub +# * "pixelfed/pixelfed" to pull from DockerHub +# * "your/fork" to pull from a custom fork +# +DOCKER_IMAGE="ghcr.io/jippi/pixelfed" + +# The container runtime to use. +# +# See: https://github.com/jippi/pixelfed/blob/jippi-fork/docker/runtimes.md +DOCKER_RUNTIME="apache" + +# Pixelfed version (image tag) to pull from the registry. +# +# See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed +DOCKER_TAG="${PIXELFED_RELEASE}-${DOCKER_RUNTIME}-${PHP_VERSION}" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data +DOCKER_DATA_ROOT="./docker-compose-state/data" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration +DOCKER_CONFIG_ROOT="./docker-compose-state/config" + +# Path (on host system) where the [db] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_DB_DATA_PATH="${DOCKER_DATA_ROOT}/db" + +# Path (on host system) where the [redis] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_REDIS_DATA_PATH="${DOCKER_DATA_ROOT}/redis" + +# Path (on host system) where the [app] + [worker] container will write +# its [storage] data (e.g uploads/images/profile pictures etc.). +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_APP_STORAGE_PATH="${DOCKER_DATA_ROOT}/pixelfed/storage" + +# Path (on host system) where the [app] + [worker] container will write +# its [cache] data. +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" + # Port that Redis will listen on *outside* the container (e.g. the host machine) -DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT}" +DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT:-6379}" # Port that the database will listen on *outside* the container (e.g. the host machine) # @@ -971,30 +1021,64 @@ DOCKER_PROXY_PROFILE="" # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data -DOCKER_DATA_ROOT="./docker-compose-state/data" - -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration -DOCKER_CONFIG_ROOT="./docker-compose-state/config" - -# Path (on host system) where the [db] container will store its data +# Automatically run "One-time setup tasks" commands. # -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DB_DATA_PATH="${DOCKER_DATA_ROOT}/db" - -# Path (on host system) where the [redis] container will store its data +# If you are migrating to this docker-compose setup or have manually run the "One time seutp" +# tasks (https://docs.pixelfed.org/running-pixelfed/installation/#setting-up-services) +# you can set this to "0" to prevent them from running. # -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -REDIS_DATA_PATH="${DOCKER_DATA_ROOT}/redis" +# Otherwise, leave it at "1" to have them run *once*. +#DOCKER_RUN_ONE_TIME_SETUP_TASKS="1" -# Path (on host system) where the [app] + [worker] container will write -# its [storage] data (e.g uploads/images/profile pictures etc.). +# A space-seperated list of paths (inside the container) to *recursively* [chown] +# to the container user/group id (UID/GID) in case of permission issues. # -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -APP_STORAGE_PATH="${DOCKER_DATA_ROOT}/pixelfed/storage" +# ! You should *not* leave this on permanently, at it can significantly slow down startup +# ! time for the container, and during normal operations there should never be permission +# ! issues. Please report a bug if you see behavior requiring this to be permanently on +# +# Example: "/var/www/storage /var/www/bootstrap/cache" +#DOCKER_ENSURE_OWNERSHIP_PATHS="" -# Path (on host system) where the [app] + [worker] container will write -# its [cache] data. +# Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) +# by setting this to "1". +#ENTRYPOINT_DEBUG="0" + +################################################################################ +# MySQL DB container configuration +################################################################################ # -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" +# See "Environment Variables" at https://hub.docker.com/_/mysql +# +# ! DO NOT CHANGE unless you know what you are doing + +MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" +MYSQL_USER="${DB_USERNAME}" +MYSQL_PASSWORD="${DB_PASSWORD}" +MYSQL_DATABASE="${DB_DATABASE}" + +################################################################################ +# MySQL (MariaDB) DB container configuration +################################################################################ +# +# See "Start a mariadb server instance with user, password and database" +# at https://hub.docker.com/_/mariadb +# +# ! DO NOT CHANGE unless you know what you are doing + +MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" +MARIADB_USER="${DB_USERNAME}" +MARIADB_PASSWORD="${DB_PASSWORD}" +MARIADB_DATABASE="${DB_DATABASE}" + +################################################################################ +# PostgreSQL DB container configuration +################################################################################ +# +# See "Environment Variables" at https://hub.docker.com/_/postgres +# +# ! DO NOT CHANGE unless you know what you are doing + +POSTGRES_USER="${DB_USERNAME}" +POSTGRES_PASSWORD="${DB_PASSWORD}" +POSTGRES_DB="${DB_DATABASE}" diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..cf98a0902 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD014": false +} diff --git a/Dockerfile b/Dockerfile index 6151d8758..3d12aba26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ ARG RUNTIME_UID=33 # often called 'www-data' ARG RUNTIME_GID=33 # often called 'www-data' # APT extra packages -ARG APT_PACKAGES_EXTRA="" +ARG APT_PACKAGES_EXTRA= # Extensions installed via [pecl install] ARG PHP_PECL_EXTENSIONS= diff --git a/docker-compose.migrate.yml b/docker-compose.migrate.yml new file mode 100644 index 000000000..b47abeb48 --- /dev/null +++ b/docker-compose.migrate.yml @@ -0,0 +1,42 @@ +--- +version: "3" + +services: + migrate: + image: "secoresearch/rsync" + entrypoint: "" + working_dir: /migrate + command: 'bash -c "exit 1"' + restart: never + volumes: + ################################ + # Storage volume + ################################ + # OLD + - "app-storage:/migrate/app-storage/old" + # NEW + - "${DOCKER_APP_STORAGE_PATH}:/migrate/app-storage/new" + + ################################ + # MySQL/DB volume + ################################ + # OLD + - "db-data:/migrate/db-data/old" + # NEW + - "${DOCKER_DB_DATA_PATH}:/migrate/db-data/new" + + ################################ + # Redis volume + ################################ + # OLD + - "redis-data:/migrate/redis-data/old" + # NEW + - "${DOCKER_REDIS_DATA_PATH}:/migrate/redis-data/new" + +# Volumes from the old [docker-compose.yml] file +# https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76 +volumes: + db-data: + redis-data: + app-storage: + app-bootstrap: diff --git a/docker-compose.yml b/docker-compose.yml index 614d3954a..76aebf609 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,15 +65,22 @@ services: container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-web" restart: unless-stopped build: - target: apache-runtime + target: ${DOCKER_RUNTIME}-runtime + args: + PHP_VERSION: "${PHP_VERSION:-8.1}" + PHP_BASE_TYPE: "${PHP_BASE_TYPE:-apache}" + PHP_DEBIAN_RELEASE: "${PHP_DEBIAN_RELEASE:-bullseye}" + APT_PACKAGES_EXTRA: "${APT_PACKAGES_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_EXTENSIONS_EXTRA: "${PHP_EXTENSIONS_EXTRA:-}" volumes: - "./.env:/var/www/.env" - - "${APP_CACHE_PATH}:/var/www/bootstrap/cache" - - "${APP_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_STORAGE_PATH}:/var/www/storage" environment: LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" - LETSENCRYPT_TEST: "${LETSENCRYPT_TEST}" + LETSENCRYPT_TEST: "${LETSENCRYPT_TEST:-}" VIRTUAL_HOST: "${APP_DOMAIN}" VIRTUAL_PORT: "80" labels: @@ -91,15 +98,19 @@ services: container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped - deploy: - mode: replicated - replicas: 1 build: - target: apache-runtime + target: ${DOCKER_RUNTIME}-runtime + args: + PHP_VERSION: "${PHP_VERSION:-8.1}" + PHP_BASE_TYPE: "${PHP_BASE_TYPE:-apache}" + PHP_DEBIAN_RELEASE: "${PHP_DEBIAN_RELEASE:-bullseye}" + APT_PACKAGES_EXTRA: "${APT_PACKAGES_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_EXTENSIONS_EXTRA: "${PHP_EXTENSIONS_EXTRA:-}" volumes: - "./.env:/var/www/.env" - - "${APP_CACHE_PATH}:/var/www/bootstrap/cache" - - "${APP_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis @@ -112,7 +123,7 @@ services: env_file: - ".env" volumes: - - "${DB_DATA_PATH}:/var/lib/mysql" + - "${DOCKER_DB_DATA_PATH}:/var/lib/mysql" ports: - "${DOCKER_DB_PORT_EXTERNAL}:3306" @@ -124,7 +135,7 @@ services: - ".env" volumes: - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - - "${REDIS_DATA_PATH}:/data" + - "${DOCKER_REDIS_DATA_PATH}:/data" ports: - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" healthcheck: diff --git a/docker/README.md b/docker/README.md index ea1870fd3..bf73662a8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,7 @@ # Pixelfed + Docker + Docker Compose * [Setting up a new Pixelfed server with Docker Compose](new-server.md) +* [Migrating to the new `docker-compose.yml` setup](migration.md) * [Frequently Asked Question / FAQ](faq.md) * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) diff --git a/docker/customizing.md b/docker/customizing.md index 8dfc534dc..dcea45d3d 100644 --- a/docker/customizing.md +++ b/docker/customizing.md @@ -73,12 +73,12 @@ Please see the ## Fixing ownership on startup -You can set the environment variable `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. +You can set the environment variable `DOCKER_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. The variable is a space-delimited list shown below and accepts both relative and absolute paths: -* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` -* `ENTRYPOINT_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` +* `DOCKER_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` +* `DOCKER_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` ## Build settings (arguments) diff --git a/docker/faq.md b/docker/faq.md index 3988be578..cc2495d15 100644 --- a/docker/faq.md +++ b/docker/faq.md @@ -10,6 +10,7 @@ No problem! All you have to do is: 1. Point your proxy upstream to the exposed `web` port (**Default**: `8080`). * The port is controlled by the `DOCKER_WEB_PORT_EXTERNAL_HTTP` key in `.env`. * The setting is near the bottom of the file. +1. Run `docker compose up -d --remove-orphans` to apply the configuration ## How do I use my own SSL certificate? @@ -26,6 +27,7 @@ No problem! All you have to do is: 1. `${APP_DOMAIN}.fullchain.pem` 1. `${APP_DOMAIN}.key.pem` * See the [`nginx-proxy` configuration file for name patterns](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl#L659-L670) +1. Run `docker compose up -d --remove-orphans` to apply the configuration ## How do I change the container name prefix? diff --git a/docker/migration.md b/docker/migration.md new file mode 100644 index 000000000..76fb0904c --- /dev/null +++ b/docker/migration.md @@ -0,0 +1,326 @@ +# Migrating to the new Pixelfed Docker setup + +There is [*a lot* of changes](https://github.com/pixelfed/pixelfed/pull/4844) in how Pixelfed Docker/Docker Compose images work - it's a complete rewrite - with a bunch of breaking changes. + +## No more anonymous Docker Compose volumes + +The old `docker-compose.yml` configuration file [declared four anonymous volumes](https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76) for storing Pixelfed related data within. + +These are no longer used, instead favoring a [Docker bind volume](https://docs.docker.com/storage/bind-mounts/) approach where content are stored directly on the server disk, outside +of a Docker volume. + +The consequence of this change is that *all* data stored in the - now unsupported - Docker volumes will no longer be accessible by Pixelfed. + +* The `db-data` volume *definitely* contain important data - it's your database after all! +* The `app-storage` volume *definitely* contain important data - it's files uploaded to - or seen by - your server! +* The `redis-data` volume *might* contain important data (depending on your configuration) +* The `app-bootstrap` volume does not contain any important data - all of it will be generated automatically in the new setup on startup. We will *not* be migrating this! + +### Migrating off anonymous Docker Compose volumes + +#### Caveats and warnings + +**NOTE**: This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! + +***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** + +We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. + +**It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** + +**YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** + +#### 0. Backup, rollout, and rollback plan + +1. Make sure to backup your server (ideally *after* step 1 below has completed, but *before* is better than not at all!) +1. Capture the current Git version / Pixelfed release you are on (e.g. `git --no-pager log -1` outputs the commit reference as the 2nd word in first line) +1. Backup your `.env` file (we will do this in step 3 as well) +1. Backup your `docker-compose.yml` file (`cp docker-compose.yml docker-compose.yml.old`) +1. Read through the *entire* document before starting + +#### 1. Migrate your ".env" file + +The new `.env` file for Docker is a bit different from the old one (many new settings!) so the easiest is to grab the new `.env.docker` file and modify it from scratch again. + +```shell +$ cp .env .env.old +$ wget -O .env.new https://raw.githubusercontent.com/pixelfed/pixelfed/dev/.env.docker +``` + +Then open your old `.env.old` configuration file, and for each of the key/value pairs within it, find and update the key in the new `.env.new` configuration file. + +Don't worry though, the file might *look* different (and significantly larger) but it behaves *exactly* the way the old file did, it just has way more options! + +If a key is missing in `.env.new`, don't worry, you can just add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. + +This is a great time to review your settings and familiarize you with all the new settings. + +In *particular* the following sections and the [Frequently Asked Question / FAQ](faq.md) + +* `PHP configuration` section (near the end of the file) where + * The `PHP_VERSION` settings controls your PHP version + * The `PHP_MEMORY_LIMIT` settings controls your PHP memory limit +* `Docker Specific configuration` section (near the end of the file) where + * The `DOCKER_DATA_ROOT` setting dictate where the new migrated data will live. + * The `DOCKER_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! + +#### 2. Stop all running containers + +Stop *all* running containers (web, worker, redis, db) + +```shell +$ docker compose down +``` + +#### 3. Pull down the new source code + +Update your project to the latest release of Pixelfed by running + +```shell +$ git pull origin $release +``` + +Where `$release` is either `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. + +#### 4. Run the migration container + +You can access the Docker container with both old and new volumes by running the following command + +```shell +$ docker compose -f docker-compose.migrate.yml run migrate bash +``` + +This will put you in the `/migrate` directory within the container, containing 8 directories like shown here + +```plain +|-- app-storage +| |-- new +| `-- old +|-- db-data +| |-- new +| `-- old +`-- redis-data + |-- new + `-- old +``` + +#### 5. Check the folders + +##### Old folders + +The following commands should all return *SOME* files and data - if they do not - then there might be an issue with the anonymous volume binding. + +```shell +$ ls app-storage/old +$ ls db-data/old +$ ls redis-data/old +``` + +##### New folders + +The following commands should all return *NO* files and data - if they contain data - you need to either delete it (backup first!) or skip that migration step. + +If you haven't run `docker compose up` since you updated your project in step (2) - they should be empty and good to go. + +```shell +$ ls app-storage/new +$ ls db-data/new +$ ls redis-data/new +``` + +#### 6. Copy the data + +Now we will copy the data from the old volumes, to the new ones. + +The migration container has [`rsync`](https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories) installed - which is perfect for that kind of work! + +**NOTE** It's important that the "source" (first path in the `rsync` command) has a trailing `/` - otherwise the directory layout will turn out wrong! + +**NOTE** Depending on your server, these commands might take some time to finish, each command should provide a progress bar with rough time estimation. + +**NOTE** `rsync` should preserve ownership, permissions, and symlinks correctly for you as well for all the files copied. + +Lets copy the data by running the following commands: + +```shell +$ rsync -avP app-storage/old/ app-storage/new +$ rsync -avP db-data/old/ db-data/new +$ rsync -avP redis-data/old/ redis-data/new +``` + +#### 7. Sanity checking + +Lets make sure everything copied over successfully! + +Each *new* directory should contain *something* like (but not always exactly) the following - **NO** directory should have a single folder called `old`, if they do, the `rsync` commands above didn't work correctly - and you need to move the content of the `old` folder into the "root" of the `new` folder like shown a bit in the following sections. + +The **redis-data/new** directory might also contain a `server.pid` + +```shell +$ ls redis-data/new +appendonlydir +``` + +The **app-storage/new** directory should look *something* like this + +```shell +$ ls app-storage/new +app debugbar docker framework logs oauth-private.key oauth-public.key purify +``` + +The **db-data/new** directory should look *something* like this. There might be a lot of files, or very few files, but there *must* be a `mysql`, `performance_schema`, and `${DB_DATABASE}` (e.g. `pixelfed_prod` directory) + +```shell +$ ls db-data/new +aria_log_control ddl_recovery-backup.log ib_buffer_pool ib_logfile0 ibdata1 mariadb_upgrade_info multi-master.info mysql performance_schema pixelfed_prod sys undo001 undo002 undo003 +``` + +If everything looks good, type `exit` to leave exit the migration container + +#### 6. Starting up the your Pixelfed server again + +With all an updated Pixelfed (step 2), updated `.env` file (step 3), migrated data (step 4, 5, 6 and 7) we're ready to start things back up again. + +But before we start your Pixelfed server back up again, lets put the new `.env` file we made in step 1 in its right place. + +```shell +$ cp .env.new .env +``` + +##### The Database + +First thing we want to try is to start up the database by running the following command and checking the logs + +```shell +$ docker compose up -d db +$ docker compose logs --tail 250 --follow db +``` + +if there are no errors and the server isn't crashing, great! If you have an easy way of connecting to the Database via a GUI or CLI client, do that as well and verify the database and tables are all there. + +##### Redis + +Next thing we want to try is to start up the Redis server by running the following command and checking the logs + +```shell +$ docker compose up -d redis +$ docker compose logs --tail 250 --follow redis +``` + +if there are no errors and the server isn't crashing, great! + +##### Worker + +Next thing we want to try is to start up the Worker server by running the following command and checking the logs + +```shell +$ docker compose up -d worker +$ docker compose logs --tail 250 --follow worker +``` + +The container should output a *lot* of logs from the [docker-entrypoint system](customizing.md#running-commands-on-container-start), but *eventually* you should see these messages + +* `Configuration complete; ready for start up` +* `Horizon started successfully.` + +If you see one or both of those messages, great, the worker seems to be running. + +If the worker is crash looping, inspect the logs and try to resolve the issues. + +You can consider the following additional steps: + +* Enabling `ENTRYPOINT_DEBUG` which will show even more log output to help understand whats going on +* Enabling `DOCKER_ENSURE_OWNERSHIP_PATHS` against the path(s) that might have permission issues +* Fixing permission issues directly on the host since your data should all be in the `${DOCKER_DATA_ROOT}` folder (`./docker-compose-state/data` by default) + +##### Web + +The final service, `web`, which will bring your site back online! What a journey it has been. + +Lets get to it, run these commands to start the `web` service and inspect the logs. + +```shell +$ docker compose up -d web +$ docker compose logs --tail 250 --follow web +``` + +The output should be pretty much identical to that of the `worker`, so please see that section for debugging tips if the container is crash looping. + +If the `web` service came online without issues, start the rest of the (optional) services, such as the `proxy`, if enabled, by running + +```shell +$ docker compose up -d +$ docker compose logs --tail 250 --follow +``` + +If you changed anything in the `.env` file while debugging, some containers might restart now, thats perfectly fine. + +#### 7. Verify + +With all services online, it's time to go to your browser and check everything is working + +1. Upload and post a picture +1. Comment on a post +1. Like a post +1. Check Horizon (`https://${APP_DOMAIN}/horizon`) for any errors +1. Check the Docker compose logs via `docker compose logs --follow` + +If everything looks fine, yay, you made it to the end! Lets do some cleanup + +#### 8. Final steps + cleanup + +With everything working, please take a new snapshot/backup of your server *before* we do any cleanup. A post-migration snapshot is incredibly useful, since it contains both the old and new configuration + data, making any recovery much easier in a rollback scenario later. + +Now, with all the data in the new folders, you can delete the old Docker Container volumes (if you want, completely optional) + +List all volumes, and give them a look: + +```shell +$ docker volume ls +``` + +The volumes we want to delete *ends* with the volume name (`db-data`, `app-storage`, `redis-data`, and `app-bootstrap`.) but has some prefix in front of them. + +Once you have found the volumes in in the list, delete each of them by running: + +```shell +$ docker volume rm $volume_name_in_column_two_of_the_output +``` + +You can also delete the `docker-compose.yml.old` and `.env.old` file since they are no longer needed + +```shell +$ rm docker-compose.yml.old +$ rm .env.old +``` + +### Rollback + +Oh no, something went wrong? No worries, we you got backups and a quick way back! + +#### Move `docker-compose.yml` back + +```shell +$ cp docker-compose.yml docker-compose.yml.new +$ cp docker-compose.yml.old docker-compose.yml +``` + +#### Move `.env` file back + +```shell +$ cp env.old .env +``` + +#### Go back to old source code version + +```shell +$ git checkout $commit_id_from_step_0 +``` + +#### Start things back up + +```shell +docker compose up -d +``` + +#### Verify it worked diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 58b2f0bb8..287d708aa 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -13,13 +13,13 @@ run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" # Optionally fix ownership of configured paths -: "${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS:=""}" +: "${DOCKER_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -ar ensure_ownership_paths <<<"${ENTRYPOINT_ENSURE_OWNERSHIP_PATHS}" +IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_ENSURE_OWNERSHIP_PATHS}" if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then - log-info "No paths has been configured for ownership fixes via [\$ENTRYPOINT_ENSURE_OWNERSHIP_PATHS]." + log-info "No paths has been configured for ownership fixes via [\$DOCKER_ENSURE_OWNERSHIP_PATHS]." exit 0 fi From daba285ea75724e7fe256048833cde3c4e24aa2a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 15 Jan 2024 23:54:41 +0000 Subject: [PATCH 256/977] tune-up --- docker/migration.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/migration.md b/docker/migration.md index 76fb0904c..4b0467ddd 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -55,7 +55,7 @@ If a key is missing in `.env.new`, don't worry, you can just add those key/value This is a great time to review your settings and familiarize you with all the new settings. -In *particular* the following sections and the [Frequently Asked Question / FAQ](faq.md) +In *particular* the following sections * `PHP configuration` section (near the end of the file) where * The `PHP_VERSION` settings controls your PHP version @@ -63,6 +63,9 @@ In *particular* the following sections and the [Frequently Asked Question / FAQ] * `Docker Specific configuration` section (near the end of the file) where * The `DOCKER_DATA_ROOT` setting dictate where the new migrated data will live. * The `DOCKER_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! +* [Frequently Asked Question / FAQ](faq.md) + * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) + * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) #### 2. Stop all running containers From 979aa55135cc9161fcc74eb788c89b793f0a83b7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 15 Jan 2024 22:43:09 -0700 Subject: [PATCH 257/977] Update migration, fixes #4863 --- .../2024_01_09_052419_create_parental_controls_table.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php index 4ef7fd2c7..bf803e4c0 100644 --- a/database/migrations/2024_01_09_052419_create_parental_controls_table.php +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -25,7 +25,11 @@ return new class extends Migration }); Schema::table('user_roles', function (Blueprint $table) { - $table->dropIndex('user_roles_profile_id_unique'); + $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $schemaManager->listTableIndexes('user_roles'); + if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { + $table->dropIndex('user_roles_profile_id_unique'); + } $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); }); } From 88ad5d6a4f25094562775fad4bdc3f33ea47e2eb Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Tue, 16 Jan 2024 20:51:37 +0000 Subject: [PATCH 258/977] ignore some shellchecks for .env files --- .env.example | 2 ++ .env.testing | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d4d7228d1..0a24d1dc1 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +# shellcheck disable=SC2034,SC2148 + APP_NAME="Pixelfed" APP_ENV="production" APP_KEY= diff --git a/.env.testing b/.env.testing index 258d8d740..63209d91b 100644 --- a/.env.testing +++ b/.env.testing @@ -1,3 +1,5 @@ +# shellcheck disable=SC2034,SC2148 + APP_NAME="Pixelfed Test" APP_ENV=local APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E= @@ -62,8 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com' CS_CW_DOMAINS='example.org,example.net,example.com' CS_UNLISTED_DOMAINS='example.org,example.net,example.com' -## Optional +## Optional #HORIZON_DARKMODE=false # Horizon theme darkmode -#HORIZON_EMBED=false # Single Docker Container mode +#HORIZON_EMBED=false # Single Docker Container mode ENABLE_CONFIG_CACHE=false From a70f108616f289d1c2826e4f784467551c726709 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Tue, 16 Jan 2024 20:53:54 +0000 Subject: [PATCH 259/977] fix shellcheck error --- .ddev/commands/redis/redis-cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ddev/commands/redis/redis-cli b/.ddev/commands/redis/redis-cli index 27bf575b3..8824c6a6b 100755 --- a/.ddev/commands/redis/redis-cli +++ b/.ddev/commands/redis/redis-cli @@ -4,4 +4,4 @@ ## Usage: redis-cli [flags] [args] ## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version" -redis-cli -p 6379 -h redis $@ +exec redis-cli -p 6379 -h redis "$@" From afa335b7b5670fd8222235c7bc67fdae29d55dd8 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 12:50:55 +0000 Subject: [PATCH 260/977] add missing pecl back --- .env.docker | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.docker b/.env.docker index 5f5152968..e7f259f2e 100644 --- a/.env.docker +++ b/.env.docker @@ -378,7 +378,7 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # Defaults to "phpredis". # -# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +# See: https://docs.pixelfed.org/technical-documentation/config/#redis_client #REDIS_CLIENT="phpredis" # Defaults to "tcp". diff --git a/Dockerfile b/Dockerfile index 3d12aba26..53037cb05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ ARG RUNTIME_GID=33 # often called 'www-data' ARG APT_PACKAGES_EXTRA= # Extensions installed via [pecl install] -ARG PHP_PECL_EXTENSIONS= +ARG PHP_PECL_EXTENSIONS="redis imagick" ARG PHP_PECL_EXTENSIONS_EXTRA= # Extensions installed via [docker-php-ext-install] From 6563d4d0b90952b7ae32b388d66e16f257332b1d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 13:49:56 +0000 Subject: [PATCH 261/977] add goss (https://github.com/goss-org/goss) validation --- .github/workflows/docker.yml | 12 ++++ goss.yaml | 125 +++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 goss.yaml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 64978070a..ea3533a3e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -187,3 +187,15 @@ jobs: PHP_BASE_TYPE=${{ matrix.php_base }} cache-from: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} cache-to: type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + + - uses: e1himself/goss-installation-action@v1 + with: + version: "v0.4.4" + + - name: Execute Goss tests + run: | + dgoss run \ + -v "./.env.testing:/var/www/.env" \ + -e EXPECTED_PHP_VERSION=${{ matrix.php_version }} \ + -e PHP_BASE_TYPE=${{ matrix.php_base }} \ + ${{ steps.meta.outputs.tags }} diff --git a/goss.yaml b/goss.yaml new file mode 100644 index 000000000..a43763b7a --- /dev/null +++ b/goss.yaml @@ -0,0 +1,125 @@ +# See: https://github.com/goss-org/goss/blob/master/docs/manual.md#goss-manual + +package: + curl: { installed: true } + ffmpeg: { installed: true } + gifsicle: { installed: true } + gosu: { installed: true } + jpegoptim: { installed: true } + locales-all: { installed: true } + locales: { installed: true } + mariadb-client: { installed: true } + nano: { installed: true } + optipng: { installed: true } + pngquant: { installed: true } + postgresql-client: { installed: true } + unzip: { installed: true } + wget: { installed: true } + zip: { installed: true } + +user: + www-data: + exists: true + uid: 33 + gid: 33 + groups: + - www-data + home: /var/www + shell: /usr/sbin/nologin + +command: + php-version: + exit-status: 0 + exec: 'php -v' + stdout: + - PHP {{ .Env.EXPECTED_PHP_VERSION }} + stderr: [] + + php-extensions: + exit-status: 0 + exec: 'php -m' + stdout: + - bcmath + - Core + - ctype + - curl + - date + - dom + - exif + - fileinfo + - filter + - ftp + - gd + - hash + - iconv + - imagick + - intl + - json + - libxml + - mbstring + - mysqlnd + - openssl + - pcntl + - pcre + - PDO + - pdo_mysql + - pdo_pgsql + - pdo_sqlite + - Phar + - posix + - random + - readline + - redis + - Reflection + - session + - SimpleXML + - sodium + - SPL + - sqlite3 + - standard + - tokenizer + - xml + - xmlreader + - xmlwriter + - zip + - zlib + stderr: [] + + forego-version: + exit-status: 0 + exec: 'forego version' + stdout: + - dev + stderr: [] + + gomplate-version: + exit-status: 0 + exec: 'gomplate -v' + stdout: + - gomplate version + stderr: [] + + gosu-version: + exit-status: 0 + exec: 'gosu -v' + stdout: + - '1.12' + stderr: [] + +{{ if eq .Env.PHP_BASE_TYPE "nginx" }} + nginx-version: + exit-status: 0 + exec: 'nginx -v' + stdout: [] + stderr: + - 'nginx version: nginx' +{{ end }} + +{{ if eq .Env.PHP_BASE_TYPE "apache" }} + nginx-version: + exit-status: 0 + exec: 'apachectl -v' + stdout: + - 'Server version: Apache/' + stderr: [] +{{ end }} From d8b37e6870730e7f39d84fd35c5160d99ddecb94 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:13:38 +0000 Subject: [PATCH 262/977] debug redis --- .github/workflows/docker.yml | 8 +++++--- docker-compose.yml | 9 ++++++--- goss.yaml | 1 - 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ea3533a3e..87b91a20f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -188,14 +188,16 @@ jobs: cache-from: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} cache-to: type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + # goss validate the image + # + # See: https://github.com/goss-org/goss - uses: e1himself/goss-installation-action@v1 with: version: "v0.4.4" - - name: Execute Goss tests run: | dgoss run \ -v "./.env.testing:/var/www/.env" \ - -e EXPECTED_PHP_VERSION=${{ matrix.php_version }} \ - -e PHP_BASE_TYPE=${{ matrix.php_base }} \ + -e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \ + -e "PHP_BASE_TYPE=${{ matrix.php_base }}" \ ${{ steps.meta.outputs.tags }} diff --git a/docker-compose.yml b/docker-compose.yml index 76aebf609..b85118411 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -128,18 +128,21 @@ services: - "${DOCKER_DB_PORT_EXTERNAL}:3306" redis: - image: redis:7 + image: redis:7.2 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped + command: ["/etc/redis/redis.conf", "--requirepass", "${REDIS_PASSWORD:-}"] + environment: + - REDISCLI_AUTH=${REDIS_PASSWORD:-} env_file: - ".env" volumes: - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - "${DOCKER_REDIS_DATA_PATH}:/data" ports: - - "${DOCKER_REDIS_PORT_EXTERNAL}:6399" + - "${DOCKER_REDIS_PORT_EXTERNAL}:6379" healthcheck: interval: 10s timeout: 5s retries: 2 - test: ["CMD", "redis-cli", "-p", "6399", "ping"] + test: ["CMD", "redis-cli", "-p", "6379", "ping"] diff --git a/goss.yaml b/goss.yaml index a43763b7a..aa5ee6058 100644 --- a/goss.yaml +++ b/goss.yaml @@ -67,7 +67,6 @@ command: - pdo_sqlite - Phar - posix - - random - readline - redis - Reflection From be2ba79dc24cbdae1b7fd6413c6bc419cd1ff8ac Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:25:31 +0000 Subject: [PATCH 263/977] bugfixes --- .env.docker | 9 +++++++++ docker-compose.yml | 2 +- .../shared/root/docker/entrypoint.d/00-check-config.sh | 3 --- docker/shared/root/docker/entrypoint.d/12-migrations.sh | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.env.docker b/.env.docker index e7f259f2e..d2578d1be 100644 --- a/.env.docker +++ b/.env.docker @@ -995,6 +995,15 @@ DOCKER_APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" # Port that Redis will listen on *outside* the container (e.g. the host machine) DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT:-6379}" +# The filename that Redis should store its config file within +# +# NOTE: The file *MUST* exists (even empty) before enabling this setting! +# +# Use a command like [touch "${DOCKER_CONFIG_ROOT}/redis/redis.conf"] to create it. +# +# Defaults to "" +#DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" + # Port that the database will listen on *outside* the container (e.g. the host machine) # # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL diff --git a/docker-compose.yml b/docker-compose.yml index b85118411..02024c390 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -131,7 +131,7 @@ services: image: redis:7.2 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped - command: ["/etc/redis/redis.conf", "--requirepass", "${REDIS_PASSWORD:-}"] + command: "${DOCKER_REDIS_CONFIG_FILE} --requirepass '${REDIS_PASSWORD:-}'" environment: - REDISCLI_AUTH=${REDIS_PASSWORD:-} env_file: diff --git a/docker/shared/root/docker/entrypoint.d/00-check-config.sh b/docker/shared/root/docker/entrypoint.d/00-check-config.sh index 290deb3fc..eecb150b4 100755 --- a/docker/shared/root/docker/entrypoint.d/00-check-config.sh +++ b/docker/shared/root/docker/entrypoint.d/00-check-config.sh @@ -16,6 +16,3 @@ for file in "${dot_env_files[@]}"; do log-info "Linting dotenv file ${file}" dotenv-linter --skip=QuoteCharacter --skip=UnorderedKey "${file}" done - -# Write the config cache -run-as-runtime-user php artisan config:cache diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index 1fb49f393..41c27e986 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -14,7 +14,7 @@ await-database-ready # Detect if we have new migrations declare -i new_migrations=0 -run-as-runtime-user php artisan migrate:status | grep No && new_migrations=1 +(run-as-runtime-user php artisan migrate:status || :) | grep No && new_migrations=1 if is-true "${new_migrations}"; then log-info "No outstanding migrations detected" From 44266b950b0b1685df8ce6d57705b6f37cf90cc6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:29:24 +0000 Subject: [PATCH 264/977] conditionally initialize passport and instance actor --- .../root/docker/entrypoint.d/11-first-time-setup.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 9559eb4de..41e28b5ae 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -19,7 +19,15 @@ fi load-config-files await-database-ready -only-once "storage:link" run-as-runtime-user php artisan storage:link only-once "key:generate" run-as-runtime-user php artisan key:generate +only-once "storage:link" run-as-runtime-user php artisan storage:link only-once "initial:migrate" run-as-runtime-user php artisan migrate --force only-once "import:cities" run-as-runtime-user php artisan import:cities + +if is-true "${ACTIVITY_PUB:-false}"; then + only-once "instance:actor" run-as-runtime-user php artisan instance:actor +fi + +if is-true "${OAUTH_ENABLED:-false}"; then + only-once "passport:keys" run-as-runtime-user php artisan passport:keys +fi From 45f1df78b05b2021e21df9f5a8b7ce8e0d045e5a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:41:48 +0000 Subject: [PATCH 265/977] update proxy-acme paths --- Dockerfile | 9 +++++---- docker-compose.yml | 8 ++++---- .../root/docker/entrypoint.d/11-first-time-setup.sh | 4 ++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 53037cb05..a5930aec0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,17 +97,17 @@ RUN set -ex \ FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base -ARG APT_PACKAGES_EXTRA ARG BUILDKIT_SBOM_SCAN_STAGE="true" + +ARG APT_PACKAGES_EXTRA +ARG DOTENV_LINTER_VERSION ARG PHP_DEBIAN_RELEASE ARG PHP_VERSION ARG RUNTIME_GID ARG RUNTIME_UID ARG TARGETPLATFORM -ARG DOTENV_LINTER_VERSION ENV DEBIAN_FRONTEND="noninteractive" -ENV DOTENV_LINTER_VERSION="${DOTENV_LINTER_VERSION}" # Ensure we run all scripts through 'bash' rather than 'sh' SHELL ["/bin/bash", "-c"] @@ -119,6 +119,7 @@ RUN set -ex \ WORKDIR /var/www/ ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} +ENV DOTENV_LINTER_VERSION="${DOTENV_LINTER_VERSION}" # Install and configure base layer COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh @@ -148,7 +149,7 @@ ENV PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh -RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php/ \ +RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/php-extensions.sh diff --git a/docker-compose.yml b/docker-compose.yml index 02024c390..ba1a211c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,12 +53,12 @@ services: depends_on: - proxy volumes: - - "${DOCKER_HOST_SOCKET_PATH}:/var/run/docker.sock:ro" + - "${DOCKER_CONFIG_ROOT}/proxy-acme:/etc/acme.sh" + - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" - - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" - "${DOCKER_DATA_ROOT}/proxy/html:/usr/share/nginx/html" - - "${DOCKER_DATA_ROOT}/proxy-acme:/etc/acme.sh" + - "${DOCKER_HOST_SOCKET_PATH}:/var/run/docker.sock:ro" web: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" @@ -131,7 +131,7 @@ services: image: redis:7.2 container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped - command: "${DOCKER_REDIS_CONFIG_FILE} --requirepass '${REDIS_PASSWORD:-}'" + command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" environment: - REDISCLI_AUTH=${REDIS_PASSWORD:-} env_file: diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index 41e28b5ae..a3582932b 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -19,6 +19,10 @@ fi load-config-files await-database-ready +# Following https://docs.pixelfed.org/running-pixelfed/installation/#one-time-setup-tasks +# +# NOTE: Caches happens in [30-cache.sh] + only-once "key:generate" run-as-runtime-user php artisan key:generate only-once "storage:link" run-as-runtime-user php artisan storage:link only-once "initial:migrate" run-as-runtime-user php artisan migrate --force From 3a7fd8eac98263bd96d51cd2b7e162c96cc2cc05 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:46:07 +0000 Subject: [PATCH 266/977] fix default variables --- .env.docker | 14 +++++++------- .../root/docker/entrypoint.d/04-defaults.envsh | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.docker b/.env.docker index d2578d1be..fe6c19d4b 100644 --- a/.env.docker +++ b/.env.docker @@ -134,6 +134,13 @@ APP_TIMEZONE="UTC" # See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb #MAX_PHOTO_SIZE="15000" +# The max number of photos allowed per post. +# +# Defaults to "4". +# +# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length +#MAX_ALBUM_LENGTH="4" + # Update the max avatar size, in kB. # # Defaults to "2000" (2MB). @@ -162,13 +169,6 @@ APP_TIMEZONE="UTC" # See: https://docs.pixelfed.org/technical-documentation/config/#max_name_length #MAX_NAME_LENGTH="30" -# The max number of photos allowed per post. -# -# Defaults to "4". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length -#MAX_ALBUM_LENGTH="4" - # Resize and optimize image uploads. # # Defaults to "true". diff --git a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh index fe906120a..a55a56e6c 100755 --- a/docker/shared/root/docker/entrypoint.d/04-defaults.envsh +++ b/docker/shared/root/docker/entrypoint.d/04-defaults.envsh @@ -12,8 +12,8 @@ entrypoint-set-script-name "${BASH_SOURCE[0]}" load-config-files -: "${MAX_PHOTO_SIZE:=}" -: "${MAX_ALBUM_LENGTH:=}" +: "${MAX_PHOTO_SIZE:=15000}" +: "${MAX_ALBUM_LENGTH:=4}" # We assign a 1MB buffer to the just-in-time calculated max post size to allow for fields and overhead : "${POST_MAX_SIZE_BUFFER:=1M}" From 9ad04a285a2f2c223256af3253b4f96df13f6a20 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:48:02 +0000 Subject: [PATCH 267/977] fix missing output colors --- docker/shared/root/docker/helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 74aa6c468..3c21af791 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -220,7 +220,7 @@ function log-info-stderr() { fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${log_prefix}$msg" >/dev/stderr + echo -e "${notice_message_color}${log_prefix}${msg}${color_clear}" >/dev/stderr fi } From 90c9d8b5a6ca11fbfeac13db38aaaf1cd19e46cb Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:50:29 +0000 Subject: [PATCH 268/977] more toned down colors --- docker/shared/root/docker/helpers.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 3c21af791..ea3a726cd 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -154,7 +154,7 @@ function log-error() { log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${error_message_color}${log_prefix}ERROR - ${msg}${color_clear}" >/dev/stderr + echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" >/dev/stderr } # @description Print the given error message to stderr and exit 1 @@ -183,7 +183,7 @@ function log-warning() { log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${warn_message_color}${log_prefix}WARNING - ${msg}${color_clear}" >/dev/stderr + echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" >/dev/stderr } # @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set @@ -201,7 +201,7 @@ function log-info() { fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${notice_message_color}${log_prefix}${msg}${color_clear}" + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" fi } @@ -220,7 +220,7 @@ function log-info-stderr() { fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${notice_message_color}${log_prefix}${msg}${color_clear}" >/dev/stderr + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" >/dev/stderr fi } From e70e13e2659c4906bb9716732a0a865f344d8387 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 14:52:22 +0000 Subject: [PATCH 269/977] possible fix is-false/true logic --- docker/shared/root/docker/helpers.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index ea3a726cd..50fd3b1d9 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -491,6 +491,8 @@ function show-call-stack() { # @see as-boolean function is-true() { as-boolean "${1:-}" && return 0 + + return 1 } # @description Helper function see if $1 could be considered falsey @@ -498,6 +500,8 @@ function is-true() { # @see as-boolean function is-false() { as-boolean "${1:-}" || return 0 + + return 1 } # @description Helper function see if $1 could be truethy or falsey. From 2d05eccb87aefd146d1cc3989111e0fbcb97a0ee Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:37:12 +0000 Subject: [PATCH 270/977] add bats testing --- .github/workflows/docker.yml | 10 +++- .gitmodules | 10 ++++ docker/shared/root/docker/helpers.sh | 6 ++- tests/bats/helpers.bats | 74 ++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 100644 tests/bats/helpers.bats diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 87b91a20f..3296ba9d6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,7 +43,7 @@ jobs: name: Shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master env: @@ -52,6 +52,14 @@ jobs: version: v0.9.0 additional_files: "*.envsh .env .env.docker .env.example .env.testing" + bats: + name: Bats Testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run bats + run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats + build: runs-on: ubuntu-latest diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..6fe990290 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,10 @@ + +[submodule "tests/shell/bats"] + path = tests/shell/bats + url = https://github.com/bats-core/bats-core.git +[submodule "tests/shell/test_helper/bats-support"] + path = tests/shell/test_helper/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "tests/shell/test_helper/bats-assert"] + path = tests/shell/test_helper/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 50fd3b1d9..823abab07 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -487,6 +487,7 @@ function show-call-stack() { } # @description Helper function see if $1 could be considered truthy +# returns [0] if input is truthy, otherwise [1] # @arg $1 string The string to evaluate # @see as-boolean function is-true() { @@ -496,12 +497,13 @@ function is-true() { } # @description Helper function see if $1 could be considered falsey +# returns [0] if input is falsey, otherwise [1] # @arg $1 string The string to evaluate # @see as-boolean function is-false() { - as-boolean "${1:-}" || return 0 + as-boolean "${1:-}" && return 1 - return 1 + return 0 } # @description Helper function see if $1 could be truethy or falsey. diff --git a/tests/bats/helpers.bats b/tests/bats/helpers.bats new file mode 100644 index 000000000..ea4c7b518 --- /dev/null +++ b/tests/bats/helpers.bats @@ -0,0 +1,74 @@ +setup() { + DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-}")" >/dev/null 2>&1 && pwd)" + ROOT="$(dirname "$(dirname "$DIR")")" + + load "$ROOT/docker/shared/root/docker/helpers.sh" +} + +@test "test [is-true]" { + is-true "1" + is-true "true" + is-true "TrUe" +} + +@test "test [is-false]" { + is-false "0" + is-false "false" + is-false "FaLsE" +} + +@test "test [is-false-expressions-0]" { + if is-false "0"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-false]" { + if is-false "false"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-FaLse]" { + if is-false "FaLse"; then + return 0 + fi + + return 1 +} + +@test "test [is-false-expressions-invalid]" { + if is-false "invalid"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-1]" { + if is-true "1"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-true]" { + if is-true "true"; then + return 0 + fi + + return 1 +} + +@test "test [is-true-expressions-TrUE]" { + if is-true "TrUE"; then + return 0 + fi + + return 1 +} From a4646df8f23584996d53bcd71f0b082fa266a632 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:47:39 +0000 Subject: [PATCH 271/977] add some health checks --- docker-compose.yml | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ba1a211c9..757d7690f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,11 @@ services: ports: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" + healthcheck: + test: 'curl --header "Host: $[APP_DOMAIN}" --fail http://localhost/api/service/health-check' + interval: 10s + retries: 2 + timeout: 5s # Proxy companion for managing letsencrypt SSL certificates # @@ -92,6 +97,11 @@ services: depends_on: - db - redis + healthcheck: + test: 'curl --header "Host: $[APP_DOMAIN}" --fail http://localhost/api/service/health-check' + interval: 10s + retries: 2 + timeout: 5s worker: image: "${DOCKER_IMAGE}:${DOCKER_TAG}" @@ -114,6 +124,11 @@ services: depends_on: - db - redis + healthcheck: + test: gosu www-data php artisan horizon:status | grep running + interval: 10s + timeout: 5s + retries: 2 db: image: mariadb:11.2 @@ -126,6 +141,18 @@ services: - "${DOCKER_DB_DATA_PATH}:/var/lib/mysql" ports: - "${DOCKER_DB_PORT_EXTERNAL}:3306" + healthcheck: + test: + [ + "CMD", + "healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized", + ] + interval: 10s + retries: 2 + timeout: 5s redis: image: redis:7.2 @@ -142,7 +169,7 @@ services: ports: - "${DOCKER_REDIS_PORT_EXTERNAL}:6379" healthcheck: - interval: 10s - timeout: 5s - retries: 2 test: ["CMD", "redis-cli", "-p", "6379", "ping"] + interval: 10s + retries: 2 + timeout: 5s From 3feb93b0346b16702a01488f3fe9c960e98388de Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:49:49 +0000 Subject: [PATCH 272/977] cleanup color output --- docker/shared/root/docker/entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 49d62afd0..77b58a019 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -59,7 +59,7 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil fi log-info "" - log-info "${notice_message_color}Sourcing [${file}]${color_clear}" + log-info "Sourcing [${file}]" log-info "" # shellcheck disable=SC1090 @@ -77,7 +77,7 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil fi log-info "" - log-info "${notice_message_color}Executing [${file}]${color_clear}" + log-info "Executing [${file}]" log-info "" "${file}" From adbd66eb38b387f0bbe4d8d189c116a321ed3ed1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:52:09 +0000 Subject: [PATCH 273/977] fix defaults --- docker/shared/root/docker/entrypoint.sh | 4 ++-- docker/shared/root/docker/templates/usr/local/etc/php/php.ini | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 77b58a019..49d62afd0 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -59,7 +59,7 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil fi log-info "" - log-info "Sourcing [${file}]" + log-info "${notice_message_color}Sourcing [${file}]${color_clear}" log-info "" # shellcheck disable=SC1090 @@ -77,7 +77,7 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil fi log-info "" - log-info "Executing [${file}]" + log-info "${notice_message_color}Executing [${file}]${color_clear}" log-info "" "${file}" diff --git a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index c34266630..6277ec080 100644 --- a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -831,10 +831,10 @@ file_uploads = On ; Maximum allowed size for uploaded files. ; http://php.net/upload-max-filesize -upload_max_filesize = {{ getenv "POST_MAX_SIZE" }} +upload_max_filesize = {{ getenv "POST_MAX_SIZE" "61M" }} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" }} +max_file_uploads = {{ getenv "MAX_ALBUM_LENGTH" "4" }} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; From eee17fe9f24393020f2ea220b5245a421da416b9 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:53:17 +0000 Subject: [PATCH 274/977] harden proxy health check to be https based --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 757d7690f..a029d4ee1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" healthcheck: - test: 'curl --header "Host: $[APP_DOMAIN}" --fail http://localhost/api/service/health-check' + test: 'curl --header "Host: $[APP_DOMAIN}" --fail https://localhost/api/service/health-check' interval: 10s retries: 2 timeout: 5s From 82ab545f1a03920fe2c2d5be14680d615cf6532b Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:55:05 +0000 Subject: [PATCH 275/977] more clear separation between log entry points --- docker/shared/root/docker/entrypoint.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 49d62afd0..1b5f193d6 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -58,9 +58,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "" - log-info "${notice_message_color}Sourcing [${file}]${color_clear}" - log-info "" + log-info "========================================" + log-info "Sourcing [${file}]" + log-info "========================================" # shellcheck disable=SC1090 source "${file}" @@ -76,9 +76,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "" - log-info "${notice_message_color}Executing [${file}]${color_clear}" - log-info "" + log-info "========================================" + log-info "Executing [${file}]" + log-info "========================================" "${file}" ;; From 921f34d42eba87c431e25d75f987e8fe1f0e38d3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 15:59:58 +0000 Subject: [PATCH 276/977] color tweaks --- docker/shared/root/docker/helpers.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 823abab07..cd4616175 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -10,6 +10,7 @@ set -e -o errexit -o nounset -o pipefail declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" declare -g notice_message_color="\033[1;34m" +declare -g success_message_color="\033[1;32m" declare -g color_clear="\033[1;0m" # Current and previous log prefix @@ -80,7 +81,7 @@ function run-command-as() { target_user=${1} shift - log-info-stderr "👷 Running [${*}] as [${target_user}]" + log-info-stderr "${notice_message_color}👷 Running [${*}] as [${target_user}]${color_clear}" if [[ ${target_user} != "root" ]]; then stream-prefix-command-output su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" @@ -91,11 +92,11 @@ function run-command-as() { exit_code=$? if [[ $exit_code != 0 ]]; then - log-error "❌ Error!" + log-error "${error_message_color}❌ Error!${color_clear}" return "$exit_code" fi - log-info-stderr "✅ OK!" + log-info-stderr "${success_message_color}✅ OK!${color_clear}" return "$exit_code" } From cc9f673eeafec0dc18a9f37bf12a33c243f6e5d5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:01:52 +0000 Subject: [PATCH 277/977] test proxy via direct url --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a029d4ee1..b2a8aa806 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" healthcheck: - test: 'curl --header "Host: $[APP_DOMAIN}" --fail https://localhost/api/service/health-check' + test: "curl --fail https://$[APP_DOMAIN}/api/service/health-check" interval: 10s retries: 2 timeout: 5s From ead7c33275d8a85b675a8ecd5cfdf898099faaa4 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:11:36 +0000 Subject: [PATCH 278/977] more docker config tuning --- .env.docker | 30 ++++++++++++++++++++++++++++++ docker-compose.yml | 14 +++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.env.docker b/.env.docker index fe6c19d4b..9c440d37e 100644 --- a/.env.docker +++ b/.env.docker @@ -260,6 +260,11 @@ LETSENCRYPT_EMAIL="__CHANGE_ME__" # Database configuration ################################################################################ +# Database version to use (as Docker tag) +# +# See: https://hub.docker.com/_/mariadb +#DB_VERSION="11.2" + # Here you may specify which of the database connections below # you wish to use as your default connection for all database work. # @@ -376,6 +381,11 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # Redis configuration ################################################################################ +# Redis version to use as Docker tag +# +# See: https://hub.docker.com/_/redis +#REDIS_VERSION="7.2" + # Defaults to "phpredis". # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_client @@ -992,6 +1002,11 @@ DOCKER_APP_STORAGE_PATH="${DOCKER_DATA_ROOT}/pixelfed/storage" # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) DOCKER_APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" +# How often Docker health check should run for all services +# +# Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down +DOCKER_DEFAULT_HEALTHCHECK_INTERVAL="10s" + # Port that Redis will listen on *outside* the container (e.g. the host machine) DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT:-6379}" @@ -1004,11 +1019,17 @@ DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT:-6379}" # Defaults to "" #DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" +# How often Docker health check should run for [redis] service +DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" + # Port that the database will listen on *outside* the container (e.g. the host machine) # # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" +# How often Docker health check should run for [db] service +DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" + # Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic DOCKER_PROXY_PORT_EXTERNAL_HTTP="80" @@ -1018,6 +1039,12 @@ DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" # Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" +# How often Docker health check should run for [web] service +DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" + +# How often Docker health check should run for [worker] service +DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" + # Path to the Docker socket on the *host* DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" @@ -1030,6 +1057,9 @@ DOCKER_PROXY_PROFILE="" # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" +# How often Docker health check should run for [proxy] service +DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" + # Automatically run "One-time setup tasks" commands. # # If you are migrating to this docker-compose setup or have manually run the "One time seutp" diff --git a/docker-compose.yml b/docker-compose.yml index b2a8aa806..4d5d6ac64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" healthcheck: test: "curl --fail https://$[APP_DOMAIN}/api/service/health-check" - interval: 10s + interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s @@ -99,7 +99,7 @@ services: - redis healthcheck: test: 'curl --header "Host: $[APP_DOMAIN}" --fail http://localhost/api/service/health-check' - interval: 10s + interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s @@ -126,12 +126,12 @@ services: - redis healthcheck: test: gosu www-data php artisan horizon:status | grep running - interval: 10s + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL}" timeout: 5s retries: 2 db: - image: mariadb:11.2 + image: mariadb:${DB_VERSION:-11.2} container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-db" command: --default-authentication-plugin=mysql_native_password restart: unless-stopped @@ -150,12 +150,12 @@ services: "--connect", "--innodb_initialized", ] - interval: 10s + interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s redis: - image: redis:7.2 + image: redis:${REDIS_VERSION:-7.2} container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" @@ -170,6 +170,6 @@ services: - "${DOCKER_REDIS_PORT_EXTERNAL}:6379" healthcheck: test: ["CMD", "redis-cli", "-p", "6379", "ping"] - interval: 10s + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL}" retries: 2 timeout: 5s From 62efe8b3d4bde9ddc21be9b17e2c153c0f10fec3 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:18:29 +0000 Subject: [PATCH 279/977] ensure default health check values --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4d5d6ac64..376bf52d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -126,7 +126,7 @@ services: - redis healthcheck: test: gosu www-data php artisan horizon:status | grep running - interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL}" + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:-10s}" timeout: 5s retries: 2 @@ -170,6 +170,6 @@ services: - "${DOCKER_REDIS_PORT_EXTERNAL}:6379" healthcheck: test: ["CMD", "redis-cli", "-p", "6379", "ping"] - interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL}" + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s From ca0a25912aa910164ccb357003c1683ed362355b Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:25:34 +0000 Subject: [PATCH 280/977] more color tuning --- docker/shared/root/docker/entrypoint.sh | 12 ++++++------ docker/shared/root/docker/helpers.sh | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 1b5f193d6..44310fa7a 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -58,9 +58,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "========================================" - log-info "Sourcing [${file}]" - log-info "========================================" + log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}Sourcing [${file}]${color_clear}" + log-info "${section_message_color}========================================${color_clear}" # shellcheck disable=SC1090 source "${file}" @@ -76,9 +76,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "========================================" - log-info "Executing [${file}]" - log-info "========================================" + log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}Executing [${file}]${color_clear}" + log-info "${section_message_color}========================================${color_clear}" "${file}" ;; diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index cd4616175..b33528e16 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -11,6 +11,7 @@ declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" declare -g notice_message_color="\033[1;34m" declare -g success_message_color="\033[1;32m" +declare -g section_message_color="\033[1;35m" declare -g color_clear="\033[1;0m" # Current and previous log prefix From dc95d4d80098d3f9e29e38a6841bbc27653cdceb Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:29:15 +0000 Subject: [PATCH 281/977] colors --- docker/shared/root/docker/helpers.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index b33528e16..802e21ea5 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -11,6 +11,7 @@ declare -g error_message_color="\033[1;31m" declare -g warn_message_color="\033[1;34m" declare -g notice_message_color="\033[1;34m" declare -g success_message_color="\033[1;32m" +# shellcheck disable=SC2034 declare -g section_message_color="\033[1;35m" declare -g color_clear="\033[1;0m" @@ -440,7 +441,7 @@ function await-database-ready() { ;; sqlite) - log-info "sqlite are always ready" + log-info "${success_message_color}sqlite is always ready${color_clear}" ;; *) @@ -448,7 +449,7 @@ function await-database-ready() { ;; esac - log-info "✅ Successfully connected to database" + log-info "${success_message_color}✅ Successfully connected to database${color_clear}" } # @description sleeps between 1 and 3 seconds to ensure a bit of randomness From a094a0bd66246d35a65a95a2f502a99e33bbb2cc Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:30:40 +0000 Subject: [PATCH 282/977] syntax fix --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 376bf52d6..88e2b692d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" healthcheck: - test: "curl --fail https://$[APP_DOMAIN}/api/service/health-check" + test: "curl --fail https://${APP_DOMAIN}/api/service/health-check" interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s @@ -98,7 +98,7 @@ services: - db - redis healthcheck: - test: 'curl --header "Host: $[APP_DOMAIN}" --fail http://localhost/api/service/health-check' + test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check' interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL:-10s}" retries: 2 timeout: 5s From f135a240cdf3b7ad64debeb2dde279d4af54ad7e Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:41:01 +0000 Subject: [PATCH 283/977] fix color --- docker/shared/root/docker/helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 802e21ea5..3f7148c5a 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -8,7 +8,7 @@ set -e -o errexit -o nounset -o pipefail # Some splash of color for important messages declare -g error_message_color="\033[1;31m" -declare -g warn_message_color="\033[1;34m" +declare -g warn_message_color="\033[1;33m" declare -g notice_message_color="\033[1;34m" declare -g success_message_color="\033[1;32m" # shellcheck disable=SC2034 From 068143639f9b01caa4fb729f360b3c4ac44b8fe6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 16:45:05 +0000 Subject: [PATCH 284/977] fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ebfef1ace..a5cdf3af1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /.idea /.vagrant /.vscode -/docker-compose/ +/docker-compose-state/ /node_modules /public/hot /public/storage From 98bae1316fa505aa4605b0999dbe4d68553613ba Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 17:51:37 +0000 Subject: [PATCH 285/977] cleanup .env.docker variable names and placement in the file --- .env.docker | 413 ++++++++++-------- docker-compose.migrate.yml | 6 +- docker-compose.yml | 112 ++--- docker/customizing.md | 8 +- docker/faq.md | 4 +- docker/migration.md | 10 +- .../docker/entrypoint.d/01-permissions.sh | 6 +- .../entrypoint.d/11-first-time-setup.sh | 6 +- docker/shared/root/docker/helpers.sh | 2 +- 9 files changed, 308 insertions(+), 259 deletions(-) diff --git a/.env.docker b/.env.docker index 9c440d37e..25914af89 100644 --- a/.env.docker +++ b/.env.docker @@ -1,3 +1,4 @@ +#!/bin/bash # -*- mode: bash -*- # vi: ft=bash @@ -7,19 +8,6 @@ # Pixelfed application configuration ################################################################################ -# The docker tag prefix to use for pulling images, can be one of -# -# * latest -# * -# * staging -# * edge -# * branch- -# * pr- -# -# Combined with [DOCKER_RUNTIME] and [PHP_VERSION] configured -# elsewhere in this file, the final Docker tag is computed. -PIXELFED_RELEASE="branch-jippi-fork" - # A random 32-character string to be used as an encryption key. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -207,7 +195,7 @@ APP_TIMEZONE="UTC" # Defaults to "Pixelfed - Photo sharing for everyone". # # See: https://docs.pixelfed.org/technical-documentation/config/#instance_description -#INSTANCE_DESCRIPTION= +#INSTANCE_DESCRIPTION="" # Defaults to "false". # @@ -227,7 +215,7 @@ INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" # Defaults to "". # # See: https://docs.pixelfed.org/technical-documentation/config/#banned_usernames -#BANNED_USERNAMES= +#BANNED_USERNAMES="" # Defaults to "false". # @@ -263,7 +251,7 @@ LETSENCRYPT_EMAIL="__CHANGE_ME__" # Database version to use (as Docker tag) # # See: https://hub.docker.com/_/mariadb -#DB_VERSION="11.2" +DB_VERSION="11.2" # Here you may specify which of the database connections below # you wish to use as your default connection for all database work. @@ -361,12 +349,12 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # Defaults to "". # # See: https://docs.pixelfed.org/technical-documentation/config/#mail_username -#MAIL_USERNAME= +#MAIL_USERNAME="" # Defaults to "". # # See: https://docs.pixelfed.org/technical-documentation/config/#mail_password -#MAIL_PASSWORD= +#MAIL_PASSWORD="" # Here you may specify the encryption protocol that should be used when the application send e-mail messages. # @@ -381,11 +369,6 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # Redis configuration ################################################################################ -# Redis version to use as Docker tag -# -# See: https://hub.docker.com/_/redis -#REDIS_VERSION="7.2" - # Defaults to "phpredis". # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_client @@ -401,17 +384,17 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # See: https://docs.pixelfed.org/technical-documentation/config/#redis_host REDIS_HOST="redis" -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_password #REDIS_PASSWORD= -# Defaults to 6379. +# Defaults to "6379". # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_port -#REDIS_PORT="6379" +REDIS_PORT="6379" -# Defaults to 0. +# Defaults to "0". # # See: https://docs.pixelfed.org/technical-documentation/config/#redis_database #REDIS_DATABASE="0" @@ -595,26 +578,26 @@ ACTIVITY_PUB="true" #MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" ################################################################################ -# Storage (cloud) - S3 andS S3 *compatible* providers (most of them) +# Storage (cloud) - S3 andS S3 *compatible* providers ################################################################################ # See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id -#AWS_ACCESS_KEY_ID= +#AWS_ACCESS_KEY_ID="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key -#AWS_SECRET_ACCESS_KEY= +#AWS_SECRET_ACCESS_KEY="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_default_region -#AWS_DEFAULT_REGION= +#AWS_DEFAULT_REGION="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_bucket -#AWS_BUCKET= +#AWS_BUCKET="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_url -#AWS_URL= +#AWS_URL="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint -#AWS_ENDPOINT= +#AWS_ENDPOINT="" # See: https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint #AWS_USE_PATH_STYLE_ENDPOINT="false" @@ -625,60 +608,60 @@ ACTIVITY_PUB="true" # Comma-separated list of domains to block. # -# Defaults to null +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains -#CS_BLOCKED_DOMAINS= +#CS_BLOCKED_DOMAINS="" # Comma-separated list of domains to add warnings. # -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains -#CS_CW_DOMAINS= +#CS_CW_DOMAINS="" # Comma-separated list of domains to remove from public timelines. # -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains -#CS_UNLISTED_DOMAINS= +#CS_UNLISTED_DOMAINS="" # Comma-separated list of keywords to block. # -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords -#CS_BLOCKED_KEYWORDS= +#CS_BLOCKED_KEYWORDS="" # Comma-separated list of keywords to add warnings. # -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords -#CS_CW_KEYWORDS= +#CS_CW_KEYWORDS="" # Comma-separated list of keywords to remove from public timelines. # -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords -#CS_UNLISTED_KEYWORDS= +#CS_UNLISTED_KEYWORDS="" -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor -#CS_BLOCKED_ACTOR= +#CS_BLOCKED_ACTOR="" -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor -#CS_CW_ACTOR= +#CS_CW_ACTOR="" -# Defaults to null. +# Defaults to null (not set/commented out). # # See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor -#CS_UNLISTED_ACTOR= +#CS_UNLISTED_ACTOR="" ############################################################### # Media @@ -730,14 +713,14 @@ LOG_CHANNEL="stderr" # Defaults to "". # # See: https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter -#LOG_STDERR_FORMATTER= +#LOG_STDERR_FORMATTER="" # Used by slack. # # Defaults to "". # # See: https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url -#LOG_SLACK_WEBHOOK_URL= +#LOG_SLACK_WEBHOOK_URL="" ############################################################### # Broadcasting settings @@ -795,7 +778,7 @@ QUEUE_DRIVER="redis" # Defaults to "https://sqs.us-east-1.amazonaws.com/your-account-id". # # See: https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix -#SQS_PREFIX= +#SQS_PREFIX="" # Defaults to "your-queue-name". # @@ -879,60 +862,18 @@ TRUST_PROXIES="*" # variables when that is more convenient. # See: https://docs.pixelfed.org/technical-documentation/config/#passport_private_key -#PASSPORT_PRIVATE_KEY= +#PASSPORT_PRIVATE_KEY="" # See: https://docs.pixelfed.org/technical-documentation/config/#passport_public_key -#PASSPORT_PUBLIC_KEY= +#PASSPORT_PUBLIC_KEY="" ############################################################### # PHP configuration ############################################################### -# The PHP version to use for [web] and [worker] container -# -# Any version published on https://hub.docker.com/_/php should work -# -# Example: -# -# * 8.1 -# * 8.2 -# * 8.2.14 -# * latest -# -# Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") -# *only* the version part. The rest of the full tag is derived from -# the [DOCKER_RUNTIME] and [PHP_DEBIAN_RELEASE] settings -PHP_VERSION="8.1" - # See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit #PHP_MEMORY_LIMIT="128M" -# The Debian release variant to use of the [php] Docker image -#PHP_DEBIAN_RELEASE="bullseye" - -# The [php] Docker image base type -# -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/runtimes.md -#PHP_BASE_TYPE="apache" - -# List of extra APT packages (separated by space) to install when building -# locally using [docker compose build]. -# -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md -#APT_PACKAGES_EXTRA="" - -# List of *extra* PECL extensions (separated by space) to install when -# building locally using [docker compose build]. -# -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md -#PHP_PECL_EXTENSIONS_EXTRA="" - -# List of *extra* PHP extensions (separated by space) to install when -# building locally using [docker compose build]. -# -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md -#PHP_EXTENSIONS_EXTRA="" - ################################################################################ # Other configuration ################################################################################ @@ -951,9 +892,64 @@ PHP_VERSION="8.1" TZ="${APP_TIMEZONE}" ################################################################################ -# Docker Specific configuration +# Docker configuraton for *all* services ################################################################################ +# Prefix for container names (without any dash at the end) +DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" + +# How often Docker health check should run for all services +# +# Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down +DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL="10s" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data +DOCKER_ALL_HOST_DATA_ROOT_PATH="./docker-compose-state/data" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration +DOCKER_ALL_HOST_CONFIG_ROOT_PATH="./docker-compose-state/config" + +################################################################################ +# Docker [web] + [worker] (also know as "app") shared service configuration +################################################################################ + +# The docker tag prefix to use for pulling images, can be one of +# +# * latest +# * +# * staging +# * edge +# * branch- +# * pr- +# +# Combined with [DOCKER_APP_RUNTIME] and [PHP_VERSION] configured +# elsewhere in this file, the final Docker tag is computed. +DOCKER_APP_RELEASE="branch-jippi-fork" + +# The PHP version to use for [web] and [worker] container +# +# Any version published on https://hub.docker.com/_/php should work +# +# Example: +# +# * 8.1 +# * 8.2 +# * 8.2.14 +# * latest +# +# Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") +# *only* the version part. The rest of the full tag is derived from +# the [DOCKER_APP_RUNTIME] and [PHP_DEBIAN_RELEASE] settings +DOCKER_APP_PHP_VERSION="8.2" + +# The [php] Docker image base type +# +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/runtimes.md +DOCKER_APP_BASE_TYPE="apache" + +# The Debian release variant to use of the [php] Docker image +DOCKER_APP_DEBIAN_RELEASE="bullseye" + # Image to pull the Pixelfed Docker images from. # # Example values: @@ -962,103 +958,29 @@ TZ="${APP_TIMEZONE}" # * "pixelfed/pixelfed" to pull from DockerHub # * "your/fork" to pull from a custom fork # -DOCKER_IMAGE="ghcr.io/jippi/pixelfed" +DOCKER_APP_IMAGE="ghcr.io/jippi/pixelfed" # The container runtime to use. # # See: https://github.com/jippi/pixelfed/blob/jippi-fork/docker/runtimes.md -DOCKER_RUNTIME="apache" +DOCKER_APP_RUNTIME="apache" # Pixelfed version (image tag) to pull from the registry. # # See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed -DOCKER_TAG="${PIXELFED_RELEASE}-${DOCKER_RUNTIME}-${PHP_VERSION}" - -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data -DOCKER_DATA_ROOT="./docker-compose-state/data" - -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration -DOCKER_CONFIG_ROOT="./docker-compose-state/config" - -# Path (on host system) where the [db] container will store its data -# -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_DB_DATA_PATH="${DOCKER_DATA_ROOT}/db" - -# Path (on host system) where the [redis] container will store its data -# -# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_REDIS_DATA_PATH="${DOCKER_DATA_ROOT}/redis" +DOCKER_APP_TAG="${DOCKER_APP_RELEASE}-${DOCKER_APP_RUNTIME}-${DOCKER_APP_PHP_VERSION}" # Path (on host system) where the [app] + [worker] container will write # its [storage] data (e.g uploads/images/profile pictures etc.). # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_APP_STORAGE_PATH="${DOCKER_DATA_ROOT}/pixelfed/storage" +DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/storage" # Path (on host system) where the [app] + [worker] container will write # its [cache] data. # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_APP_CACHE_PATH="${DOCKER_DATA_ROOT}/pixelfed/cache" - -# How often Docker health check should run for all services -# -# Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down -DOCKER_DEFAULT_HEALTHCHECK_INTERVAL="10s" - -# Port that Redis will listen on *outside* the container (e.g. the host machine) -DOCKER_REDIS_PORT_EXTERNAL="${REDIS_PORT:-6379}" - -# The filename that Redis should store its config file within -# -# NOTE: The file *MUST* exists (even empty) before enabling this setting! -# -# Use a command like [touch "${DOCKER_CONFIG_ROOT}/redis/redis.conf"] to create it. -# -# Defaults to "" -#DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" - -# How often Docker health check should run for [redis] service -DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" - -# Port that the database will listen on *outside* the container (e.g. the host machine) -# -# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL -DOCKER_DB_PORT_EXTERNAL="${DB_PORT}" - -# How often Docker health check should run for [db] service -DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" - -# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic -DOCKER_PROXY_PORT_EXTERNAL_HTTP="80" - -# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic -DOCKER_PROXY_PORT_EXTERNAL_HTTPS="443" - -# Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only -DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" - -# How often Docker health check should run for [web] service -DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" - -# How often Docker health check should run for [worker] service -DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" - -# Path to the Docker socket on the *host* -DOCKER_HOST_SOCKET_PATH="/var/run/docker.sock" - -# Prefix for container names (without any dash at the end) -DOCKER_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" - -# Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service -DOCKER_PROXY_PROFILE="" - -# Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service -DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" - -# How often Docker health check should run for [proxy] service -DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" +DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" # Automatically run "One-time setup tasks" commands. # @@ -1067,7 +989,7 @@ DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" # you can set this to "0" to prevent them from running. # # Otherwise, leave it at "1" to have them run *once*. -#DOCKER_RUN_ONE_TIME_SETUP_TASKS="1" +#DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS="1" # A space-seperated list of paths (inside the container) to *recursively* [chown] # to the container user/group id (UID/GID) in case of permission issues. @@ -1077,14 +999,135 @@ DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_DEFAULT_HEALTHCHECK_INTERVAL}" # ! issues. Please report a bug if you see behavior requiring this to be permanently on # # Example: "/var/www/storage /var/www/bootstrap/cache" -#DOCKER_ENSURE_OWNERSHIP_PATHS="" +#DOCKER_APP_ENSURE_OWNERSHIP_PATHS="" # Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) # by setting this to "1". -#ENTRYPOINT_DEBUG="0" +#DOCKER_APP_ENTRYPOINT_DEBUG="0" + +# List of extra APT packages (separated by space) to install when building +# locally using [docker compose build]. +# +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#DOCKER_APP_APT_PACKAGES_EXTRA="" + +# List of *extra* PECL extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA="" + +# List of *extra* PHP extensions (separated by space) to install when +# building locally using [docker compose build]. +# +# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +#DOCKER_APP_PHP_EXTENSIONS_EXTRA="" ################################################################################ -# MySQL DB container configuration +# Docker [redis] service configuration +################################################################################ + +# Redis version to use as Docker tag +# +# See: https://hub.docker.com/_/redis +DOCKER_REDIS_VERSION="7.2" + +# Path (on host system) where the [redis] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_REDIS_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/redis" + +# Port that Redis will listen on *outside* the container (e.g. the host machine) +DOCKER_REDIS_HOST_PORT="${REDIS_PORT}" + +# The filename that Redis should store its config file within +# +# NOTE: The file *MUST* exists (even empty) before enabling this setting! +# +# Use a command like [touch "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis/redis.conf"] to create it. +# +# Defaults to "" +#DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" + +# How often Docker health check should run for [redis] service +# +# Defaults to "10s" +DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" + +################################################################################ +# Docker [db] service configuration +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [db] service +#DOCKER_DB_PROFILE="" + +# Path (on host system) where the [db] container will store its data +# +# Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) +DOCKER_DB_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/db" + +# Port that the database will listen on *outside* the container (e.g. the host machine) +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +DOCKER_DB_HOST_PORT="${DB_PORT}" + +# How often Docker health check should run for [db] service +DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" + +################################################################################ +# Docker [web] service configuration +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [web] service +#DOCKER_WEB_PROFILE="" + +# Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only +DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" + +# How often Docker health check should run for [web] service +DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" + +################################################################################ +# Docker [worker] service configuration +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [worker] service +#DOCKER_WORKER_PROFILE="" + +# How often Docker health check should run for [worker] service +DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" + +################################################################################ +# Docker [proxy] + [proxy-acme] service configuration +################################################################################ + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service +#DOCKER_PROXY_PROFILE="" + +# Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service +#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" + +# How often Docker health check should run for [proxy] service +DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic +DOCKER_PROXY_HOST_PORT_HTTP="80" + +# Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +DOCKER_PROXY_HOST_PORT_HTTPS="443" + +# Path to the Docker socket on the *host* +DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH="/var/run/docker.sock" + +# ! ---------------------------------------------------------------------------- +# ! STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP +# ! ---------------------------------------------------------------------------- +# ! Below this line is default environment variables for various [db] backends +# ! You very likely do *NOT* need to modify any of this, ever. +# ! ---------------------------------------------------------------------------- + +################################################################################ +# Docker [db] service environment variables for MySQL (Oracle) ################################################################################ # # See "Environment Variables" at https://hub.docker.com/_/mysql @@ -1097,7 +1140,7 @@ MYSQL_PASSWORD="${DB_PASSWORD}" MYSQL_DATABASE="${DB_DATABASE}" ################################################################################ -# MySQL (MariaDB) DB container configuration +# Docker [db] service environment variables for MySQL (MariaDB) ################################################################################ # # See "Start a mariadb server instance with user, password and database" @@ -1111,7 +1154,7 @@ MARIADB_PASSWORD="${DB_PASSWORD}" MARIADB_DATABASE="${DB_DATABASE}" ################################################################################ -# PostgreSQL DB container configuration +# Docker [db] service environment variables for PostgreSQL ################################################################################ # # See "Environment Variables" at https://hub.docker.com/_/postgres diff --git a/docker-compose.migrate.yml b/docker-compose.migrate.yml index b47abeb48..b31771f27 100644 --- a/docker-compose.migrate.yml +++ b/docker-compose.migrate.yml @@ -15,7 +15,7 @@ services: # OLD - "app-storage:/migrate/app-storage/old" # NEW - - "${DOCKER_APP_STORAGE_PATH}:/migrate/app-storage/new" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/migrate/app-storage/new" ################################ # MySQL/DB volume @@ -23,7 +23,7 @@ services: # OLD - "db-data:/migrate/db-data/old" # NEW - - "${DOCKER_DB_DATA_PATH}:/migrate/db-data/new" + - "${DOCKER_DB_HOST_DATA_PATH}:/migrate/db-data/new" ################################ # Redis volume @@ -31,7 +31,7 @@ services: # OLD - "redis-data:/migrate/redis-data/old" # NEW - - "${DOCKER_REDIS_DATA_PATH}:/migrate/redis-data/new" + - "${DOCKER_REDIS_HOST_DATA_PATH}:/migrate/redis-data/new" # Volumes from the old [docker-compose.yml] file # https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76 diff --git a/docker-compose.yml b/docker-compose.yml index 88e2b692d..a3516f7ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,22 +20,22 @@ services: # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs proxy: image: nginxproxy/nginx-proxy:1.4 - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" restart: unless-stopped profiles: - ${DOCKER_PROXY_PROFILE:-} volumes: - - "${DOCKER_HOST_SOCKET_PATH}:/tmp/docker.sock:ro" - - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" - - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" - - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" - - "${DOCKER_DATA_ROOT}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" ports: - - "${DOCKER_PROXY_PORT_EXTERNAL_HTTP}:80" - - "${DOCKER_PROXY_PORT_EXTERNAL_HTTPS}:443" + - "${DOCKER_PROXY_HOST_PORT_HTTP}:80" + - "${DOCKER_PROXY_HOST_PORT_HTTPS}:443" healthcheck: test: "curl --fail https://${APP_DOMAIN}/api/service/health-check" - interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL:-10s}" + interval: "${DOCKER_PROXY_HEALTHCHECK_INTERVAL}" retries: 2 timeout: 5s @@ -47,41 +47,43 @@ services: # See: https://github.com/nginx-proxy/acme-companion/tree/main/docs proxy-acme: image: nginxproxy/acme-companion - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy-acme" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy-acme" restart: unless-stopped profiles: - ${DOCKER_PROXY_ACME_PROFILE:-} environment: DEBUG: 0 DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" - NGINX_PROXY_CONTAINER: "${DOCKER_CONTAINER_NAME_PREFIX}-proxy" + NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" depends_on: - proxy volumes: - - "${DOCKER_CONFIG_ROOT}/proxy-acme:/etc/acme.sh" - - "${DOCKER_CONFIG_ROOT}/proxy/certs:/etc/nginx/certs" - - "${DOCKER_CONFIG_ROOT}/proxy/conf.d:/etc/nginx/conf.d" - - "${DOCKER_CONFIG_ROOT}/proxy/vhost.d:/etc/nginx/vhost.d" - - "${DOCKER_DATA_ROOT}/proxy/html:/usr/share/nginx/html" - - "${DOCKER_HOST_SOCKET_PATH}:/var/run/docker.sock:ro" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy-acme:/etc/acme.sh" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs:/etc/nginx/certs" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/vhost.d:/etc/nginx/vhost.d" + - "${DOCKER_ALL_HOST_DATA_ROOT_PATH}/proxy/html:/usr/share/nginx/html" + - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock:ro" web: - image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-web" + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-web" restart: unless-stopped + profiles: + - ${DOCKER_WEB_PROFILE:-} build: - target: ${DOCKER_RUNTIME}-runtime + target: ${DOCKER_APP_RUNTIME}-runtime args: - PHP_VERSION: "${PHP_VERSION:-8.1}" - PHP_BASE_TYPE: "${PHP_BASE_TYPE:-apache}" - PHP_DEBIAN_RELEASE: "${PHP_DEBIAN_RELEASE:-bullseye}" - APT_PACKAGES_EXTRA: "${APT_PACKAGES_EXTRA:-}" - PHP_PECL_EXTENSIONS_EXTRA: "${PHP_PECL_EXTENSIONS_EXTRA:-}" - PHP_EXTENSIONS_EXTRA: "${PHP_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" volumes: - "./.env:/var/www/.env" - - "${DOCKER_APP_CACHE_PATH}:/var/www/bootstrap/cache" - - "${DOCKER_APP_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" environment: LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -93,54 +95,58 @@ services: com.github.nginx-proxy.nginx-proxy.http2.enable: true com.github.nginx-proxy.nginx-proxy.http3.enable: true ports: - - "${DOCKER_WEB_PORT_EXTERNAL_HTTP:-8080}:80" + - "${DOCKER_WEB_PORT_EXTERNAL_HTTP}:80" depends_on: - db - redis healthcheck: test: 'curl --header "Host: ${APP_DOMAIN}" --fail http://localhost/api/service/health-check' - interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL:-10s}" + interval: "${DOCKER_WEB_HEALTHCHECK_INTERVAL}" retries: 2 timeout: 5s worker: - image: "${DOCKER_IMAGE}:${DOCKER_TAG}" - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-worker" + image: "${DOCKER_APP_IMAGE}:${DOCKER_APP_TAG}" + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped + profiles: + - ${DOCKER_WORKER_PROFILE:-} build: - target: ${DOCKER_RUNTIME}-runtime + target: ${DOCKER_APP_RUNTIME}-runtime args: - PHP_VERSION: "${PHP_VERSION:-8.1}" - PHP_BASE_TYPE: "${PHP_BASE_TYPE:-apache}" - PHP_DEBIAN_RELEASE: "${PHP_DEBIAN_RELEASE:-bullseye}" - APT_PACKAGES_EXTRA: "${APT_PACKAGES_EXTRA:-}" - PHP_PECL_EXTENSIONS_EXTRA: "${PHP_PECL_EXTENSIONS_EXTRA:-}" - PHP_EXTENSIONS_EXTRA: "${PHP_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION}" + PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" + PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" volumes: - "./.env:/var/www/.env" - - "${DOCKER_APP_CACHE_PATH}:/var/www/bootstrap/cache" - - "${DOCKER_APP_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis healthcheck: test: gosu www-data php artisan horizon:status | grep running - interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:-10s}" + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL}" timeout: 5s retries: 2 db: - image: mariadb:${DB_VERSION:-11.2} - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-db" + image: mariadb:${DB_VERSION} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db" command: --default-authentication-plugin=mysql_native_password restart: unless-stopped + profiles: + - ${DOCKER_DB_PROFILE:-} env_file: - ".env" volumes: - - "${DOCKER_DB_DATA_PATH}:/var/lib/mysql" + - "${DOCKER_DB_HOST_DATA_PATH}:/var/lib/mysql" ports: - - "${DOCKER_DB_PORT_EXTERNAL}:3306" + - "${DOCKER_DB_HOST_PORT}:3306" healthcheck: test: [ @@ -150,13 +156,13 @@ services: "--connect", "--innodb_initialized", ] - interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:-10s}" + interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL}" retries: 2 timeout: 5s redis: - image: redis:${REDIS_VERSION:-7.2} - container_name: "${DOCKER_CONTAINER_NAME_PREFIX}-redis" + image: redis:${DOCKER_REDIS_VERSION} + container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" environment: @@ -164,12 +170,12 @@ services: env_file: - ".env" volumes: - - "${DOCKER_CONFIG_ROOT}/redis:/etc/redis" - - "${DOCKER_REDIS_DATA_PATH}:/data" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis" + - "${DOCKER_REDIS_HOST_DATA_PATH}:/data" ports: - - "${DOCKER_REDIS_PORT_EXTERNAL}:6379" + - "${DOCKER_REDIS_HOST_PORT}:6379" healthcheck: test: ["CMD", "redis-cli", "-p", "6379", "ping"] - interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:-10s}" + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL}" retries: 2 timeout: 5s diff --git a/docker/customizing.md b/docker/customizing.md index dcea45d3d..e7c66e842 100644 --- a/docker/customizing.md +++ b/docker/customizing.md @@ -15,7 +15,7 @@ When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/ ### Debugging -You can set environment variable `ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. +You can set environment variable `DOCKER_APP_ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. You can also `docker exec` or `docker run` into a container and run `/` @@ -73,12 +73,12 @@ Please see the ## Fixing ownership on startup -You can set the environment variable `DOCKER_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. +You can set the environment variable `DOCKER_APP_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. The variable is a space-delimited list shown below and accepts both relative and absolute paths: -* `DOCKER_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` -* `DOCKER_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` +* `DOCKER_APP_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` +* `DOCKER_APP_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` ## Build settings (arguments) diff --git a/docker/faq.md b/docker/faq.md index cc2495d15..bbd47fcf5 100644 --- a/docker/faq.md +++ b/docker/faq.md @@ -19,7 +19,7 @@ No problem! All you have to do is: 1. Change the `DOCKER_PROXY_ACME_PROFILE` key/value pair in your `.env` file to `"disabled"`. * This disabled the `proxy-acme` service in `docker-compose.yml`. * It does *not* disable the `proxy` service. -1. Put your certificates in `${DOCKER_CONFIG_ROOT}/proxy/certs` (e.g. `./docker-compose/config/proxy/certs`) +1. Put your certificates in `${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs` (e.g. `./docker-compose/config/proxy/certs`) * You may need to create this folder manually if it does not exists. * The following files are expected to exist in the directory for the proxy to detect and use them automatically (this is the same directory and file names as LetsEncrypt uses) 1. `${APP_DOMAIN}.cert.pem` @@ -31,4 +31,4 @@ No problem! All you have to do is: ## How do I change the container name prefix? -Change the `DOCKER_CONTAINER_NAME_PREFIX` key/value pair in your `.env` file. +Change the `DOCKER_ALL_CONTAINER_NAME_PREFIX` key/value pair in your `.env` file. diff --git a/docker/migration.md b/docker/migration.md index 4b0467ddd..458be4321 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -61,8 +61,8 @@ In *particular* the following sections * The `PHP_VERSION` settings controls your PHP version * The `PHP_MEMORY_LIMIT` settings controls your PHP memory limit * `Docker Specific configuration` section (near the end of the file) where - * The `DOCKER_DATA_ROOT` setting dictate where the new migrated data will live. - * The `DOCKER_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! + * The `DOCKER_ALL_HOST_DATA_ROOT_PATH` setting dictate where the new migrated data will live. + * The `DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! * [Frequently Asked Question / FAQ](faq.md) * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) @@ -232,9 +232,9 @@ If the worker is crash looping, inspect the logs and try to resolve the issues. You can consider the following additional steps: -* Enabling `ENTRYPOINT_DEBUG` which will show even more log output to help understand whats going on -* Enabling `DOCKER_ENSURE_OWNERSHIP_PATHS` against the path(s) that might have permission issues -* Fixing permission issues directly on the host since your data should all be in the `${DOCKER_DATA_ROOT}` folder (`./docker-compose-state/data` by default) +* Enabling `DOCKER_APP_ENTRYPOINT_DEBUG` which will show even more log output to help understand whats going on +* Enabling `DOCKER_APP_ENSURE_OWNERSHIP_PATHS` against the path(s) that might have permission issues +* Fixing permission issues directly on the host since your data should all be in the `${DOCKER_ALL_HOST_DATA_ROOT_PATH}` folder (`./docker-compose-state/data` by default) ##### Web diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 287d708aa..11766a742 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -13,13 +13,13 @@ run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" # Optionally fix ownership of configured paths -: "${DOCKER_ENSURE_OWNERSHIP_PATHS:=""}" +: "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_ENSURE_OWNERSHIP_PATHS}" +IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then - log-info "No paths has been configured for ownership fixes via [\$DOCKER_ENSURE_OWNERSHIP_PATHS]." + log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]." exit 0 fi diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index a3582932b..d3d83c532 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -7,11 +7,11 @@ source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Allow automatic applying of outstanding/new migrations on startup -: "${DOCKER_RUN_ONE_TIME_SETUP_TASKS:=1}" +: "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS:=1}" -if is-false "${DOCKER_RUN_ONE_TIME_SETUP_TASKS}"; then +if is-false "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS}"; then log-warning "Automatic run of the 'One-time setup tasks' is disabled." - log-warning "Please set [DOCKER_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this." + log-warning "Please set [DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS=1] in your [.env] file to enable this." exit 0 fi diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 3f7148c5a..fb8c11c97 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -o errexit -o nounset -o pipefail -[[ ${ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x +[[ ${DOCKER_APP_ENTRYPOINT_DEBUG:=0} == 1 ]] && set -x : "${RUNTIME_UID:="33"}" : "${RUNTIME_GID:="33"}" From a3fd3737963608de1f03d2ad0cd8b236ae08a4e5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:02:38 +0000 Subject: [PATCH 286/977] try github alerts --- docker/migration.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index 458be4321..ec9102562 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -20,15 +20,18 @@ The consequence of this change is that *all* data stored in the - now unsupporte #### Caveats and warnings -**NOTE**: This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! - -***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** +> [!NOTE] +> This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! +> +> [!CAUTION] +> ***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. -**It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** - -**YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** +> [!CAUTION] +> **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** +> +> **YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** #### 0. Backup, rollout, and rollback plan From eba2db76f22da3aed5009cd132ec253d2b529d98 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:03:18 +0000 Subject: [PATCH 287/977] try github alerts --- docker/migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/migration.md b/docker/migration.md index ec9102562..d705dd346 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -22,7 +22,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte > [!NOTE] > This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! -> + > [!CAUTION] > ***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** From 83d92c4819641bdf30249344722e113064842396 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:04:08 +0000 Subject: [PATCH 288/977] try github alerts --- docker/migration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index d705dd346..1a8433992 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -25,13 +25,13 @@ The consequence of this change is that *all* data stored in the - now unsupporte > [!CAUTION] > ***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** +> +> **YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. -> [!CAUTION] +> [!WARNING] > **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** -> -> **YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** #### 0. Backup, rollout, and rollback plan From 4729ffb7d5249863d31c62f3d28a52a027b998c0 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:05:02 +0000 Subject: [PATCH 289/977] try github alerts --- docker/migration.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index 1a8433992..0225e5324 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -20,7 +20,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte #### Caveats and warnings -> [!NOTE] +> [!IMPORTANT] > This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! > [!CAUTION] @@ -28,10 +28,9 @@ The consequence of this change is that *all* data stored in the - now unsupporte > > **YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** -We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. - > [!WARNING] > **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** +> We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. #### 0. Backup, rollout, and rollback plan From e858a453be052454c18cd54eacfa496d26193509 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:06:43 +0000 Subject: [PATCH 290/977] try github alerts --- docker/migration.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/migration.md b/docker/migration.md index 0225e5324..a6f5b1243 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -71,6 +71,9 @@ In *particular* the following sections #### 2. Stop all running containers +> [!CAUTION] +> This will take your Pixelfed instance offline + Stop *all* running containers (web, worker, redis, db) ```shell @@ -135,6 +138,9 @@ $ ls redis-data/new #### 6. Copy the data +> [!WARNING] +> This is where we potentially will double your disk usage (temporarily) + Now we will copy the data from the old volumes, to the new ones. The migration container has [`rsync`](https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories) installed - which is perfect for that kind of work! From 150079119869faa943c353dacfedd4f2a18d9ff5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:07:45 +0000 Subject: [PATCH 291/977] try github alerts --- docker/migration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/migration.md b/docker/migration.md index a6f5b1243..04163d2e4 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -53,7 +53,8 @@ Then open your old `.env.old` configuration file, and for each of the key/value Don't worry though, the file might *look* different (and significantly larger) but it behaves *exactly* the way the old file did, it just has way more options! -If a key is missing in `.env.new`, don't worry, you can just add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. +> [!TIP] +> If a key is missing in `.env.new`, don't worry, you can just add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. This is a great time to review your settings and familiarize you with all the new settings. From 32ad4266d0b5e9069bf043f6f96f614bd3a93562 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:10:12 +0000 Subject: [PATCH 292/977] try github alerts --- docker/migration.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index 04163d2e4..1ce0cff57 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -54,21 +54,22 @@ Then open your old `.env.old` configuration file, and for each of the key/value Don't worry though, the file might *look* different (and significantly larger) but it behaves *exactly* the way the old file did, it just has way more options! > [!TIP] -> If a key is missing in `.env.new`, don't worry, you can just add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. +> Don't worry if a key is missing in `.env.new`, you can add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. This is a great time to review your settings and familiarize you with all the new settings. -In *particular* the following sections - -* `PHP configuration` section (near the end of the file) where - * The `PHP_VERSION` settings controls your PHP version - * The `PHP_MEMORY_LIMIT` settings controls your PHP memory limit -* `Docker Specific configuration` section (near the end of the file) where - * The `DOCKER_ALL_HOST_DATA_ROOT_PATH` setting dictate where the new migrated data will live. - * The `DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! -* [Frequently Asked Question / FAQ](faq.md) - * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) - * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) +> [!NOTE] +> In *particular* the following sections +> +> * `PHP configuration` section (near the end of the file) where +> * The `DOCKER_APP_PHP_VERSION` settings controls your PHP version +> * The `PHP_MEMORY_LIMIT` settings controls your PHP memory limit +> * `Docker Specific configuration` section (near the end of the file) where +> * The `DOCKER_ALL_HOST_DATA_ROOT_PATH` setting dictate where the new migrated data will live. +> * The `DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! +> * [Frequently Asked Question / FAQ](faq.md) +> * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) +> * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) #### 2. Stop all running containers From a383233710659e825ac3cf5ef84d9a15c5998733 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:12:30 +0000 Subject: [PATCH 293/977] try github alerts --- docker/migration.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index 1ce0cff57..a587fd803 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -32,7 +32,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte > **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** > We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. -#### 0. Backup, rollout, and rollback plan +#### Step 0. Backup, rollout, and rollback plan 1. Make sure to backup your server (ideally *after* step 1 below has completed, but *before* is better than not at all!) 1. Capture the current Git version / Pixelfed release you are on (e.g. `git --no-pager log -1` outputs the commit reference as the 2nd word in first line) @@ -40,7 +40,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte 1. Backup your `docker-compose.yml` file (`cp docker-compose.yml docker-compose.yml.old`) 1. Read through the *entire* document before starting -#### 1. Migrate your ".env" file +#### Step 1. Migrate your ".env" file The new `.env` file for Docker is a bit different from the old one (many new settings!) so the easiest is to grab the new `.env.docker` file and modify it from scratch again. @@ -71,7 +71,7 @@ This is a great time to review your settings and familiarize you with all the ne > * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) > * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) -#### 2. Stop all running containers +#### Step 2. Stop all running containers > [!CAUTION] > This will take your Pixelfed instance offline @@ -82,7 +82,7 @@ Stop *all* running containers (web, worker, redis, db) $ docker compose down ``` -#### 3. Pull down the new source code +#### Step 3. Pull down the new source code Update your project to the latest release of Pixelfed by running @@ -92,7 +92,7 @@ $ git pull origin $release Where `$release` is either `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. -#### 4. Run the migration container +#### Step 4. Run the migration container You can access the Docker container with both old and new volumes by running the following command @@ -114,7 +114,7 @@ This will put you in the `/migrate` directory within the container, containing 8 `-- old ``` -#### 5. Check the folders +#### Step 5. Check the folders ##### Old folders @@ -138,7 +138,7 @@ $ ls db-data/new $ ls redis-data/new ``` -#### 6. Copy the data +#### Step 6. Copy the data > [!WARNING] > This is where we potentially will double your disk usage (temporarily) @@ -161,7 +161,7 @@ $ rsync -avP db-data/old/ db-data/new $ rsync -avP redis-data/old/ redis-data/new ``` -#### 7. Sanity checking +#### Step 7. Sanity checking Lets make sure everything copied over successfully! @@ -190,7 +190,7 @@ aria_log_control ddl_recovery-backup.log ib_buffer_pool ib_logfile0 ibdata1 If everything looks good, type `exit` to leave exit the migration container -#### 6. Starting up the your Pixelfed server again +#### Step 8. Starting up the your Pixelfed server again With all an updated Pixelfed (step 2), updated `.env` file (step 3), migrated data (step 4, 5, 6 and 7) we're ready to start things back up again. @@ -268,7 +268,7 @@ $ docker compose logs --tail 250 --follow If you changed anything in the `.env` file while debugging, some containers might restart now, thats perfectly fine. -#### 7. Verify +#### Step 9. Verify With all services online, it's time to go to your browser and check everything is working @@ -280,7 +280,7 @@ With all services online, it's time to go to your browser and check everything i If everything looks fine, yay, you made it to the end! Lets do some cleanup -#### 8. Final steps + cleanup +#### Step 10. Final steps + cleanup With everything working, please take a new snapshot/backup of your server *before* we do any cleanup. A post-migration snapshot is incredibly useful, since it contains both the old and new configuration + data, making any recovery much easier in a rollback scenario later. From 033db841f43f8865330d26ebfa3d8e57efb5a446 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:19:04 +0000 Subject: [PATCH 294/977] try github alerts --- docker/migration.md | 49 +++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index a587fd803..bc516bc43 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -44,7 +44,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte The new `.env` file for Docker is a bit different from the old one (many new settings!) so the easiest is to grab the new `.env.docker` file and modify it from scratch again. -```shell +```bash $ cp .env .env.old $ wget -O .env.new https://raw.githubusercontent.com/pixelfed/pixelfed/dev/.env.docker ``` @@ -78,7 +78,7 @@ This is a great time to review your settings and familiarize you with all the ne Stop *all* running containers (web, worker, redis, db) -```shell +```bash $ docker compose down ``` @@ -86,17 +86,18 @@ $ docker compose down Update your project to the latest release of Pixelfed by running -```shell +```bash $ git pull origin $release ``` -Where `$release` is either `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. +> [!NOTE] +> The `$release` can be any valid git reference like `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. #### Step 4. Run the migration container You can access the Docker container with both old and new volumes by running the following command -```shell +```bash $ docker compose -f docker-compose.migrate.yml run migrate bash ``` @@ -120,7 +121,7 @@ This will put you in the `/migrate` directory within the container, containing 8 The following commands should all return *SOME* files and data - if they do not - then there might be an issue with the anonymous volume binding. -```shell +```bash $ ls app-storage/old $ ls db-data/old $ ls redis-data/old @@ -132,7 +133,7 @@ The following commands should all return *NO* files and data - if they contain d If you haven't run `docker compose up` since you updated your project in step (2) - they should be empty and good to go. -```shell +```bash $ ls app-storage/new $ ls db-data/new $ ls redis-data/new @@ -155,7 +156,7 @@ The migration container has [`rsync`](https://www.digitalocean.com/community/tut Lets copy the data by running the following commands: -```shell +```bash $ rsync -avP app-storage/old/ app-storage/new $ rsync -avP db-data/old/ db-data/new $ rsync -avP redis-data/old/ redis-data/new @@ -169,21 +170,21 @@ Each *new* directory should contain *something* like (but not always exactly) th The **redis-data/new** directory might also contain a `server.pid` -```shell +```bash $ ls redis-data/new appendonlydir ``` The **app-storage/new** directory should look *something* like this -```shell +```bash $ ls app-storage/new app debugbar docker framework logs oauth-private.key oauth-public.key purify ``` The **db-data/new** directory should look *something* like this. There might be a lot of files, or very few files, but there *must* be a `mysql`, `performance_schema`, and `${DB_DATABASE}` (e.g. `pixelfed_prod` directory) -```shell +```bash $ ls db-data/new aria_log_control ddl_recovery-backup.log ib_buffer_pool ib_logfile0 ibdata1 mariadb_upgrade_info multi-master.info mysql performance_schema pixelfed_prod sys undo001 undo002 undo003 ``` @@ -196,7 +197,7 @@ With all an updated Pixelfed (step 2), updated `.env` file (step 3), migrated da But before we start your Pixelfed server back up again, lets put the new `.env` file we made in step 1 in its right place. -```shell +```bash $ cp .env.new .env ``` @@ -204,7 +205,7 @@ $ cp .env.new .env First thing we want to try is to start up the database by running the following command and checking the logs -```shell +```bash $ docker compose up -d db $ docker compose logs --tail 250 --follow db ``` @@ -215,7 +216,7 @@ if there are no errors and the server isn't crashing, great! If you have an easy Next thing we want to try is to start up the Redis server by running the following command and checking the logs -```shell +```bash $ docker compose up -d redis $ docker compose logs --tail 250 --follow redis ``` @@ -226,7 +227,7 @@ if there are no errors and the server isn't crashing, great! Next thing we want to try is to start up the Worker server by running the following command and checking the logs -```shell +```bash $ docker compose up -d worker $ docker compose logs --tail 250 --follow worker ``` @@ -252,7 +253,7 @@ The final service, `web`, which will bring your site back online! What a journey Lets get to it, run these commands to start the `web` service and inspect the logs. -```shell +```bash $ docker compose up -d web $ docker compose logs --tail 250 --follow web ``` @@ -261,7 +262,7 @@ The output should be pretty much identical to that of the `worker`, so please se If the `web` service came online without issues, start the rest of the (optional) services, such as the `proxy`, if enabled, by running -```shell +```bash $ docker compose up -d $ docker compose logs --tail 250 --follow ``` @@ -288,7 +289,7 @@ Now, with all the data in the new folders, you can delete the old Docker Contain List all volumes, and give them a look: -```shell +```bash $ docker volume ls ``` @@ -296,13 +297,13 @@ The volumes we want to delete *ends* with the volume name (`db-data`, `app-stora Once you have found the volumes in in the list, delete each of them by running: -```shell +```bash $ docker volume rm $volume_name_in_column_two_of_the_output ``` You can also delete the `docker-compose.yml.old` and `.env.old` file since they are no longer needed -```shell +```bash $ rm docker-compose.yml.old $ rm .env.old ``` @@ -313,26 +314,26 @@ Oh no, something went wrong? No worries, we you got backups and a quick way back #### Move `docker-compose.yml` back -```shell +```bash $ cp docker-compose.yml docker-compose.yml.new $ cp docker-compose.yml.old docker-compose.yml ``` #### Move `.env` file back -```shell +```bash $ cp env.old .env ``` #### Go back to old source code version -```shell +```bash $ git checkout $commit_id_from_step_0 ``` #### Start things back up -```shell +```bash docker compose up -d ``` From 29564a58096461b80170672cf4c44a24a4860c12 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:20:45 +0000 Subject: [PATCH 295/977] docs tuning --- docker/migration.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/migration.md b/docker/migration.md index bc516bc43..5e820555d 100644 --- a/docker/migration.md +++ b/docker/migration.md @@ -32,7 +32,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte > **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** > We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. -#### Step 0. Backup, rollout, and rollback plan +#### Step 0) Backup, rollout, and rollback plan 1. Make sure to backup your server (ideally *after* step 1 below has completed, but *before* is better than not at all!) 1. Capture the current Git version / Pixelfed release you are on (e.g. `git --no-pager log -1` outputs the commit reference as the 2nd word in first line) @@ -40,7 +40,7 @@ The consequence of this change is that *all* data stored in the - now unsupporte 1. Backup your `docker-compose.yml` file (`cp docker-compose.yml docker-compose.yml.old`) 1. Read through the *entire* document before starting -#### Step 1. Migrate your ".env" file +#### Step 1) Migrate your ".env" file The new `.env` file for Docker is a bit different from the old one (many new settings!) so the easiest is to grab the new `.env.docker` file and modify it from scratch again. @@ -71,7 +71,7 @@ This is a great time to review your settings and familiarize you with all the ne > * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) > * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) -#### Step 2. Stop all running containers +#### Step 2) Stop all running containers > [!CAUTION] > This will take your Pixelfed instance offline @@ -82,7 +82,7 @@ Stop *all* running containers (web, worker, redis, db) $ docker compose down ``` -#### Step 3. Pull down the new source code +#### Step 3) Pull down the new source code Update your project to the latest release of Pixelfed by running @@ -93,7 +93,7 @@ $ git pull origin $release > [!NOTE] > The `$release` can be any valid git reference like `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. -#### Step 4. Run the migration container +#### Step 4) Run the migration container You can access the Docker container with both old and new volumes by running the following command @@ -115,7 +115,7 @@ This will put you in the `/migrate` directory within the container, containing 8 `-- old ``` -#### Step 5. Check the folders +#### Step 5) Check the folders ##### Old folders @@ -139,7 +139,7 @@ $ ls db-data/new $ ls redis-data/new ``` -#### Step 6. Copy the data +#### Step 6) Copy the data > [!WARNING] > This is where we potentially will double your disk usage (temporarily) @@ -162,7 +162,7 @@ $ rsync -avP db-data/old/ db-data/new $ rsync -avP redis-data/old/ redis-data/new ``` -#### Step 7. Sanity checking +#### Step 7) Sanity checking Lets make sure everything copied over successfully! @@ -191,7 +191,7 @@ aria_log_control ddl_recovery-backup.log ib_buffer_pool ib_logfile0 ibdata1 If everything looks good, type `exit` to leave exit the migration container -#### Step 8. Starting up the your Pixelfed server again +#### Step 8) Starting up the your Pixelfed server again With all an updated Pixelfed (step 2), updated `.env` file (step 3), migrated data (step 4, 5, 6 and 7) we're ready to start things back up again. @@ -269,7 +269,7 @@ $ docker compose logs --tail 250 --follow If you changed anything in the `.env` file while debugging, some containers might restart now, thats perfectly fine. -#### Step 9. Verify +#### Step 9) Verify With all services online, it's time to go to your browser and check everything is working @@ -281,7 +281,7 @@ With all services online, it's time to go to your browser and check everything i If everything looks fine, yay, you made it to the end! Lets do some cleanup -#### Step 10. Final steps + cleanup +#### Step 10) Final steps + cleanup With everything working, please take a new snapshot/backup of your server *before* we do any cleanup. A post-migration snapshot is incredibly useful, since it contains both the old and new configuration + data, making any recovery much easier in a rollback scenario later. From 3598f9f8f47067e47ccc32e10dbcff02ae19b9ae Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:24:35 +0000 Subject: [PATCH 296/977] move check-config to after fix-permissions --- .../entrypoint.d/{00-check-config.sh => 02-check-config.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker/shared/root/docker/entrypoint.d/{00-check-config.sh => 02-check-config.sh} (100%) diff --git a/docker/shared/root/docker/entrypoint.d/00-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh similarity index 100% rename from docker/shared/root/docker/entrypoint.d/00-check-config.sh rename to docker/shared/root/docker/entrypoint.d/02-check-config.sh From f2b28ece6ed837faf92f247bea03559cbf08010a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 17 Jan 2024 18:26:04 +0000 Subject: [PATCH 297/977] longer entrypoint bars --- docker/shared/root/docker/entrypoint.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 44310fa7a..ed698250b 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -58,9 +58,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" log-info "${section_message_color}Sourcing [${file}]${color_clear}" - log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" # shellcheck disable=SC1090 source "${file}" @@ -76,9 +76,9 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" fi - log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" log-info "${section_message_color}Executing [${file}]${color_clear}" - log-info "${section_message_color}========================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" "${file}" ;; From 2d223d61edf839cb6ac799c4927b0f82f120901b Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 18 Jan 2024 16:22:26 +0000 Subject: [PATCH 298/977] remove docs they now live in https://github.com/pixelfed/docs-next/pull/1 and https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/ --- docker/README.md | 16 +- docker/customizing.md | 219 --------------------------- docker/faq.md | 34 ----- docker/migration.md | 340 ------------------------------------------ docker/new-server.md | 145 ------------------ docker/runtimes.md | 94 ------------ 6 files changed, 1 insertion(+), 847 deletions(-) delete mode 100644 docker/customizing.md delete mode 100644 docker/faq.md delete mode 100644 docker/migration.md delete mode 100644 docker/new-server.md delete mode 100644 docker/runtimes.md diff --git a/docker/README.md b/docker/README.md index bf73662a8..23584a4ab 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,17 +1,3 @@ # Pixelfed + Docker + Docker Compose -* [Setting up a new Pixelfed server with Docker Compose](new-server.md) -* [Migrating to the new `docker-compose.yml` setup](migration.md) -* [Frequently Asked Question / FAQ](faq.md) - * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) - * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) -* [Understanding Pixelfed Container runtimes (Apache, FPM, Nginx + FPM)](runtimes.md) - * [Apache](runtimes.md#apache) - * [FPM](runtimes.md#fpm) - * [Nginx + FPM](runtimes.md#nginx) -* [Customizing Docker image](customizing.md) - * [Running commands on container start](customizing.md#running-commands-on-container-start) - * [Disabling entrypoint or individual scripts](customizing.md#disabling-entrypoint-or-individual-scripts) - * [Templating](customizing.md#templating) - * [Fixing ownership on startup](customizing.md#fixing-ownership-on-startup) - * [Build settings (arguments)](customizing.md#build-settings-arguments) +Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage diff --git a/docker/customizing.md b/docker/customizing.md deleted file mode 100644 index e7c66e842..000000000 --- a/docker/customizing.md +++ /dev/null @@ -1,219 +0,0 @@ -# Customizing your `Dockerfile` - -## Running commands on container start - -### Description - -When a Pixelfed container starts up, the [`ENTRYPOINT`](https://docs.docker.com/engine/reference/builder/#entrypoint) script will - -1. Search the `/docker/entrypoint.d/` directory for files and for each file (in lexical order). -1. Check if the file is executable. - 1. If the file is *not* executable, print an error and exit the container. -1. If the file has the extension `.envsh` the file will be [sourced](https://superuser.com/a/46146). -1. If the file has the extension `.sh` the file will be run like a normal script. -1. Any other file extension will log a warning and will be ignored. - -### Debugging - -You can set environment variable `DOCKER_APP_ENTRYPOINT_DEBUG=1` to show verbose output of what each `entrypoint.d` script is doing. - -You can also `docker exec` or `docker run` into a container and run `/` - -### Included scripts - -* `/docker/entrypoint.d/04-defaults.envsh` calculates Docker container environment variables needed for [templating](#templating) configuration files. -* `/docker/entrypoint.d/05-templating.sh` renders [template](#templating) configuration files. -* `/docker/entrypoint.d/10-storage.sh` ensures Pixelfed storage related permissions and commands are run. -* `//docker/entrypoint.d/15-storage-permissions.sh` (optionally) ensures permissions for files are corrected (see [fixing ownership on startup](#fixing-ownership-on-startup)) -* `/docker/entrypoint.d/20-horizon.sh` ensures [Laravel Horizon](https://laravel.com/docs/master/horizon) used by Pixelfed is configured -* `/docker/entrypoint.d/30-cache.sh` ensures all Pixelfed caches (router, view, config) is warmed - -### Disabling entrypoint or individual scripts - -To disable the entire entrypoint you can set the variable `ENTRYPOINT_SKIP=1`. - -To disable individual entrypoint scripts you can add the filename to the space (`" "`) separated variable `ENTRYPOINT_SKIP_SCRIPTS`. (example: `ENTRYPOINT_SKIP_SCRIPTS="10-storage.sh 30-cache.sh"`) - -## Templating - -The Docker container can do some basic templating (more like variable replacement) as part of the entrypoint scripts via [gomplate](https://docs.gomplate.ca/). - -Any file put in the `/docker/templates/` directory will be templated and written to the right directory. - -### File path examples - -1. To template `/usr/local/etc/php/php.ini` in the container put the source file in `/docker/templates/usr/local/etc/php/php.ini`. -1. To template `/a/fantastic/example.txt` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. -1. To template `/some/path/anywhere` in the container put the source file in `/docker/templates/a/fantastic/example.txt`. - -### Available variables - -Variables available for templating are sourced (in order, so *last* source takes precedence) like this: - -1. `env:` in your `docker-compose.yml` or `-e` in your `docker run` / `docker compose run` -1. Any exported variables in `.envsh` files loaded *before* `05-templating.sh` (e.g. any file with `04-`, `03-`, `02-`, `01-` or `00-` prefix) -1. All key/value pairs in `/var/www/.env.docker` -1. All key/value pairs in `/var/www/.env` - -### Template guide 101 - -Please see the [`gomplate` documentation](https://docs.gomplate.ca/) for a more comprehensive overview. - -The most frequent use-case you have is likely to print a environment variable (or a default value if it's missing), so this is how to do that: - -* `{{ getenv "VAR_NAME" }}` print an environment variable and **fail** if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) -* `{{ getenv "VAR_NAME" "default" }}` print an environment variable and print `default` if the variable is not set. ([docs](https://docs.gomplate.ca/functions/env/#envgetenv)) - -The script will *fail* if you reference a variable that does not exist (and don't have a default value) in a template. - -Please see the - -* [`gomplate` syntax documentation](https://docs.gomplate.ca/syntax/) -* [`gomplate` functions documentation](https://docs.gomplate.ca/functions/) - -## Fixing ownership on startup - -You can set the environment variable `DOCKER_APP_ENSURE_OWNERSHIP_PATHS` to a list of paths that should have their `$USER` and `$GROUP` ownership changed to the configured runtime user and group during container bootstrapping. - -The variable is a space-delimited list shown below and accepts both relative and absolute paths: - -* `DOCKER_APP_ENSURE_OWNERSHIP_PATHS="./storage ./bootstrap"` -* `DOCKER_APP_ENSURE_OWNERSHIP_PATHS="/some/other/folder"` - -## Build settings (arguments) - -The Pixelfed Dockerfile utilizes [Docker Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) and [Build arguments](https://docs.docker.com/build/guide/build-args/). - -Using *build arguments* allow us to create a flexible and more maintainable Dockerfile, supporting [multiple runtimes](runtimes.md) ([FPM](runtimes.md#fpm), [Nginx](runtimes.md#nginx), [Apache + mod_php](runtimes.md#apache)) and end-user flexibility without having to fork or copy the Dockerfile. - -*Build arguments* can be configured using `--build-arg 'name=value'` for `docker build`, `docker compose build` and `docker buildx build`. For `docker-compose.yml` the `args` key for [`build`](https://docs.docker.com/compose/compose-file/compose-file-v3/#build) can be used. - -### `PHP_VERSION` - -The `PHP` version to use when building the runtime container. - -Any valid Docker Hub PHP version is acceptable here, as long as it's [published to Docker Hub](https://hub.docker.com/_/php/tags) - -**Example values**: - -* `8` will use the latest version of PHP 8 -* `8.1` will use the latest version of PHP 8.1 -* `8.2.14` will use PHP 8.2.14 -* `latest` will use whatever is the latest PHP version - -**Default value**: `8.1` - -### `PHP_PECL_EXTENSIONS` - -PECL extensions to install via `pecl install` - -Use [PHP_PECL_EXTENSIONS_EXTRA](#php_pecl_extensions_extra) if you want to add *additional* extenstions. - -Only change this setting if you want to change the baseline extensions. - -See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `imagick redis` - -### `PHP_PECL_EXTENSIONS_EXTRA` - -Extra PECL extensions (separated by space) to install via `pecl install` - -See the [`PECL extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `""` - -### `PHP_EXTENSIONS` - -PHP Extensions to install via `docker-php-ext-install`. - -**NOTE:** use [`PHP_EXTENSIONS_EXTRA`](#php_extensions_extra) if you want to add *additional* extensions, only override this if you want to change the baseline extensions. - -See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information - -**Default value**: `intl bcmath zip pcntl exif curl gd` - -### `PHP_EXTENSIONS_EXTRA` - -Extra PHP Extensions (separated by space) to install via `docker-php-ext-install`. - -See the [`How to install more PHP extensions` documentation on Docker Hub](https://hub.docker.com/_/php) for more information. - -**Default value**: `""` - -### `PHP_EXTENSIONS_DATABASE` - -PHP database extensions to install. - -By default we install both `pgsql` and `mysql` since it's more convinient (and adds very little build time! but can be overwritten here if required. - -**Default value**: `pdo_pgsql pdo_mysql pdo_sqlite` - -### `COMPOSER_VERSION` - -The version of Composer to install. - -Please see the [Docker Hub `composer` page](https://hub.docker.com/_/composer) for valid values. - -**Default value**: `2.6` - -### `APT_PACKAGES_EXTRA` - -Extra APT packages (separated by space) that should be installed inside the image by `apt-get install` - -**Default value**: `""` - -### `NGINX_VERSION` - -Version of `nginx` to when targeting [`nginx-runtime`](runtimes.md#nginx). - -Please see the [Docker Hub `nginx` page](https://hub.docker.com/_/nginx) for available versions. - -**Default value**: `1.25.3` - -### `FOREGO_VERSION` - -Version of [`forego`](https://github.com/ddollar/forego) to install. - -**Default value**: `0.17.2` - -### `GOMPLATE_VERSION` - -Version of [`goplate`](https://github.com/hairyhenderson/gomplate) to install. - -**Default value**: `v3.11.6` - -### `DOTENV_LINTER_VERSION` - -Version of [`dotenv-linter`](https://github.com/dotenv-linter/dotenv-linter) to install. - -**Default value**: `v3.2.0` - -### `PHP_BASE_TYPE` - -The `PHP` base image layer to use when building the runtime container. - -When targeting - -* [`apache-runtime`](runtimes.md#apache) use `apache` -* [`fpm-runtime`](runtimes.md#fpm) use `fpm` -* [`nginx-runtime`](runtimes.md#nginx) use `fpm` - -**Valid values**: - -* `apache` -* `fpm` -* `cli` - -**Default value**: `apache` - -### `PHP_DEBIAN_RELEASE` - -The `Debian` Operation System version to use. - -**Valid values**: - -* `bullseye` -* `bookworm` - -**Default value**: `bullseye` diff --git a/docker/faq.md b/docker/faq.md deleted file mode 100644 index bbd47fcf5..000000000 --- a/docker/faq.md +++ /dev/null @@ -1,34 +0,0 @@ -# Pixelfed Docker FAQ - -## How do I use my own Proxy server? - -No problem! All you have to do is: - -1. Change the `DOCKER_PROXY_PROFILE` key/value pair in your `.env` file to `"disabled"`. - * This disables the `proxy` *and* `proxy-acme` services in `docker-compose.yml`. - * The setting is near the bottom of the file. -1. Point your proxy upstream to the exposed `web` port (**Default**: `8080`). - * The port is controlled by the `DOCKER_WEB_PORT_EXTERNAL_HTTP` key in `.env`. - * The setting is near the bottom of the file. -1. Run `docker compose up -d --remove-orphans` to apply the configuration - -## How do I use my own SSL certificate? - -No problem! All you have to do is: - -1. Change the `DOCKER_PROXY_ACME_PROFILE` key/value pair in your `.env` file to `"disabled"`. - * This disabled the `proxy-acme` service in `docker-compose.yml`. - * It does *not* disable the `proxy` service. -1. Put your certificates in `${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/certs` (e.g. `./docker-compose/config/proxy/certs`) - * You may need to create this folder manually if it does not exists. - * The following files are expected to exist in the directory for the proxy to detect and use them automatically (this is the same directory and file names as LetsEncrypt uses) - 1. `${APP_DOMAIN}.cert.pem` - 1. `${APP_DOMAIN}.chain.pem` - 1. `${APP_DOMAIN}.fullchain.pem` - 1. `${APP_DOMAIN}.key.pem` - * See the [`nginx-proxy` configuration file for name patterns](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl#L659-L670) -1. Run `docker compose up -d --remove-orphans` to apply the configuration - -## How do I change the container name prefix? - -Change the `DOCKER_ALL_CONTAINER_NAME_PREFIX` key/value pair in your `.env` file. diff --git a/docker/migration.md b/docker/migration.md deleted file mode 100644 index 5e820555d..000000000 --- a/docker/migration.md +++ /dev/null @@ -1,340 +0,0 @@ -# Migrating to the new Pixelfed Docker setup - -There is [*a lot* of changes](https://github.com/pixelfed/pixelfed/pull/4844) in how Pixelfed Docker/Docker Compose images work - it's a complete rewrite - with a bunch of breaking changes. - -## No more anonymous Docker Compose volumes - -The old `docker-compose.yml` configuration file [declared four anonymous volumes](https://github.com/pixelfed/pixelfed/blob/b1ff44ca2f75c088a11576fb03b5bad2fbed4d5c/docker-compose.yml#L72-L76) for storing Pixelfed related data within. - -These are no longer used, instead favoring a [Docker bind volume](https://docs.docker.com/storage/bind-mounts/) approach where content are stored directly on the server disk, outside -of a Docker volume. - -The consequence of this change is that *all* data stored in the - now unsupported - Docker volumes will no longer be accessible by Pixelfed. - -* The `db-data` volume *definitely* contain important data - it's your database after all! -* The `app-storage` volume *definitely* contain important data - it's files uploaded to - or seen by - your server! -* The `redis-data` volume *might* contain important data (depending on your configuration) -* The `app-bootstrap` volume does not contain any important data - all of it will be generated automatically in the new setup on startup. We will *not* be migrating this! - -### Migrating off anonymous Docker Compose volumes - -#### Caveats and warnings - -> [!IMPORTANT] -> This is a best-effort guide to help migrate off the old system, the operation is potentially rather complicated (and risky), so please do be careful! - -> [!CAUTION] -> ***PLEASE MAKE SURE TO BACKUP YOUR SERVER AND DATA BEFORE ATTEMPTING A MIGRATION*** -> -> **YOUR INSTANCE WILL BE *DOWN* WHILE DOING THE MIGRATION, PLEASE PLAN ACCORDINGLY, DEPENDING ON DATA SIZE IT COULD TAKE ANYWHERE FROM 5 *MINUTES* TO 5 *HOURS*** - -> [!WARNING] -> **It's important to note that this is a *copy* operation - so disk usage will (temporarily) double while you migrate** -> We provide a "migration container" for your convenience that can access both the new and old volumes, allowing you to copy the data into the setup. - -#### Step 0) Backup, rollout, and rollback plan - -1. Make sure to backup your server (ideally *after* step 1 below has completed, but *before* is better than not at all!) -1. Capture the current Git version / Pixelfed release you are on (e.g. `git --no-pager log -1` outputs the commit reference as the 2nd word in first line) -1. Backup your `.env` file (we will do this in step 3 as well) -1. Backup your `docker-compose.yml` file (`cp docker-compose.yml docker-compose.yml.old`) -1. Read through the *entire* document before starting - -#### Step 1) Migrate your ".env" file - -The new `.env` file for Docker is a bit different from the old one (many new settings!) so the easiest is to grab the new `.env.docker` file and modify it from scratch again. - -```bash -$ cp .env .env.old -$ wget -O .env.new https://raw.githubusercontent.com/pixelfed/pixelfed/dev/.env.docker -``` - -Then open your old `.env.old` configuration file, and for each of the key/value pairs within it, find and update the key in the new `.env.new` configuration file. - -Don't worry though, the file might *look* different (and significantly larger) but it behaves *exactly* the way the old file did, it just has way more options! - -> [!TIP] -> Don't worry if a key is missing in `.env.new`, you can add those key/value pairs back to the new file - ideally in the `Other configuration` section near the end of the file - but anywhere *should* be fine. - -This is a great time to review your settings and familiarize you with all the new settings. - -> [!NOTE] -> In *particular* the following sections -> -> * `PHP configuration` section (near the end of the file) where -> * The `DOCKER_APP_PHP_VERSION` settings controls your PHP version -> * The `PHP_MEMORY_LIMIT` settings controls your PHP memory limit -> * `Docker Specific configuration` section (near the end of the file) where -> * The `DOCKER_ALL_HOST_DATA_ROOT_PATH` setting dictate where the new migrated data will live. -> * The `DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS` controls if the `One time setup tasks` should run or not. We do *not* want this, since your Pixelfed instance already is set up! -> * [Frequently Asked Question / FAQ](faq.md) -> * [How do I use my own Proxy server?](faq.md#how-do-i-use-my-own-proxy-server) -> * [How do I use my own SSL certificate?](faq.md#how-do-i-use-my-own-ssl-certificate) - -#### Step 2) Stop all running containers - -> [!CAUTION] -> This will take your Pixelfed instance offline - -Stop *all* running containers (web, worker, redis, db) - -```bash -$ docker compose down -``` - -#### Step 3) Pull down the new source code - -Update your project to the latest release of Pixelfed by running - -```bash -$ git pull origin $release -``` - -> [!NOTE] -> The `$release` can be any valid git reference like `dev`, `staging` or a [tagged release](https://github.com/pixelfed/pixelfed/releases) such as `v0.12.0`. - -#### Step 4) Run the migration container - -You can access the Docker container with both old and new volumes by running the following command - -```bash -$ docker compose -f docker-compose.migrate.yml run migrate bash -``` - -This will put you in the `/migrate` directory within the container, containing 8 directories like shown here - -```plain -|-- app-storage -| |-- new -| `-- old -|-- db-data -| |-- new -| `-- old -`-- redis-data - |-- new - `-- old -``` - -#### Step 5) Check the folders - -##### Old folders - -The following commands should all return *SOME* files and data - if they do not - then there might be an issue with the anonymous volume binding. - -```bash -$ ls app-storage/old -$ ls db-data/old -$ ls redis-data/old -``` - -##### New folders - -The following commands should all return *NO* files and data - if they contain data - you need to either delete it (backup first!) or skip that migration step. - -If you haven't run `docker compose up` since you updated your project in step (2) - they should be empty and good to go. - -```bash -$ ls app-storage/new -$ ls db-data/new -$ ls redis-data/new -``` - -#### Step 6) Copy the data - -> [!WARNING] -> This is where we potentially will double your disk usage (temporarily) - -Now we will copy the data from the old volumes, to the new ones. - -The migration container has [`rsync`](https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories) installed - which is perfect for that kind of work! - -**NOTE** It's important that the "source" (first path in the `rsync` command) has a trailing `/` - otherwise the directory layout will turn out wrong! - -**NOTE** Depending on your server, these commands might take some time to finish, each command should provide a progress bar with rough time estimation. - -**NOTE** `rsync` should preserve ownership, permissions, and symlinks correctly for you as well for all the files copied. - -Lets copy the data by running the following commands: - -```bash -$ rsync -avP app-storage/old/ app-storage/new -$ rsync -avP db-data/old/ db-data/new -$ rsync -avP redis-data/old/ redis-data/new -``` - -#### Step 7) Sanity checking - -Lets make sure everything copied over successfully! - -Each *new* directory should contain *something* like (but not always exactly) the following - **NO** directory should have a single folder called `old`, if they do, the `rsync` commands above didn't work correctly - and you need to move the content of the `old` folder into the "root" of the `new` folder like shown a bit in the following sections. - -The **redis-data/new** directory might also contain a `server.pid` - -```bash -$ ls redis-data/new -appendonlydir -``` - -The **app-storage/new** directory should look *something* like this - -```bash -$ ls app-storage/new -app debugbar docker framework logs oauth-private.key oauth-public.key purify -``` - -The **db-data/new** directory should look *something* like this. There might be a lot of files, or very few files, but there *must* be a `mysql`, `performance_schema`, and `${DB_DATABASE}` (e.g. `pixelfed_prod` directory) - -```bash -$ ls db-data/new -aria_log_control ddl_recovery-backup.log ib_buffer_pool ib_logfile0 ibdata1 mariadb_upgrade_info multi-master.info mysql performance_schema pixelfed_prod sys undo001 undo002 undo003 -``` - -If everything looks good, type `exit` to leave exit the migration container - -#### Step 8) Starting up the your Pixelfed server again - -With all an updated Pixelfed (step 2), updated `.env` file (step 3), migrated data (step 4, 5, 6 and 7) we're ready to start things back up again. - -But before we start your Pixelfed server back up again, lets put the new `.env` file we made in step 1 in its right place. - -```bash -$ cp .env.new .env -``` - -##### The Database - -First thing we want to try is to start up the database by running the following command and checking the logs - -```bash -$ docker compose up -d db -$ docker compose logs --tail 250 --follow db -``` - -if there are no errors and the server isn't crashing, great! If you have an easy way of connecting to the Database via a GUI or CLI client, do that as well and verify the database and tables are all there. - -##### Redis - -Next thing we want to try is to start up the Redis server by running the following command and checking the logs - -```bash -$ docker compose up -d redis -$ docker compose logs --tail 250 --follow redis -``` - -if there are no errors and the server isn't crashing, great! - -##### Worker - -Next thing we want to try is to start up the Worker server by running the following command and checking the logs - -```bash -$ docker compose up -d worker -$ docker compose logs --tail 250 --follow worker -``` - -The container should output a *lot* of logs from the [docker-entrypoint system](customizing.md#running-commands-on-container-start), but *eventually* you should see these messages - -* `Configuration complete; ready for start up` -* `Horizon started successfully.` - -If you see one or both of those messages, great, the worker seems to be running. - -If the worker is crash looping, inspect the logs and try to resolve the issues. - -You can consider the following additional steps: - -* Enabling `DOCKER_APP_ENTRYPOINT_DEBUG` which will show even more log output to help understand whats going on -* Enabling `DOCKER_APP_ENSURE_OWNERSHIP_PATHS` against the path(s) that might have permission issues -* Fixing permission issues directly on the host since your data should all be in the `${DOCKER_ALL_HOST_DATA_ROOT_PATH}` folder (`./docker-compose-state/data` by default) - -##### Web - -The final service, `web`, which will bring your site back online! What a journey it has been. - -Lets get to it, run these commands to start the `web` service and inspect the logs. - -```bash -$ docker compose up -d web -$ docker compose logs --tail 250 --follow web -``` - -The output should be pretty much identical to that of the `worker`, so please see that section for debugging tips if the container is crash looping. - -If the `web` service came online without issues, start the rest of the (optional) services, such as the `proxy`, if enabled, by running - -```bash -$ docker compose up -d -$ docker compose logs --tail 250 --follow -``` - -If you changed anything in the `.env` file while debugging, some containers might restart now, thats perfectly fine. - -#### Step 9) Verify - -With all services online, it's time to go to your browser and check everything is working - -1. Upload and post a picture -1. Comment on a post -1. Like a post -1. Check Horizon (`https://${APP_DOMAIN}/horizon`) for any errors -1. Check the Docker compose logs via `docker compose logs --follow` - -If everything looks fine, yay, you made it to the end! Lets do some cleanup - -#### Step 10) Final steps + cleanup - -With everything working, please take a new snapshot/backup of your server *before* we do any cleanup. A post-migration snapshot is incredibly useful, since it contains both the old and new configuration + data, making any recovery much easier in a rollback scenario later. - -Now, with all the data in the new folders, you can delete the old Docker Container volumes (if you want, completely optional) - -List all volumes, and give them a look: - -```bash -$ docker volume ls -``` - -The volumes we want to delete *ends* with the volume name (`db-data`, `app-storage`, `redis-data`, and `app-bootstrap`.) but has some prefix in front of them. - -Once you have found the volumes in in the list, delete each of them by running: - -```bash -$ docker volume rm $volume_name_in_column_two_of_the_output -``` - -You can also delete the `docker-compose.yml.old` and `.env.old` file since they are no longer needed - -```bash -$ rm docker-compose.yml.old -$ rm .env.old -``` - -### Rollback - -Oh no, something went wrong? No worries, we you got backups and a quick way back! - -#### Move `docker-compose.yml` back - -```bash -$ cp docker-compose.yml docker-compose.yml.new -$ cp docker-compose.yml.old docker-compose.yml -``` - -#### Move `.env` file back - -```bash -$ cp env.old .env -``` - -#### Go back to old source code version - -```bash -$ git checkout $commit_id_from_step_0 -``` - -#### Start things back up - -```bash -docker compose up -d -``` - -#### Verify it worked diff --git a/docker/new-server.md b/docker/new-server.md deleted file mode 100644 index dbbf325f4..000000000 --- a/docker/new-server.md +++ /dev/null @@ -1,145 +0,0 @@ -# New Pixelfed + Docker + Docker Compose server - -This guide will help you install and run Pixelfed on **your** server using [Docker Compose](https://docs.docker.com/compose/). - -## Prerequisites - -Recommendations and requirements for hardware and software needed to run Pixelfed using Docker Compose. - -It's highly recommended that you have *some* experience with Linux (e.g. Ubuntu or Debian), SSH, and lightweight server administration. - -### Server - -A VPS or dedicated server you can SSH into, for example - -* [linode.com VPS](https://www.linode.com/) -* [DigitalOcean VPS](https://digitalocean.com/) -* [Hetzner](https://www.hetzner.com/) - -### Hardware - -Hardware requirements depends on the amount of users you have (or plan to have), and how active they are. - -A safe starter/small instance hardware for 25 users and blow are: - -* **CPU/vCPU** `2` cores. -* **RAM** `2-4 GB` as your instance grow, memory requirements will increase for the database. -* **Storage** `20-50 GB` HDD is fine, but ideally SSD or NVMe, *especially* for the database. -* **Network** `100 Mbit/s` or faster. - -### Domain and DNS - -* A **Domain** (or subdomain) is needed for the Pixelfed server (for example, `pixelfed.social` or `pixelfed.mydomain.com`) -* Having the required `A`/`CNAME` DNS records for your domain (above) pointing to your server. - * Typically an `A` record for the root (sometimes shown as `@`) record for `mydomain.com`. - * Possibly an `A` record for `www.` subdomain as well. - -### Network - -* Port `80` (HTTP) and `443` (HTTPS) ports forwarded to the server. - * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `80`: `ufw allow 80` - * Example for Ubuntu using [`ufw`](https://help.ubuntu.com/community/UFW) for port `443`: `ufw allow 443` - -### Optional - -* An **Email/SMTP provider** for sending e-mails to your users, such as e-mail confirmation and notifications. -* An **Object Storage** provider for storing all images remotely, rather than locally on your server. - -#### E-mail / SMTP provider - -**NOTE**: If you don't plan to use en e-mail/SMTP provider, then make sure to set `ENFORCE_EMAIL_VERIFICATION="false"` in your `.env` file! - -There are *many* providers out there, with wildly different pricing structures, features, and reliability. - -It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: - -* [Simple Email Service (SES)](https://aws.amazon.com/ses/) by Amazon Web Services (AWS) is pay-as-you-go with a cost of $0.10/1000 emails. -* [Brevo](https://www.brevo.com/) (formerly SendInBlue) has a Free Tier with 300 emails/day. -* [Postmark](https://postmarkapp.com/) has a Free Tier with 100 emails/month. -* [Forward Email](https://forwardemail.net/en/private-business-email?pricing=true) has a $3/mo/domain plan with both sending and receiving included. -* [Mailtrap](https://mailtrap.io/email-sending/) has a 1000 emails/month free-tier (their `Email Sending` product, *not* the `Email Testing` one). - -#### Object Storage - -**NOTE**: This is *entirely* optional - by default Pixelfed will store all uploads (videos, images, etc.) directly on your servers storage. - -> Object storage is a technology that stores and manages data in an unstructured format called objects. Modern organizations create and analyze large volumes of unstructured data such as photos, videos, email, web pages, sensor data, and audio files -> -> -- [*What is object storage?*](https://aws.amazon.com/what-is/object-storage/) by Amazon Web Services - -It's beyond the cope of this document to detail which provider to pick, or how to correctly configure them, but some providers that is known to be working well - with generous free tiers and affordable packages - are included for your convince (*in no particular order*) below: - -* [R2](https://www.cloudflare.com/developer-platform/r2/) by CloudFlare has cheap storage, free *egress* (e.g. people downloading images) and included (and free) Content Delivery Network (CDN). -* [B2 cloud storage](https://www.backblaze.com/cloud-storage) by Backblaze. -* [Simple Storage Service (S3)](https://aws.amazon.com/s3/) by Amazon Web Services. - -### Software - -Required software to be installed on your server - -* `git` can be installed with `apt-get install git` on Debian/Ubuntu -* `docker` can be installed by [following the official Docker documentation](https://docs.docker.com/engine/install/) - -## Getting things ready - -Connect via SSH to your server and decide where you want to install Pixelfed. - -In this guide I'm going to assume it will be installed at `/data/pixelfed`. - -1. **Install required software** as mentioned in the [Software Prerequisites section above](#software) -1. **Create the parent directory** by running `mkdir -p /data` -1. **Clone the Pixelfed repository** by running `git clone https://github.com/pixelfed/pixelfed.git /data/pixelfed` -1. **Change to the Pixelfed directory** by running `cd /data/pixelfed` - -## Modifying your settings (`.env` file) - -### Copy the example configuration file - -Pixelfed contains a default configuration file (`.env.docker`) you should use as a starter, however, before editing anything, make a copy of it and put it in the *right* place (`.env`). - -Run the following command to copy the file: `cp .env.docker .env` - -### Modifying the configuration file - -The configuration file is *quite* long, but the good news is that you can ignore *most* of it, most of the *server-specific* settings are configured for you out of the box. - -The minimum required settings you **must** change is: - -* (required) `APP_DOMAIN` which is the hostname you plan to run your Pixelfed server on (e.g. `pixelfed.social`) - must **not** include `http://` or a trailing slash (`/`)! -* (required) `DB_PASSWORD` which is the database password, you can use a service like [pwgen.io](https://pwgen.io/en/) to generate a secure one. -* (optional) `ENFORCE_EMAIL_VERIFICATION` should be set to `"false"` if you don't plan to send emails. -* (optional) `MAIL_DRIVER` and related `MAIL_*` settings if you plan to use an [email/SMTP provider](#e-mail--smtp-provider) - See [Email variables documentation](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables). -* (optional) `PF_ENABLE_CLOUD` / `FILESYSTEM_CLOUD` if you plan to use an [Object Storage provider](#object-storage). - -See the [`Configure environment variables`](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) documentation for details! - -You need to mainly focus on following sections - -* [App variables](https://docs.pixelfed.org/running-pixelfed/installation/#app-variables) -* [Email variables](https://docs.pixelfed.org/running-pixelfed/installation/#email-variables) - -You can skip the following sections, since they are already configured/automated for you: - -* `Redis` -* `Database` (except for `DB_PASSWORD`) -* `One-time setup tasks` - -### Starting the service - -With everything in place and (hopefully) well-configured, we can now go ahead and start our services by running - -```shell -docker compose up -d -``` - -This will download all the required Docker images, start the containers, and being the automatic setup. - -You can follow the logs by running `docker compose logs` - you might want to scroll to the top to logs from the start. - -You can use the CLI flag `--tail=100` to only see the most recent (`100` in this example) log lines for each container. - -You can use the CLI flag `--follow` to continue to see log output from the containers. - -You can combine `--tail=100` and `--follow` like this `docker compose logs --tail=100 --follow`. - -If you only care about specific contaieners, you can add them to the end of the command like this `docker compose logs web worker proxy`. diff --git a/docker/runtimes.md b/docker/runtimes.md deleted file mode 100644 index 68d7ee943..000000000 --- a/docker/runtimes.md +++ /dev/null @@ -1,94 +0,0 @@ -# Pixelfed Docker container runtimes - -The Pixelfed Dockerfile support multiple target *runtimes* ([Apache](#apache), [Nginx + FPM](#nginx), and [fpm](#fpm)). - -You can consider a *runtime* target as individual Dockerfiles, but instead, all of them are build from the same optimized Dockerfile, sharing +90% of their configuration and packages. - -**If you are unsure of which runtime to choose, please use the [Apache runtime](#apache) it's the most straightforward one and also the default** - -## Apache - -Building a custom Pixelfed Docker image using Apache + mod_php can be achieved the following way. - -### docker build (Apache) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target apache-runtime \ - --tag / \ - . -``` - -### docker compose (Apache) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: apache-runtime -``` - -## Nginx - -Building a custom Pixelfed Docker image using nginx + FPM can be achieved the following way. - -### docker build (nginx) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target nginx-runtime \ - --build-arg 'PHP_BASE_TYPE=fpm' \ - --tag / \ - . -``` - -### docker compose (nginx) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: nginx-runtime - args: - PHP_BASE_TYPE: fpm -``` - -## FPM - -Building a custom Pixelfed Docker image using FPM (only) can be achieved the following way. - -### docker build (fpm) - -```shell -docker build \ - -f contrib/docker/Dockerfile \ - --target fpm-runtime \ - --build-arg 'PHP_BASE_TYPE=fpm' \ - --tag / \ - . -``` - -### docker compose (fpm) - -```yaml -version: "3" - -services: - app: - build: - context: . - dockerfile: contrib/docker/Dockerfile - target: fpm-runtime - args: - PHP_BASE_TYPE: fpm -``` From a940bedf9e257c77888e37c433b523a6091647eb Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 18 Jan 2024 16:23:05 +0000 Subject: [PATCH 299/977] reference docs PR --- docker/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 23584a4ab..5230f60fd 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,3 +1,5 @@ # Pixelfed + Docker + Docker Compose -Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage +Please see the [Pixelfed Docs (Next)](https://jippi.github.io/pixelfed-docs-next/pr-preview/pr-1/running-pixelfed/) for current documentation on Docker usage. + +The docs can be [reviewed in the pixelfed/docs-next](https://github.com/pixelfed/docs-next/pull/1) repository. From 70f4bc06a824451c671f31e13e157461caae4244 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 18 Jan 2024 17:29:15 +0000 Subject: [PATCH 300/977] remove unsuded .gitmodules --- .gitmodules | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 6fe990290..000000000 --- a/.gitmodules +++ /dev/null @@ -1,10 +0,0 @@ - -[submodule "tests/shell/bats"] - path = tests/shell/bats - url = https://github.com/bats-core/bats-core.git -[submodule "tests/shell/test_helper/bats-support"] - path = tests/shell/test_helper/bats-support - url = https://github.com/bats-core/bats-support.git -[submodule "tests/shell/test_helper/bats-assert"] - path = tests/shell/test_helper/bats-assert - url = https://github.com/bats-core/bats-assert.git From 347ac6f82b78eae660d8880008b11ffc7a9abd5a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 18 Jan 2024 17:33:24 +0000 Subject: [PATCH 301/977] fix Dockerfile indent --- Dockerfile | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index a5930aec0..5158f2348 100644 --- a/Dockerfile +++ b/Dockerfile @@ -87,7 +87,12 @@ ARG BUILDOS ARG GOMPLATE_VERSION RUN set -ex \ - && curl --silent --show-error --location --output /usr/local/bin/gomplate https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${BUILDOS}-${BUILDARCH} \ + && curl \ + --silent \ + --show-error \ + --location \ + --output /usr/local/bin/gomplate \ + https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${BUILDOS}-${BUILDARCH} \ && chmod +x /usr/local/bin/gomplate \ && /usr/local/bin/gomplate --version @@ -113,8 +118,8 @@ ENV DEBIAN_FRONTEND="noninteractive" SHELL ["/bin/bash", "-c"] RUN set -ex \ - && mkdir -pv /var/www/ \ - && chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www + && mkdir -pv /var/www/ \ + && chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www WORKDIR /var/www/ @@ -124,7 +129,7 @@ ENV DOTENV_LINTER_VERSION="${DOTENV_LINTER_VERSION}" # Install and configure base layer COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ - --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/base.sh ####################################################### @@ -151,7 +156,7 @@ ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ - --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/php-extensions.sh ####################################################### @@ -187,8 +192,8 @@ COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} composer.json composer.lock /var/www/ # Install composer dependencies # NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/cache/composer \ - set -ex \ - && composer install --prefer-dist --no-autoloader --ignore-platform-reqs + set -ex \ + && composer install --prefer-dist --no-autoloader --ignore-platform-reqs # Copy all other files over COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ @@ -220,14 +225,14 @@ USER ${RUNTIME_UID}:${RUNTIME_GID} # Generate optimized autoloader now that we have all files around RUN set -ex \ - && composer dump-autoload --optimize + && composer dump-autoload --optimize USER root # for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862 RUN set -ex \ - && cp --recursive --link --preserve=all storage storage.skel \ - && rm -rf html && ln -s public html + && cp --recursive --link --preserve=all storage storage.skel \ + && rm -rf html && ln -s public html COPY docker/shared/root / @@ -242,8 +247,8 @@ FROM shared-runtime AS apache-runtime COPY docker/apache/root / RUN set -ex \ - && a2enmod rewrite remoteip proxy proxy_http \ - && a2enconf remoteip + && a2enmod rewrite remoteip proxy proxy_http \ + && a2enconf remoteip CMD ["apache2-foreground"] @@ -272,14 +277,13 @@ ARG TARGETPLATFORM # Install nginx dependencies RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \ - --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ - set -ex \ - && gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \ - && gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \ - && echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - nginx=${NGINX_VERSION}* + --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + set -ex \ + && gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \ + && gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \ + && echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}* # copy docker entrypoints from the *real* nginx image directly COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ From d76f01685ce34bc7119c1220c1a4a6f9146cee3c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 02:03:39 -0700 Subject: [PATCH 302/977] Update login view, add email prefill logic --- resources/views/auth/login.blade.php | 59 ++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 3403cd6b3..af6c506e1 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -4,8 +4,12 @@
-
-
{{ __('Login') }}
+
+
+

+ Account Login +

+
@@ -14,6 +18,7 @@
+ @if ($errors->has('email')) @@ -27,6 +32,7 @@
+ @if ($errors->has('password')) @@ -34,6 +40,12 @@ {{ $errors->first('password') }} @endif + +

+ + {{ __('Forgot Password') }} + +

@@ -64,14 +76,9 @@
@endif -
-
- - -
-
+ @if( @@ -91,20 +98,38 @@ @endif + @if(config_cache('pixelfed.open_registration'))
-

- @if(config_cache('pixelfed.open_registration')) +

Register - · - @endif - - {{ __('Forgot Password') }} -

+ @endif
@endsection + +@push('scripts') + +@endpush From 74423b52ca79611cb99c6dc5fc2e58a8bdd2d4db Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 02:05:01 -0700 Subject: [PATCH 303/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c1a9720..dcedd4896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ - Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc)) - Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1)) - Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) +- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 0325e171155c1e91ce946145a88bb3087ab61e98 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 03:06:14 -0700 Subject: [PATCH 304/977] Update LoginController, fix captcha validation error message --- app/Http/Controllers/Auth/LoginController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 3861d3272..627a879cc 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -71,6 +71,7 @@ class LoginController extends Controller $this->username() => 'required|email', 'password' => 'required|string|min:6', ]; + $messages = []; if( config('captcha.enabled') || @@ -82,9 +83,9 @@ class LoginController extends Controller ) ) { $rules['h-captcha-response'] = 'required|filled|captcha|min:5'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; } - - $this->validate($request, $rules); + $request->validate($rules, $messages); } /** From 67c650b1950151678974798912177d40c1794453 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:26:01 -0700 Subject: [PATCH 305/977] Add forgot email feature --- .../Controllers/UserEmailForgotController.php | 132 ++++++++++++++++++ app/Mail/UserEmailForgotReminder.php | 55 ++++++++ app/Models/UserEmailForgot.php | 17 +++ config/security.php | 13 ++ resources/views/auth/email/forgot.blade.php | 127 +++++++++++++++++ resources/views/auth/login.blade.php | 17 ++- .../views/auth/passwords/email.blade.php | 51 +++---- .../views/auth/passwords/reset.blade.php | 22 ++- .../emails/forgot-email/message.blade.php | 33 +++++ routes/web.php | 3 + 10 files changed, 433 insertions(+), 37 deletions(-) create mode 100644 app/Http/Controllers/UserEmailForgotController.php create mode 100644 app/Mail/UserEmailForgotReminder.php create mode 100644 app/Models/UserEmailForgot.php create mode 100644 resources/views/auth/email/forgot.blade.php create mode 100644 resources/views/emails/forgot-email/message.blade.php diff --git a/app/Http/Controllers/UserEmailForgotController.php b/app/Http/Controllers/UserEmailForgotController.php new file mode 100644 index 000000000..c97add2a7 --- /dev/null +++ b/app/Http/Controllers/UserEmailForgotController.php @@ -0,0 +1,132 @@ +middleware('guest'); + abort_unless(config('security.forgot-email.enabled'), 404); + } + + public function index(Request $request) + { + abort_if($request->user(), 404); + return view('auth.email.forgot'); + } + + public function store(Request $request) + { + $rules = [ + 'username' => 'required|min:2|max:15|exists:users' + ]; + + $messages = [ + 'username.exists' => 'This username is no longer active or does not exist!' + ]; + + if(config('captcha.enabled') || config('captcha.active.register')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'You need to complete the captcha!'; + } + + $randomDelay = random_int(500000, 2000000); + usleep($randomDelay); + + $this->validate($request, $rules, $messages); + $check = self::checkLimits(); + + if(!$check) { + return redirect()->back()->withErrors([ + 'username' => 'Please try again later, we\'ve reached our quota and cannot process any more requests at this time.' + ]); + } + + $user = User::whereUsername($request->input('username')) + ->whereNotNull('email_verified_at') + ->whereNull('status') + ->whereIsAdmin(false) + ->first(); + + if(!$user) { + return redirect()->back()->withErrors([ + 'username' => 'Invalid username or account. It may not exist, or does not have a verified email, is an admin account or is disabled.' + ]); + } + + $exists = UserEmailForgot::whereUserId($user->id) + ->where('email_sent_at', '>', now()->subHours(24)) + ->count(); + + if($exists) { + return redirect()->back()->withErrors([ + 'username' => 'An email reminder was recently sent to this account, please try again after 24 hours!' + ]); + } + + return $this->storeHandle($request, $user); + } + + protected function storeHandle($request, $user) + { + UserEmailForgot::create([ + 'user_id' => $user->id, + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'referrer' => $request->headers->get('referer'), + 'email_sent_at' => now() + ]); + + Mail::to($user->email)->send(new UserEmailForgotReminder($user)); + self::getLimits(true); + return redirect()->back()->with(['status' => 'Successfully sent an email reminder!']); + } + + public static function checkLimits() + { + $limits = self::getLimits(); + + if( + $limits['current']['hourly'] >= $limits['max']['hourly'] || + $limits['current']['daily'] >= $limits['max']['daily'] || + $limits['current']['weekly'] >= $limits['max']['weekly'] || + $limits['current']['monthly'] >= $limits['max']['monthly'] + ) { + return false; + } + + return true; + } + + public static function getLimits($forget = false) + { + return [ + 'max' => config('security.forgot-email.limits.max'), + 'current' => [ + 'hourly' => self::activeCount(60, $forget), + 'daily' => self::activeCount(1440, $forget), + 'weekly' => self::activeCount(10080, $forget), + 'monthly' => self::activeCount(43800, $forget) + ] + ]; + } + + public static function activeCount($mins, $forget = false) + { + if($forget) { + Cache::forget('pf:auth:forgot-email:active-count:dur-' . $mins); + } + return Cache::remember('pf:auth:forgot-email:active-count:dur-' . $mins, 14200, function() use($mins) { + return UserEmailForgot::where('email_sent_at', '>', now()->subMinutes($mins))->count(); + }); + } +} diff --git a/app/Mail/UserEmailForgotReminder.php b/app/Mail/UserEmailForgotReminder.php new file mode 100644 index 000000000..f7e5dbc10 --- /dev/null +++ b/app/Mail/UserEmailForgotReminder.php @@ -0,0 +1,55 @@ +user = $user; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[' . config('pixelfed.domain.app') . '] Pixelfed Account Email Reminder', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.forgot-email.message', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/UserEmailForgot.php b/app/Models/UserEmailForgot.php new file mode 100644 index 000000000..9e549aff4 --- /dev/null +++ b/app/Models/UserEmailForgot.php @@ -0,0 +1,17 @@ + 'datetime', + ]; +} diff --git a/config/security.php b/config/security.php index a8f92360d..929c05214 100644 --- a/config/security.php +++ b/config/security.php @@ -5,5 +5,18 @@ return [ 'verify_dns' => env('PF_SECURITY_URL_VERIFY_DNS', false), 'trusted_domains' => env('PF_SECURITY_URL_TRUSTED_DOMAINS', 'pixelfed.social,pixelfed.art,mastodon.social'), + ], + + 'forgot-email' => [ + 'enabled' => env('PF_AUTH_ALLOW_EMAIL_FORGOT', true), + + 'limits' => [ + 'max' => [ + 'hourly' => env('PF_AUTH_FORGOT_EMAIL_MAX_HOURLY', 50), + 'daily' => env('PF_AUTH_FORGOT_EMAIL_MAX_DAILY', 100), + 'weekly' => env('PF_AUTH_FORGOT_EMAIL_MAX_WEEKLY', 200), + 'monthly' => env('PF_AUTH_FORGOT_EMAIL_MAX_MONTHLY', 500), + ] + ] ] ]; diff --git a/resources/views/auth/email/forgot.blade.php b/resources/views/auth/email/forgot.blade.php new file mode 100644 index 000000000..73d01811c --- /dev/null +++ b/resources/views/auth/email/forgot.blade.php @@ -0,0 +1,127 @@ +@extends('layouts.blank') + +@push('styles') + + +@endpush + +@section('content') +
+
+
+
+
+ + + +

Forgot Email

+

Recover your account by sending an email to an associated username

+
+ + @if(session('status')) +
+
+ + + {{ session('status') }} +
+
+ @endif + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+
+ + {{ $error }} +
+
+ @endforeach + @endif + +
+
{{ __('Recover Email') }}
+ +
+ +
+ @csrf + +
+
+ + + @if ($errors->has('username') ) + + {{ $errors->first('username') }} + + @endif +
+
+ + @if(config('captcha.enabled')) + +
+ {!! Captcha::display(['data-theme' => 'dark']) !!} +
+ @if ($errors->has('h-captcha-response')) +
+ {{ $errors->first('h-captcha-response') }} +
+ @endif + @endif + +
+
+ +
+
+
+
+
+ + +
+
+
+
+@endsection + +@push('scripts') + +@endpush + +@push('styles') + +@endpush diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index af6c506e1..dadd08a4f 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -11,11 +11,18 @@
+ @if ($errors->any()) + @foreach ($errors->all() as $error) +
+ {{ $error }} +
+ @endforeach + @endif
@csrf -
+
@@ -26,10 +33,16 @@ {{ $errors->first('email') }} @endif + +
-
+
diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php index d144e142a..4f2825e29 100644 --- a/resources/views/auth/passwords/email.blade.php +++ b/resources/views/auth/passwords/email.blade.php @@ -9,7 +9,7 @@
-
+
@if(session('status') || $errors->has('email')) -
-
- + @@ -86,29 +92,6 @@ @push('scripts') @endpush + +@push('styles') + +@endpush diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index efe59ac95..1a740fa7d 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -9,7 +9,7 @@
-
+
@@ -41,14 +41,14 @@ + style="opacity:.5"> @if ($errors->has('email')) @@ -67,7 +67,7 @@ @endpush + +@push('styles') + +@endpush diff --git a/resources/views/emails/forgot-email/message.blade.php b/resources/views/emails/forgot-email/message.blade.php new file mode 100644 index 000000000..af94df3df --- /dev/null +++ b/resources/views/emails/forgot-email/message.blade.php @@ -0,0 +1,33 @@ +@component('mail::message') +Hello, + +You recently requested to know the email address associated with your username [**{{'@' . $user->username}}**]({{$user->url()}}) on [**{{config('pixelfed.domain.app')}}**]({{config('app.url')}}). + +We're here to assist! Simply tap on the Login button below. + + +Login to my {{'@' . $user->username}} account + + +---- +
+ +The email address linked to your username is: + +

+{{$user->email}} +

+
+ +You can use this email address to log in to your account. + +If needed, you can [reset your password]({{ route('password.request')}}). For security reasons, we recommend keeping your account information, including your email address, updated and secure. If you did not make this request or if you have any other questions or concerns, please feel free to [contact our support team]({{route('site.contact')}}). + +Thank you for being a part of our community! + +Best regards,
+
{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/routes/web.php b/routes/web.php index e71e6fd9e..6c765ba56 100644 --- a/routes/web.php +++ b/routes/web.php @@ -203,6 +203,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); + Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot'); + Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail'); + Route::get('discover', 'DiscoverController@home')->name('discover'); Route::group(['prefix' => 'api'], function () { From 5afe7abdfbdfbe0b8da78004fe3eadca0eb4ea41 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:26:57 -0700 Subject: [PATCH 306/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcedd4896..d251b3df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46)) - Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) - Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59)) +- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) @@ -87,6 +88,7 @@ - Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1)) - Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) - Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) +- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From c26a3d281742ecceee53b3364491704d20dec123 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:35:30 -0700 Subject: [PATCH 307/977] Add migration --- ...090048_create_user_email_forgots_table.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2024_01_22_090048_create_user_email_forgots_table.php diff --git a/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php b/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php new file mode 100644 index 000000000..845b63934 --- /dev/null +++ b/database/migrations/2024_01_22_090048_create_user_email_forgots_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedInteger('user_id')->index(); + $table->string('ip_address')->nullable(); + $table->string('user_agent')->nullable(); + $table->string('referrer')->nullable(); + $table->timestamp('email_sent_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_email_forgots'); + } +}; From 96366ab3de86064460bb6114e4dfd9ced97b69fd Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:51:50 -0700 Subject: [PATCH 308/977] Update forgot email captcha config --- app/Http/Controllers/UserEmailForgotController.php | 2 +- resources/views/auth/email/forgot.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/UserEmailForgotController.php b/app/Http/Controllers/UserEmailForgotController.php index c97add2a7..4baa4ead4 100644 --- a/app/Http/Controllers/UserEmailForgotController.php +++ b/app/Http/Controllers/UserEmailForgotController.php @@ -34,7 +34,7 @@ class UserEmailForgotController extends Controller 'username.exists' => 'This username is no longer active or does not exist!' ]; - if(config('captcha.enabled') || config('captcha.active.register')) { + if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) { $rules['h-captcha-response'] = 'required|captcha'; $messages['h-captcha-response.required'] = 'You need to complete the captcha!'; } diff --git a/resources/views/auth/email/forgot.blade.php b/resources/views/auth/email/forgot.blade.php index 73d01811c..898d19fb5 100644 --- a/resources/views/auth/email/forgot.blade.php +++ b/resources/views/auth/email/forgot.blade.php @@ -65,7 +65,7 @@
- @if(config('captcha.enabled')) + @if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register'))
{!! Captcha::display(['data-theme' => 'dark']) !!} From efe8e890463b038c481a31a3f71e12455e86928f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:57:40 -0700 Subject: [PATCH 309/977] Update forgot mail template, urlencode email --- resources/views/emails/forgot-email/message.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/emails/forgot-email/message.blade.php b/resources/views/emails/forgot-email/message.blade.php index af94df3df..dcae7cfbc 100644 --- a/resources/views/emails/forgot-email/message.blade.php +++ b/resources/views/emails/forgot-email/message.blade.php @@ -5,7 +5,7 @@ You recently requested to know the email address associated with your username [ We're here to assist! Simply tap on the Login button below. - + Login to my {{'@' . $user->username}} account From 6167ebc65482d2a4b598848adfae7e5b802f79eb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 22 Jan 2024 05:59:37 -0700 Subject: [PATCH 310/977] Update UserEmailForgotController --- app/Http/Controllers/UserEmailForgotController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/UserEmailForgotController.php b/app/Http/Controllers/UserEmailForgotController.php index 4baa4ead4..33378c4d0 100644 --- a/app/Http/Controllers/UserEmailForgotController.php +++ b/app/Http/Controllers/UserEmailForgotController.php @@ -82,7 +82,6 @@ class UserEmailForgotController extends Controller 'user_id' => $user->id, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), - 'referrer' => $request->headers->get('referer'), 'email_sent_at' => now() ]); From 86724535960603c33ecd4cf1a847d70a7dcc2a89 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 22 Jan 2024 13:48:01 +0000 Subject: [PATCH 311/977] fix configuration loading before referencing config --- docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh index d3d83c532..fb5c86a39 100755 --- a/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh +++ b/docker/shared/root/docker/entrypoint.d/11-first-time-setup.sh @@ -6,6 +6,8 @@ source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" +load-config-files + # Allow automatic applying of outstanding/new migrations on startup : "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS:=1}" @@ -16,7 +18,6 @@ if is-false "${DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS}"; then exit 0 fi -load-config-files await-database-ready # Following https://docs.pixelfed.org/running-pixelfed/installation/#one-time-setup-tasks From c9a3e3aea7c1444017c59eedf36710493a614494 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Tue, 23 Jan 2024 19:34:48 +0000 Subject: [PATCH 312/977] automatically + by default turn off proxy acme if proxy is off --- .env.docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.docker b/.env.docker index 25914af89..c3e0a55ae 100644 --- a/.env.docker +++ b/.env.docker @@ -1105,7 +1105,7 @@ DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" #DOCKER_PROXY_PROFILE="" # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service -#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" +DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" # How often Docker health check should run for [proxy] service DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" From 934f2ffdb40c3884fefe569f3dd46bfed3c89ddb Mon Sep 17 00:00:00 2001 From: Shlee Date: Wed, 24 Jan 2024 12:45:21 +1030 Subject: [PATCH 313/977] Update home.blade.php --- resources/views/admin/diagnostics/home.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/admin/diagnostics/home.blade.php b/resources/views/admin/diagnostics/home.blade.php index bf2b5d742..db44a2332 100644 --- a/resources/views/admin/diagnostics/home.blade.php +++ b/resources/views/admin/diagnostics/home.blade.php @@ -654,7 +654,7 @@ MEDIA MEDIA_EXIF_DATABASE - {{config_cache('media.exif.batabase') ? '✅ true' : '❌ false' }} + {{config_cache('media.exif.database') ? '✅ true' : '❌ false' }} From f263dfc4e10f192cb2646f14e39d9d3b1a06d18e Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 14:41:44 +0000 Subject: [PATCH 314/977] apply editorconfig + shellcheck + shellfmt to all files --- .editorconfig | 6 +- .gitignore | 1 - .../docker/entrypoint.d/01-permissions.sh | 2 +- .../root/docker/entrypoint.d/05-templating.sh | 2 +- docker/shared/root/docker/entrypoint.sh | 56 +++--- docker/shared/root/docker/helpers.sh | 190 +++++++++++------- docker/shared/root/docker/install/base.sh | 6 +- 7 files changed, 149 insertions(+), 114 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8b31962a6..9551fdc60 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,20 +8,20 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[{*.yml,*.yaml}] +[*.{yml,yaml}] indent_style = space indent_size = 2 -[*.sh] +[*.{sh,envsh,env,env*}] indent_style = space indent_size = 4 +# ShellCheck config shell_variant = bash binary_next_line = true case-indent = true switch_case_indent = true space_redirects = true -keep_padding = true function_next_line = true simplify = true space-redirects = true diff --git a/.gitignore b/.gitignore index a5cdf3af1..689c2e13a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ /.composer/ /.idea /.vagrant -/.vscode /docker-compose-state/ /node_modules /public/hot diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 11766a742..f5624721b 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -16,7 +16,7 @@ run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" : "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" +IFS=' ' read -ar ensure_ownership_paths <<< "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]." diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh index 4d229b11c..23d01487a 100755 --- a/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -51,7 +51,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat # Render the template log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" - gomplate <"${template_file}" >"${output_file_path}" + gomplate < "${template_file}" > "${output_file_path}" # Show the diff from the envsubst command if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index ed698250b..57378d23f 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -25,7 +25,7 @@ entrypoint-set-script-name "entrypoint.sh" # Convert ENTRYPOINT_SKIP_SCRIPTS into a native bash array for easier lookup declare -a skip_scripts # shellcheck disable=SC2034 -IFS=' ' read -ar skip_scripts <<<"$ENTRYPOINT_SKIP_SCRIPTS" +IFS=' ' read -ar skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" # Ensure the entrypoint root folder exists mkdir -p "${ENTRYPOINT_D_ROOT}" @@ -52,40 +52,40 @@ find "${ENTRYPOINT_D_ROOT}" -follow -type f -print | sort -V | while read -r fil # Inspect the file extension of the file we're processing case "${file}" in - *.envsh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi + *.envsh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi - log-info "${section_message_color}============================================================${color_clear}" - log-info "${section_message_color}Sourcing [${file}]${color_clear}" - log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Sourcing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" - # shellcheck disable=SC1090 - source "${file}" + # shellcheck disable=SC1090 + source "${file}" - # the sourced file will (should) than the log prefix, so this restores our own - # "global" log prefix once the file is done being sourced - entrypoint-restore-script-name - ;; + # the sourced file will (should) than the log prefix, so this restores our own + # "global" log prefix once the file is done being sourced + entrypoint-restore-script-name + ;; - *.sh) - if ! is-executable "${file}"; then - # warn on shell scripts without exec bit - log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" - fi + *.sh) + if ! is-executable "${file}"; then + # warn on shell scripts without exec bit + log-error-and-exit "File [${file}] is not executable (please 'chmod +x' it)" + fi - log-info "${section_message_color}============================================================${color_clear}" - log-info "${section_message_color}Executing [${file}]${color_clear}" - log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" + log-info "${section_message_color}Executing [${file}]${color_clear}" + log-info "${section_message_color}============================================================${color_clear}" - "${file}" - ;; + "${file}" + ;; - *) - log-warning "Ignoring unrecognized file [${file}]" - ;; + *) + log-warning "Ignoring unrecognized file [${file}]" + ;; esac done diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index fb8c11c97..5826f3c9a 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -43,7 +43,8 @@ cd /var/www || log-error-and-exit "could not change to /var/www" # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] # @arg $1 string The name (or path) of the entrypoint script being run -function entrypoint-set-script-name() { +function entrypoint-set-script-name() +{ script_name_previous="${script_name}" script_name="${1}" @@ -51,7 +52,8 @@ function entrypoint-set-script-name() { } # @description Restore the log prefix to the previous value that was captured in [entrypoint-set-script-name ] -function entrypoint-restore-script-name() { +function entrypoint-restore-script-name() +{ entrypoint-set-script-name "${script_name_previous}" } @@ -59,7 +61,8 @@ function entrypoint-restore-script-name() { # @arg $@ string The command to run # @exitcode 0 if the command succeeeds # @exitcode 1 if the command fails -function run-as-runtime-user() { +function run-as-runtime-user() +{ run-command-as "${runtime_username}" "${@}" } @@ -67,7 +70,8 @@ function run-as-runtime-user() { # @arg $@ string The command to run # @exitcode 0 if the command succeeeds # @exitcode 1 if the command fails -function run-as-current-user() { +function run-as-current-user() +{ run-command-as "$(id -un)" "${@}" } @@ -76,7 +80,8 @@ function run-as-current-user() { # @arg $@ string The command to run # @exitcode 0 If the command succeeeds # @exitcode 1 If the command fails -function run-command-as() { +function run-command-as() +{ local -i exit_code local target_user @@ -105,7 +110,8 @@ function run-command-as() { # @description Streams stdout from the command and echo it # with log prefixing. # @see stream-prefix-command-output -function stream-stdout-handler() { +function stream-stdout-handler() +{ while read -r line; do log-info "(stdout) ${line}" done @@ -114,7 +120,8 @@ function stream-stdout-handler() { # @description Streams stderr from the command and echo it # with a bit of color and log prefixing. # @see stream-prefix-command-output -function stream-stderr-handler() { +function stream-stderr-handler() +{ while read -r line; do log-info-stderr "(${error_message_color}stderr${color_clear}) ${line}" done @@ -124,7 +131,8 @@ function stream-stderr-handler() { # and stdout/stderr prefix. If stdout or stderr is being piped/redirected # it will automatically fall back to non-prefixed output. # @arg $@ string The command to run -function stream-prefix-command-output() { +function stream-prefix-command-output() +{ local stdout=stream-stdout-handler local stderr=stream-stderr-handler @@ -146,7 +154,8 @@ function stream-prefix-command-output() { # @description Print the given error message to stderr # @arg $message string A error message. # @stderr The error message provided with log prefix -function log-error() { +function log-error() +{ local msg if [[ $# -gt 0 ]]; then @@ -157,14 +166,15 @@ function log-error() { log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" >/dev/stderr + echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" > /dev/stderr } # @description Print the given error message to stderr and exit 1 # @arg $@ string A error message. # @stderr The error message provided with log prefix # @exitcode 1 -function log-error-and-exit() { +function log-error-and-exit() +{ log-error "$@" show-call-stack @@ -175,7 +185,8 @@ function log-error-and-exit() { # @description Print the given warning message to stderr # @arg $@ string A warning message. # @stderr The warning message provided with log prefix -function log-warning() { +function log-warning() +{ local msg if [[ $# -gt 0 ]]; then @@ -186,13 +197,14 @@ function log-warning() { log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" >/dev/stderr + echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" > /dev/stderr } # @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set # @arg $@ string A info message. # @stdout The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS -function log-info() { +function log-info() +{ local msg if [[ $# -gt 0 ]]; then @@ -211,7 +223,8 @@ function log-info() { # @description Print the given message to stderr unless [ENTRYPOINT_QUIET_LOGS] is set # @arg $@ string A info message. # @stderr The info message provided with log prefix unless $ENTRYPOINT_QUIET_LOGS -function log-info-stderr() { +function log-info-stderr() +{ local msg if [[ $# -gt 0 ]]; then @@ -223,13 +236,14 @@ function log-info-stderr() { fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" >/dev/stderr + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" > /dev/stderr fi } # @description Loads the dot-env files used by Docker and track the keys present in the configuration. # @sets seen_dot_env_variables array List of config keys discovered during loading -function load-config-files() { +function load-config-files() +{ # Associative array (aka map/dictionary) holding the unique keys found in dot-env files local -A _tmp_dot_env_keys @@ -260,7 +274,8 @@ function load-config-files() { # @arg $2 array The haystack (array) to search in # @exitcode 0 If $needle was found in $haystack # @exitcode 1 If $needle was *NOT* found in $haystack -function in-array() { +function in-array() +{ local -r needle="\<${1}\>" local -nr haystack=$2 @@ -271,7 +286,8 @@ function in-array() { # @arg $1 string The path to check # @exitcode 0 If $1 has executable bit # @exitcode 1 If $1 does *NOT* have executable bit -function is-executable() { +function is-executable() +{ [[ -x "$1" ]] } @@ -279,7 +295,8 @@ function is-executable() { # @arg $1 string The path to check # @exitcode 0 If $1 is writable # @exitcode 1 If $1 is *NOT* writable -function is-writable() { +function is-writable() +{ [[ -w "$1" ]] } @@ -287,7 +304,8 @@ function is-writable() { # @arg $1 string The path to check # @exitcode 0 If $1 exists # @exitcode 1 If $1 does *NOT* exists -function path-exists() { +function path-exists() +{ [[ -e "$1" ]] } @@ -295,29 +313,33 @@ function path-exists() { # @arg $1 string The path to check # @exitcode 0 If $1 exists # @exitcode 1 If $1 does *NOT* exists -function file-exists() { +function file-exists() +{ [[ -f "$1" ]] } # @description Checks if $1 contains any files or not # @arg $1 string The path to check # @exitcode 0 If $1 contains files # @exitcode 1 If $1 does *NOT* contain files -function is-directory-empty() { - ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read -r +function is-directory-empty() +{ + ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2> /dev/null | read -r } # @description Ensures a directory exists (via mkdir) # @arg $1 string The path to create # @exitcode 0 If $1 If the path exists *or* was created # @exitcode 1 If $1 If the path does *NOT* exists and could *NOT* be created -function ensure-directory-exists() { +function ensure-directory-exists() +{ stream-prefix-command-output mkdir -pv "$@" } # @description Find the relative path for a entrypoint script by removing the ENTRYPOINT_D_ROOT prefix # @arg $1 string The path to manipulate # @stdout The relative path to the entrypoint script -function get-entrypoint-script-name() { +function get-entrypoint-script-name() +{ echo "${1#"$ENTRYPOINT_D_ROOT"}" } @@ -325,7 +347,8 @@ function get-entrypoint-script-name() { # The 'lock' is only written if the passed in command ($2) successfully ran. # @arg $1 string The name of the lock file # @arg $@ string The command to run -function only-once() { +function only-once() +{ local name="${1:-$script_name}" local file="${docker_once_path}/${name}" shift @@ -349,7 +372,8 @@ function only-once() { # @description Best effort file lock to ensure *something* is not running in multiple containers. # The script uses "trap" to clean up after itself if the script crashes # @arg $1 string The lock identifier -function acquire-lock() { +function acquire-lock() +{ local name="${1:-$script_name}" local file="${docker_locks_path}/${name}" @@ -371,7 +395,8 @@ function acquire-lock() { # @description Release a lock aquired by [acquire-lock] # @arg $1 string The lock identifier -function release-lock() { +function release-lock() +{ local name="${1:-$script_name}" local file="${docker_locks_path}/${name}" @@ -384,7 +409,8 @@ function release-lock() { # the bash [trap] logic # @arg $1 string The command to run # @arg $@ string The list of trap signals to register -function on-trap() { +function on-trap() +{ local trap_add_cmd=$1 shift || log-error-and-exit "${FUNCNAME[0]} usage error" @@ -394,13 +420,16 @@ function on-trap() { # of trap -p # # shellcheck disable=SC2317 - extract_trap_cmd() { printf '%s\n' "${3:-}"; } + extract_trap_cmd() + { + printf '%s\n' "${3:-}" + } # print existing trap command with newline eval "extract_trap_cmd $(trap -p "${trap_add_name}")" # print the new trap command printf '%s\n' "${trap_add_cmd}" - )" "${trap_add_name}" || - log-error-and-exit "unable to add to trap ${trap_add_name}" + )" "${trap_add_name}" \ + || log-error-and-exit "unable to add to trap ${trap_add_name}" done } @@ -411,42 +440,43 @@ function on-trap() { declare -f -t on-trap # @description Waits for the database to be healthy and responsive -function await-database-ready() { +function await-database-ready() +{ log-info "❓ Waiting for database to be ready" load-config-files case "${DB_CONNECTION:-}" in - mysql) - # shellcheck disable=SC2154 - while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do - staggered-sleep - done - ;; + mysql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent > /dev/null; do + staggered-sleep + done + ;; - pgsql) - # shellcheck disable=SC2154 - while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" >/dev/null; do - staggered-sleep - done - ;; + pgsql) + # shellcheck disable=SC2154 + while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" > /dev/null; do + staggered-sleep + done + ;; - sqlsrv) - log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" + sqlsrv) + log-warning "Don't know how to check if SQLServer is *truely* ready or not - so will just check if we're able to connect to it" - # shellcheck disable=SC2154 - while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do - staggered-sleep - done - ;; + # shellcheck disable=SC2154 + while ! timeout 1 bash -c "cat < /dev/null > /dev/tcp/${DB_HOST}/${DB_PORT}"; do + staggered-sleep + done + ;; - sqlite) - log-info "${success_message_color}sqlite is always ready${color_clear}" - ;; + sqlite) + log-info "${success_message_color}sqlite is always ready${color_clear}" + ;; - *) - log-error-and-exit "Unknown database type: [${DB_CONNECTION:-}]" - ;; + *) + log-error-and-exit "Unknown database type: [${DB_CONNECTION:-}]" + ;; esac log-info "${success_message_color}✅ Successfully connected to database${color_clear}" @@ -454,14 +484,16 @@ function await-database-ready() { # @description sleeps between 1 and 3 seconds to ensure a bit of randomness # in multiple scripts/containers doing work almost at the same time. -function staggered-sleep() { +function staggered-sleep() +{ sleep "$(get-random-number-between 1 3)" } # @description Helper function to get a random number between $1 and $2 # @arg $1 int Minimum number in the range (inclusive) # @arg $2 int Maximum number in the range (inclusive) -function get-random-number-between() { +function get-random-number-between() +{ local -i from=${1:-1} local -i to="${2:-10}" @@ -470,7 +502,8 @@ function get-random-number-between() { # @description Helper function to show the bask call stack when something # goes wrong. Is super useful when needing to debug an issue -function show-call-stack() { +function show-call-stack() +{ local stack_size=${#FUNCNAME[@]} local func local lineno @@ -493,7 +526,8 @@ function show-call-stack() { # returns [0] if input is truthy, otherwise [1] # @arg $1 string The string to evaluate # @see as-boolean -function is-true() { +function is-true() +{ as-boolean "${1:-}" && return 0 return 1 @@ -503,7 +537,8 @@ function is-true() { # returns [0] if input is falsey, otherwise [1] # @arg $1 string The string to evaluate # @see as-boolean -function is-false() { +function is-false() +{ as-boolean "${1:-}" && return 1 return 0 @@ -516,28 +551,29 @@ function is-false() { # This is a bit confusing, *especially* in a PHP world where [1] would be truthy and # [0] would be falsely as return values # @arg $1 string The string to evaluate -function as-boolean() { +function as-boolean() +{ local input="${1:-}" local var="${input,,}" # convert input to lower-case case "$var" in - 1 | true) - log-info "[as-boolean] variable [${var}] was detected as truthy/true, returning [0]" + 1 | true) + log-info "[as-boolean] variable [${var}] was detected as truthy/true, returning [0]" - return 0 - ;; + return 0 + ;; - 0 | false) - log-info "[as-boolean] variable [${var}] was detected as falsey/false, returning [1]" + 0 | false) + log-info "[as-boolean] variable [${var}] was detected as falsey/false, returning [1]" - return 1 - ;; + return 1 + ;; - *) - log-warning "[as-boolean] variable [${var}] could not be detected as true or false, returning [1] (false) as default" + *) + log-warning "[as-boolean] variable [${var}] could not be detected as true or false, returning [1] (false) as default" - return 1 - ;; + return 1 + ;; esac } diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index 7fa43b0f9..e20788b56 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -3,13 +3,13 @@ set -ex -o errexit -o nounset -o pipefail # Ensure we keep apt cache around in a Docker environment rm -f /etc/apt/apt.conf.d/docker-clean -echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache +echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache # Don't install recommended packages by default -echo 'APT::Install-Recommends "false";' >>/etc/apt/apt.conf +echo 'APT::Install-Recommends "false";' >> /etc/apt/apt.conf # Don't install suggested packages by default -echo 'APT::Install-Suggests "false";' >>/etc/apt/apt.conf +echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf declare -a packages=() From 627fffd1ce5b6160772608dca2b95e0b0d4b3c25 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 14:42:24 +0000 Subject: [PATCH 315/977] add .vscode with recommended plugins + settings which will give a *great* out of the box experience for folks wanting to contribute and uses VS Code --- .vscode/extensions.json | 13 +++++++++++++ .vscode/settings.json | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..ea1c99893 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "foxundermoon.shell-format", + "timonwong.shellcheck", + "jetmartin.bats", + "aaron-bond.better-comments", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "bmewburn.vscode-intelephense-client", + "redhat.vscode-yaml" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9a1ddb073 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "shellformat.useEditorConfig": true, + "files.associations": { + ".env": "shellscript", + ".env.*": "shellscript" + } +} From 6fee842b7a0e16fed70a28e3904c44191d22f1da Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 18:24:15 +0000 Subject: [PATCH 316/977] also build and push staging images --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3296ba9d6..ca6e73630 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,6 +9,7 @@ on: push: branches: - dev + - staging - jippi-fork # TODO(jippi): remove me before merge tags: - "*" From c859367e1058c0b740d552ab1d4c7a5fd1a52310 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:14:40 +0000 Subject: [PATCH 317/977] fix 02-check-config.sh logic and bad .env.docker syntax --- .env.docker | 4 ++-- docker/shared/root/docker/entrypoint.d/02-check-config.sh | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.env.docker b/.env.docker index c3e0a55ae..7be6e3e05 100644 --- a/.env.docker +++ b/.env.docker @@ -1102,10 +1102,10 @@ DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" ################################################################################ # Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service -#DOCKER_PROXY_PROFILE="" +DOCKER_PROXY_PROFILE="" # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service -DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" +DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" # How often Docker health check should run for [proxy] service DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" diff --git a/docker/shared/root/docker/entrypoint.d/02-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh index eecb150b4..601cf153e 100755 --- a/docker/shared/root/docker/entrypoint.d/02-check-config.sh +++ b/docker/shared/root/docker/entrypoint.d/02-check-config.sh @@ -8,11 +8,10 @@ entrypoint-set-script-name "$0" # Validating dot-env files for any issues for file in "${dot_env_files[@]}"; do - if file-exists "$file"; then + if ! file-exists "${file}"; then log-warning "Could not source file [${file}]: does not exists" continue fi - log-info "Linting dotenv file ${file}" - dotenv-linter --skip=QuoteCharacter --skip=UnorderedKey "${file}" + run-as-current-user dotenv-linter --skip=QuoteCharacter --skip=UnorderedKey "${file}" done From 8d61b8d2506fb1b5b6c05f4054f1f169ed9574a1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:14:59 +0000 Subject: [PATCH 318/977] add Docker as recommended vscode plugin --- .vscode/extensions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ea1c99893..128e0a295 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,6 +8,7 @@ "editorconfig.editorconfig", "github.vscode-github-actions", "bmewburn.vscode-intelephense-client", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "ms-azuretools.vscode-docker" ] } From d2ed117d3fd9ee168420eeeaea6da661e004a4e2 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:15:33 +0000 Subject: [PATCH 319/977] improve Dockerfile for composer.json+composer.lock --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5158f2348..e6d8071fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -186,12 +186,11 @@ COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer #! Changing user to runtime user USER ${RUNTIME_UID}:${RUNTIME_GID} -# Copy over only composer related files so docker layer cache isn't invalidated on PHP file changes -COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} composer.json composer.lock /var/www/ - # Install composer dependencies # NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/cache/composer \ + --mount=type=bind,source=composer.json,target=/var/www/composer.json \ + --mount=type=bind,source=composer.lock,target=/var/www/composer.lock \ set -ex \ && composer install --prefer-dist --no-autoloader --ignore-platform-reqs From d372b9dee7cc0deba33c10130c10cec888a13325 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:15:57 +0000 Subject: [PATCH 320/977] Set stop_signal for worker to stop Horizon more correct --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index a3516f7ee..fd62bd86e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,6 +110,7 @@ services: container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped + stop_signal: SIGTERM profiles: - ${DOCKER_WORKER_PROFILE:-} build: From 8189b01a268ce9468ee9d2e8bbace8d58a31dc1c Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:17:54 +0000 Subject: [PATCH 321/977] improve naming of directory-is-empty --- docker/shared/root/docker/entrypoint.sh | 2 +- docker/shared/root/docker/helpers.sh | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 57378d23f..9a170be13 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -31,7 +31,7 @@ IFS=' ' read -ar skip_scripts <<< "$ENTRYPOINT_SKIP_SCRIPTS" mkdir -p "${ENTRYPOINT_D_ROOT}" # If ENTRYPOINT_D_ROOT directory is empty, warn and run the regular command -if is-directory-empty "${ENTRYPOINT_D_ROOT}"; then +if directory-is-empty "${ENTRYPOINT_D_ROOT}"; then log-warning "No files found in ${ENTRYPOINT_D_ROOT}, skipping configuration" exec "$@" diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 5826f3c9a..d2913e7d3 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -317,13 +317,14 @@ function file-exists() { [[ -f "$1" ]] } + # @description Checks if $1 contains any files or not # @arg $1 string The path to check # @exitcode 0 If $1 contains files # @exitcode 1 If $1 does *NOT* contain files -function is-directory-empty() +function directory-is-empty() { - ! find "${1}" -mindepth 1 -maxdepth 1 -type f -print -quit 2> /dev/null | read -r + [ -z "$(ls -A "${1}")" ] } # @description Ensures a directory exists (via mkdir) From aa2669c32791d5ac595b9c22ba3b14432b921294 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:18:29 +0000 Subject: [PATCH 322/977] remove invalid/confusing statement in migrations about running migrations when it isn't enabled --- docker/shared/root/docker/entrypoint.d/12-migrations.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index 41c27e986..e7d52d7f5 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -22,7 +22,7 @@ if is-true "${new_migrations}"; then exit 0 fi -log-warning "New migrations available, will automatically apply them now" +log-warning "New migrations available!" if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then log-info "Automatic applying of new database migrations is disabled" From 5c208d05198d133510cdf91dd6f95d1fa43b027f Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:19:34 +0000 Subject: [PATCH 323/977] allow easy overrides of any and all files in container via new override mount --- .env.docker | 3 +++ docker-compose.yml | 2 ++ docker/shared/root/docker/entrypoint.sh | 9 +++++++++ 3 files changed, 14 insertions(+) diff --git a/.env.docker b/.env.docker index 7be6e3e05..9cc8dacbd 100644 --- a/.env.docker +++ b/.env.docker @@ -982,6 +982,9 @@ DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/storage # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides +DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/overrides" + # Automatically run "One-time setup tasks" commands. # # If you are migrating to this docker-compose setup or have manually run the "One time seutp" diff --git a/docker-compose.yml b/docker-compose.yml index fd62bd86e..986bf351b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: - "./.env:/var/www/.env" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" environment: LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" @@ -126,6 +127,7 @@ services: - "./.env:/var/www/.env" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" depends_on: - db - redis diff --git a/docker/shared/root/docker/entrypoint.sh b/docker/shared/root/docker/entrypoint.sh index 9a170be13..73f6a4f3e 100755 --- a/docker/shared/root/docker/entrypoint.sh +++ b/docker/shared/root/docker/entrypoint.sh @@ -11,6 +11,9 @@ export ENTRYPOINT_ROOT : "${ENTRYPOINT_D_ROOT:="${ENTRYPOINT_ROOT}/entrypoint.d/"}" export ENTRYPOINT_D_ROOT +: "${DOCKER_APP_HOST_OVERRIDES_PATH:="${ENTRYPOINT_ROOT}/overrides"}" +export DOCKER_APP_HOST_OVERRIDES_PATH + # Space separated list of scripts the entrypoint runner should skip : "${ENTRYPOINT_SKIP_SCRIPTS:=""}" @@ -37,6 +40,12 @@ if directory-is-empty "${ENTRYPOINT_D_ROOT}"; then exec "$@" fi +# If the overridess directory exists, then copy all files into the container +if ! directory-is-empty "${DOCKER_APP_HOST_OVERRIDES_PATH}"; then + log-info "Overrides directory is not empty, copying files" + run-as-current-user cp --verbose --recursive "${DOCKER_APP_HOST_OVERRIDES_PATH}/." / +fi + acquire-lock "entrypoint.sh" # Start scanning for entrypoint.d files to source or run From 335e6954d2035a388e746cc878e8313a9baaa282 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:19:50 +0000 Subject: [PATCH 324/977] remove noisy log statements in as-boolean --- docker/shared/root/docker/helpers.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index d2913e7d3..51be85506 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -559,14 +559,10 @@ function as-boolean() case "$var" in 1 | true) - log-info "[as-boolean] variable [${var}] was detected as truthy/true, returning [0]" - return 0 ;; 0 | false) - log-info "[as-boolean] variable [${var}] was detected as falsey/false, returning [1]" - return 1 ;; From a6651680313f9ec93a3b8d60557daf84def74725 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:45:08 +0000 Subject: [PATCH 325/977] change where overrides are placed --- .env.docker | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.env.docker b/.env.docker index 9cc8dacbd..1efd91843 100644 --- a/.env.docker +++ b/.env.docker @@ -903,11 +903,18 @@ DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" # Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL="10s" +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will *all* data +# will be stored (data, config, overrides) +DOCKER_ALL_HOST_ROOT_PATH="./docker-compose-state" + # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data -DOCKER_ALL_HOST_DATA_ROOT_PATH="./docker-compose-state/data" +DOCKER_ALL_HOST_DATA_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/data" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration -DOCKER_ALL_HOST_CONFIG_ROOT_PATH="./docker-compose-state/config" +DOCKER_ALL_HOST_CONFIG_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/config" + +# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides +DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/overrides" ################################################################################ # Docker [web] + [worker] (also know as "app") shared service configuration @@ -982,9 +989,6 @@ DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/storage # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" -# Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides -DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/overrides" - # Automatically run "One-time setup tasks" commands. # # If you are migrating to this docker-compose setup or have manually run the "One time seutp" From ca5710b5aeb842dab6f72d5640947b870efe0db5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:45:38 +0000 Subject: [PATCH 326/977] fix 12-migrations.sh properly detecting new migrations and printing output --- .../root/docker/entrypoint.d/12-migrations.sh | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docker/shared/root/docker/entrypoint.d/12-migrations.sh b/docker/shared/root/docker/entrypoint.d/12-migrations.sh index e7d52d7f5..3b87daf1f 100755 --- a/docker/shared/root/docker/entrypoint.d/12-migrations.sh +++ b/docker/shared/root/docker/entrypoint.d/12-migrations.sh @@ -12,17 +12,25 @@ entrypoint-set-script-name "$0" # Wait for the database to be ready await-database-ready -# Detect if we have new migrations -declare -i new_migrations=0 -(run-as-runtime-user php artisan migrate:status || :) | grep No && new_migrations=1 +# Run the migrate:status command and capture output +output=$(run-as-runtime-user php artisan migrate:status || :) -if is-true "${new_migrations}"; then - log-info "No outstanding migrations detected" +# By default we have no new migrations +declare -i new_migrations=0 + +# Detect if any new migrations are available by checking for "No" in the output +echo "$output" | grep No && new_migrations=1 + +if is-false "${new_migrations}"; then + log-info "No new migrations detected" exit 0 fi -log-warning "New migrations available!" +log-warning "New migrations available" + +# Print the output +echo "$output" if is-false "${DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY}"; then log-info "Automatic applying of new database migrations is disabled" From 1616c7cb11d5e90ef7b48967b22afd0765e04361 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:45:56 +0000 Subject: [PATCH 327/977] make directory-is-empty more robust --- docker/shared/root/docker/helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 51be85506..1589e0780 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -324,7 +324,7 @@ function file-exists() # @exitcode 1 If $1 does *NOT* contain files function directory-is-empty() { - [ -z "$(ls -A "${1}")" ] + path-exists "${1}" && [[ -z "$(ls -A "${1}")" ]] } # @description Ensures a directory exists (via mkdir) From c4f984b205358a335be21022fc271a7e86a45940 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 20:46:09 +0000 Subject: [PATCH 328/977] remove php extension FTP requirement --- goss.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/goss.yaml b/goss.yaml index aa5ee6058..f558f788a 100644 --- a/goss.yaml +++ b/goss.yaml @@ -48,7 +48,6 @@ command: - exif - fileinfo - filter - - ftp - gd - hash - iconv From 8bdb0ca77bb7c3aabbf1d696c29147f2f08954f0 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 21:22:43 +0000 Subject: [PATCH 329/977] fix directory-is-empty and add tests to avoid regressions --- docker/shared/root/docker/helpers.sh | 2 +- tests/bats/helpers.bats | 29 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 1589e0780..51e955682 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -324,7 +324,7 @@ function file-exists() # @exitcode 1 If $1 does *NOT* contain files function directory-is-empty() { - path-exists "${1}" && [[ -z "$(ls -A "${1}")" ]] + ! path-exists "${1}" || [[ -z "$(ls -A "${1}")" ]] } # @description Ensures a directory exists (via mkdir) diff --git a/tests/bats/helpers.bats b/tests/bats/helpers.bats index ea4c7b518..6ba65c2c3 100644 --- a/tests/bats/helpers.bats +++ b/tests/bats/helpers.bats @@ -5,6 +5,12 @@ setup() { load "$ROOT/docker/shared/root/docker/helpers.sh" } +teardown() { + if [[ -e test_dir ]]; then + rm -rf test_dir + fi +} + @test "test [is-true]" { is-true "1" is-true "true" @@ -72,3 +78,26 @@ setup() { return 1 } + +@test "test [directory-is-empty] - non existing" { + directory-is-empty test_dir +} + +@test "test [directory-is-empty] - actually empty" { + mkdir -p test_dir + + directory-is-empty test_dir +} + +@test "test [directory-is-empty] - not empty (directory)" { + mkdir -p test_dir/sub-dir + + ! directory-is-empty test_dir +} + +@test "test [directory-is-empty] - not empty (file)" { + mkdir -p test_dir/ + touch test_dir/hello-world.txt + + ! directory-is-empty test_dir +} From 1a6e97c98b92696ed9f96807d5c6ad04edfee13d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 22:51:15 +0000 Subject: [PATCH 330/977] try to make 8.3 build working by building imagick from master branch --- .github/workflows/docker.yml | 4 -- Dockerfile | 38 ++++++++++++------- .../root/docker/entrypoint.d/10-storage.sh | 2 +- docker/shared/root/docker/install/base.sh | 19 ---------- .../root/docker/install/php-extensions.sh | 21 ++-------- 5 files changed, 28 insertions(+), 56 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ca6e73630..e7b49f419 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -84,10 +84,6 @@ jobs: # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations # See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude exclude: - # Broken for imagick on arm64 due to https://github.com/Imagick/imagick/pull/641 - # Could probably figure out how to do a matrix only ignoring 8.3 + linux/arm64, but this is easier atm - - php_version: 8.3 - # targeting [apache] runtime with [fpm] base type doesn't make sense - target_runtime: apache php_base: fpm diff --git a/Dockerfile b/Dockerfile index e6d8071fa..f5aca8235 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,10 @@ # Configuration ####################################################### -# See: https://github.com/composer/composer/releases +# See: https://github.com/mlocati/docker-php-extension-installer +ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80" + +# See: https://github.com/composer/composer ARG COMPOSER_VERSION="2.6" # See: https://nginx.org/ @@ -17,7 +20,7 @@ ARG FOREGO_VERSION="0.17.2" # See: https://github.com/hairyhenderson/gomplate ARG GOMPLATE_VERSION="v3.11.6" -# See: https://github.com/dotenv-linter/dotenv-linter/releases +# See: https://github.com/dotenv-linter/dotenv-linter # # WARN: v3.3.0 and above requires newer libc version than Ubuntu ships with ARG DOTENV_LINTER_VERSION="v3.2.0" @@ -40,7 +43,10 @@ ARG RUNTIME_GID=33 # often called 'www-data' ARG APT_PACKAGES_EXTRA= # Extensions installed via [pecl install] -ARG PHP_PECL_EXTENSIONS="redis imagick" +# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't +# ! been released yet (after +10 months)! +# ! See: https://github.com/Imagick/imagick/pull/641 +ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58" ARG PHP_PECL_EXTENSIONS_EXTRA= # Extensions installed via [docker-php-ext-install] @@ -63,6 +69,11 @@ ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg" # NOTE: Docker will *not* pull this image unless it's referenced (via build target) FROM composer:${COMPOSER_VERSION} AS composer-image +# php-extension-installer image from Docker Hub +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer + # nginx webserver from Docker Hub. # Used to copy some docker-entrypoint files for [nginx-runtime] # @@ -147,23 +158,24 @@ ARG PHP_PECL_EXTENSIONS_EXTRA ARG PHP_VERSION ARG TARGETPLATFORM -ENV PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} -ENV PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} -ENV PHP_EXTENSIONS=${PHP_EXTENSIONS} -ENV PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} -ENV PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} +COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh -RUN --mount=type=cache,id=pixelfed-php-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/usr/src/php \ +RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ + PHP_EXTENSIONS=${PHP_EXTENSIONS} \ + PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \ + PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \ + PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \ + PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \ /docker/install/php-extensions.sh ####################################################### # PHP: composer and source code ####################################################### -FROM base AS composer-and-src +FROM php-extensions AS composer-and-src ARG PHP_VERSION ARG PHP_DEBIAN_RELEASE @@ -188,7 +200,7 @@ USER ${RUNTIME_UID}:${RUNTIME_GID} # Install composer dependencies # NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet) -RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/cache/composer \ +RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \ --mount=type=bind,source=composer.json,target=/var/www/composer.json \ --mount=type=bind,source=composer.lock,target=/var/www/composer.lock \ set -ex \ @@ -201,7 +213,7 @@ COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ # Runtime: base ####################################################### -FROM base AS shared-runtime +FROM php-extensions AS shared-runtime ARG BUILDARCH ARG BUILDOS @@ -212,8 +224,6 @@ ARG RUNTIME_UID ENV RUNTIME_UID=${RUNTIME_UID} ENV RUNTIME_GID=${RUNTIME_GID} -COPY --link --from=php-extensions /usr/local/lib/php/extensions /usr/local/lib/php/extensions -COPY --link --from=php-extensions /usr/local/etc/php /usr/local/etc/php COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer diff --git a/docker/shared/root/docker/entrypoint.d/10-storage.sh b/docker/shared/root/docker/entrypoint.d/10-storage.sh index f0c28241d..54145a365 100755 --- a/docker/shared/root/docker/entrypoint.d/10-storage.sh +++ b/docker/shared/root/docker/entrypoint.d/10-storage.sh @@ -7,7 +7,7 @@ source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" # Copy the [storage/] skeleton files over the "real" [storage/] directory so assets are updated between versions -run-as-runtime-user cp --recursive storage.skel/. ./storage/ +run-as-runtime-user cp --force --recursive storage.skel/. ./storage/ # Ensure storage linkk are correctly configured run-as-runtime-user php artisan storage:link diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index e20788b56..4e97e82bb 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -21,8 +21,6 @@ packages+=( git gnupg1 gosu - libcurl4-openssl-dev - libzip-dev locales locales-all moreutils @@ -42,21 +40,6 @@ packages+=( pngquant ) -# Image Processing -packages+=( - libjpeg62-turbo-dev - libmagickwand-dev - libpng-dev -) - -# Required for GD -packages+=( - libwebp-dev - libwebp6 - libxpm-dev - libxpm4 -) - # Video Processing packages+=( ffmpeg @@ -64,8 +47,6 @@ packages+=( # Database packages+=( - libpq-dev - libsqlite3-dev mariadb-client postgresql-client ) diff --git a/docker/shared/root/docker/install/php-extensions.sh b/docker/shared/root/docker/install/php-extensions.sh index 42c149d76..222f2374d 100755 --- a/docker/shared/root/docker/install/php-extensions.sh +++ b/docker/shared/root/docker/install/php-extensions.sh @@ -2,6 +2,7 @@ set -ex -o errexit -o nounset -o pipefail declare -a pecl_extensions=() + readarray -d ' ' -t pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS:-}") readarray -d ' ' -t -O "${#pecl_extensions[@]}" pecl_extensions < <(echo -n "${PHP_PECL_EXTENSIONS_EXTRA:-}") @@ -10,16 +11,6 @@ readarray -d ' ' -t php_extensions < <(echo -n "${PHP_EXTENSIONS:-}") readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_EXTRA:-}") readarray -d ' ' -t -O "${#php_extensions[@]}" php_extensions < <(echo -n "${PHP_EXTENSIONS_DATABASE:-}") -# Grab the PHP source code so we can compile against it -docker-php-source extract - -# PHP GD extensions -docker-php-ext-configure gd \ - --with-freetype \ - --with-jpeg \ - --with-webp \ - --with-xpm - # Optional script folks can copy into their image to do any [docker-php-ext-configure] work before the [docker-php-ext-install] # this can also overwirte the [gd] configure above by simply running it again declare -r custom_pre_configure_script="" @@ -32,11 +23,5 @@ if [[ -e "${custom_pre_configure_script}" ]]; then "${custom_pre_configure_script}" fi -# Install pecl extensions -pecl install "${pecl_extensions[@]}" - -# PHP extensions (dependencies) -docker-php-ext-install -j "$(nproc)" "${php_extensions[@]}" - -# Enable all extensions -docker-php-ext-enable "${pecl_extensions[@]}" "${php_extensions[@]}" +# PECL + PHP extensions +IPE_KEEP_SYSPKG_CACHE=1 install-php-extensions "${pecl_extensions[@]}" "${php_extensions[@]}" From b73d4522554adc2700793b2e9bc8791b30a6caf5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 23:39:18 +0000 Subject: [PATCH 331/977] sort ARG in Dockerfile --- .env.docker | 18 ++++++++++-------- Dockerfile | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.env.docker b/.env.docker index 1efd91843..8ca07c21f 100644 --- a/.env.docker +++ b/.env.docker @@ -949,14 +949,21 @@ DOCKER_APP_RELEASE="branch-jippi-fork" # the [DOCKER_APP_RUNTIME] and [PHP_DEBIAN_RELEASE] settings DOCKER_APP_PHP_VERSION="8.2" -# The [php] Docker image base type +# The container runtime to use. # -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/runtimes.md -DOCKER_APP_BASE_TYPE="apache" +# See: https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +DOCKER_APP_RUNTIME="apache" # The Debian release variant to use of the [php] Docker image +# +# Examlpe: [bookworm] or [bullseye] DOCKER_APP_DEBIAN_RELEASE="bullseye" +# The [php] Docker image base type +# +# See: https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +DOCKER_APP_BASE_TYPE="apache" + # Image to pull the Pixelfed Docker images from. # # Example values: @@ -967,11 +974,6 @@ DOCKER_APP_DEBIAN_RELEASE="bullseye" # DOCKER_APP_IMAGE="ghcr.io/jippi/pixelfed" -# The container runtime to use. -# -# See: https://github.com/jippi/pixelfed/blob/jippi-fork/docker/runtimes.md -DOCKER_APP_RUNTIME="apache" - # Pixelfed version (image tag) to pull from the registry. # # See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed diff --git a/Dockerfile b/Dockerfile index f5aca8235..1c8a30155 100644 --- a/Dockerfile +++ b/Dockerfile @@ -161,6 +161,7 @@ ARG TARGETPLATFORM COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh + RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \ --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ From ef37c8f234558582baaa68bc58269bb3ac35a92c Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 23:48:04 +0000 Subject: [PATCH 332/977] name CI jobs --- .github/workflows/docker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e7b49f419..186919ed7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,6 +25,7 @@ on: jobs: lint: + name: hadolint runs-on: ubuntu-latest permissions: @@ -41,7 +42,7 @@ jobs: failure-threshold: error shellcheck: - name: Shellcheck + name: ShellCheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -62,6 +63,7 @@ jobs: run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats build: + name: Build, Test, and Push runs-on: ubuntu-latest strategy: From 3723f36043a6b7a5c25920fe987172ef26967e7d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 23:54:24 +0000 Subject: [PATCH 333/977] remove unneeded workflow triggers --- .github/workflows/docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 186919ed7..ea119bec3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,9 +17,7 @@ on: # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request pull_request: types: - - labeled - opened - - ready_for_review - reopened - synchronize From 043f914c8c62010e67e602b765599233009c276d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 26 Jan 2024 23:58:38 +0000 Subject: [PATCH 334/977] test: change prefix for docker tags --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ea119bec3..511ac911e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -168,8 +168,8 @@ jobs: type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=branch,prefix=branch- - type=ref,event=pr,prefix=pr- + type=ref,event=branch,prefix=branch/ + type=ref,event=pr,prefix=pr/ type=ref,event=tag env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index From 2aeccf885f80a4de9fc0014464fbe538c23613f6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 27 Jan 2024 00:01:06 +0000 Subject: [PATCH 335/977] test: remove slash in tags --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 511ac911e..ea119bec3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -168,8 +168,8 @@ jobs: type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=branch,prefix=branch/ - type=ref,event=pr,prefix=pr/ + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- type=ref,event=tag env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index From 92ff114d2d0649bde17478cb8915fed2af720f94 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 29 Jan 2024 21:45:59 -0700 Subject: [PATCH 336/977] Update migrations, fixes #4883 --- ...0_add_account_status_to_profiles_table.php | 14 ++++++++------ .../migrations/2019_01_12_054413_stories.php | 10 ++-------- ...021_01_14_034521_add_cache_locks_table.php | 2 +- ...ompose_settings_to_user_settings_table.php | 19 +++++++++++++++---- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php b/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php index 04a88060e..097e86753 100644 --- a/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php +++ b/database/migrations/2018_12_22_055940_add_account_status_to_profiles_table.php @@ -54,12 +54,14 @@ class AddAccountStatusToProfilesTable extends Migration $table->string('hub_url')->nullable(); }); - Schema::table('stories', function (Blueprint $table) { - $table->dropColumn('id'); - }); - Schema::table('stories', function (Blueprint $table) { - $table->bigIncrements('bigIncrements')->first(); - }); + if (Schema::hasTable('stories')) { + Schema::table('stories', function (Blueprint $table) { + $table->dropColumn('id'); + }); + Schema::table('stories', function (Blueprint $table) { + $table->bigIncrements('bigIncrements')->first(); + }); + } Schema::table('profiles', function (Blueprint $table) { $table->dropColumn('status'); diff --git a/database/migrations/2019_01_12_054413_stories.php b/database/migrations/2019_01_12_054413_stories.php index a61c447de..f58a8cf38 100644 --- a/database/migrations/2019_01_12_054413_stories.php +++ b/database/migrations/2019_01_12_054413_stories.php @@ -60,13 +60,7 @@ class Stories extends Migration { Schema::dropIfExists('story_items'); Schema::dropIfExists('story_views'); - - Schema::table('stories', function (Blueprint $table) { - $table->dropColumn(['title','preview_photo','local_only','is_live','broadcast_url','broadcast_key']); - }); - - Schema::table('story_reactions', function (Blueprint $table) { - $table->dropColumn('story_id'); - }); + Schema::dropIfExists('story_reactions'); + Schema::dropIfExists('stories'); } } diff --git a/database/migrations/2021_01_14_034521_add_cache_locks_table.php b/database/migrations/2021_01_14_034521_add_cache_locks_table.php index 121c69a37..07889b490 100644 --- a/database/migrations/2021_01_14_034521_add_cache_locks_table.php +++ b/database/migrations/2021_01_14_034521_add_cache_locks_table.php @@ -27,6 +27,6 @@ class AddCacheLocksTable extends Migration */ public function down() { - Schema::dropTable('cache_locks'); + Schema::dropIfExists('cache_locks'); } } diff --git a/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php b/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php index 58837cab3..49a9b2c58 100644 --- a/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php +++ b/database/migrations/2021_07_23_062326_add_compose_settings_to_user_settings_table.php @@ -33,14 +33,25 @@ class AddComposeSettingsToUserSettingsTable extends Migration public function down() { Schema::table('user_settings', function (Blueprint $table) { - $table->dropColumn('compose_settings'); + if (Schema::hasColumn('user_settings', 'compose_settings')) { + $table->dropColumn('compose_settings'); + } }); Schema::table('media', function (Blueprint $table) { $table->string('caption')->change(); - $table->dropIndex('profile_id'); - $table->dropIndex('mime'); - $table->dropIndex('license'); + + $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $schemaManager->listTableIndexes('media'); + if (array_key_exists('media_profile_id_index', $indexesFound)) { + $table->dropIndex('media_profile_id_index'); + } + if (array_key_exists('media_mime_index', $indexesFound)) { + $table->dropIndex('media_mime_index'); + } + if (array_key_exists('media_license_index', $indexesFound)) { + $table->dropIndex('media_license_index'); + } }); } } From 61b1523368af6efafc7c3081fbc3b4bee4c8d212 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 29 Jan 2024 22:13:09 -0700 Subject: [PATCH 337/977] Fix newsroom migration --- database/migrations/2019_12_10_023604_create_newsroom_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2019_12_10_023604_create_newsroom_table.php b/database/migrations/2019_12_10_023604_create_newsroom_table.php index 2651d5c4d..b463f5624 100644 --- a/database/migrations/2019_12_10_023604_create_newsroom_table.php +++ b/database/migrations/2019_12_10_023604_create_newsroom_table.php @@ -40,6 +40,6 @@ class CreateNewsroomTable extends Migration */ public function down() { - Schema::dropIfExists('site_news'); + Schema::dropIfExists('newsroom'); } } From 8a9a7c0e478386dda2b705c64b30075c3133845b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 29 Jan 2024 22:24:50 -0700 Subject: [PATCH 338/977] Fix parental_controls migration --- .../2024_01_09_052419_create_parental_controls_table.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php index bf803e4c0..6713e6849 100644 --- a/database/migrations/2024_01_09_052419_create_parental_controls_table.php +++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php @@ -28,7 +28,7 @@ return new class extends Migration $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); $indexesFound = $schemaManager->listTableIndexes('user_roles'); if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { - $table->dropIndex('user_roles_profile_id_unique'); + $table->dropUnique('user_roles_profile_id_unique'); } $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); }); @@ -42,7 +42,11 @@ return new class extends Migration Schema::dropIfExists('parental_controls'); Schema::table('user_roles', function (Blueprint $table) { - $table->dropIndex('user_roles_profile_id_unique'); + $schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); + $indexesFound = $schemaManager->listTableIndexes('user_roles'); + if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { + $table->dropUnique('user_roles_profile_id_unique'); + } $table->unsignedBigInteger('profile_id')->unique()->index()->change(); }); } From fd4f41a14e22c0162d15f754af49846a4cd796f7 Mon Sep 17 00:00:00 2001 From: mbliznikova Date: Tue, 30 Jan 2024 19:19:25 +0000 Subject: [PATCH 339/977] Added an informative UI error message for attempt to create a mixed media album --- .../assets/js/components/ComposeModal.vue | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 2c4e3ba42..e4ae59c87 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -1094,6 +1094,16 @@ export default { return `${parseFloat((bytes / Math.pow(1024, quotient)).toFixed(dec))} ${units[quotient]}` }, + defineErrorMessage(errObject) { + if (errObject.response) { + let msg = errObject.response.data.message ? errObject.response.data.message : 'An unexpected error occured.'; + } + else { + let msg = errObject.message; + } + return swal('Oops, something went wrong!', msg, 'error'); + }, + fetchProfile() { let tags = { public: 'Public', @@ -1395,15 +1405,23 @@ export default { location.href = res.data; } }).catch(err => { - if(err.response) { - let msg = err.response.data.message ? err.response.data.message : 'An unexpected error occured.' - swal('Oops, something went wrong!', msg, 'error'); - } else { - swal('Oops, something went wrong!', err.message, 'error'); - } - }); - return; - break; + switch(err.response.status) { + case 400: + if (err.response.data.error == "Must contain a single photo or video or multiple photos.") { + swal("Wrong types of mixed media", "The album must contain a single photo or video or multiple photos.", 'error'); + } + else { + this.defineErrorMessage(err); + } + break; + + default: + this.defineErrorMessage(err); + break; + } + }); + return; + break; case 'delete' : this.ids = []; From 0aff126aa0bb001f0137848a578adad7a1e5797c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Feb 2024 22:46:13 -0700 Subject: [PATCH 340/977] Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 --- app/Http/Controllers/Api/ApiV1Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index b342d68ce..dd0cbd062 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3031,7 +3031,7 @@ class ApiV1Controller extends Controller $content = strip_tags($request->input('status')); $rendered = Autolink::create()->autolink($content); - $cw = $user->profile->cw == true ? true : $request->input('sensitive', false); + $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false); $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; if($in_reply_to_id) { From 339857ffa28f411ab201800d1439094b6556f3b5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Feb 2024 22:46:39 -0700 Subject: [PATCH 341/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d251b3df3..5c73b4e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ - Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) - Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) - Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) +- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 59aa6a4b0237e6818767ad6a85d1623d544407d3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Feb 2024 22:49:04 -0700 Subject: [PATCH 342/977] Update AccountImport.vue, fix new IG export format --- resources/assets/components/AccountImport.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/assets/components/AccountImport.vue b/resources/assets/components/AccountImport.vue index 6e1fe9fcf..f7b7279f5 100644 --- a/resources/assets/components/AccountImport.vue +++ b/resources/assets/components/AccountImport.vue @@ -381,7 +381,7 @@ let file = this.$refs.zipInput.files[0]; let entries = await this.model(file); if (entries && entries.length) { - let files = await entries.filter(e => e.filename === 'content/posts_1.json'); + let files = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json'); if(!files || !files.length) { this.contactModal( @@ -402,7 +402,7 @@ let entries = await this.model(file); if (entries && entries.length) { this.zipFiles = entries; - let media = await entries.filter(e => e.filename === 'content/posts_1.json')[0].getData(new zip.TextWriter()); + let media = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json')[0].getData(new zip.TextWriter()); this.filterPostMeta(media); let imgs = await Promise.all(entries.filter(entry => { From cf005423369eba9ed2c3bba374dce2a1cf792fd0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 1 Feb 2024 23:19:25 -0700 Subject: [PATCH 343/977] Update compiled assets --- public/js/account-import.js | Bin 27575 -> 27695 bytes public/js/admin.js | Bin 215887 -> 216009 bytes public/js/collectioncompose.js | Bin 10455 -> 10483 bytes public/js/collections.js | Bin 21432 -> 21570 bytes public/js/compose.chunk.10e7f993dcc726f9.js | Bin 93403 -> 0 bytes public/js/compose.chunk.1ac292c93b524406.js | Bin 0 -> 93554 bytes public/js/compose.js | Bin 68808 -> 68959 bytes ...89d7.js => daci.chunk.8d4acc1db3f27a51.js} | Bin 126782 -> 126677 bytes ....js => discover.chunk.b1846efb6bd1e43c.js} | Bin 71794 -> 71802 bytes ...ver~findfriends.chunk.941b524eee8b8d63.js} | Bin 125681 -> 125576 bytes ...iscover~hashtag.bundle.6c2ff384b17ea58d.js | Bin 0 -> 50871 bytes ...iscover~hashtag.bundle.9cfffc517f35044e.js | Bin 50916 -> 0 bytes ...scover~memories.chunk.7d917826c3e9f17b.js} | Bin 126019 -> 125914 bytes ...over~myhashtags.chunk.a72fc4882db8afd3.js} | Bin 172907 -> 172802 bytes ...over~serverfeed.chunk.8365948d1867de3a.js} | Bin 125271 -> 125166 bytes ...scover~settings.chunk.be88dc5ba1a24a7d.js} | Bin 129546 -> 129441 bytes ... => dms~message.chunk.76edeafda3d92320.js} | Bin 69772 -> 69671 bytes public/js/home.chunk.351f55e9d09b6482.js | Bin 239870 -> 0 bytes public/js/home.chunk.f3f4f632025b560f.js | Bin 0 -> 240076 bytes ...ome.chunk.f3f4f632025b560f.js.LICENSE.txt} | 0 public/js/landing.js | Bin 184612 -> 184645 bytes public/js/manifest.js | Bin 4006 -> 4006 bytes public/js/portfolio.js | Bin 45143 -> 45145 bytes ...dc83.js => post.chunk.eb9804ff282909ae.js} | Bin 221808 -> 221711 bytes ...ost.chunk.eb9804ff282909ae.js.LICENSE.txt} | 0 ...5.js => profile.chunk.d52916cb68c9a146.js} | Bin 223228 -> 222906 bytes public/js/profile.js | Bin 114832 -> 114769 bytes ...file~followers.bundle.5deed93248f20662.js} | Bin 27333 -> 27232 bytes ...file~following.bundle.d2b3b1fc2e05dbd3.js} | Bin 27304 -> 27203 bytes public/js/spa.js | Bin 202814 -> 203755 bytes public/js/status.js | Bin 136281 -> 136372 bytes public/js/stories.js | Bin 29453 -> 29421 bytes public/js/timeline.js | Bin 139435 -> 140280 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 34 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/js/compose.chunk.10e7f993dcc726f9.js create mode 100644 public/js/compose.chunk.1ac292c93b524406.js rename public/js/{daci.chunk.b17a0b11877389d7.js => daci.chunk.8d4acc1db3f27a51.js} (67%) rename public/js/{discover.chunk.9606885dad3c8a99.js => discover.chunk.b1846efb6bd1e43c.js} (61%) rename public/js/{discover~findfriends.chunk.02be60ab26503531.js => discover~findfriends.chunk.941b524eee8b8d63.js} (67%) create mode 100644 public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js delete mode 100644 public/js/discover~hashtag.bundle.9cfffc517f35044e.js rename public/js/{discover~memories.chunk.ce9cc6446020e9b3.js => discover~memories.chunk.7d917826c3e9f17b.js} (67%) rename public/js/{discover~myhashtags.chunk.6eab2414b2b16e19.js => discover~myhashtags.chunk.a72fc4882db8afd3.js} (73%) rename public/js/{discover~serverfeed.chunk.0f2dcc473fdce17e.js => discover~serverfeed.chunk.8365948d1867de3a.js} (67%) rename public/js/{discover~settings.chunk.732c1f76a00d9204.js => discover~settings.chunk.be88dc5ba1a24a7d.js} (68%) rename public/js/{dms~message.chunk.15157ff4a6c17cc7.js => dms~message.chunk.76edeafda3d92320.js} (63%) delete mode 100644 public/js/home.chunk.351f55e9d09b6482.js create mode 100644 public/js/home.chunk.f3f4f632025b560f.js rename public/js/{home.chunk.351f55e9d09b6482.js.LICENSE.txt => home.chunk.f3f4f632025b560f.js.LICENSE.txt} (100%) rename public/js/{post.chunk.23fc9e82d4fadc83.js => post.chunk.eb9804ff282909ae.js} (72%) rename public/js/{post.chunk.23fc9e82d4fadc83.js.LICENSE.txt => post.chunk.eb9804ff282909ae.js.LICENSE.txt} (100%) rename public/js/{profile.chunk.0e5bd852054d6355.js => profile.chunk.d52916cb68c9a146.js} (64%) rename public/js/{profile~followers.bundle.731f680cfb96563d.js => profile~followers.bundle.5deed93248f20662.js} (70%) rename public/js/{profile~following.bundle.3d95796c9f1678dd.js => profile~following.bundle.d2b3b1fc2e05dbd3.js} (70%) diff --git a/public/js/account-import.js b/public/js/account-import.js index 74d4d9e4db029e7d746d36f7b010d5559de66f2d..b68bfb4953f60434fced647f83e429bf13ae9504 100644 GIT binary patch delta 236 zcmdmfopJpQ#trjyrE6-GD)UQ=;xqG#OA^zI5_97dlS?woGD|A;H`nOCWx}Uma*eL) z<{X2&0xU-6hQ^cqGt@R0JN{8+F*LL=1@eq0>t^vx-jtrsZe(s?Vqk7GSs_D<)zI9? z*b*ok#0*q6d1=NJR$~i8WAn*&nZB&%CdOu#lP6^Q!gV~!Tnm-=&yeG@E=w#@$h6JV aFa%p@mu1ghs{_%5Dh;*+C_TA2+XVoY!%pS^ delta 158 zcmZ2~gK_(H#trjyH{aGh&9wQk!2})vV7rM eW3tvlMhnU&d4v^j+%hpXvEXtmK-hoL{57SMxwK~zo zdhul%N-=pcB@hU5w*t`N+hd%VdS9~|nHpM{Pd9XDwwtb_$Lz_GYG`1RY?f@Wy;hGo zPmsmX%*0@_prz5oy*$(P9GFEQ>bJW)FsCsK8(W$hnP}={+9qmP7#Ug^Y3k%ncXVSG zp61A`#g?dHX>MdV{kIFV52q5`)JPZRI967$Mxe2B)AxBXbFzZGWj>wXgV}z%x)*Z^ R3rHhe7VHFwB)2!S3joA@d%XYv delta 261 zcmX>(o%j4S-VL)ivenks>X;c!o*3>p+27KzdD5oslQuCHJwt&> zNr|&KH90>oC9$Y-`bB>x$>{~rOzhKlI5Y7wDH={^Ok@&ean8vv-hRu8srNOjp{bdr z(PTkOqv`kEnR%wK(qs1IFfuo`OifC%*#29OIZu$)%*f2x5Ugar1G54|$Mz!*%xTQ5 z=BB2mmeciJn0;9cO^ht3*Sj$LA+%q0VUA;kr~zu17RH8?`_$DopWqReVl_23F|*kGK+S~_!UW35@mZH87AX|lrfNVGUsSiBJVnD9 E0BMsbp8x;= delta 86 zcmewycs+1K6Ni{mL1|J>X0np4ZKhsXW^raxW=>{F<>oCMtSqdiMuui4oA>cpO0gQ6 j85ejC7ETZYHFKhc>B18bV@QZi}f;7ifv(x$&9?xlk>#5Ca;m`V$7UuE-5db zsh5&ilBlVZ3R0+7P+FX!Q37Ocj*;YNVKFqawAh?4D`voIX<%qzKDj_vZSn@+1dc>= zOJma%6Qjuzenkk*BtHuT=aQc-f+Ox9g5VVU$00a3{aq0pwE#_KV*_KTH9V7B0u)$4 z7EfLgFoo5`(8AbqvSXkRtC6XZh3Vu;fxZZx&jVLO6%@$I@mZH87AfT0W@>=kZ#LOJ b$eyiM2c&CqV1PVA*4zwa8Blg|Nw5n5wnKH< delta 272 zcmX@Kf^o-k#tkyOlQ(fmZLZ=i;hwB4*~3^ed4r@pXNg`)Vo9Q=PU__I;$oZsOMYW! zH8wXeH=C>&EVo%g?z=v#p^=5TIfy$kTV(Q%AfCxVejXgA$;L*;Mn*}K7x@(-I5Peg z2u_T@ErPSqKLo+~>mP^UBnP-6I2!^qnT^a%VYcxFDzJd8pKKI3h1Jy3$kJr;=|CS= qQwtL_^T{$nz6gzlL93w(6ob{FKG+}Zz@`IH3lcOp1DU)(xETQTs!*x` diff --git a/public/js/compose.chunk.10e7f993dcc726f9.js b/public/js/compose.chunk.10e7f993dcc726f9.js deleted file mode 100644 index 106a118f4ead46c9ff824895ae1ca74cb51f1065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93403 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 diff --git a/public/js/compose.chunk.1ac292c93b524406.js b/public/js/compose.chunk.1ac292c93b524406.js new file mode 100644 index 0000000000000000000000000000000000000000..ed1b9307165c18197ef96a8506c9df975246e5c9 GIT binary patch literal 93554 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 literal 0 HcmV?d00001 diff --git a/public/js/compose.js b/public/js/compose.js index 699a19dad47b38629223f57f7ecbbedd4bf0236d..e643034014c94a6b6bcd1ece89505781d306cd51 100644 GIT binary patch delta 270 zcmX>xljZ&_mJRD9m<)_3H%cqpIVLCPm*$ly6lYeYD&%D5W|k-vr6wk4q^2n8C^_fn zl_VyYC{*T`7AYjAjmJRD9HXBK<=bUVy#5(z*(#_46l-;#hjZ6#-&8KhRVU(LL%f-mC*~Kq! zJFA(onI%F{Wcy22MnM)kg zT9{22xWt&So%t4HER?w+cYMU?F#W+x#u65gexTFjr`x|^2Xfj1}8`HZitJ zGMY{<=yjgH?-ZjDOzs3@!1mfRj4fRJCYA<9S&R&fOeQO`NlkXJ%%04;R($j6m9GUSpE{9nGMZNCM&W@O?I%%p1kXv_~tV!UkeH(CMV~Y=9TEDWEQ0+m*|&l zKDB!$BZlbYQ@hZUHn!ZSsZd9-HTVyUWIGVrnwk;g1_DNXBBaz^{PK zlK-N(Sj`L!Ee)qva4`C^nj4rKnN3e%Vocnw&B^!!CdVkqD8&MFl*ROq{EUv&6~_77&t7IUVPrKoH#9St&UTg2m&MS~ z!fd+Zb;gA4+pjX_@Uwt)O%I4*l%L-4n302R5j6qPT z3C9?Nw_BfJtm0xdu{1C=+rH}(<6TBpQv)Molj(D=F#58Xnwl9+PdLMvu-*46V-7zH zNZEA5my8-PzdmAgoUZ+pu?VWk@FAl*E6|(9hSME^Qrr2TG0HPTOaVzjTmX`pKK~`- FO8}9fYqbCX diff --git a/public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js b/public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js new file mode 100644 index 0000000000000000000000000000000000000000..3ef77ac5dce3366d458bf459cea31e701119dc79 GIT binary patch literal 50871 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 literal 0 HcmV?d00001 diff --git a/public/js/discover~hashtag.bundle.9cfffc517f35044e.js b/public/js/discover~hashtag.bundle.9cfffc517f35044e.js deleted file mode 100644 index afb2b3de0fe07582cc3cbfc84f701ecb18683c2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50916 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 KJIESl=>G#ByCPu# diff --git a/public/js/discover~memories.chunk.ce9cc6446020e9b3.js b/public/js/discover~memories.chunk.7d917826c3e9f17b.js similarity index 67% rename from public/js/discover~memories.chunk.ce9cc6446020e9b3.js rename to public/js/discover~memories.chunk.7d917826c3e9f17b.js index ec86c87f7d14df1c59c4315deb87e844c5d7b8ea..f8ed91112991bc5e2d23c485396bd760269070a3 100644 GIT binary patch delta 249 zcmX?ngZx(+y5A3PIFP ze}9ZIaQpj{j7?npW|kHvhMGE=w#6EzMy3YS3$HTH-!6ZZ(S(uJ+}zO2V0y$gMqd^~ zLkqL%8?G`YZ0Eern9I)s(l!0!V@3_AOCLUAbe#U;8DkL(NYnHS&w(;v$4&>yY(Mjy bQH~j6$i?T3>MS6KO&7e!=rBFvHREdlB7#$v delta 329 zcmcb0o&E3)_J%Eto*t|w<|c+FlMRK`CM$f*-hSVMv5JXB$w5hbd!#p`JwJp(lP8#SfR*S)GX}9uH%MX>ny&B1$mv{Al%JNFld6}JSzM5lSQ(#}n44-}qK8e` z3L#utoLU4@sj00LlQ+5HfcW%)Y(}x|OA;B4eOWDy%?*u#ZZevFU^}DR_K@|AEs`vz zMnIP|9%T%INKZF7&KS78^(12x7psY-fuY%Uo-2%Z8CgvYjEqgDzq`ul%VKJ3W;Ffa z8ODU|^RF@H@hK`{gDi#%62{hra)g-GXrB|qv?r$OcmSj#4@!?vRYbN znwo(GjHWx3F)6?llrRNs=c!=&#>;AEX<=fxoxP3e922X#xuKcC^jjTFzAT1@7C;-T zm=d;kcQP#yW&tUiZa9-kgB9dRgXxa5m>j3ioy+uu1*B=Z;X)=&785f=pu{33r|nC+i}{%XeVHwd&8IK0VyfC+7|YZu$!uz5 zFnysFlkxQ05+((RWC>I7_QmB)UwK(gEDa3Jw$E*4I>*FnYG7n+GQFUK$(O~{)XZr4 z!44)77LbDJh4Yy-n1TMC-mrkldAjjjrY9^Q#US6yPoFTIiIWB7&*_abn4GqE&SR2i Y1{*M)aW+t!5m3T#GL!vu@kLCp0nJir2><{9 diff --git a/public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js b/public/js/discover~serverfeed.chunk.8365948d1867de3a.js similarity index 67% rename from public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js rename to public/js/discover~serverfeed.chunk.8365948d1867de3a.js index ed30efc2f88164fc646bc142750fa9370e4431e6..fba06dbcabb3fc7afe1c302b8364b4bc51fb0c10 100644 GIT binary patch delta 235 zcmcb9iT&M0_J%EtHmWsfi_u!0mDPycv~F<|?&V~j0atR|KQhGyI4FEZX`WHmJ~GB%mc zc$v|c#njZyX!`!sj0xK}U1rSXX8|dj?syYq$umYS7EmBe54_LlIDO({#v+Ku$r~5R kO;32l$O#P$ko5M_CyesUVAEjIT+BuW#t^-%FBo3}0RAd;p#T5? diff --git a/public/js/discover~settings.chunk.732c1f76a00d9204.js b/public/js/discover~settings.chunk.be88dc5ba1a24a7d.js similarity index 68% rename from public/js/discover~settings.chunk.732c1f76a00d9204.js rename to public/js/discover~settings.chunk.be88dc5ba1a24a7d.js index 959ab4d3fa5f9feac1746e6e89bb4921a638e177..76bbfcf7d1cd6fc94f413c76160a564ae0ac7103 100644 GIT binary patch delta 225 zcmeDB!oKh`d&3sSXQ8a-W@g3)lLHS=+Fl#RSjjZ~q!}aMc8*9!Lw;5h6Ej1I)P)Sj zK(^@*%NSL*ugPbO@?|kIFgBj4`*goMaqargi(2VI0Kyj%3fnOP&rYrqqd;tK- CjZMP< delta 355 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 D!O$kr diff --git a/public/js/home.chunk.f3f4f632025b560f.js b/public/js/home.chunk.f3f4f632025b560f.js new file mode 100644 index 0000000000000000000000000000000000000000..76c5f028a02ab58d6d219fb46075ff11c339fddb GIT binary patch literal 240076 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 literal 0 HcmV?d00001 diff --git a/public/js/home.chunk.351f55e9d09b6482.js.LICENSE.txt b/public/js/home.chunk.f3f4f632025b560f.js.LICENSE.txt similarity index 100% rename from public/js/home.chunk.351f55e9d09b6482.js.LICENSE.txt rename to public/js/home.chunk.f3f4f632025b560f.js.LICENSE.txt diff --git a/public/js/landing.js b/public/js/landing.js index 8a3b2322cdd1f7df3a19254143461b144b5aed7b..c9da7051de0f96448bb51dc1bc12745859352bf3 100644 GIT binary patch delta 276 zcmZ3oi2LXw?hOl9uo#<}nN4@pV3eQiP#(K^{n0YU&C^${W|qszPfW?oOSe+WNvx_= z(osrDEJ@T&Dp0b@*DEc`+3dJ3FND?7(#*nOx}yxE-SkX$M%(Fn%#6Yui3VvFriP}8 z)90}=rXzT~Y>Zk6o;@4m)#>|L7{%a1+wZb4vazrj1I?T+D930#y?~2Rkr`;gc3V!y zXFM#1hL#4?)x{Y7Sj~)#49uq&N-+9xD#0wZk6UOXG))#>+H7{%a1+yAmKvazt57@3%w zPF6HBnr_9(sK5-gV0#=V<1-!>BNHRb=^kQ?eym0YhDK)77m70aa4Nwp+x|g}F;9>c zq{wu7q6(ur#Ld$SRT&+(U(jF-VPpYmo9-yYC_kNFgOLkpo~bEVx_z1!X{m!2Mo zaVL7@1eo^ZkpmZwYdq@Fvws3E`Wv*GL`=L?Qct}f&->K-ygGPw@Z!&j{^2h=4eDk_ ztjd=5ut{rZ9`p~Fc0R}GCY|+XXL>lAd_wFBWBh58u21V=0ohx(2*nE&b1CT$^=yW; z>M>phkHYznlb~zk;_>{3u_8#D@{e@S7x+YB!y+(b14ihLFdv9&vvHQq!Zd&>J0a+)Hxk~ z9V;-y4fU2mmOC>}E~_jmB6*QlLOLpo(gD}!JC~kLCVF^&Ik^ys9e%$2^24C~(sZD| z-fqL$8M5PFqA=9EoQ1RxC(v$sMOj8%+Su7Fq-*H!^nDl1rU#TXZDS#M_lQXt{@6|C zF{HB!FzJqArH zeGH#H47H1#wpZ?d8Mc^E1|lxX`!buQhTr$-B|T4Y636{}@1yHc zrEg9_^|4mT}8%p|L6lvqSPzZkacuO+-^O5zkhHqV3)QiL`CA}yYwW= pq(VI28NlLEv07Q=@Yc5->SnVJHF&u`)c+c69Ss&c4R7~<`v)5%5sCl+ delta 908 zcmZ8gJ&zPO7}g4LaG*Ou0|kg%rdZioGqT5Hd%SzTBtk?HNc@4*N_hMMBj#o+c5=E_1qO!19c8wXNe2iIhp5@W;E!MCj^`Tp7H2(C^DZ% z>s`TzZ??vW22uJ#WK`r{av>6;DAThPt*7#D%l%*wOg;KoGUJZW$ z1NT}eEuD@`s=`B3g{F`QjTtnZD;~WIOoh{}b5BPjH#|R|oUx=D-kksSiZyXwcELVg zukFbRsy1(l>`-s>%%**H6xvm9SVoCY8+av~E<^uy-*;8H>H|uews9f(Sj5B*e{UyK zIMO*2FzF7%!W_1X`4iiXR|Ykt#pd8go3>5py0$ZRxN$)tn;zRPH2wY7hmFAJUD}1C z_DQ(?sH#19T3>i@&#p0HS;+DHGtDY3tHz^dRoCP{6x@JL-CA${U*CWj+D8|!kL>B4 zi*H9aobl=L)Tb-d!9~-A@5gsG7Uw>#ksI#rJb>7K+qpKf!(Th>oSP;%fp*{TT)hS< z{cs#i4|-krZWWfjail$D3vkSK4=y_bQ7=qGmYWHQ!1nX*-nSO}v_%#YfuHZwBZ$dZ nSUwIwg*S=Sra~UCW80x_GU-r*=gWQfX~32+V4yU--Tm_e?#K>g diff --git a/public/js/portfolio.js b/public/js/portfolio.js index ce1e7097f3d39f860b828a19bffb63d3c9efbcdd..f28a7af06b642500909073b5d56234c5e26246b1 100644 GIT binary patch delta 75 zcmccqfa&G~rVXkstj3l=F!|zI$;l;!*_+q4^eZzOPZpdZHaTww`{t+><}9q{rskGr co2RXjVP*z0CkL#TVF78HT(I78vgigU0A+j|kpKVy delta 70 zcmcclfa&@JrVXksEJjAAW|I@wNKP&(%-+1NrC)jSo_5>KRVyS|Sj~*g3=B3;Un9c| TVNQ-;DFso_yWU~4_y#8c(3-6;f(;B_JfHAK`zh>eA)ja;RL*vQ?9 zkXIkq+FT*i-6>dzjhDjeI53JJ*eF<7x|6lAeDC|^dpwJOJBxo8iR!Wnn7gtf0->0y zRCn^bdFwb&;%n5G(!sG9r18%-S=D!IkwJUxP5EX9l5YfmG78a_~9}N!v*M2rlMeAd!$N zaCtUY8QIKu!ebIE0!HzK!J-}-fdSIt`&_b!Aeq3jfN@)L9!RtTDC^iL3_+*IUFAn4 z$8l7|5}b1tvF^tq(H&L^4jbFaS^-*ZvnIo*CCKAy32v#0F+KZgOVCEcDMN4$#`MWN z%v#j=V`CaJwplTVpuGh+BD7hhQG&E>HjqsSf0jc_laHs*q^}w1g+oN1_)>wbR~ucE A4*&oF delta 590 zcmZWmOK1~O6y?49rdlKzUHAc^7%goY+s@2;^Rt$=BeMteIJpS3@sy z&5Ec7mC)sn{1u;U_%zdG!Vw0pkH8R?S7}}TXwoIha?`njoNv*uXPGN>j?cEpbJ<;` zH~R2Xhn~dJJJOHSFX+*UN-bWf2TSvf;C?jK9$RQNtF?MGb`ZY24==`1ODXe(+}#E~ z`AJ6QOowjd!Eglrz6S&NVT;80ZJVCP?j#wO-A(%OL^cS+=3=8gUa7UBusztE4c1rLEjnX8IGHJDMoHyfX5nGrk<*|M?h$5 znB$uvn3aF>aGQXzg!<751wslkBtP8$o zTb6C&@+g$$+bJkO*47QrNL8G3uP1YK$Fx(~%2mjoVrnrGHwmy@gnmucB#_L(3?8`y z-*O(aS1}Q>@Ht>8S>BDs>x)_fj?Vv!bEq2iRqn`@V20 zs=w;?9~=5W4a#U!2ixpnh6`J|K23}uO`8cw)m4L)dI;O2tsa)7yDXZhoInq0P_OJp zWGvX)w%CnHvogEz1>>s4#dpJ$QU-LGgS9a}@{z`Dv}(WSxukn0l;z`3oC}%q9ktho zW5xrS@*Mj84Wm5e%TVw9ih@e_BV;@b9bwb*07gy6Nr@`z9K`h}Xa(&&vR1mtp$CEXku6g*u%>hw%!Isen=d delta 559 zcmZuu&r4KM6y~0DZ|FlygbJas9+VS<<9)wo=Cw#)jZA2QQ!Xs4@s%%-oN;s}s97bn z4TPXW3!x}#>ms_)Lh=tNa}zCs7_v=2AZcsRea(QImgoD<_wk+cy?)!dv(b6?9(xI+ z3GQZGixvd-(eg8PQ#BG$2W}3TNi~&(4?Wy+rS)L*d6Oo&0&Qwz7}gkdZ33st8XTqN zvtYDepD&f7a;<7oFakNl+?_ukg5zyxS~RWqz%g|{4~+?tb){p`6O%ns9V;-hhw3j_ z_c=3JOBJh$TB@=*e`9toO3lnxD|6wUVmT~Dga3z5?!vdK(PF%LB57VOQ(=l7q}4ZY zK-HhZ?~wc1j73Z1cuK7bd}VOo(K8!Hhki|92Wc>;es*Edi=K|Q)Q=ZXOGpl2L4A}s z$-uKI4Dkp(?nhU|a)+z|99N%gq`kuRrIl8<1AGU<_obhu(HxEmM`qI=H3o57{mEel zgePssB|nd2-1E{ltp^yY?|J;PpT`zdIFG%wFGPdudR$_cFrcBUxYaVyI7H3PR>H9vy GZ+`)N6s>pw diff --git a/public/js/profile.js b/public/js/profile.js index a9fcd40df7425c630591ac17440d6d66e0696eea..0c28afe41fb385927f3b46dd472bdc5f5c6e06e0 100644 GIT binary patch delta 455 zcmYLF%}X0`6lC1rVhlYWn*YJGL3e&rB9Mwnj>8rDvt zgI7in5v;p%J74+;*+m=Q(8O=U7;0mRuIp4gfycVaxv2F!(!7#^AhyJ3&8gVTbe}hF zW*5BO-YGL<_Dm+ded*r$xj04F5aO@1IBI8xp$Ex4#5?+u$GF>+z6LeFW`16ze_1rs zpFGA~Pasvd(DkxKc5p$L+h~4)Y{47u&f{JX6vMDeIhCRZc;$Ha^Xx7@)QL83e@qn+ z7M)2V&mP8kqJRhlBM_iu0TYf$Qn<~eSfipys`9owHjwryhNh@g-op@6C0VAMpNR7J zzo3Ho{f0!#zcI>mS(d1}kJ0B?ymKGtZsr(S<(y};hz7x~mukldQS1bn$7{PtpXgMo q(OnUPe5{CIo#@yFsCI_V|IrpbpjzaXQgy9Fpw3m(m delta 478 zcmYLFze`(D8094InHnTkEY>0jQP5U2+;?+taublK1v@mNSPCsDs0fmwDO5VNMNrVi zr3o26oRWYF!M36lFI1@(ap-8o$w7ldK@j`{I_P^QI)3LD=R1e9p46OY_9s^Q4WVR& zN?EiMvoM;eJ@o153tYNrKZ-_g{QdZ3A~_WZ1ju>`q5lWb1f6<~R%A|=eVLwvdR>U) z$h75;YVPGDNh(P^rq~Rsb$S?mev|WXNM-Wx+URT%)4Dm0H_(4SV$Dtd1;ljm6Si92 zN-?|6S8qr$sV0;d{X)B+(Lkvj1|L*F`jry$uM1QBDS@R zd6y};kdmwwymBd~PghZZ5mZvx(6Di%RdlwBAssBhZnM85VSj$1-Lw~H)VqUT>e|K! p)5#)-awc*pUAF_}Dkt9Bobrxs`!~+ZooveBH&0NH9q8M`^J^w@mp%Xh diff --git a/public/js/profile~followers.bundle.731f680cfb96563d.js b/public/js/profile~followers.bundle.5deed93248f20662.js similarity index 70% rename from public/js/profile~followers.bundle.731f680cfb96563d.js rename to public/js/profile~followers.bundle.5deed93248f20662.js index 51805c382e38769537647ef7ba3b16dc547c3648..171ddf72d22d9d134c4aab433171c92a15a28e79 100644 GIT binary patch delta 144 zcmX?lmGQw9#tlncSWQgK3=JkLq)ATxU{So;)YXe^vZA@lW-gy&9IR#r#>PfKIm69t zp=&u=EG;cfCr?aOn>;^Okp(2RnK6Dh3#+-gp_#$tgUP-uhK3eqlMf~&Y%WUK3lm?E jE(NutG0S1{n>1NTsH>QcPY<2?#1_#!gl=SJ}MS`xpm{rLno; zl7I#trQ*tQIB)#)gvvLnJ3Vn3`?A?b6ORS;$8yNJ+ayFTPl#M6aMIKP@vS zRWBv8xF9F7GCnUcH`N}yuoXhMv^cd0q*7B`DJCzb1OzAFv{IkE)`w$rpZ7ivR!d`Z zL*vbXA@?|0OpOeTfeuieEFG%=5ufZ38?^aCTqX;vsezHP$z;1^UlvnSGo#J@$+E0a hv5gs0P`58kcbqJkc^a%@GEf*|;KFnVmfBcNYXBEbPSpSa diff --git a/public/js/spa.js b/public/js/spa.js index b496ba43251a148ddfce642a4907f3d58aa748e7..baee816fde3938837fdd8bc3557024567fec61c9 100644 GIT binary patch delta 365 zcmdnDf#>yho(;*lEapar7SjV`8PzARKNUJTec;1RA2@145kOhFa~V*ie`Mn%xY?4X>2^*I-YSFN3wyTp@n&> z+4h_9j4sS9hK7~~(-o5${aDS6j10`DXQwjya4Nyvvi)Q-<1-GZcE=I4j?(l?Jx2ek1;}xgvddB3zTbDC}iBOP{<^=8~~d8aC86w delta 372 zcmaF8ooC+$o(;*l%oY}A(=Qe6wb|WazAlRZmu_*e4*80^8O}{%}UkV zI8X#++6)a@jf~9AEG92BRGaK)5;}SPIl0XnjzlnScHX&~dGdly&YJ`F);F_QSQr=p z)fi0=$YbQ0UKPdYFnzf*qX37Qg@LI>l7-3i{mzW(2%eq`qZWdf;KF!y`U59MF}Tq7 ze@=|P(kv!MCZ>}Ex2sQF&of;&o>2kl2ZQN>@r;4nzsE7YVP-WkH#aq!{vwHS8b^wW zxlyu(LF)FlWJVWe79$fQ%jxIR82wm{3=ECTrW+0o9#Yz^Kj&3>%>O zhoy{;+s_p+hBAWn18tU{{-K1ClLZt6(;pTxI&Gg?#CVJmY9?4G$cd(4o$Y5!7`LA- IVUk-80OiDfUjP6A diff --git a/public/js/status.js b/public/js/status.js index fb8eb6b7e86a03c42ac511fad336168ad25accdf..0bced46dd3d20d9e7b270163514ab76dc3e32da8 100644 GIT binary patch delta 1028 zcmZ8gU1%I-6y1LGn`o$47gBukL0TB_q9THdSVXB99i>pHV4-Ls3RM(EioZ}uP18WEzcb1127LSOx%ZxP z@8N#mycJzIADv&olii%Gsxp~>pq+Lt;&D2cMD2(B@jbV-)H!T*2t~`W#_5KDOl;OJ z+9zzscIRl zj?-a@u30!~xneHw+PSG>XNPy=cw*bSjB8Dll1cHjnfJcfn>aK&YdSNQU{AHV2V8L@ z%DUn@8q_@)JgXCRS9wp*IIf*{tOLbtaabBDmFVNcc$U7th`5)(+31fQ#12N2G8$;{ zBeeK$Jc1zx#a}yw5%6kqB$=i9K6H33=eJYOEbj4N?ZZ|0r;cE#kI5<8{w+#0){iFd zpNm`k;TP}=BjY%B`}87|sAv3;cwc%a2byogPI^v26Fn10)W7gLUT)=7P18KXZ1)4R zKZ9?4innW8oSZpfPo2yPj``G_&?!uquKB=;ypS!5GX=-Hw7QQzzl4Op?<#&0cwN^r z61`W;_S3cB(M`WDp)qDklbN)fHbiYD&@Q^3mWD25#apx^f#0(e#9BS&E{B6bV|rjpj`#u>yHB8e0wu##gX6R_O_5 z8T<{+bYTVE|Er7L!RRJ5eFyjRs;*0Fb*+c|##KDZLR8Cm)2}Gha~Ew~7}3}o2K~qy zo&c|BR9UK~`EY~anAb4CDSB5|c-BK@;t=!~?xL6RX+{rh=1YU7{;sM|n+Aq^@#%#cY3yvFBrDk?A_t zUK9K<8i+81PSvrW%e)(jRECxtSTB7NXN7gz;51t4Y#poR!$V2anK(22H@C42<3cSu tvz1ly>sEw$8faiYmNyA$xzH4vkF(yIVDQj`jg}2d1|zJ85TbKMG*wX| zA)+K3t_L1O1VKbpKao^a;(>ao7l{Wc>MyCZszgJGoo#hIp6}js&pqe69oL*MuR0HP zU|$6@bZQ%bhuZIB1D*CDvvUz%2^&lHL~G4Rprt7o*=q(8-llLoToaCj6U`-|U?fr# z4DAlo?X3?b!qNI-)6?>QM}^s1V(yPMn(=t58Yr~6)O8H`$=E;+|GgZ!j8-Uc(UQB! zO@8SWdHD)#X7KSdt56M5^;CNT)bGfPVxqCIJpee^vnau4#4&Qek_e6`BJJ}{~tEaXjm`)F~;JmZoW(;)5jeP2Bg^TxH z!j%F+QT#f+`G6w6{WW^P4?M>845~SoT6gdf^7!#5cvqNV7?QQGP|#btWwwL8)^B#YQ08D6R2e1oQ2u;Sp@IQK}(fqi5eRCqvf!mem#>{f?DP zP!&~6E)Du<@CV#!i(R8w&qJfw1kvaBsYqW(9XsTUE%{1u2DAsY; zFH|y7m32*`Ex)lw)MQCE=;9w#WfuN((fJ1&6a8g|B%cYeHKHyVvTF70ygR^DWGkvt zrg=)j74c$4lO%=QBC8Y?NmlgnN&kL;_b_&Hl3?jsUsFK6Q(2~H=w&{Gy0h3)dOC&m zk15e@7b_C2Ym>%Gv$9yDU`f&J99Aq^H(>A-shN#SS%h>xsmo!Nbi>8^14$0Xm`G#D!>@T_^L7D&n diff --git a/public/js/stories.js b/public/js/stories.js index e38874d8ef59090d7b352ad949113541f47f1c33..5455268298a882e6f82b36d1155233eb0fb864b1 100644 GIT binary patch delta 415 zcmeBu#`yLrx60&38>$j9ASr4J?d6 z@{`kkyyljV!x9LzF7aQXAvHNaJscr$F5O2ACL3Fwk(!yF zQDS9jP?k|Ud11Q5WRDC%gxbUm4+Ljp#${xV;pDxUtq6g{43o)qS&|5W`B^3i&h0E0 z1ZRGh+GO8sE`&gQwkd*>m?1KGL$(n@;8pfc1ZQ>5T?A)BZWw~|E;kXuS)C(0xg>87 zLck*5liAeLabgR3eXU{n?ISw$i&iQa%q7ttEq{Rx%uSd1-=MfGKH%l@^(e) Y&`8NDa$wVeC diff --git a/public/js/timeline.js b/public/js/timeline.js index a0ec8756310e378e673bdf4ac86d04ada613f94e..ed0fc12141bf522d7956e39d2ec2ad665cf561a4 100644 GIT binary patch delta 539 zcmYL`&ubGw6vugQXU))B5o{~1HY6#SDph7@Hv6LorHu!LRuZA!Jgio$5TaBoCQdz+arpYQkk-mJFx_6Dyn)|e|T zj~ag=+pN|Kz0FGfJT}W~ule3-JK^1^0u~ z*Y0K}?wwg&DCzbGNS|Q7@9b7mRQ(AT>3s)|@Ic)3a#k*&tq#oZ-wHqAR3g^s>@WC4 z*nz?Sx=wYVoG_K|3q|k;WaM~3AN&n33>LmR^lTdxE$u*RFSXuS7eIonr?rU94vbzxbad&ALR0qp~(Qo>d=%5jF-LP{$3;WTX|uwQ@VXaln> zUr=8UUNRjig&XNJ`(@XA@LO-}UL3-#8U2l*U=SyD|51E}v43#{#|+!^cNG%DxL}0F x)EdE)X1I7K7bC~;xWS?zDh!20I4`w_F|SvWI16#XFowx+RB9)2l5V9i{|}fbr#%1w delta 601 zcmZuuPe>GD6z9G9#+}m4Vzm%=+c83r7-m1enfX?N(!F%ha-lALSVdMG&+RbtyUpT_iko2rNWg=pb7&v({eXo!aIA zvM!~|Gyc%`JFE5b_I`iyT8t??BV3_aIpGwF;eT`~$3?(4@3OCv)^_8{&p5 zZAV^~f~*=$HM@lx^f$XtHTX>DccAOvNKNlRF{Y&*kIo#%)3mY)r_`lgSk#!{!trm% z2Ig%{u|UkGTph+L%g9h0kPdBhVuVG3mRrY7oO*Edd%>lF)2JgA?+(_P)E_C zdl_k`=|(G#>aH!*f|fdPM0K>`Szui74CN2tIi1^S?uO84(@Ji$N#lh4IQtg-pkdr3Q)kjT$g*k(Z zd6W}7hM2saIl@mWK-Y?0g1{4CID!LZbbFVSvyoUL03XUYVGrk|mo1?wo>f<{UiaOr=Yg0gIiZFHi0*CPL^gxaM6-l_7cG zB$vj?72l(z@yn@elQL)}iIKW5tdxq=TG)v4T_Di-`_zpo+LJY;9#EyH6sq`SefqYH>M2qUN+HX>Y+xQ;|5nfBoS^!HEsB1}a#xD%DsTGGuReX^7*UGkb?U^k`jr z=dmKWesK4}{;g^M=G_N3Z{Pb$58CZ-zy9Ljm{XtLk&jIas39d60h~Gmt)5D?P&Otp z&STzNLqKUh`<6pRAETr3_{G{^t@zfXE24;E2kpxSrM30(1Ps&Z`3 zMS1> zB-m7D)Vuv-qxR@)xy%$h_TV_zHmncj%MI$9e~}a8T8I4Yl=5> zYjhu6WM{QKJ2Wel+8J{mNtWZ?N)@tPL?SLQmm)A^^uLjq>23vWhYqu?7 z_A;G`PKyzxQM2vaawBy<@x)t0Hr(Lbw4ul)zP Cu?S%R delta 1848 zcmZuy&5o5t5Y0tEm_g_AJ3?akK-7sLLs$P*mquN=^$ljx-BlI9pP2+*8WZ9JDE$H^ zu1!ql0SGH!LfsmlfrZwc`AFb8i@T}n@6@TPbLu|ddA{@8-eOWs(o}qRscQ6<9D;)q zTdasF(D-ig!Q_jF*bB5Q3N6NfAqh9|HHfbo#_x;WDOYt7bD@?w76f~rrBQ3_Q{mi4 zJ#~5PoVq>%_SzkU3V;|>1Y~k0WS>iJvFKqv^~2)5*oC4Kux}xGV`rewcyQp-+4b9Z zH>PPCw@zOfH&5S~x+_p)jo_MnM+a7MBmdJiWb`5+#m}C>7D_n47ZX zSk;%L5^@^9oVhavjugF8CGyVR0ys646fKkpY5aZW?u3q7@D6;A%w7UwRu`m)K!L55 z5ze}s8e_-zq?bNZA?9o+bC2^^v7i-oq)A;l3prmF(D-<&##S-0Y)FItt_6@m%eB$)t{jVt@+8VhxAKlaY6DdftvL^B3X>!X-6xxOHb>=%%(af6Hm{D)Hnx_g=iYOS*?A`>oB!uS^=0TQv>a*uk{O z$!Nd7biSE2-p(1o|u8n?+qeLCo;`(^*#ktM6j;SkMofKy|A^!;5#VhYN9tbc1w)0 zCGvV=2KF`uND_Q8-$^OXykU(7h|sjv@#E&z7Y?Cp*Qq3~9D1>1W6+mBnTNcddqFz` zxuh678eVMGE{4<)YgRUGr^9D&Tw7d)1PQHt&E4*8Ki=bb`sQ24?0=cn?tHApP6-Er zDp;B9f1yNGF##5{i9GSFaQq})j%V_|->ykp Date: Thu, 1 Feb 2024 23:20:12 -0700 Subject: [PATCH 344/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c73b4e37..d762a135b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ - Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) - Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) - Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a)) +- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 622e9cee9736e319285384bdd9194d4a8415828e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Feb 2024 02:29:33 -0700 Subject: [PATCH 345/977] Add S3 IG Import Media Storage --- .../ImportUploadMediaToCloudStorage.php | 54 +++ .../ImportMediaToCloudPipeline.php | 124 +++++ app/Services/MediaStorageService.php | 445 ++++++++++-------- config/import.php | 6 + 4 files changed, 431 insertions(+), 198 deletions(-) create mode 100644 app/Console/Commands/ImportUploadMediaToCloudStorage.php create mode 100644 app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php diff --git a/app/Console/Commands/ImportUploadMediaToCloudStorage.php b/app/Console/Commands/ImportUploadMediaToCloudStorage.php new file mode 100644 index 000000000..bf23794c9 --- /dev/null +++ b/app/Console/Commands/ImportUploadMediaToCloudStorage.php @@ -0,0 +1,54 @@ +error('Aborted. Cloud storage is not enabled for IG imports.'); + return; + } + + $limit = $this->option('limit'); + + $progress = progress(label: 'Migrating import media', steps: $limit); + + $progress->start(); + + $posts = ImportPost::whereUploadedToS3(false)->take($limit)->get(); + + foreach($posts as $post) { + ImportMediaToCloudPipeline::dispatch($post)->onQueue('low'); + $progress->advance(); + } + + $progress->finish(); + } +} diff --git a/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php new file mode 100644 index 000000000..f884e7f2a --- /dev/null +++ b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php @@ -0,0 +1,124 @@ +importPost->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("import-media-to-cloud-pipeline:ip-id:{$this->importPost->id}"))->shared()->dontRelease()]; + } + + /** + * Delete the job if its models no longer exist. + * + * @var bool + */ + public $deleteWhenMissingModels = true; + + /** + * Create a new job instance. + */ + public function __construct(ImportPost $importPost) + { + $this->importPost = $importPost; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $ip = $this->importPost; + + if( + $ip->status_id === null || + $ip->uploaded_to_s3 === true || + (bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $media = Media::whereStatusId($ip->status_id)->get(); + + if(!$media || !$media->count()) { + $importPost = ImportPost::find($ip->id); + $importPost->uploaded_to_s3 = true; + $importPost->save(); + return; + } + + foreach($media as $mediaPart) { + $this->handleMedia($mediaPart); + } + } + + protected function handleMedia($media) + { + $ip = $this->importPost; + + $importPost = ImportPost::find($ip->id); + + if(!$importPost) { + return; + } + + $res = MediaStorageService::move($media); + + $importPost->uploaded_to_s3 = true; + $importPost->save(); + + if(!$res) { + return; + } + + if($res === 'invalid file') { + return; + } + + if($res === 'success') { + Storage::disk('local')->delete($media->media_path); + } + } +} diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php index 128001de2..216e37497 100644 --- a/app/Services/MediaStorageService.php +++ b/app/Services/MediaStorageService.php @@ -21,28 +21,40 @@ use App\Jobs\AvatarPipeline\AvatarStorageCleanup; class MediaStorageService { - public static function store(Media $media) - { - if(config_cache('pixelfed.cloud_storage') == true) { - (new self())->cloudStore($media); - } + public static function store(Media $media) + { + if(config_cache('pixelfed.cloud_storage') == true) { + (new self())->cloudStore($media); + } - return; - } + return; + } - public static function avatar($avatar, $local = false, $skipRecentCheck = false) - { - return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck); - } + public static function move(Media $media) + { + if($media->remote_media) { + return; + } - public static function head($url) - { - $c = new Client(); - try { - $r = $c->request('HEAD', $url); - } catch (RequestException $e) { - return false; - } + if(config_cache('pixelfed.cloud_storage') == true) { + return (new self())->cloudMove($media); + } + return; + } + + public static function avatar($avatar, $local = false, $skipRecentCheck = false) + { + return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck); + } + + public static function head($url) + { + $c = new Client(); + try { + $r = $c->request('HEAD', $url); + } catch (RequestException $e) { + return false; + } $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) { return [strtolower($key) => last($item)]; @@ -55,224 +67,261 @@ class MediaStorageService { $len = (int) $h['content-length']; $mime = $h['content-type']; - if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { - return false; - } + if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { + return false; + } - return [ - 'length' => $len, - 'mime' => $mime - ]; - } + return [ + 'length' => $len, + 'mime' => $mime + ]; + } - protected function cloudStore($media) - { - if($media->remote_media == true) { - if(config('media.storage.remote.cloud')) { - (new self())->remoteToCloud($media); - } - } else { - (new self())->localToCloud($media); - } - } + protected function cloudStore($media) + { + if($media->remote_media == true) { + if(config('media.storage.remote.cloud')) { + (new self())->remoteToCloud($media); + } + } else { + (new self())->localToCloud($media); + } + } - protected function localToCloud($media) - { - $path = storage_path('app/'.$media->media_path); - $thumb = storage_path('app/'.$media->thumbnail_path); + protected function localToCloud($media) + { + $path = storage_path('app/'.$media->media_path); + $thumb = storage_path('app/'.$media->thumbnail_path); - $p = explode('/', $media->media_path); - $name = array_pop($p); - $pt = explode('/', $media->thumbnail_path); - $thumbname = array_pop($pt); - $storagePath = implode('/', $p); + $p = explode('/', $media->media_path); + $name = array_pop($p); + $pt = explode('/', $media->thumbnail_path); + $thumbname = array_pop($pt); + $storagePath = implode('/', $p); - $url = ResilientMediaStorageService::store($storagePath, $path, $name); - if($thumb) { - $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); - $media->thumbnail_url = $thumbUrl; - } - $media->cdn_url = $url; - $media->optimized_url = $url; - $media->replicated_at = now(); - $media->save(); - if($media->status_id) { - Cache::forget('status:transformer:media:attachments:' . $media->status_id); - MediaService::del($media->status_id); - StatusService::del($media->status_id, false); - } - } + $url = ResilientMediaStorageService::store($storagePath, $path, $name); + if($thumb) { + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + } + $media->cdn_url = $url; + $media->optimized_url = $url; + $media->replicated_at = now(); + $media->save(); + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + MediaService::del($media->status_id); + StatusService::del($media->status_id, false); + } + } - protected function remoteToCloud($media) - { - $url = $media->remote_url; + protected function remoteToCloud($media) + { + $url = $media->remote_url; - if(!Helpers::validateUrl($url)) { - return; - } + if(!Helpers::validateUrl($url)) { + return; + } - $head = $this->head($media->remote_url); + $head = $this->head($media->remote_url); - if(!$head) { - return; - } + if(!$head) { + return; + } - $mimes = [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]; + $mimes = [ + 'image/jpeg', + 'image/png', + 'video/mp4' + ]; - $mime = $head['mime']; - $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000; - $media->size = $head['length']; - $media->remote_media = true; - $media->save(); + $mime = $head['mime']; + $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000; + $media->size = $head['length']; + $media->remote_media = true; + $media->save(); - if(!in_array($mime, $mimes)) { - return; - } + if(!in_array($mime, $mimes)) { + return; + } - if($head['length'] >= $max_size) { - return; - } + if($head['length'] >= $max_size) { + return; + } - switch ($mime) { - case 'image/png': - $ext = '.png'; - break; + switch ($mime) { + case 'image/png': + $ext = '.png'; + break; - case 'image/gif': - $ext = '.gif'; - break; + case 'image/gif': + $ext = '.gif'; + break; - case 'image/jpeg': - $ext = '.jpg'; - break; + case 'image/jpeg': + $ext = '.jpg'; + break; - case 'video/mp4': - $ext = '.mp4'; - break; - } + case 'video/mp4': + $ext = '.mp4'; + break; + } - $base = MediaPathService::get($media->profile); - $path = Str::random(40) . $ext; - $tmpBase = storage_path('app/remcache/'); - $tmpPath = $media->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; - $data = file_get_contents($url, false, null, 0, $head['length']); - file_put_contents($tmpName, $data); - $hash = hash_file('sha256', $tmpName); + $base = MediaPathService::get($media->profile); + $path = Str::random(40) . $ext; + $tmpBase = storage_path('app/remcache/'); + $tmpPath = $media->profile_id . '-' . $path; + $tmpName = $tmpBase . $tmpPath; + $data = file_get_contents($url, false, null, 0, $head['length']); + file_put_contents($tmpName, $data); + $hash = hash_file('sha256', $tmpName); - $disk = Storage::disk(config('filesystems.cloud')); - $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); - $permalink = $disk->url($file); + $disk = Storage::disk(config('filesystems.cloud')); + $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); + $permalink = $disk->url($file); - $media->media_path = $file; - $media->cdn_url = $permalink; - $media->original_sha256 = $hash; - $media->replicated_at = now(); - $media->save(); + $media->media_path = $file; + $media->cdn_url = $permalink; + $media->original_sha256 = $hash; + $media->replicated_at = now(); + $media->save(); - if($media->status_id) { - Cache::forget('status:transformer:media:attachments:' . $media->status_id); - } + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + } - unlink($tmpName); - } + unlink($tmpName); + } - protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false) - { - $queue = random_int(1, 15) > 5 ? 'mmo' : 'low'; - $url = $avatar->remote_url; - $driver = $local ? 'local' : config('filesystems.cloud'); + protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false) + { + $queue = random_int(1, 15) > 5 ? 'mmo' : 'low'; + $url = $avatar->remote_url; + $driver = $local ? 'local' : config('filesystems.cloud'); - if(empty($url) || Helpers::validateUrl($url) == false) { - return; - } + if(empty($url) || Helpers::validateUrl($url) == false) { + return; + } - $head = $this->head($url); + $head = $this->head($url); - if($head == false) { - return; - } + if($head == false) { + return; + } - $mimes = [ - 'application/octet-stream', - 'image/jpeg', - 'image/png', - ]; + $mimes = [ + 'application/octet-stream', + 'image/jpeg', + 'image/png', + ]; - $mime = $head['mime']; - $max_size = (int) config('pixelfed.max_avatar_size') * 1000; + $mime = $head['mime']; + $max_size = (int) config('pixelfed.max_avatar_size') * 1000; - if(!$skipRecentCheck) { - if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { - return; - } - } + if(!$skipRecentCheck) { + if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { + return; + } + } - Cache::forget('avatar:' . $avatar->profile_id); - AccountService::del($avatar->profile_id); + Cache::forget('avatar:' . $avatar->profile_id); + AccountService::del($avatar->profile_id); - // handle pleroma edge case - if(Str::endsWith($mime, '; charset=utf-8')) { - $mime = str_replace('; charset=utf-8', '', $mime); - } + // handle pleroma edge case + if(Str::endsWith($mime, '; charset=utf-8')) { + $mime = str_replace('; charset=utf-8', '', $mime); + } - if(!in_array($mime, $mimes)) { - return; - } + if(!in_array($mime, $mimes)) { + return; + } - if($head['length'] >= $max_size) { - return; - } + if($head['length'] >= $max_size) { + return; + } - $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; - $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; - $path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext; - $tmpBase = storage_path('app/remcache/'); - $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; - $tmpName = $tmpBase . $tmpPath; - $data = @file_get_contents($url, false, null, 0, $head['length']); - if(!$data) { - return; - } - file_put_contents($tmpName, $data); + $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; + $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; + $path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext; + $tmpBase = storage_path('app/remcache/'); + $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; + $tmpName = $tmpBase . $tmpPath; + $data = @file_get_contents($url, false, null, 0, $head['length']); + if(!$data) { + return; + } + file_put_contents($tmpName, $data); - $mimeCheck = Storage::mimeType('remcache/' . $tmpPath); + $mimeCheck = Storage::mimeType('remcache/' . $tmpPath); - if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) { - $avatar->last_fetched_at = now(); - $avatar->save(); - unlink($tmpName); - return; - } + if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) { + $avatar->last_fetched_at = now(); + $avatar->save(); + unlink($tmpName); + return; + } - $disk = Storage::disk($driver); - $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); - $permalink = $disk->url($file); + $disk = Storage::disk($driver); + $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); + $permalink = $disk->url($file); - $avatar->media_path = $base . '/' . $path; - $avatar->is_remote = true; - $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; - $avatar->size = $head['length']; - $avatar->change_count = $avatar->change_count + 1; - $avatar->last_fetched_at = now(); - $avatar->save(); + $avatar->media_path = $base . '/' . $path; + $avatar->is_remote = true; + $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; + $avatar->size = $head['length']; + $avatar->change_count = $avatar->change_count + 1; + $avatar->last_fetched_at = now(); + $avatar->save(); - Cache::forget('avatar:' . $avatar->profile_id); - AccountService::del($avatar->profile_id); - AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); + Cache::forget('avatar:' . $avatar->profile_id); + AccountService::del($avatar->profile_id); + AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); - unlink($tmpName); - } + unlink($tmpName); + } - public static function delete(Media $media, $confirm = false) - { - if(!$confirm) { - return; - } - MediaDeletePipeline::dispatch($media)->onQueue('mmo'); - } + public static function delete(Media $media, $confirm = false) + { + if(!$confirm) { + return; + } + MediaDeletePipeline::dispatch($media)->onQueue('mmo'); + } + + protected function cloudMove($media) + { + if(!Storage::exists($media->media_path)) { + return 'invalid file'; + } + + $path = storage_path('app/'.$media->media_path); + $thumb = false; + if($media->thumbnail_path) { + $thumb = storage_path('app/'.$media->thumbnail_path); + $pt = explode('/', $media->thumbnail_path); + $thumbname = array_pop($pt); + } + + $p = explode('/', $media->media_path); + $name = array_pop($p); + $storagePath = implode('/', $p); + + $url = ResilientMediaStorageService::store($storagePath, $path, $name); + if($thumb) { + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + } + $media->cdn_url = $url; + $media->optimized_url = $url; + $media->replicated_at = now(); + $media->save(); + + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + MediaService::del($media->status_id); + StatusService::del($media->status_id, false); + } + + return 'success'; + } } diff --git a/config/import.php b/config/import.php index 2d1af28e1..f754da490 100644 --- a/config/import.php +++ b/config/import.php @@ -39,6 +39,12 @@ return [ // Limit to specific user ids, in comma separated format 'user_ids' => env('PF_IMPORT_IG_PERM_ONLY_USER_IDS', null), + ], + + 'storage' => [ + 'cloud' => [ + 'enabled' => env('PF_IMPORT_IG_CLOUD_STORAGE', env('PF_ENABLE_CLOUD', false)), + ] ] ] ]; From edbb07cc37a6636bb4e4a1931b6ebe7f25652ce0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Feb 2024 05:30:32 -0700 Subject: [PATCH 346/977] Add import video thumbnail job --- .../ImportMediaToCloudPipeline.php | 7 +- .../VideoThumbnailToCloudPipeline.php | 147 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php diff --git a/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php index f884e7f2a..cdf91e376 100644 --- a/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php +++ b/app/Jobs/ImportPipeline/ImportMediaToCloudPipeline.php @@ -14,6 +14,7 @@ use App\Models\ImportPost; use App\Media; use App\Services\MediaStorageService; use Illuminate\Support\Facades\Storage; +use App\Jobs\VideoPipeline\VideoThumbnailToCloudPipeline; class ImportMediaToCloudPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -118,7 +119,11 @@ class ImportMediaToCloudPipeline implements ShouldQueue, ShouldBeUniqueUntilProc } if($res === 'success') { - Storage::disk('local')->delete($media->media_path); + if($media->mime === 'video/mp4') { + VideoThumbnailToCloudPipeline::dispatch($media)->onQueue('low'); + } else { + Storage::disk('local')->delete($media->media_path); + } } } } diff --git a/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php b/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php new file mode 100644 index 000000000..87931bd7a --- /dev/null +++ b/app/Jobs/VideoPipeline/VideoThumbnailToCloudPipeline.php @@ -0,0 +1,147 @@ +media->id; + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware(): array + { + return [(new WithoutOverlapping("media:video-thumb-to-cloud:id-{$this->media->id}"))->shared()->dontRelease()]; + } + + /** + * Create a new job instance. + */ + public function __construct(Media $media) + { + $this->media = $media; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if((bool) config_cache('pixelfed.cloud_storage') === false) { + return; + } + + $media = $this->media; + + if($media->mime != 'video/mp4') { + return; + } + + if($media->profile_id === null || $media->status_id === null) { + return; + } + + if($media->thumbnail_url) { + return; + } + + $base = $media->media_path; + $path = explode('/', $base); + $name = last($path); + + try { + $t = explode('.', $name); + $t = $t[0].'_thumb.jpeg'; + $i = count($path) - 1; + $path[$i] = $t; + $save = implode('/', $path); + $video = FFMpeg::open($base) + ->getFrameFromSeconds(1) + ->export() + ->toDisk('local') + ->save($save); + + if(!$save) { + return; + } + + $media->thumbnail_path = $save; + $p = explode('/', $media->media_path); + array_pop($p); + $pt = explode('/', $save); + $thumbname = array_pop($pt); + $storagePath = implode('/', $p); + $thumb = storage_path('app/' . $save); + $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); + $media->thumbnail_url = $thumbUrl; + $media->save(); + + $blurhash = Blurhash::generate($media); + if($blurhash) { + $media->blurhash = $blurhash; + $media->save(); + } + + if(str_starts_with($save, 'public/m/_v2/') && str_ends_with($save, '.jpeg')) { + Storage::delete($save); + } + + if(str_starts_with($media->media_path, 'public/m/_v2/') && str_ends_with($media->media_path, '.mp4')) { + Storage::disk('local')->delete($media->media_path); + } + } catch (Exception $e) { + } + + if($media->status_id) { + Cache::forget('status:transformer:media:attachments:' . $media->status_id); + MediaService::del($media->status_id); + Cache::forget('status:thumb:nsfw0' . $media->status_id); + Cache::forget('status:thumb:nsfw1' . $media->status_id); + Cache::forget('pf:services:sh:id:' . $media->status_id); + StatusService::del($media->status_id); + } + } +} From 081360b905a1541c0e2ecc84226d8df5c511e406 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Feb 2024 05:45:06 -0700 Subject: [PATCH 347/977] Update console kernel, add ig import s3 job --- app/Console/Kernel.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4148e38ab..aadaa1f7a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -42,6 +42,10 @@ class Kernel extends ConsoleKernel $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51); $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37); $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); + + if(config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { + $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39); + } } $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25); From 04c5e550a529308d0db93a7c92aba8bdc986adab Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 2 Feb 2024 05:46:28 -0700 Subject: [PATCH 348/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d762a135b..e6e4513d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) - Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59)) - Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1)) +- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9)) ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) From 32c59f044030b875607fcca8c45723968b40bfb9 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 3 Feb 2024 13:30:02 -0700 Subject: [PATCH 349/977] Update TransformImports command, fix import service condition --- app/Console/Commands/TransformImports.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Console/Commands/TransformImports.php b/app/Console/Commands/TransformImports.php index b88401178..a5a4dbb7a 100644 --- a/app/Console/Commands/TransformImports.php +++ b/app/Console/Commands/TransformImports.php @@ -70,6 +70,11 @@ class TransformImports extends Command } $idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day); + if(!$idk) { + $ip->skip_missing_media = true; + $ip->save(); + continue; + } if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) { ImportService::clearAttempts($profile->id); From d3ff89e538076debf916386ba92b19eae81aa0c0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 3 Feb 2024 13:31:11 -0700 Subject: [PATCH 350/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e4513d5..cc42cb51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ - Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) - Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a)) - Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b)) +- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 7caed381fb784c0b228423888b69d1fb38dbfbe7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 02:40:04 -0700 Subject: [PATCH 351/977] Update AP helpers, more efficently update post counts --- .../Commands/AccountPostCountStatUpdate.php | 57 +++++++++++++++++++ app/Console/Kernel.php | 3 +- app/Services/Account/AccountStatService.php | 26 +++++++++ app/Util/ActivityPub/Helpers.php | 3 +- 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/AccountPostCountStatUpdate.php create mode 100644 app/Services/Account/AccountStatService.php diff --git a/app/Console/Commands/AccountPostCountStatUpdate.php b/app/Console/Commands/AccountPostCountStatUpdate.php new file mode 100644 index 000000000..6d5ba00a6 --- /dev/null +++ b/app/Console/Commands/AccountPostCountStatUpdate.php @@ -0,0 +1,57 @@ +count(); + if($statusCount != $acct['statuses_count']) { + $profile = Profile::find($id); + if(!$profile) { + AccountStatService::removeFromPostCount($id); + continue; + } + $profile->status_count = $statusCount; + $profile->save(); + AccountService::del($id); + } + AccountStatService::removeFromPostCount($id); + } + return; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index aadaa1f7a..7953ea783 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -38,7 +38,7 @@ class Kernel extends ConsoleKernel } if(config('import.instagram.enabled')) { - $schedule->command('app:transform-imports')->everyFourMinutes(); + $schedule->command('app:transform-imports')->everyTenMinutes(); $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51); $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37); $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32); @@ -49,6 +49,7 @@ class Kernel extends ConsoleKernel } $schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); $schedule->command('app:hashtag-cached-count-update')->hourlyAt(25); + $schedule->command('app:account-post-count-stat-update')->everySixHours(25); } /** diff --git a/app/Services/Account/AccountStatService.php b/app/Services/Account/AccountStatService.php new file mode 100644 index 000000000..0b5d45a3e --- /dev/null +++ b/app/Services/Account/AccountStatService.php @@ -0,0 +1,26 @@ +onQueue('low'); + AccountStatService::incrementPostCount($pid); if( $status->in_reply_to_id === null && in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) From ddf7f09ad41bd2b935ebfb360286e48a4c6370f0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 02:40:59 -0700 Subject: [PATCH 352/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc42cb51f..c37659fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ - Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a)) - Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b)) - Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04)) +- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From b81ae5773f82e823ce53892e4abdc8cdae509c78 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 02:50:48 -0700 Subject: [PATCH 353/977] Update AP helpers, refactor post count decrement logic --- app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php | 7 ++----- app/Jobs/StatusPipeline/RemoteStatusDelete.php | 6 ++---- app/Services/Account/AccountStatService.php | 5 +++++ app/Util/ActivityPub/Helpers.php | 2 -- app/Util/ActivityPub/Inbox.php | 2 -- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php index 4969fca2f..77cd5286f 100644 --- a/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php +++ b/app/Jobs/DeletePipeline/DeleteRemoteStatusPipeline.php @@ -22,9 +22,9 @@ use App\Notification; use App\Services\AccountService; use App\Services\NetworkTimelineService; use App\Services\StatusService; -use App\Jobs\ProfilePipeline\DecrementPostCount; use App\Jobs\MediaPipeline\MediaDeletePipeline; use Cache; +use App\Services\Account\AccountStatService; class DeleteRemoteStatusPipeline implements ShouldQueue { @@ -56,10 +56,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue { $status = $this->status; - if(AccountService::get($status->profile_id, true)) { - DecrementPostCount::dispatch($status->profile_id)->onQueue('low'); - } - + AccountStatService::decrementPostCount($status->profile_id); NetworkTimelineService::del($status->id); StatusService::del($status->id, true); Bookmark::whereStatusId($status->id)->delete(); diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php index 07a2f6236..a81607755 100644 --- a/app/Jobs/StatusPipeline/RemoteStatusDelete.php +++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php @@ -39,8 +39,8 @@ use App\Services\AccountService; use App\Services\CollectionService; use App\Services\StatusService; use App\Jobs\MediaPipeline\MediaDeletePipeline; -use App\Jobs\ProfilePipeline\DecrementPostCount; use App\Services\NotificationService; +use App\Services\Account\AccountStatService; class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing { @@ -109,9 +109,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing } StatusService::del($status->id, true); - - DecrementPostCount::dispatch($status->profile_id)->onQueue('inbox'); - + AccountStatService::decrementPostCount($status->profile_id); return $this->unlinkRemoveMedia($status); } diff --git a/app/Services/Account/AccountStatService.php b/app/Services/Account/AccountStatService.php index 0b5d45a3e..12fd3f94f 100644 --- a/app/Services/Account/AccountStatService.php +++ b/app/Services/Account/AccountStatService.php @@ -14,6 +14,11 @@ class AccountStatService return Redis::zadd(self::REFRESH_CACHE_KEY, $pid, $pid); } + public static function decrementPostCount($pid) + { + return Redis::zadd(self::REFRESH_CACHE_KEY, $pid, $pid); + } + public static function removeFromPostCount($pid) { return Redis::zrem(self::REFRESH_CACHE_KEY, $pid); diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 4e71a2fae..511ef2502 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -39,8 +39,6 @@ use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline; use App\Util\Media\License; use App\Models\Poll; use Illuminate\Contracts\Cache\LockTimeoutException; -use App\Jobs\ProfilePipeline\IncrementPostCount; -use App\Jobs\ProfilePipeline\DecrementPostCount; use App\Services\DomainService; use App\Services\UserFilterService; use App\Services\Account\AccountStatService; diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index e26f0a48c..5c9959e17 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -48,8 +48,6 @@ use App\Services\UserFilterService; use App\Services\NetworkTimelineService; use App\Models\Conversation; use App\Models\RemoteReport; -use App\Jobs\ProfilePipeline\IncrementPostCount; -use App\Jobs\ProfilePipeline\DecrementPostCount; use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; class Inbox From 8b843d620cea62b3a272f8f305574c7ad0ab25a0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 02:53:40 -0700 Subject: [PATCH 354/977] Update ProfilePipeline jobs --- .../ProfilePipeline/DecrementPostCount.php | 15 +----- .../ProfilePipeline/IncrementPostCount.php | 49 ++----------------- 2 files changed, 5 insertions(+), 59 deletions(-) diff --git a/app/Jobs/ProfilePipeline/DecrementPostCount.php b/app/Jobs/ProfilePipeline/DecrementPostCount.php index b463f1dda..74d0523b5 100644 --- a/app/Jobs/ProfilePipeline/DecrementPostCount.php +++ b/app/Jobs/ProfilePipeline/DecrementPostCount.php @@ -35,18 +35,7 @@ class DecrementPostCount implements ShouldQueue */ public function handle() { - $id = $this->id; - - $profile = Profile::find($id); - - if(!$profile) { - return 1; - } - - $profile->status_count = $profile->status_count ? $profile->status_count - 1 : 0; - $profile->save(); - AccountService::del($id); - - return 1; + // deprecated + return; } } diff --git a/app/Jobs/ProfilePipeline/IncrementPostCount.php b/app/Jobs/ProfilePipeline/IncrementPostCount.php index 1a94f1e6c..a1f9ceca7 100644 --- a/app/Jobs/ProfilePipeline/IncrementPostCount.php +++ b/app/Jobs/ProfilePipeline/IncrementPostCount.php @@ -14,42 +14,12 @@ use App\Profile; use App\Status; use App\Services\AccountService; -class IncrementPostCount implements ShouldQueue, ShouldBeUniqueUntilProcessing +class IncrementPostCount implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $id; - public $timeout = 900; - public $tries = 3; - public $maxExceptions = 1; - public $failOnTimeout = true; - - /** - * The number of seconds after which the job's unique lock will be released. - * - * @var int - */ - public $uniqueFor = 3600; - - /** - * Get the unique ID for the job. - */ - public function uniqueId(): string - { - return 'propipe:ipc:' . $this->id; - } - - /** - * Get the middleware the job should pass through. - * - * @return array - */ - public function middleware(): array - { - return [(new WithoutOverlapping("propipe:ipc:{$this->id}"))->shared()->dontRelease()]; - } - /** * Create a new job instance. * @@ -67,20 +37,7 @@ class IncrementPostCount implements ShouldQueue, ShouldBeUniqueUntilProcessing */ public function handle() { - $id = $this->id; - - $profile = Profile::find($id); - - if(!$profile) { - return 1; - } - - $profile->status_count = $profile->status_count + 1; - $profile->last_status_at = now(); - $profile->save(); - AccountService::del($id); - AccountService::get($id); - - return 1; + // deprecated + return; } } From 09ca96cc2be7910287e5064b1d590e092865cb13 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 02:54:37 -0700 Subject: [PATCH 355/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37659fba..6e0d1f104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ - Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b)) - Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04)) - Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381)) +- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 00ed330cf3aa2442c73591c34c2ae8dae8cb5c0f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 03:16:57 -0700 Subject: [PATCH 356/977] Update AP helpers, fix sensitive bug --- app/Util/ActivityPub/Helpers.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 511ef2502..5819dc0bc 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -548,10 +548,11 @@ class Helpers { public static function getSensitive($activity, $url) { - $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url); - $url = isset($activity['url']) ? self::pluckval($activity['url']) : $id; - $urlDomain = parse_url($url, PHP_URL_HOST); + if(!$url || !strlen($url)) { + return true; + } + $urlDomain = parse_url($url, PHP_URL_HOST); $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; if(in_array($urlDomain, InstanceService::getNsfwDomains())) { From 152b6eab9acd9b008ac961ffaa6ad73794c3587c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 03:18:58 -0700 Subject: [PATCH 357/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0d1f104..becf2298d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,7 @@ - Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04)) - Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381)) - Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577)) +- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 4d4013896c936a7541277ccda4a6832b31ffbcd8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 07:11:05 -0700 Subject: [PATCH 358/977] Update NotificationEpochUpdatePipeline, use more efficient query --- .../InternalPipeline/NotificationEpochUpdatePipeline.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php index 477b1f9b3..79df5aa9a 100644 --- a/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php +++ b/app/Jobs/InternalPipeline/NotificationEpochUpdatePipeline.php @@ -61,7 +61,12 @@ class NotificationEpochUpdatePipeline implements ShouldQueue, ShouldBeUniqueUnti */ public function handle(): void { - $rec = Notification::where('created_at', '>', now()->subMonths(6))->first(); + $pid = Cache::get(NotificationService::EPOCH_CACHE_KEY . '6'); + if($pid && $pid > 1) { + $rec = Notification::where('id', '>', $pid)->whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first(); + } else { + $rec = Notification::whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first(); + } $id = 1; if($rec) { $id = $rec->id; From fa97a1f38e1a0f37c4f1a59e4995e8e78ffb8802 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 07:18:05 -0700 Subject: [PATCH 359/977] Update notification pipelines, fix non-local saving --- app/Jobs/CommentPipeline/CommentPipeline.php | 26 ++++++++++--------- app/Jobs/FollowPipeline/FollowPipeline.php | 22 +++++++++------- app/Jobs/LikePipeline/LikePipeline.php | 20 +++++++------- .../StatusPipeline/StatusReplyPipeline.php | 24 +++++++++-------- 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/app/Jobs/CommentPipeline/CommentPipeline.php b/app/Jobs/CommentPipeline/CommentPipeline.php index 3b2d896af..1917ecea5 100644 --- a/app/Jobs/CommentPipeline/CommentPipeline.php +++ b/app/Jobs/CommentPipeline/CommentPipeline.php @@ -91,19 +91,21 @@ class CommentPipeline implements ShouldQueue return; } - DB::transaction(function() use($target, $actor, $comment) { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'comment'; - $notification->item_id = $comment->id; - $notification->item_type = "App\Status"; - $notification->save(); + if($target->user_id && $target->domain === null) { + DB::transaction(function() use($target, $actor, $comment) { + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'comment'; + $notification->item_id = $comment->id; + $notification->item_type = "App\Status"; + $notification->save(); - NotificationService::setNotification($notification); - NotificationService::set($notification->profile_id, $notification->id); - StatusService::del($comment->id); - }); + NotificationService::setNotification($notification); + NotificationService::set($notification->profile_id, $notification->id); + StatusService::del($comment->id); + }); + } if($exists = Cache::get('status:replies:all:' . $status->id)) { if($exists && $exists->count() == 3) { diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php index 225334304..67733919f 100644 --- a/app/Jobs/FollowPipeline/FollowPipeline.php +++ b/app/Jobs/FollowPipeline/FollowPipeline.php @@ -72,16 +72,18 @@ class FollowPipeline implements ShouldQueue $target->save(); AccountService::del($target->id); - try { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'follow'; - $notification->item_id = $target->id; - $notification->item_type = "App\Profile"; - $notification->save(); - } catch (Exception $e) { - Log::error($e); + if($target->user_id && $target->domain === null) { + try { + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'follow'; + $notification->item_id = $target->id; + $notification->item_type = "App\Profile"; + $notification->save(); + } catch (Exception $e) { + Log::error($e); + } } } } diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index b44c90c8b..e55c64f80 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -79,16 +79,18 @@ class LikePipeline implements ShouldQueue return true; } - try { - $notification = new Notification(); - $notification->profile_id = $status->profile_id; - $notification->actor_id = $actor->id; - $notification->action = 'like'; - $notification->item_id = $status->id; - $notification->item_type = "App\Status"; - $notification->save(); + if($status->uri === null && $status->object_url === null && $status->url === null) { + try { + $notification = new Notification(); + $notification->profile_id = $status->profile_id; + $notification->actor_id = $actor->id; + $notification->action = 'like'; + $notification->item_id = $status->id; + $notification->item_type = "App\Status"; + $notification->save(); - } catch (Exception $e) { + } catch (Exception $e) { + } } } diff --git a/app/Jobs/StatusPipeline/StatusReplyPipeline.php b/app/Jobs/StatusPipeline/StatusReplyPipeline.php index 35238d293..d8af7b96b 100644 --- a/app/Jobs/StatusPipeline/StatusReplyPipeline.php +++ b/app/Jobs/StatusPipeline/StatusReplyPipeline.php @@ -87,18 +87,20 @@ class StatusReplyPipeline implements ShouldQueue Cache::forget('status:replies:all:' . $reply->id); Cache::forget('status:replies:all:' . $status->id); - DB::transaction(function() use($target, $actor, $status) { - $notification = new Notification(); - $notification->profile_id = $target->id; - $notification->actor_id = $actor->id; - $notification->action = 'comment'; - $notification->item_id = $status->id; - $notification->item_type = "App\Status"; - $notification->save(); + if($target->user_id && $target->domain === null) { + DB::transaction(function() use($target, $actor, $status) { + $notification = new Notification(); + $notification->profile_id = $target->id; + $notification->actor_id = $actor->id; + $notification->action = 'comment'; + $notification->item_id = $status->id; + $notification->item_type = "App\Status"; + $notification->save(); - NotificationService::setNotification($notification); - NotificationService::set($notification->profile_id, $notification->id); - }); + NotificationService::setNotification($notification); + NotificationService::set($notification->profile_id, $notification->id); + }); + } if($exists = Cache::get('status:replies:all:' . $reply->id)) { if($exists && $exists->count() == 3) { From 80e0ada946c489497835b41fa670286af512da60 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 4 Feb 2024 07:18:54 -0700 Subject: [PATCH 360/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index becf2298d..591025bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,8 @@ - Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381)) - Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577)) - Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c)) +- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389)) +- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 240e6bbe4f57b320bb00a3b30f0a1a907c8975d7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:47:34 -0700 Subject: [PATCH 361/977] Update NodeinfoService, disable redirects --- app/Services/NodeinfoService.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Services/NodeinfoService.php b/app/Services/NodeinfoService.php index 10575ff9f..6284538f0 100644 --- a/app/Services/NodeinfoService.php +++ b/app/Services/NodeinfoService.php @@ -22,7 +22,10 @@ class NodeinfoService $wk = $url . '/.well-known/nodeinfo'; try { - $res = Http::withHeaders($headers) + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) ->timeout(5) ->get($wk); } catch (RequestException $e) { @@ -61,7 +64,10 @@ class NodeinfoService } try { - $res = Http::withHeaders($headers) + $res = Http::withOptions([ + 'allow_redirects' => false, + ]) + ->withHeaders($headers) ->timeout(5) ->get($href); } catch (RequestException $e) { From 289cad470b0a04c2836e19aa57d6566df95ce668 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:49:29 -0700 Subject: [PATCH 362/977] Update Instance model, add entity casts --- app/Instance.php | 120 ++++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/app/Instance.php b/app/Instance.php index 6a7b8e6f2..77752d498 100644 --- a/app/Instance.php +++ b/app/Instance.php @@ -6,63 +6,77 @@ use Illuminate\Database\Eloquent\Model; class Instance extends Model { - protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes']; + protected $casts = [ + 'last_crawled_at' => 'datetime', + 'actors_last_synced_at' => 'datetime', + 'notes' => 'array', + 'nodeinfo_last_fetched' => 'datetime', + 'delivery_next_after' => 'datetime', + ]; - public function profiles() - { - return $this->hasMany(Profile::class, 'domain', 'domain'); - } + protected $fillable = [ + 'domain', + 'banned', + 'auto_cw', + 'unlisted', + 'notes' + ]; - public function statuses() - { - return $this->hasManyThrough( - Status::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function profiles() + { + return $this->hasMany(Profile::class, 'domain', 'domain'); + } - public function reported() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'reported_profile_id', - 'domain', - 'id' - ); - } + public function statuses() + { + return $this->hasManyThrough( + Status::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } - public function reports() - { - return $this->hasManyThrough( - Report::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function reported() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'reported_profile_id', + 'domain', + 'id' + ); + } - public function media() - { - return $this->hasManyThrough( - Media::class, - Profile::class, - 'domain', - 'profile_id', - 'domain', - 'id' - ); - } + public function reports() + { + return $this->hasManyThrough( + Report::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } - public function getUrl() - { - return url("/i/admin/instances/show/{$this->id}"); - } + public function media() + { + return $this->hasManyThrough( + Media::class, + Profile::class, + 'domain', + 'profile_id', + 'domain', + 'id' + ); + } + + public function getUrl() + { + return url("/i/admin/instances/show/{$this->id}"); + } } From ac01f51ab66df0b4f4657e70bbe0befe2c022b9f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:51:50 -0700 Subject: [PATCH 363/977] Update FetchNodeinfoPipeline, use more efficient dispatch --- .../FetchNodeinfoPipeline.php | 97 ++++++++++++------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php index b8c79d67f..943281bb4 100644 --- a/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php +++ b/app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php @@ -4,6 +4,7 @@ namespace App\Jobs\InstancePipeline; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -12,45 +13,71 @@ use Illuminate\Support\Facades\Http; use App\Instance; use App\Profile; use App\Services\NodeinfoService; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Support\Facades\Cache; -class FetchNodeinfoPipeline implements ShouldQueue +class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected $instance; + protected $instance; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct(Instance $instance) - { - $this->instance = $instance; - } + /** + * Create a new job instance. + * + * @return void + */ + public function __construct(Instance $instance) + { + $this->instance = $instance; + } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $instance = $this->instance; + /** + * The number of seconds after which the job's unique lock will be released. + * + * @var int + */ + public $uniqueFor = 14400; - $ni = NodeinfoService::get($instance->domain); - if($ni) { - if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { - $software = $ni['software']['name']; - $instance->software = strtolower(strip_tags($software)); - $instance->last_crawled_at = now(); - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->save(); - } - } else { - $instance->user_count = Profile::whereDomain($instance->domain)->count(); - $instance->last_crawled_at = now(); - $instance->save(); - } - } + /** + * Get the unique ID for the job. + */ + public function uniqueId(): string + { + return $this->instance->id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $instance = $this->instance; + + if( $instance->nodeinfo_last_fetched && + $instance->nodeinfo_last_fetched->gt(now()->subHours(12)) || + $instance->delivery_timeout && + $instance->delivery_next_after->gt(now()) + ) { + return; + } + + $ni = NodeinfoService::get($instance->domain); + $instance->last_crawled_at = now(); + if($ni) { + if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { + $software = $ni['software']['name']; + $instance->software = strtolower(strip_tags($software)); + $instance->user_count = Profile::whereDomain($instance->domain)->count(); + $instance->nodeinfo_last_fetched = now(); + $instance->save(); + } + } else { + $instance->delivery_timeout = 1; + $instance->delivery_next_after = now()->addHours(14); + $instance->save(); + } + } } From 1e3acadefb200fbb4d594909083c5a275e413b77 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 02:52:37 -0700 Subject: [PATCH 364/977] Update horizon.php config --- config/horizon.php | 349 +++++++++++++++++++++++---------------------- 1 file changed, 175 insertions(+), 174 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index f9cfd960e..5aa37f2fe 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -2,201 +2,202 @@ 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-'), + 'prefix' => env('HORIZON_PREFIX', 'horizon-'), - /* - |-------------------------------------------------------------------------- - | 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:follow' => 30, - 'redis:shared' => 30, - 'redis:default' => 30, - 'redis:inbox' => 30, - 'redis:low' => 30, - 'redis:high' => 30, - 'redis:delete' => 30, - 'redis:story' => 30, - 'redis:mmo' => 30, - ], + 'waits' => [ + 'redis:feed' => 30, + 'redis:follow' => 30, + 'redis:shared' => 30, + 'redis:default' => 30, + 'redis:inbox' => 30, + 'redis:low' => 30, + 'redis:high' => 30, + 'redis:delete' => 30, + 'redis:story' => 30, + 'redis:mmo' => 30, + 'redis:intbg' => 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' => env('HORIZON_MEMORY_LIMIT', 64), + 'memory_limit' => env('HORIZON_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', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), - 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), - 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), - 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), - 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), - 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), - ], - ], + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), + 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), + 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), + 'nice' => env('HORIZON_SUPERVISOR_NICE', 0), + 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), + ], + ], - 'local' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], - 'balance' => 'auto', - 'minProcesses' => 1, - 'maxProcesses' => 20, - 'memory' => 128, - 'tries' => 3, - 'nice' => 0, - 'timeout' => 300 - ], - ], - ], + 'local' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'], + 'balance' => 'auto', + 'minProcesses' => 1, + 'maxProcesses' => 20, + 'memory' => 128, + 'tries' => 3, + 'nice' => 0, + 'timeout' => 300 + ], + ], + ], - 'darkmode' => env('HORIZON_DARKMODE', false), + 'darkmode' => env('HORIZON_DARKMODE', false), ]; From 01b33fb37efdb595709b5379ba5482b72cf4093f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 03:43:20 -0700 Subject: [PATCH 365/977] Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints --- app/Http/Controllers/PublicApiController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index f888eb512..78008eda4 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -42,6 +42,7 @@ use App\Services\{ use App\Jobs\StatusPipeline\NewStatusPipeline; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use App\Services\InstanceService; class PublicApiController extends Controller { @@ -661,6 +662,10 @@ class PublicApiController extends Controller public function account(Request $request, $id) { $res = AccountService::get($id); + if($res && isset($res['local'], $res['url']) && !$res['local']) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } return response()->json($res); } @@ -680,6 +685,11 @@ class PublicApiController extends Controller $profile = AccountService::get($id); abort_if(!$profile, 404); + if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $limit = $request->limit ?? 9; $max_id = $request->max_id; $min_id = $request->min_id; From 5b284cacea474367701041093c445cff545ad742 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:41:12 -0700 Subject: [PATCH 366/977] Update ApiV1Controller, enforce blocked instance domain logic --- app/Http/Controllers/Api/ApiV1Controller.php | 69 ++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index dd0cbd062..b94a11c92 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -219,6 +219,10 @@ class ApiV1Controller extends Controller if(!$res) { return response()->json(['error' => 'Record not found'], 404); } + if($res && strpos($res['acct'], '@') != -1) { + $domain = parse_url($res['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } return $this->json($res); } @@ -483,6 +487,11 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 10); $napi = $request->has(self::PF_API_ENTITY_KEY); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { @@ -575,6 +584,11 @@ class ApiV1Controller extends Controller $limit = $request->input('limit', 10); $napi = $request->has(self::PF_API_ENTITY_KEY); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($pid) !== intval($account['id'])) { if($account['locked']) { if(!FollowerService::follows($pid, $account['id'])) { @@ -676,6 +690,11 @@ class ApiV1Controller extends Controller return $this->json(['error' => 'Account not found'], 404); } + if($profile && strpos($profile['acct'], '@') != -1) { + $domain = parse_url($profile['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $limit = $request->limit ?? 20; $max_id = $request->max_id; $min_id = $request->min_id; @@ -766,6 +785,11 @@ class ApiV1Controller extends Controller ->whereNull('status') ->findOrFail($id); + if($target && $target->domain) { + $domain = $target->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $private = (bool) $target->is_private; $remote = (bool) $target->domain; $blocked = UserFilter::whereUserId($target->id) @@ -1252,14 +1276,19 @@ class ApiV1Controller extends Controller $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); - AccountService::setLastActive($user->id); - $status = StatusService::getMastodon($id, false); - abort_unless($status, 400); + abort_unless($status, 404); + + if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } $spid = $status['account']['id']; + AccountService::setLastActive($user->id); + if(intval($spid) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { abort_if(!FollowerService::follows($user->profile_id, $spid), 403); @@ -1404,6 +1433,11 @@ class ApiV1Controller extends Controller return response()->json(['error' => 'Record not found'], 404); } + if($target && strpos($target['acct'], '@') != -1) { + $domain = parse_url($target['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); if(!$followRequest) { @@ -2011,6 +2045,11 @@ class ApiV1Controller extends Controller $account = Profile::findOrFail($id); + if($account && $account->domain) { + $domain = $account->domain; + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $count = UserFilterService::muteCount($pid); $maxLimit = intval(config('instance.user_filters.max_user_mutes')); if($count == 0) { @@ -2653,6 +2692,11 @@ class ApiV1Controller extends Controller abort(404); } + if($res && isset($res['account'], $res['account']['acct'], $res['account']['url']) && strpos($res['account']['acct'], '@') != -1) { + $domain = parse_url($res['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + $scope = $res['visibility']; if(!in_array($scope, ['public', 'unlisted'])) { if($scope === 'private') { @@ -2697,6 +2741,11 @@ class ApiV1Controller extends Controller return response('', 404); } + if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) { + $domain = parse_url($status['account']['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } + if(intval($status['account']['id']) !== intval($user->profile_id)) { if($status['visibility'] == 'private') { if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { @@ -2780,6 +2829,10 @@ class ApiV1Controller extends Controller $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); abort_if(!$account, 404); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } $author = intval($status->profile_id) === intval($pid) || $user->is_admin; $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -2871,6 +2924,10 @@ class ApiV1Controller extends Controller $pid = $user->profile_id; $status = Status::findOrFail($id); $account = AccountService::get($status->profile_id, true); + if($account && strpos($account['acct'], '@') != -1) { + $domain = parse_url($account['url'], PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } abort_if(!$account, 404); $author = intval($status->profile_id) === intval($pid) || $user->is_admin; $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -3200,7 +3257,11 @@ class ApiV1Controller extends Controller abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); - + if($status && ($status->uri || $status->url || $status->object_url)) { + $url = $status->uri ?? $status->url ?? $status->object_url; + $domain = parse_url($url, PHP_URL_HOST); + abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); + } if(intval($status->profile_id) !== intval($user->profile_id)) { if($status->scope == 'private') { abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); From 6921d3568e7662b55268a0b2ab5799758735b74a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:42:27 -0700 Subject: [PATCH 367/977] Add InstanceMananger command --- app/Console/Commands/InstanceManager.php | 298 +++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 app/Console/Commands/InstanceManager.php diff --git a/app/Console/Commands/InstanceManager.php b/app/Console/Commands/InstanceManager.php new file mode 100644 index 000000000..a495d9617 --- /dev/null +++ b/app/Console/Commands/InstanceManager.php @@ -0,0 +1,298 @@ +recalculateStats(); + break; + + case 'Unlisted Instances': + return $this->viewUnlistedInstances(); + break; + + case 'Banned Instances': + return $this->viewBannedInstances(); + break; + + case 'Unlist Instance': + return $this->unlistInstance(); + break; + + case 'Ban Instance': + return $this->banInstance(); + break; + + case 'Unban Instance': + return $this->unbanInstance(); + break; + + case 'Relist Instance': + return $this->relistInstance(); + break; + } + } + + protected function recalculateStats() + { + $instanceCount = Instance::count(); + $confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?'); + if(!$confirmed) { + $this->error('Aborting...'); + exit; + } + + $users = progress( + label: 'Updating instance stats...', + steps: Instance::all(), + callback: fn ($instance) => $this->updateInstanceStats($instance), + ); + } + + protected function updateInstanceStats($instance) + { + FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg'); + } + + protected function unlistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unlist this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unlisting'); + exit; + } + + $instance->unlisted = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully unlisted ' . $instance->domain . '!'); + exit; + } + + protected function relistInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to re-list this instance?'); + if(!$confirmed) { + $this->error('Aborting instance re-listing'); + exit; + } + + $instance->unlisted = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully re-listed ' . $instance->domain . '!'); + exit; + } + + protected function banInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to ban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance ban'); + exit; + } + + $instance->banned = true; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully banned ' . $instance->domain . '!'); + exit; + } + + protected function unbanInstance() + { + $id = search( + 'Search by domain', + fn (string $value) => strlen($value) > 0 + ? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all() + : [] + ); + + $instance = Instance::find($id); + if(!$instance) { + $this->error('Oops, an error occured'); + exit; + } + + $tbl = [ + [ + $instance->domain, + number_format($instance->status_count), + number_format($instance->user_count), + ] + ]; + table( + ['Domain', 'Status Count', 'User Count'], + $tbl + ); + + $confirmed = confirm('Are you sure you want to unban this instance?'); + if(!$confirmed) { + $this->error('Aborting instance unban'); + exit; + } + + $instance->banned = false; + $instance->save(); + InstanceService::refresh(); + $this->info('Successfully un-banned ' . $instance->domain . '!'); + exit; + } + + protected function viewBannedInstances() + { + $data = Instance::whereBanned(true) + ->get(['domain', 'user_count', 'status_count']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count'], + $data + ); + } + + protected function viewUnlistedInstances() + { + $data = Instance::whereUnlisted(true) + ->get(['domain', 'user_count', 'status_count', 'banned']) + ->map(function($d) { + return [ + 'domain' => $d->domain, + 'user_count' => number_format($d->user_count), + 'status_count' => number_format($d->status_count), + 'banned' => $d->banned ? '✅' : null + ]; + }) + ->toArray(); + table( + ['Domain', 'User Count', 'Status Count', 'Banned'], + $data + ); + } +} From 1f3f0cae65f6ef100e0710ddb0919e2a2ed986e8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:43:32 -0700 Subject: [PATCH 368/977] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 591025bfd..640f6531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,12 @@ - Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c)) - Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389)) - Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3)) +- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe)) +- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47)) +- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a)) +- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade)) +- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3)) +- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 97b7cb2719924ca70a330842fa4b1e3772eb37d5 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 04:49:44 -0700 Subject: [PATCH 369/977] Add migration --- ..._add_active_deliver_to_instances_table.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php diff --git a/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php b/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php new file mode 100644 index 000000000..d6e413768 --- /dev/null +++ b/database/migrations/2023_12_05_092152_add_active_deliver_to_instances_table.php @@ -0,0 +1,36 @@ +boolean('active_deliver')->nullable()->index()->after('domain'); + $table->boolean('valid_nodeinfo')->nullable(); + $table->timestamp('nodeinfo_last_fetched')->nullable(); + $table->boolean('delivery_timeout')->default(false); + $table->timestamp('delivery_next_after')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instances', function (Blueprint $table) { + $table->dropColumn('active_deliver'); + $table->dropColumn('valid_nodeinfo'); + $table->dropColumn('nodeinfo_last_fetched'); + $table->dropColumn('delivery_timeout'); + $table->dropColumn('delivery_next_after'); + }); + } +}; From 4d02d6f12e460958560f9314f07b9215bc5560d4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 05:59:12 -0700 Subject: [PATCH 370/977] Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! --- app/Http/Controllers/Api/ApiV2Controller.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 93f930cd5..ce15a8a49 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -96,6 +96,9 @@ class ApiV2Controller extends Controller 'streaming' => 'wss://' . config('pixelfed.domain.app'), 'status' => null ], + 'vapid' => [ + 'public_key' => config('webpush.vapid.public_key'), + ], 'accounts' => [ 'max_featured_tags' => 0, ], From 2becd273c4af81efab27052d671926f830eee930 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 7 Feb 2024 06:00:09 -0700 Subject: [PATCH 371/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640f6531a..d939ea1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ - Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade)) - Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3)) - Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac)) +- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 7b0a6060b2053eb18a02758288280b7b0de6e3e3 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 8 Feb 2024 02:49:35 +0100 Subject: [PATCH 372/977] Return access tokens' scopes, not hardcoded list --- app/Auth/BearerTokenResponse.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Auth/BearerTokenResponse.php b/app/Auth/BearerTokenResponse.php index 79cd97c85..a3175d798 100644 --- a/app/Auth/BearerTokenResponse.php +++ b/app/Auth/BearerTokenResponse.php @@ -18,8 +18,8 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke protected function getExtraParams(AccessTokenEntityInterface $accessToken) { return [ - 'created_at' => time(), - 'scope' => 'read write follow push' + 'created_at' => time(), + 'scope' => implode(' ', $accessToken->getScopes()) ]; } } From 9330cd02f7c05fe3f4ade786e8429fb804a93cb2 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 8 Feb 2024 02:50:02 +0100 Subject: [PATCH 373/977] Implement proper OAuth authorization on Admin API endpoints --- .../Controllers/Api/AdminApiController.php | 67 ++++++++++++++----- .../Controllers/Api/ApiV1Dot1Controller.php | 3 +- app/Providers/AuthServiceProvider.php | 4 +- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/Api/AdminApiController.php b/app/Http/Controllers/Api/AdminApiController.php index 76f73e720..69ba54cee 100644 --- a/app/Http/Controllers/Api/AdminApiController.php +++ b/app/Http/Controllers/Api/AdminApiController.php @@ -40,16 +40,20 @@ class AdminApiController extends Controller { public function supported(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); return response()->json(['supported' => true]); } public function getStats(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $res = AdminStatsService::summary(); $res['autospam_count'] = AccountInterstitial::whereType('post.autospam') @@ -60,8 +64,10 @@ class AdminApiController extends Controller public function autospam(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $appeals = AccountInterstitial::whereType('post.autospam') ->whereNull('appeal_handled_at') @@ -95,8 +101,10 @@ class AdminApiController extends Controller public function autospamHandle(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account', @@ -239,8 +247,10 @@ class AdminApiController extends Controller public function modReports(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $reports = Report::whereNull('admin_seen') ->orderBy('created_at','desc') @@ -285,8 +295,10 @@ class AdminApiController extends Controller public function modReportHandle(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'action' => 'required|string', @@ -343,8 +355,11 @@ class AdminApiController extends Controller public function getConfiguration(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); + abort_unless(config('instance.enable_cc'), 400); return collect([ @@ -386,8 +401,11 @@ class AdminApiController extends Controller public function updateConfiguration(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); + abort_unless(config('instance.enable_cc'), 400); $this->validate($request, [ @@ -448,8 +466,11 @@ class AdminApiController extends Controller public function getUsers(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); + $this->validate($request, [ 'sort' => 'sometimes|in:asc,desc', ]); @@ -466,8 +487,10 @@ class AdminApiController extends Controller public function getUser(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $id = $request->input('user_id'); $key = 'pf-admin-api:getUser:byId:' . $id; @@ -497,8 +520,10 @@ class AdminApiController extends Controller public function userAdminAction(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', @@ -669,8 +694,10 @@ class AdminApiController extends Controller public function instances(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'q' => 'sometimes', @@ -707,8 +734,10 @@ class AdminApiController extends Controller public function getInstance(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); $id = $request->input('id'); $res = Instance::findOrFail($id); @@ -718,8 +747,10 @@ class AdminApiController extends Controller public function moderateInstance(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', @@ -742,8 +773,10 @@ class AdminApiController extends Controller public function refreshInstanceStats(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin == 1, 404); + abort_unless($request->user()->tokenCan('admin:write'), 404); $this->validate($request, [ 'id' => 'required', @@ -760,8 +793,10 @@ class AdminApiController extends Controller public function getAllStats(Request $request) { - abort_if(!$request->user(), 404); + abort_if(!$request->user() || !$request->user()->token(), 404); + abort_unless($request->user()->is_admin === 1, 404); + abort_unless($request->user()->tokenCan('admin:read'), 404); if($request->has('refresh')) { Cache::forget('admin-api:instance-all-stats-v1'); diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 75d0fe984..22aa40c9b 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -757,8 +757,9 @@ class ApiV1Dot1Controller extends Controller public function moderatePost(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); abort_if($request->user()->is_admin != true, 403); + abort_unless($request->user()->tokenCan('admin:write'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 8e1a6a98d..34d25ac76 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -41,7 +41,9 @@ class AuthServiceProvider extends ServiceProvider 'read' => 'Full read access to your account', 'write' => 'Full write access to your account', 'follow' => 'Ability to follow other profiles', - 'push' => '' + 'admin:read' => 'Read all data on the server', + 'admin:write' => 'Modify all data on the server', + 'push' => 'Receive your push notifications' ]); } From 0f8e45fe7588bd50eac26c731f9ebd87449f6cdf Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 8 Feb 2024 02:48:17 +0100 Subject: [PATCH 374/977] Implement proper OAuth authorization on API endpoints --- app/Http/Controllers/Api/ApiV1Controller.php | 295 +++++++++++------- .../Controllers/Api/ApiV1Dot1Controller.php | 59 ++-- app/Http/Controllers/Api/ApiV2Controller.php | 6 +- .../Controllers/Api/BaseApiController.php | 26 +- .../Api/V1/DomainBlockController.php | 10 +- .../Controllers/Api/V1/TagsController.php | 16 +- 6 files changed, 269 insertions(+), 143 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index b94a11c92..50e9eacc5 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -125,11 +125,15 @@ class ApiV1Controller extends Controller return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES); } + /** + * GET /api/v1/apps/verify_credentials + */ public function getApp(Request $request) { - if(!$request->user()) { - return response('', 403); - } + # FIXME: /api/v1/apps/verify_credentials should be accessible with any + # valid Access Token, not just a user's access token (i.e., client + # credentails grant flow access tokens) + abort_if(!$request->user() || !$request->user()->token(), 403); $client = $request->user()->token()->client; $res = [ @@ -141,6 +145,9 @@ class ApiV1Controller extends Controller return $this->json($res); } + /** + * POST /api/v1/apps + */ public function apps(Request $request) { abort_if(!config_cache('pixelfed.oauth_enabled'), 404); @@ -187,9 +194,11 @@ class ApiV1Controller extends Controller */ public function verifyCredentials(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); AccountService::setLastActive($user->id); @@ -215,6 +224,9 @@ class ApiV1Controller extends Controller */ public function accountById(Request $request, $id) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true); if(!$res) { return response()->json(['error' => 'Record not found'], 404); @@ -233,7 +245,8 @@ class ApiV1Controller extends Controller */ public function accountUpdateCredentials(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_api')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -476,7 +489,8 @@ class ApiV1Controller extends Controller */ public function accountFollowersById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $account = AccountService::get($id); abort_if(!$account, 404); @@ -573,7 +587,8 @@ class ApiV1Controller extends Controller */ public function accountFollowingById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $account = AccountService::get($id); abort_if(!$account, 404); @@ -670,6 +685,9 @@ class ApiV1Controller extends Controller */ public function accountStatusesById(Request $request, $id) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); $this->validate($request, [ @@ -774,7 +792,8 @@ class ApiV1Controller extends Controller */ public function accountFollowById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-follow', $user->id), 403, 'Invalid permissions for this action'); @@ -866,7 +885,8 @@ class ApiV1Controller extends Controller */ public function accountUnfollowById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); $user = $request->user(); @@ -936,7 +956,8 @@ class ApiV1Controller extends Controller */ public function accountRelationshipsById(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'id' => 'required|array|min:1|max:20', @@ -965,7 +986,8 @@ class ApiV1Controller extends Controller */ public function accountSearch(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'q' => 'required|string|min:1|max:255', @@ -1008,7 +1030,8 @@ class ApiV1Controller extends Controller */ public function accountBlocks(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'nullable|integer|min:1|max:40', @@ -1045,7 +1068,8 @@ class ApiV1Controller extends Controller */ public function accountBlockById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; @@ -1138,7 +1162,8 @@ class ApiV1Controller extends Controller */ public function accountUnblockById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); $pid = $user->profile_id ?? $user->profile->id; @@ -1189,7 +1214,9 @@ class ApiV1Controller extends Controller */ public function accountDomainBlocks(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + return response()->json([]); } @@ -1202,7 +1229,9 @@ class ApiV1Controller extends Controller */ public function accountEndorsements(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + return response()->json([]); } @@ -1215,7 +1244,9 @@ class ApiV1Controller extends Controller */ public function accountFavourites(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $this->validate($request, [ 'limit' => 'sometimes|integer|min:1|max:40' ]); @@ -1271,7 +1302,8 @@ class ApiV1Controller extends Controller */ public function statusFavouriteById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); @@ -1338,7 +1370,8 @@ class ApiV1Controller extends Controller */ public function statusUnfavouriteById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); @@ -1381,7 +1414,8 @@ class ApiV1Controller extends Controller */ public function accountFilters(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); return response()->json([]); } @@ -1395,7 +1429,9 @@ class ApiV1Controller extends Controller */ public function accountFollowRequests(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $this->validate($request, [ 'limit' => 'sometimes|integer|min:1|max:100' ]); @@ -1425,7 +1461,9 @@ class ApiV1Controller extends Controller */ public function accountFollowRequestAccept(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + $pid = $request->user()->profile_id; $target = AccountService::getMastodon($id); @@ -1482,7 +1520,9 @@ class ApiV1Controller extends Controller */ public function accountFollowRequestReject(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); + $pid = $request->user()->profile_id; $target = AccountService::getMastodon($id); @@ -1518,7 +1558,8 @@ class ApiV1Controller extends Controller */ public function accountSuggestions(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); // todo @@ -1619,7 +1660,8 @@ class ApiV1Controller extends Controller */ public function accountLists(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); return response()->json([]); } @@ -1633,7 +1675,8 @@ class ApiV1Controller extends Controller */ public function accountListsById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); return response()->json([]); } @@ -1646,7 +1689,8 @@ class ApiV1Controller extends Controller */ public function mediaUpload(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'file.*' => [ @@ -1782,7 +1826,8 @@ class ApiV1Controller extends Controller */ public function mediaUpdate(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length') @@ -1835,7 +1880,8 @@ class ApiV1Controller extends Controller */ public function mediaGet(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); @@ -1858,7 +1904,8 @@ class ApiV1Controller extends Controller */ public function mediaUploadV2(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'file.*' => [ @@ -1999,7 +2046,8 @@ class ApiV1Controller extends Controller */ public function accountMutes(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'nullable|integer|min:1|max:40' @@ -2034,7 +2082,8 @@ class ApiV1Controller extends Controller */ public function accountMuteById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); $pid = $user->profile_id; @@ -2092,7 +2141,8 @@ class ApiV1Controller extends Controller */ public function accountUnmuteById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); $pid = $user->profile_id; @@ -2128,7 +2178,8 @@ class ApiV1Controller extends Controller */ public function accountNotifications(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'nullable|integer|min:1|max:100', @@ -2204,7 +2255,10 @@ class ApiV1Controller extends Controller */ public function timelineHome(Request $request) { - $this->validate($request,[ + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + + $this->validate($request, [ 'page' => 'sometimes|integer|max:40', 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, @@ -2606,7 +2660,9 @@ class ApiV1Controller extends Controller */ public function conversations(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $this->validate($request, [ 'limit' => 'min:1|max:40', 'scope' => 'nullable|in:inbox,sent,requests' @@ -2683,7 +2739,9 @@ class ApiV1Controller extends Controller */ public function statusById(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + AccountService::setLastActive($request->user()->id); $pid = $request->user()->profile_id; @@ -2730,7 +2788,8 @@ class ApiV1Controller extends Controller */ public function statusContext(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $user = $request->user(); AccountService::setLastActive($user->id); @@ -2803,7 +2862,9 @@ class ApiV1Controller extends Controller */ public function statusCard(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $res = []; return response()->json($res); } @@ -2817,7 +2878,8 @@ class ApiV1Controller extends Controller */ public function statusRebloggedBy(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'sometimes|integer|min:1|max:80' @@ -2913,7 +2975,8 @@ class ApiV1Controller extends Controller */ public function statusFavouritedBy(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'nullable|integer|min:1|max:80' @@ -3010,7 +3073,8 @@ class ApiV1Controller extends Controller */ public function statusCreate(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'status' => 'nullable|string', @@ -3225,7 +3289,9 @@ class ApiV1Controller extends Controller */ public function statusDelete(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + AccountService::setLastActive($request->user()->id); $status = Status::whereProfileId($request->user()->profile->id) ->findOrFail($id); @@ -3251,7 +3317,8 @@ class ApiV1Controller extends Controller */ public function statusShare(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); @@ -3303,7 +3370,8 @@ class ApiV1Controller extends Controller */ public function statusUnshare(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $user = $request->user(); abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); @@ -3346,7 +3414,8 @@ class ApiV1Controller extends Controller */ public function timelineHashtag(Request $request, $hashtag) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request,[ 'page' => 'nullable|integer|max:40', @@ -3447,7 +3516,8 @@ class ApiV1Controller extends Controller */ public function bookmarks(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'nullable|integer|min:1|max:40', @@ -3514,7 +3584,8 @@ class ApiV1Controller extends Controller */ public function bookmarkStatus(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $status = Status::findOrFail($id); $pid = $request->user()->profile_id; @@ -3554,7 +3625,8 @@ class ApiV1Controller extends Controller */ public function unbookmarkStatus(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $status = Status::findOrFail($id); $pid = $request->user()->profile_id; @@ -3586,7 +3658,8 @@ class ApiV1Controller extends Controller */ public function discoverPosts(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'integer|min:1|max:40' @@ -3596,29 +3669,30 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id; $filters = UserFilterService::filters($pid); $forYou = DiscoverService::getForYou(); - $posts = $forYou->take(50)->map(function($post) { + $posts = $forYou->take(50)->map(function ($post) { return StatusService::getMastodon($post); }) - ->filter(function($post) use($filters) { - return $post && - isset($post['account']) && - isset($post['account']['id']) && - !in_array($post['account']['id'], $filters); - }) - ->take(12) - ->values(); + ->filter(function ($post) use ($filters) { + return $post && + isset($post['account']) && + isset($post['account']['id']) && + !in_array($post['account']['id'], $filters); + }) + ->take(12) + ->values(); return $this->json(compact('posts')); } /** - * GET /api/v2/statuses/{id}/replies - * - * - * @return array - */ + * GET /api/v2/statuses/{id}/replies + * + * + * @return array + */ public function statusReplies(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'limit' => 'int|min:1|max:10', @@ -3707,14 +3781,15 @@ class ApiV1Controller extends Controller } /** - * GET /api/v2/statuses/{id}/state - * - * - * @return array - */ + * GET /api/v2/statuses/{id}/state + * + * + * @return array + */ public function statusState(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $status = Status::findOrFail($id); $pid = $request->user()->profile_id; @@ -3724,14 +3799,15 @@ class ApiV1Controller extends Controller } /** - * GET /api/v1.1/discover/accounts/popular - * - * - * @return array - */ + * GET /api/v1.1/discover/accounts/popular + * + * + * @return array + */ public function discoverAccountsPopular(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $pid = $request->user()->profile_id; @@ -3766,14 +3842,15 @@ class ApiV1Controller extends Controller } /** - * GET /api/v1/preferences - * - * - * @return array - */ + * GET /api/v1/preferences + * + * + * @return array + */ public function getPreferences(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -3788,40 +3865,43 @@ class ApiV1Controller extends Controller } /** - * GET /api/v1/trends - * - * - * @return array - */ + * GET /api/v1/trends + * + * + * @return array + */ public function getTrends(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); return $this->json([]); } /** - * GET /api/v1/announcements - * - * - * @return array - */ + * GET /api/v1/announcements + * + * + * @return array + */ public function getAnnouncements(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); return $this->json([]); } /** - * GET /api/v1/markers - * - * - * @return array - */ + * GET /api/v1/markers + * + * + * @return array + */ public function getMarkers(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $type = $request->input('timeline'); if(is_array($type)) { @@ -3835,14 +3915,15 @@ class ApiV1Controller extends Controller } /** - * POST /api/v1/markers - * - * - * @return array - */ + * POST /api/v1/markers + * + * + * @return array + */ public function setMarkers(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $pid = $request->user()->profile_id; $home = $request->input('home[last_read_id]'); diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 22aa40c9b..c34b9d968 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -68,9 +68,10 @@ class ApiV1Dot1Controller extends Controller public function report(Request $request) { - $user = $request->user(); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - abort_if(!$user, 403); + $user = $request->user(); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { @@ -175,9 +176,10 @@ class ApiV1Dot1Controller extends Controller */ public function deleteAvatar(Request $request) { - $user = $request->user(); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); - abort_if(!$user, 403); + $user = $request->user(); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { @@ -215,9 +217,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountPosts(Request $request, $id) { - $user = $request->user(); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); - abort_if(!$user, 403); + $user = $request->user(); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { @@ -255,8 +258,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountChangePassword(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -296,8 +301,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountLoginActivity(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -336,8 +343,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountTwoFactor(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { @@ -358,8 +367,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountEmailsFromPixelfed(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -433,8 +444,10 @@ class ApiV1Dot1Controller extends Controller */ public function accountApps(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); abort_if($user->status != null, 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { @@ -640,7 +653,8 @@ class ApiV1Dot1Controller extends Controller public function archive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -672,7 +686,8 @@ class ApiV1Dot1Controller extends Controller public function unarchive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -703,7 +718,8 @@ class ApiV1Dot1Controller extends Controller public function archivedPosts(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -719,7 +735,8 @@ class ApiV1Dot1Controller extends Controller public function placesById(Request $request, $id, $slug) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); @@ -865,7 +882,9 @@ class ApiV1Dot1Controller extends Controller public function getWebSettings(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $uid = $request->user()->id; $settings = UserSetting::firstOrCreate([ 'user_id' => $uid @@ -878,7 +897,9 @@ class ApiV1Dot1Controller extends Controller public function setWebSettings(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $this->validate($request, [ 'field' => 'required|in:enable_reblogs,hide_reblog_banner', 'value' => 'required' @@ -902,7 +923,9 @@ class ApiV1Dot1Controller extends Controller public function getMutualAccounts(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follows'), 403); + $account = AccountService::get($id, true); if(!$account || !isset($account['id'])) { return []; } $res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id)) diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index ce15a8a49..2380588cf 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -148,7 +148,8 @@ class ApiV2Controller extends Controller */ public function search(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ 'q' => 'required|string|min:1|max:100', @@ -199,7 +200,8 @@ class ApiV2Controller extends Controller */ public function mediaUploadV2(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'file.*' => [ diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 2e1fb5678..72e7f1574 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -56,7 +56,8 @@ class BaseApiController extends Controller public function notifications(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $pid = $request->user()->profile_id; $limit = $request->input('limit', 20); @@ -98,7 +99,9 @@ class BaseApiController extends Controller public function avatarUpdate(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); + $this->validate($request, [ 'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'), ]); @@ -134,9 +137,11 @@ class BaseApiController extends Controller public function verifyCredentials(Request $request) { + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $user = $request->user(); - abort_if(!$user, 403); - if($user->status != null) { + if ($user->status != null) { Auth::logout(); abort(403); } @@ -146,7 +151,9 @@ class BaseApiController extends Controller public function accountLikes(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $this->validate($request, [ 'page' => 'sometimes|int|min:1|max:20', 'limit' => 'sometimes|int|min:1|max:10' @@ -173,7 +180,8 @@ class BaseApiController extends Controller public function archive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') @@ -201,7 +209,8 @@ class BaseApiController extends Controller public function unarchive(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') @@ -228,7 +237,8 @@ class BaseApiController extends Controller public function archivedPosts(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $statuses = Status::whereProfileId($request->user()->profile_id) ->whereScope('archived') diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php index 2186c0936..3a4a4c793 100644 --- a/app/Http/Controllers/Api/V1/DomainBlockController.php +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -23,7 +23,9 @@ class DomainBlockController extends Controller public function index(Request $request) { - abort_unless($request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $this->validate($request, [ 'limit' => 'sometimes|integer|min:1|max:200' ]); @@ -52,7 +54,8 @@ class DomainBlockController extends Controller public function store(Request $request) { - abort_unless($request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'domain' => 'required|active_url|min:1|max:120' @@ -99,7 +102,8 @@ class DomainBlockController extends Controller public function delete(Request $request) { - abort_unless($request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('write'), 403); $this->validate($request, [ 'domain' => 'required|min:1|max:120' diff --git a/app/Http/Controllers/Api/V1/TagsController.php b/app/Http/Controllers/Api/V1/TagsController.php index 7314ce09d..5226b67ed 100644 --- a/app/Http/Controllers/Api/V1/TagsController.php +++ b/app/Http/Controllers/Api/V1/TagsController.php @@ -32,7 +32,9 @@ class TagsController extends Controller */ public function relatedTags(Request $request, $tag) { - abort_unless($request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); + $tag = Hashtag::whereSlug($tag)->firstOrFail(); return HashtagRelatedService::get($tag->id); } @@ -45,7 +47,8 @@ class TagsController extends Controller */ public function followHashtag(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -87,7 +90,8 @@ class TagsController extends Controller */ public function unfollowHashtag(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('follow'), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -132,7 +136,8 @@ class TagsController extends Controller */ public function getHashtag(Request $request, $id) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -172,7 +177,8 @@ class TagsController extends Controller */ public function getFollowedTags(Request $request) { - abort_if(!$request->user(), 403); + abort_if(!$request->user() || !$request->user()->token(), 403); + abort_unless($request->user()->tokenCan('read'), 403); $account = AccountService::get($request->user()->profile_id); From d9a9507cc857b06cca2729cb381ae02c649c6a6d Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 10 Feb 2024 00:30:06 +0000 Subject: [PATCH 375/977] sync --- app/Console/Kernel.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c35af3a20..dcee73ee1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -33,13 +33,17 @@ class Kernel extends ConsoleKernel $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); - if(config('import.instagram.enabled')) { + if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { + $schedule->command('media:s3gc')->hourlyAt(15); + } + + if (config('import.instagram.enabled')) { $schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer(); $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer(); $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer(); $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer(); - if(config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { + if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) { $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer(); } } @@ -56,11 +60,7 @@ class Kernel extends ConsoleKernel */ protected function commands() { -<<<<<<< HEAD $this->load(__DIR__ . '/Commands'); -======= - $this->load(__DIR__.'/Commands'); ->>>>>>> d374d73ba74860d6eb90752d596cf2e22f189860 require base_path('routes/console.php'); } From 2e6100f2758fc6e18fb6b84d5efc71643e81a2bd Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:04:15 -0700 Subject: [PATCH 376/977] Update BearerTokenResponse, fix scope bug --- app/Auth/BearerTokenResponse.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Auth/BearerTokenResponse.php b/app/Auth/BearerTokenResponse.php index a3175d798..0e1aa8a19 100644 --- a/app/Auth/BearerTokenResponse.php +++ b/app/Auth/BearerTokenResponse.php @@ -19,7 +19,6 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke { return [ 'created_at' => time(), - 'scope' => implode(' ', $accessToken->getScopes()) ]; } } From 607b239c1a930d6858718152b35db508d8f5bf7c Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:05:22 -0700 Subject: [PATCH 377/977] Bump version to v0.11.10 --- CHANGELOG.md | 4 +++- config/pixelfed.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d939ea1e0..b61cdd706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.9...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.10...dev) + +## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10) ### Added - Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6)) diff --git a/config/pixelfed.php b/config/pixelfed.php index fc7da598a..3fe962513 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your Pixelfed instance. | */ - 'version' => '0.11.9', + 'version' => '0.11.10', /* |-------------------------------------------------------------------------- From e354750808792fa258eec6f799f8e2ba3ea45d73 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:41:12 -0700 Subject: [PATCH 378/977] Fix api endpoints --- .../Controllers/Api/BaseApiController.php | 21 +++++++------------ .../Api/V1/DomainBlockController.php | 9 +++----- .../Controllers/Api/V1/TagsController.php | 12 ++++------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 72e7f1574..7ac73b4d0 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -56,8 +56,7 @@ class BaseApiController extends Controller public function notifications(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $pid = $request->user()->profile_id; $limit = $request->input('limit', 20); @@ -99,8 +98,7 @@ class BaseApiController extends Controller public function avatarUpdate(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'), @@ -137,8 +135,7 @@ class BaseApiController extends Controller public function verifyCredentials(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $user = $request->user(); if ($user->status != null) { @@ -151,8 +148,7 @@ class BaseApiController extends Controller public function accountLikes(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'page' => 'sometimes|int|min:1|max:20', @@ -180,8 +176,7 @@ class BaseApiController extends Controller public function archive(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 403); + abort_if(!$request->user(), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') @@ -209,8 +204,7 @@ class BaseApiController extends Controller public function unarchive(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 403); + abort_if(!$request->user(), 403); $status = Status::whereNull('in_reply_to_id') ->whereNull('reblog_of_id') @@ -237,8 +231,7 @@ class BaseApiController extends Controller public function archivedPosts(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $statuses = Status::whereProfileId($request->user()->profile_id) ->whereScope('archived') diff --git a/app/Http/Controllers/Api/V1/DomainBlockController.php b/app/Http/Controllers/Api/V1/DomainBlockController.php index 3a4a4c793..5a2698361 100644 --- a/app/Http/Controllers/Api/V1/DomainBlockController.php +++ b/app/Http/Controllers/Api/V1/DomainBlockController.php @@ -23,8 +23,7 @@ class DomainBlockController extends Controller public function index(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'limit' => 'sometimes|integer|min:1|max:200' @@ -54,8 +53,7 @@ class DomainBlockController extends Controller public function store(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'domain' => 'required|active_url|min:1|max:120' @@ -102,8 +100,7 @@ class DomainBlockController extends Controller public function delete(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('write'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'domain' => 'required|min:1|max:120' diff --git a/app/Http/Controllers/Api/V1/TagsController.php b/app/Http/Controllers/Api/V1/TagsController.php index 5226b67ed..2f7acf4a0 100644 --- a/app/Http/Controllers/Api/V1/TagsController.php +++ b/app/Http/Controllers/Api/V1/TagsController.php @@ -47,8 +47,7 @@ class TagsController extends Controller */ public function followHashtag(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('follow'), 403); + abort_if(!$request->user(), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -90,8 +89,7 @@ class TagsController extends Controller */ public function unfollowHashtag(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('follow'), 403); + abort_if(!$request->user(), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -136,8 +134,7 @@ class TagsController extends Controller */ public function getHashtag(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $pid = $request->user()->profile_id; $account = AccountService::get($pid); @@ -177,8 +174,7 @@ class TagsController extends Controller */ public function getFollowedTags(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $account = AccountService::get($request->user()->profile_id); From fd7f5dbba13818f60d1c2b3ab110b499e996aa81 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:45:10 -0700 Subject: [PATCH 379/977] Fix api endpoints --- app/Http/Controllers/Api/ApiV1Controller.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 50e9eacc5..8ae65eb44 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -3691,8 +3691,7 @@ class ApiV1Controller extends Controller */ public function statusReplies(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'limit' => 'int|min:1|max:10', @@ -3788,8 +3787,7 @@ class ApiV1Controller extends Controller */ public function statusState(Request $request, $id) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $status = Status::findOrFail($id); $pid = $request->user()->profile_id; From 62b9eef8056f737086352c790fede60cb081c041 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:51:37 -0700 Subject: [PATCH 380/977] Fix api endpoints --- app/Http/Controllers/Api/ApiV1Controller.php | 3 +-- app/Http/Controllers/ComposeController.php | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 8ae65eb44..d1bd9cac2 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -956,8 +956,7 @@ class ApiV1Controller extends Controller */ public function accountRelationshipsById(Request $request) { - abort_if(!$request->user() || !$request->user()->token(), 403); - abort_unless($request->user()->tokenCan('read'), 403); + abort_if(!$request->user(), 403); $this->validate($request, [ 'id' => 'required|array|min:1|max:20', diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index e17a37fd7..36bd5a66c 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -260,6 +260,8 @@ class ComposeController extends Controller $q = mb_substr($q, 1); } + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $blocked = UserFilter::whereFilterableType('App\Profile') From 7a7b4bc717c47b37bcad447a95bb0e00c2a68d26 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:54:17 -0700 Subject: [PATCH 381/977] Update AuthServiceProvider --- app/Providers/AuthServiceProvider.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 34d25ac76..52e992ce0 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -31,11 +31,6 @@ class AuthServiceProvider extends ServiceProvider if(config('instance.oauth.pat.enabled')) { Passport::personalAccessClientId(config('instance.oauth.pat.id')); } - Passport::setDefaultScope([ - 'read', - 'write', - 'follow', - ]); Passport::tokensCan([ 'read' => 'Full read access to your account', @@ -45,6 +40,12 @@ class AuthServiceProvider extends ServiceProvider 'admin:write' => 'Modify all data on the server', 'push' => 'Receive your push notifications' ]); + + Passport::setDefaultScope([ + 'read', + 'write', + 'follow', + ]); } // Gate::define('viewWebSocketsDashboard', function ($user = null) { From e5bbe9340a33df98a0941cb31b854bcef3638b1f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 9 Feb 2024 20:57:45 -0700 Subject: [PATCH 382/977] Bump version to v0.11.11 --- CHANGELOG.md | 7 ++++++- config/pixelfed.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61cdd706..de5f73b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.10...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.11...dev) + +## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) + +### Fixes +- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb)) ## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10) diff --git a/config/pixelfed.php b/config/pixelfed.php index 3fe962513..9ed7fc616 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your Pixelfed instance. | */ - 'version' => '0.11.10', + 'version' => '0.11.11', /* |-------------------------------------------------------------------------- From bc66b6da18ae2ab7a038651b90a8f2c77dfc1aa6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 10 Feb 2024 20:03:04 +0000 Subject: [PATCH 383/977] many small fixes and improvements --- .editorconfig | 14 +- .env.docker | 1132 +++++++++-------- .env.example | 80 -- .vscode/settings.json | 14 + Dockerfile | 16 +- docker-compose.yml | 60 +- .../docker/templates/etc/nginx/nginx.conf | 41 + .../docker/entrypoint.d/02-check-config.sh | 6 +- docker/shared/root/docker/helpers.sh | 26 +- docker/shared/root/docker/install/base.sh | 3 - .../templates/usr/local/etc/php/php.ini | 7 +- goss.yaml | 2 +- 12 files changed, 723 insertions(+), 678 deletions(-) delete mode 100644 .env.example create mode 100644 docker/nginx/root/docker/templates/etc/nginx/nginx.conf diff --git a/.editorconfig b/.editorconfig index 9551fdc60..a56c5a37a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,11 +17,11 @@ indent_style = space indent_size = 4 # ShellCheck config -shell_variant = bash -binary_next_line = true -case-indent = true -switch_case_indent = true -space_redirects = true -function_next_line = true +shell_variant = bash # like -ln=bash +binary_next_line = true # like -bn +switch_case_indent = true # like -ci +space_redirects = false # like -sr +keep_padding = false # like -kp +function_next_line = true # like -fn +never_split = true # like -ns simplify = true -space-redirects = true diff --git a/.env.docker b/.env.docker index 8ca07c21f..0fa9e1428 100644 --- a/.env.docker +++ b/.env.docker @@ -2,47 +2,45 @@ # -*- mode: bash -*- # vi: ft=bash +# Use Dottie (https://github.com/jippi/dottie) to manage this .env file easier! +# +# @dottie/source .env.docker +# # shellcheck disable=SC2034,SC2148 ################################################################################ -# Pixelfed application configuration +# app ################################################################################ -# A random 32-character string to be used as an encryption key. -# -# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# ! NOTE: This will be auto-generated by Docker during bootstrap -# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# -# This key is used by the Illuminate encrypter service and should be set to a random, -# 32 character string, otherwise these encrypted strings will not be safe. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_key -APP_KEY= - -# See: https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +# @see https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +# @dottie/validate required APP_NAME="Pixelfed Prod" # Application domains used for routing. # -# See: https://docs.pixelfed.org/technical-documentation/config/#app_domain +# @see https://docs.pixelfed.org/technical-documentation/config/#app_domain +# @dottie/validate required,fqdn APP_DOMAIN="__CHANGE_ME__" # This URL is used by the console to properly generate URLs when using the Artisan command line tool. # You should set this to the root of your application so that it is used when running Artisan tasks. # -# See: https://docs.pixelfed.org/technical-documentation/config/#app_url +# @see https://docs.pixelfed.org/technical-documentation/config/#app_url +# @dottie/validate required,http_url APP_URL="https://${APP_DOMAIN}" # Application domains used for routing. # -# See: https://docs.pixelfed.org/technical-documentation/config/#admin_domain +# @see https://docs.pixelfed.org/technical-documentation/config/#admin_domain +# @dottie/validate required,fqdn ADMIN_DOMAIN="${APP_DOMAIN}" # This value determines the “environment” your application is currently running in. # This may determine how you prefer to configure various services your application utilizes. # -# See: https://docs.pixelfed.org/technical-documentation/config/#app_env +# @default "production" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_env +# @dottie/validate required,oneof='production,dev,staging' #APP_ENV="production" # When your application is in debug mode, detailed error messages with stack traces will @@ -50,207 +48,289 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # # If disabled, a simple generic error page is shown. # -# See: https://docs.pixelfed.org/technical-documentation/config/#app_debug +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_debug +# @dottie/validate required,boolean #APP_DEBUG="false" # Enable/disable new local account registrations. # -# See: https://docs.pixelfed.org/technical-documentation/config/#open_registration +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#open_registration +# @dottie/validate required,boolean #OPEN_REGISTRATION="true" # Require email verification before a new user can do anything. # -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#enforce_email_verification +# @dottie/validate required,boolean #ENFORCE_EMAIL_VERIFICATION="true" # Allow a maximum number of user accounts. # -# Defaults to "1000". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +# @default "1000" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_max_users +# @dottie/validate required,number #PF_MAX_USERS="1000" # Enforce the maximum number of user accounts # -# Defaults to "true". +# @default "true" +# @dottie/validate boolean #PF_ENFORCE_MAX_USERS="true" -# See: https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled -OAUTH_ENABLED="true" +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#oauth_enabled +# @dottie/validate required,boolean +#OAUTH_ENABLED="false" -# Defaults to "UTC". -# # ! Do not edit your timezone once the service is running - or things will break! # -# See: https://docs.pixelfed.org/technical-documentation/config/#app_timezone -# See: https://www.php.net/manual/en/timezones.php +# @default "UTC" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_timezone +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone APP_TIMEZONE="UTC" # The application locale determines the default locale that will be used by the translation service provider. # You are free to set this value to any of the locales which will be supported by the application. # -# Defaults to "en". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_locale +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_locale +# @dottie/validate required #APP_LOCALE="en" # The fallback locale determines the locale to use when the current one is not available. # # You may change the value to correspond to any of the language folders that are provided through your application. # -# Defaults to "en". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +# @default "en" +# @see https://docs.pixelfed.org/technical-documentation/config/#app_fallback_locale +# @dottie/validate required #APP_FALLBACK_LOCALE="en" -# See: https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +# @see https://docs.pixelfed.org/technical-documentation/config/#limit_account_size +# @dottie/validate required,boolean #LIMIT_ACCOUNT_SIZE="true" # Update the max account size, the per user limit of files in kB. # -# Defaults to "1000000" (1GB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +# @default "1000000" (1GB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_account_size-kb +# @dottie/validate required,number #MAX_ACCOUNT_SIZE="1000000" # Update the max photo size, in kB. # -# Defaults to "15000" (15MB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +# @default "15000" (15MB) +# @see https://docs.pixelfed.org/technical-documentation/config/#max_photo_size-kb +# @dottie/validate required,number #MAX_PHOTO_SIZE="15000" # The max number of photos allowed per post. # -# Defaults to "4". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_album_length +# @default "4" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_album_length +# @dottie/validate required,number #MAX_ALBUM_LENGTH="4" # Update the max avatar size, in kB. # -# Defaults to "2000" (2MB). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +# @default "2000" (2MB). +# @see https://docs.pixelfed.org/technical-documentation/config/#max_avatar_size-kb +# @dottie/validate required,number #MAX_AVATAR_SIZE="2000" # Change the caption length limit for new local posts. # -# Defaults to "500". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +# @default "500" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_caption_length +# @dottie/validate required,number #MAX_CAPTION_LENGTH="500" # Change the bio length limit for user profiles. # -# Defaults to "125". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +# @default "125" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_bio_length +# @dottie/validate required,number #MAX_BIO_LENGTH="125" # Change the length limit for user names. # -# Defaults to "30". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#max_name_length +# @default "30" +# @see https://docs.pixelfed.org/technical-documentation/config/#max_name_length +# @dottie/validate required,number #MAX_NAME_LENGTH="30" # Resize and optimize image uploads. # -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_images +# @dottie/validate required,boolean #PF_OPTIMIZE_IMAGES="true" # Set the image optimization quality, must be a value between 1-100. # -# Defaults to "80". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#image_quality +# @default "80" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_quality +# @dottie/validate required,number #IMAGE_QUALITY="80" # Resize and optimize video uploads. # -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_optimize_videos +# @dottie/validate required,boolean #PF_OPTIMIZE_VIDEOS="true" # Enable account deletion. # -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#account_deletion +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_deletion +# @dottie/validate required,boolean #ACCOUNT_DELETION="true" # Set account deletion queue after X days, set to false to delete accounts immediately. # -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#account_delete_after +# @dottie/validate required,boolean #ACCOUNT_DELETE_AFTER="false" -# Defaults to "Pixelfed - Photo sharing for everyone". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_description +# @default "Pixelfed - Photo sharing for everyone" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_description +# @dottie/validate required #INSTANCE_DESCRIPTION="" -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_hashtags +# @dottie/validate required,boolean #INSTANCE_PUBLIC_HASHTAGS="false" -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email +# @dottie/validate required,email INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline +# @dottie/validate required,boolean #INSTANCE_PUBLIC_LOCAL_TIMELINE="false" -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#banned_usernames +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#banned_usernames #BANNED_USERNAMES="" -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#stories_enabled +# @dottie/validate required,boolean #STORIES_ENABLED="false" -# Defaults to "false". -# # Level is hardcoded to 1. # -# See: https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#restricted_instance +# @dottie/validate required,boolean #RESTRICTED_INSTANCE="false" -################################################################################ -# Lets Encrypt configuration -################################################################################ +# @default false +# @see https://docs.pixelfed.org/technical-documentation/config/#media_exif_database +# @dottie/validate required,boolean +MEDIA_EXIF_DATABASE="true" -# The host to request LetsEncrypt certificate for -LETSENCRYPT_HOST="${APP_DOMAIN}" - -# The e-mail to use for Lets Encrypt certificate requests. -LETSENCRYPT_EMAIL="__CHANGE_ME__" - -# Lets Encrypt staging/test servers for certificate requests. +# Pixelfed supports GD or ImageMagick to process images. # -# Setting this to any value will change to letsencrypt test servers. -#LETSENCRYPT_TEST="1" +# Possible values: +# - "gd" (default) +# - "imagick" +# +# @default "gd" +# @see https://docs.pixelfed.org/technical-documentation/config/#image_driver +# @dottie/validate required,oneof=gd imagick +#IMAGE_DRIVER="gd" + +# Set trusted proxy IP addresses. +# +# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. +# +# The “*” character is syntactic sugar within TrustedProxy to trust any +# proxy that connects directly to your server, a requirement when you cannot +# know the address of your proxy (e.g. if using Rackspace balancers). +# +# The “**” character is syntactic sugar within TrustedProxy to trust not just any +# proxy that connects directly to your server, but also proxies that connect to those proxies, +# and all the way back until you reach the original source IP. It will mean that +# $request->getClientIp() always gets the originating client IP, no matter how many proxies +# that client’s request has subsequently passed through. +# +# @default "*" +# @see https://docs.pixelfed.org/technical-documentation/config/#trust_proxies +# @dottie/validate required +#TRUST_PROXIES="*" + +# This option controls the default cache connection that gets used while using this caching library. +# +# This connection is used when another is not explicitly specified when executing a given caching function. +# +# Possible values: +# - "apc" +# - "array" +# - "database" +# - "file" (default) +# - "memcached" +# - "redis" +# +# @default "file" +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_driver +# @dottie/validate required,oneof=apc array database file memcached redis +CACHE_DRIVER="redis" + +# @default ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. +# @see https://docs.pixelfed.org/technical-documentation/config/#cache_prefix +# @dottie/validate required +#CACHE_PREFIX="{APP_NAME}_cache" + +# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. +# +# Possible values: +# - "pusher" +# - "redis" +# - "log" +# - "null" (default) +# +# @default null +# @see https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver +# @dottie/validate required,oneof=pusher redis log null +BROADCAST_DRIVER="redis" + +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types +# @dottie/validate required,boolean +#RESTRICT_HTML_TYPES="true" + +# Passport uses encryption keys while generating secure access tokens +# for your application. +# +# By default, the keys are stored as local files but can be set via environment +# variables when that is more convenient. + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_private_key +# @dottie/validate required +#PASSPORT_PRIVATE_KEY="" + +# @see https://docs.pixelfed.org/technical-documentation/config/#passport_public_key +# @dottie/validate required +#PASSPORT_PUBLIC_KEY="" ################################################################################ -# Database configuration +# database ################################################################################ # Database version to use (as Docker tag) # -# See: https://hub.docker.com/_/mariadb +# @see https://hub.docker.com/_/mariadb +# @dottie/validate required DB_VERSION="11.2" # Here you may specify which of the database connections below @@ -265,31 +345,38 @@ DB_VERSION="11.2" # - "pgsql" # - "sqlsrv" # -# See: https://docs.pixelfed.org/technical-documentation/config/#db_connection +# @see https://docs.pixelfed.org/technical-documentation/config/#db_connection +# @dottie/validate required,oneof=sqlite mysql pgsql sqlsrv DB_CONNECTION="mysql" -# See: https://docs.pixelfed.org/technical-documentation/config/#db_host +# @see https://docs.pixelfed.org/technical-documentation/config/#db_host +# @dottie/validate required,hostname DB_HOST="db" -# See: https://docs.pixelfed.org/technical-documentation/config/#db_username +# @see https://docs.pixelfed.org/technical-documentation/config/#db_username +# @dottie/validate required DB_USERNAME="pixelfed" -# See: https://docs.pixelfed.org/technical-documentation/config/#db_password -DB_PASSWORD="__CHANGE_ME__" +# @see https://docs.pixelfed.org/technical-documentation/config/#db_password +# @dottie/validate required +DB_PASSWORD= -# See: https://docs.pixelfed.org/technical-documentation/config/#db_database +# @see https://docs.pixelfed.org/technical-documentation/config/#db_database +# @dottie/validate required DB_DATABASE="pixelfed_prod" # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL # -# See: https://docs.pixelfed.org/technical-documentation/config/#db_port +# @see https://docs.pixelfed.org/technical-documentation/config/#db_port +# @dottie/validate required,number DB_PORT="3306" # Automatically run [artisan migrate --force] if new migrations are detected. +# @dottie/validate required,boolean DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" ################################################################################ -# Mail configuration +# mail ################################################################################ # Laravel supports both SMTP and PHP’s “mail” function as drivers for the sending of e-mail. @@ -306,39 +393,41 @@ DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" # "log" # "array" # -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_driver +# @default "smtp" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_driver +# @dottie/validate required,oneof=smtp sendmail mailgun mandrill ses sparkpost log array #MAIL_DRIVER="smtp" # The host address of the SMTP server used by your applications. # # A default option is provided that is compatible with the Mailgun mail service which will provide reliable deliveries. # -# Defaults to "smtp.mailgun.org". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_host +# @default "smtp.mailgun.org" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_host +# @dottie/validate required_with=MAIL_DRIVER,fqdn #MAIL_HOST="smtp.mailgun.org" # This is the SMTP port used by your application to deliver e-mails to users of the application. # # Like the host we have set this value to stay compatible with the Mailgun e-mail application by default. # -# Defaults to 587. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_port +# @default 587. +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_port +# @dottie/validate required_with=MAIL_DRIVER,number #MAIL_PORT="587" # You may wish for all e-mails sent by your application to be sent from the same address. # # Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. # -# Defaults to "hello@example.com". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +# @default "hello@example.com" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_address +# @dottie/validate required_with=MAIL_DRIVER,email MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}" -# Defaults to "Example". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +# @default "Example" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_name +# @dottie/validate required_with=MAIL_DRIVER MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # If your SMTP server requires a username for authentication, you should set it here. @@ -346,220 +435,148 @@ MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" # This will get used to authenticate with your server on connection. # You may also set the “password” value below this one. # -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_username +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_username +# @dottie/validate required_with=MAIL_DRIVER #MAIL_USERNAME="" -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_password +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_password +# @dottie/validate required_with=MAIL_DRIVER #MAIL_PASSWORD="" # Here you may specify the encryption protocol that should be used when the application send e-mail messages. # # A sensible default using the transport layer security protocol should provide great security. # -# Defaults to "tls". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +# @default "tls" +# @see https://docs.pixelfed.org/technical-documentation/config/#mail_encryption +# @dottie/validate required_with=MAIL_DRIVER #MAIL_ENCRYPTION="tls" ################################################################################ -# Redis configuration +# redis ################################################################################ -# Defaults to "phpredis". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_client +# @default "phpredis" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_client +# @dottie/validate required #REDIS_CLIENT="phpredis" -# Defaults to "tcp". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +# @default "tcp" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_scheme +# @dottie/validate required #REDIS_SCHEME="tcp" -# Defaults to "localhost". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_host +# @default "localhost" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_host +# @dottie/validate required REDIS_HOST="redis" -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_password +# @default "null" (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_password +# @dottie/validate omitempty #REDIS_PASSWORD= -# Defaults to "6379". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_port +# @default "6379" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_port +# @dottie/validate required,number REDIS_PORT="6379" -# Defaults to "0". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#redis_database +# @default "0" +# @see https://docs.pixelfed.org/technical-documentation/config/#redis_database +# @dottie/validate required,number #REDIS_DATABASE="0" ################################################################################ -# Cache settings -################################################################################ - -# This option controls the default cache connection that gets used while using this caching library. -# -# This connection is used when another is not explicitly specified when executing a given caching function. -# -# Possible values: -# - "apc" -# - "array" -# - "database" -# - "file" (default) -# - "memcached" -# - "redis" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cache_driver -CACHE_DRIVER="redis" - -# Defaults to ${APP_NAME}_cache, or laravel_cache if no APP_NAME is set. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cache_prefix -#CACHE_PREFIX="{APP_NAME}_cache" - -################################################################################ -# Horizon settings -################################################################################ - -# 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. -# -# Defaults to "horizon-". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix -#HORIZON_PREFIX="horizon-" - -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode -#HORIZON_DARKMODE="false" - -# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker -# may consume before it is terminated and restarted. -# -# You should set this value according to the resources available to your server. -# -# Defaults to "64". -#HORIZON_MEMORY_LIMIT="64" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy -#HORIZON_BALANCE_STRATEGY="auto" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes -#HORIZON_MIN_PROCESSES="1" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes -#HORIZON_MAX_PROCESSES="20" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory -#HORIZON_SUPERVISOR_MEMORY="64" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries -#HORIZON_SUPERVISOR_TRIES="3" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice -#HORIZON_SUPERVISOR_NICE="0" - -# See: https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout -#HORIZON_SUPERVISOR_TIMEOUT="300" - -################################################################################ -# Experiments +# experiments ################################################################################ # Text only posts (alpha). # -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_top +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_top +# @dottie/validate required,boolean #EXP_TOP="false" # Poll statuses (alpha). # -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_polls +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_polls +# @dottie/validate required,boolean #EXP_POLLS="false" # Cached public timeline for larger instances (beta). # -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_cpt +# @dottie/validate required,boolean #EXP_CPT="false" # Enforce Mastodon API Compatibility (alpha). # -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#exp_emc +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#exp_emc +# @dottie/validate required,boolean #EXP_EMC="true" ################################################################################ -# ActivityPub confguration +# ActivityPub ################################################################################ -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#activity_pub -ACTIVITY_PUB="true" +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#activity_pub +# @dottie/validate required,boolean +#ACTIVITY_PUB="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_remote_follow +# @dottie/validate required,boolean #AP_REMOTE_FOLLOW="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_sharedinbox +# @dottie/validate required,boolean #AP_SHAREDINBOX="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_inbox +# @dottie/validate required,boolean #AP_INBOX="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#ap_outbox +# @dottie/validate required,boolean #AP_OUTBOX="true" ################################################################################ -# Federation confguration +# Federation ################################################################################ -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#atom_feeds +# @dottie/validate required,boolean #ATOM_FEEDS="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#nodeinfo +# @dottie/validate required,boolean #NODEINFO="true" -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#webfinger +# @default "true" +# @see https://docs.pixelfed.org/technical-documentation/config/#webfinger +# @dottie/validate required,boolean #WEBFINGER="true" ################################################################################ -# Storage (cloud) +# Storage ################################################################################ # Store media on object storage like S3, Digital Ocean Spaces, Rackspace # -# Defaults to "false". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#pf_enable_cloud +# @dottie/validate required,boolean #PF_ENABLE_CLOUD="false" # Many applications store files both locally and in the cloud. @@ -567,125 +584,108 @@ ACTIVITY_PUB="true" # For this reason, you may specify a default “cloud” driver here. # This driver will be bound as the Cloud disk implementation in the container. # -# Defaults to "s3". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +# @default "s3" +# @see https://docs.pixelfed.org/technical-documentation/config/#filesystem_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD #FILESYSTEM_CLOUD="s3" -# Defaults to true. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +# @default true. +# @see https://docs.pixelfed.org/technical-documentation/config/#media_delete_local_after_cloud +# @dottie/validate required_with=PF_ENABLE_CLOUD,boolean #MEDIA_DELETE_LOCAL_AFTER_CLOUD="true" -################################################################################ -# Storage (cloud) - S3 andS S3 *compatible* providers -################################################################################ - -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_access_key_id +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_ACCESS_KEY_ID="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_secret_access_key +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_SECRET_ACCESS_KEY="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_default_region +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_default_region +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_DEFAULT_REGION="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_bucket +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_bucket +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_BUCKET="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_url +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_url +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_URL="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_ENDPOINT="" -# See: https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +# @see https://docs.pixelfed.org/technical-documentation/config/#aws_use_path_style_endpoint +# @dottie/validate required_if=FILESYSTEM_CLOUD s3 #AWS_USE_PATH_STYLE_ENDPOINT="false" -############################################################### -# COSTAR - Confirm Object Sentiment Transform and Reduce -############################################################### +################################################################################ +# COSTAR +################################################################################ # Comma-separated list of domains to block. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_domains +# @dottie/validate #CS_BLOCKED_DOMAINS="" # Comma-separated list of domains to add warnings. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_domains +# @dottie/validate #CS_CW_DOMAINS="" # Comma-separated list of domains to remove from public timelines. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_domains +# @dottie/validate #CS_UNLISTED_DOMAINS="" # Comma-separated list of keywords to block. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_keywords +# @dottie/validate #CS_BLOCKED_KEYWORDS="" # Comma-separated list of keywords to add warnings. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_keywords +# @dottie/validate #CS_CW_KEYWORDS="" # Comma-separated list of keywords to remove from public timelines. # -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_keywords +# @dottie/validate #CS_UNLISTED_KEYWORDS="" -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_blocked_actor +# @dottie/validate #CS_BLOCKED_ACTOR="" -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_cw_actor +# @dottie/validate #CS_CW_ACTOR="" -# Defaults to null (not set/commented out). -# -# See: https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +# @default null (not set/commented out). +# @see https://docs.pixelfed.org/technical-documentation/config/#cs_unlisted_actor +# @dottie/validate #CS_UNLISTED_ACTOR="" -############################################################### -# Media -############################################################### - -# Defaults to false. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#media_exif_database -MEDIA_EXIF_DATABASE="true" - -# Pixelfed supports GD or ImageMagick to process images. -# -# Defaults to "gd". -# -# Possible values: -# - "gd" (default) -# - "imagick" -# -# See: https://docs.pixelfed.org/technical-documentation/config/#image_driver -#IMAGE_DRIVER="gd" - -############################################################### -# Logging -############################################################### +################################################################################ +# logging +################################################################################ # Possible values: # @@ -699,56 +699,35 @@ MEDIA_EXIF_DATABASE="true" # - "null" # - "emergency" # - "media" +# +# @default "stack" +# @dottie/validate required,oneof=stack single daily slack stderr syslog errorlog null emergency media LOG_CHANNEL="stderr" # Used by single, stderr and syslog. # -# Defaults to "debug" for all of those. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_level +# @default "debug" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_level +# @dottie/validate required,boolean #LOG_LEVEL="debug" # Used by stderr. # -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter +# @dottie/validate required #LOG_STDERR_FORMATTER="" # Used by slack. # -# Defaults to "". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +# @default "" +# @see https://docs.pixelfed.org/technical-documentation/config/#log_slack_webhook_url +# @dottie/validate required,http_url #LOG_SLACK_WEBHOOK_URL="" -############################################################### -# Broadcasting settings -############################################################### - -# This option controls the default broadcaster that will be used by the framework when an event needs to be broadcast. -# -# Possible values: -# - "pusher" -# - "redis" -# - "log" -# - "null" (default) -# -# See: https://docs.pixelfed.org/technical-documentation/config/#broadcast_driver -BROADCAST_DRIVER="redis" - -############################################################### -# Sanitizing settings -############################################################### - -# Defaults to "true". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#restrict_html_types -#RESTRICT_HTML_TYPES="true" - -############################################################### -# Queue configuration -############################################################### +################################################################################ +# queue +################################################################################ # Possible values: # - "sync" (default) @@ -758,41 +737,39 @@ BROADCAST_DRIVER="redis" # - "redis" # - "null" # -# See: https://docs.pixelfed.org/technical-documentation/config/#queue_driver +# @default "sync" +# @see https://docs.pixelfed.org/technical-documentation/config/#queue_driver +# @dottie/validate required,oneof=sync database beanstalkd sqs redis null QUEUE_DRIVER="redis" -############################################################### -# Queue (SQS) configuration -############################################################### - -# Defaults to "your-public-key". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_key +# @default "your-public-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_key +# @dottie/validate required_if=QUEUE_DRIVER sqs #SQS_KEY="your-public-key" -# Defaults to "your-secret-key". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +# @default "your-secret-key" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_secret +# @dottie/validate required_if=QUEUE_DRIVER sqs #SQS_SECRET="your-secret-key" -# Defaults to "https://sqs.us-east-1.amazonaws.com/your-account-id". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +# @default "https://sqs.us-east-1.amazonaws.com/your-account-id" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_prefix +# @dottie/validate required_if=QUEUE_DRIVER sqs #SQS_PREFIX="" -# Defaults to "your-queue-name". -# -# https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +# @default "your-queue-name" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_queue +# @dottie/validate required_if=QUEUE_DRIVER sqs #SQS_QUEUE="your-queue-name" -# Defaults to "us-east-1". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#sqs_region +# @default "us-east-1" +# @see https://docs.pixelfed.org/technical-documentation/config/#sqs_region +# @dottie/validate required_if=QUEUE_DRIVER sqs #SQS_REGION="us-east-1" -############################################################### -# Session configuration -############################################################### +################################################################################ +# session +################################################################################ # This option controls the default session “driver” that will be used on requests. # @@ -806,15 +783,18 @@ QUEUE_DRIVER="redis" # - "memcached" # - "redis" # - "array" +# +# @default "database" +# @dottie/validate required,oneof=file cookie database apc memcached redis array SESSION_DRIVER="redis" # Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires. # # If you want them to immediately expire on the browser closing, set that option. # -# Defaults to 86400. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +# @default 86400. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_lifetime +# @dottie/validate required,number #SESSION_LIFETIME="86400" # Here you may change the domain of the cookie used to identify a session in your application. @@ -823,101 +803,130 @@ SESSION_DRIVER="redis" # # A sensible default has been set. # -# Defaults to the value of APP_DOMAIN, or null. -# -# See: https://docs.pixelfed.org/technical-documentation/config/#session_domain +# @default the value of APP_DOMAIN, or null. +# @see https://docs.pixelfed.org/technical-documentation/config/#session_domain +# @dottie/validate required,domain #SESSION_DOMAIN="${APP_DOMAIN}" -############################################################### -# Proxy configuration -############################################################### - -# Set trusted proxy IP addresses. -# -# Both IPv4 and IPv6 addresses are supported, along with CIDR notation. -# -# The “*” character is syntactic sugar within TrustedProxy to trust any -# proxy that connects directly to your server, a requirement when you cannot -# know the address of your proxy (e.g. if using Rackspace balancers). -# -# The “**” character is syntactic sugar within TrustedProxy to trust not just any -# proxy that connects directly to your server, but also proxies that connect to those proxies, -# and all the way back until you reach the original source IP. It will mean that -# $request->getClientIp() always gets the originating client IP, no matter how many proxies -# that client’s request has subsequently passed through. -# -# Defaults to "*". -# -# See: https://docs.pixelfed.org/technical-documentation/config/#trust_proxies -TRUST_PROXIES="*" - -############################################################### -# Passport configuration -############################################################### -# -# Passport uses encryption keys while generating secure access tokens -# for your application. -# -# By default, the keys are stored as local files but can be set via environment -# variables when that is more convenient. - -# See: https://docs.pixelfed.org/technical-documentation/config/#passport_private_key -#PASSPORT_PRIVATE_KEY="" - -# See: https://docs.pixelfed.org/technical-documentation/config/#passport_public_key -#PASSPORT_PUBLIC_KEY="" - -############################################################### -# PHP configuration -############################################################### - -# See: https://www.php.net/manual/en/ini.core.php#ini.memory-limit -#PHP_MEMORY_LIMIT="128M" - ################################################################################ -# Other configuration +# horizon ################################################################################ -# ? Add your own configuration here - -################################################################################ -# Timezone configuration -################################################################################ - -# Set timezone used by *all* containers - these must be in sync. +# This prefix will be used when storing all Horizon data in Redis. # -# ! Do not edit your timezone once the service is running - or things will break! +# You may modify the prefix when you are running multiple installations +# of Horizon on the same server so that they don’t have problems. # -# See: https://www.php.net/manual/en/timezones.php -TZ="${APP_TIMEZONE}" +# @default "horizon-" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_prefix +# @dottie/validate required +#HORIZON_PREFIX="horizon-" + +# @default "false" +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_darkmode +# @dottie/validate required,boolean +#HORIZON_DARKMODE="false" + +# This value (in MB) describes the maximum amount of memory (in MB) the Horizon worker +# may consume before it is terminated and restarted. +# +# You should set this value according to the resources available to your server. +# +# @default "64" +# @dottie/validate required,number +#HORIZON_MEMORY_LIMIT="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_balance_strategy +# @dottie/validate required +#HORIZON_BALANCE_STRATEGY="auto" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_min_processes +# @dottie/validate required,number +#HORIZON_MIN_PROCESSES="1" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_max_processes +# @dottie/validate required,number +#HORIZON_MAX_PROCESSES="20" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_memory +# @dottie/validate required,number +#HORIZON_SUPERVISOR_MEMORY="64" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_tries +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TRIES="3" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_nice +# @dottie/validate required,number +#HORIZON_SUPERVISOR_NICE="0" + +# @see https://docs.pixelfed.org/technical-documentation/config/#horizon_supervisor_timeout +# @dottie/validate required,number +#HORIZON_SUPERVISOR_TIMEOUT="300" ################################################################################ -# Docker configuraton for *all* services +# docker shared ################################################################################ +# A random 32-character string to be used as an encryption key. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# ! NOTE: This will be auto-generated by Docker during bootstrap +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# This key is used by the Illuminate encrypter service and should be set to a random, +# 32 character string, otherwise these encrypted strings will not be safe. +# +# @see https://docs.pixelfed.org/technical-documentation/config/#app_key +APP_KEY= + # Prefix for container names (without any dash at the end) +# @dottie/validate required DOCKER_ALL_CONTAINER_NAME_PREFIX="${APP_DOMAIN}" # How often Docker health check should run for all services # # Can be overridden by individual [DOCKER_*_HEALTHCHECK_INTERVAL] settings further down +# +# @default "10s" +# @dottie/validate required DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL="10s" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will *all* data # will be stored (data, config, overrides) +# +# @default "./docker-compose-state" +# @dottie/validate required,dir DOCKER_ALL_HOST_ROOT_PATH="./docker-compose-state" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their data -DOCKER_ALL_HOST_DATA_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/data" +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/data" +# @dottie/validate required,dir +DOCKER_ALL_HOST_DATA_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/data" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store their confguration -DOCKER_ALL_HOST_CONFIG_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/config" +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/config" +# @dottie/validate required,dir +DOCKER_ALL_HOST_CONFIG_ROOT_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/config" # Path (relative to the docker-compose.yml) or absolute (/some/other/path) where containers will store overrides -DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/overrides" +# +# @default "${DOCKER_ALL_HOST_ROOT_PATH}/overrides" +# @dottie/validate required,dir +DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH:?error}/overrides" + +# Set timezone used by *all* containers - these must be in sync. +# +# ! Do not edit your timezone once the service is running - or things will break! +# +# @see https://www.php.net/manual/en/timezones.php +# @dottie/validate required,timezone +TZ="${APP_TIMEZONE}" ################################################################################ -# Docker [web] + [worker] (also know as "app") shared service configuration +# docker app ################################################################################ # The docker tag prefix to use for pulling images, can be one of @@ -931,6 +940,7 @@ DOCKER_APP_HOST_OVERRIDES_PATH="${DOCKER_ALL_HOST_ROOT_PATH}/overrides" # # Combined with [DOCKER_APP_RUNTIME] and [PHP_VERSION] configured # elsewhere in this file, the final Docker tag is computed. +# @dottie/validate required DOCKER_APP_RELEASE="branch-jippi-fork" # The PHP version to use for [web] and [worker] container @@ -947,21 +957,25 @@ DOCKER_APP_RELEASE="branch-jippi-fork" # Do *NOT* use the full Docker tag (e.g. "8.3.2RC1-fpm-bullseye") # *only* the version part. The rest of the full tag is derived from # the [DOCKER_APP_RUNTIME] and [PHP_DEBIAN_RELEASE] settings +# @dottie/validate required DOCKER_APP_PHP_VERSION="8.2" # The container runtime to use. # -# See: https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache nginx fpm DOCKER_APP_RUNTIME="apache" # The Debian release variant to use of the [php] Docker image # # Examlpe: [bookworm] or [bullseye] +# @dottie/validate required,oneof=bookwork bullseye DOCKER_APP_DEBIAN_RELEASE="bullseye" # The [php] Docker image base type # -# See: https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @see https://docs.pixelfed.org/running-pixelfed/docker/runtimes.html +# @dottie/validate required,oneof=apache fpm cli DOCKER_APP_BASE_TYPE="apache" # Image to pull the Pixelfed Docker images from. @@ -972,24 +986,28 @@ DOCKER_APP_BASE_TYPE="apache" # * "pixelfed/pixelfed" to pull from DockerHub # * "your/fork" to pull from a custom fork # +# @dottie/validate required DOCKER_APP_IMAGE="ghcr.io/jippi/pixelfed" # Pixelfed version (image tag) to pull from the registry. # -# See: https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed -DOCKER_APP_TAG="${DOCKER_APP_RELEASE}-${DOCKER_APP_RUNTIME}-${DOCKER_APP_PHP_VERSION}" +# @see https://github.com/pixelfed/pixelfed/pkgs/container/pixelfed +# @dottie/validate required +DOCKER_APP_TAG="${DOCKER_APP_RELEASE:?error}-${DOCKER_APP_RUNTIME:?error}-${DOCKER_APP_PHP_VERSION:?error}" # Path (on host system) where the [app] + [worker] container will write # its [storage] data (e.g uploads/images/profile pictures etc.). # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/storage" +# @dottie/validate required,dir +DOCKER_APP_HOST_STORAGE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/storage" # Path (on host system) where the [app] + [worker] container will write # its [cache] data. # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" +# @dottie/validate required,dir +DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/cache" # Automatically run "One-time setup tasks" commands. # @@ -998,6 +1016,7 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" # you can set this to "0" to prevent them from running. # # Otherwise, leave it at "1" to have them run *once*. +# @dottie/validate required,boolean #DOCKER_APP_RUN_ONE_TIME_SETUP_TASKS="1" # A space-seperated list of paths (inside the container) to *recursively* [chown] @@ -1008,46 +1027,59 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/pixelfed/cache" # ! issues. Please report a bug if you see behavior requiring this to be permanently on # # Example: "/var/www/storage /var/www/bootstrap/cache" +# @dottie/validate required #DOCKER_APP_ENSURE_OWNERSHIP_PATHS="" # Enable Docker Entrypoint debug mode (will call [set -x] in bash scripts) -# by setting this to "1". +# by setting this to "1" +# @dottie/validate required,boolean #DOCKER_APP_ENTRYPOINT_DEBUG="0" # List of extra APT packages (separated by space) to install when building # locally using [docker compose build]. # -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required #DOCKER_APP_APT_PACKAGES_EXTRA="" # List of *extra* PECL extensions (separated by space) to install when # building locally using [docker compose build]. # -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required #DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA="" # List of *extra* PHP extensions (separated by space) to install when # building locally using [docker compose build]. # -# See: https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @see https://github.com/pixelfed/pixelfed/blob/dev/docker/customizing.md +# @dottie/validate required #DOCKER_APP_PHP_EXTENSIONS_EXTRA="" +# @default "128M" +# @see https://www.php.net/manual/en/ini.core.php#ini.memory-limit +# @dottie/validate required +#DOCKER_APP_PHP_MEMORY_LIMIT="128M" + ################################################################################ -# Docker [redis] service configuration +# docker redis ################################################################################ # Redis version to use as Docker tag # -# See: https://hub.docker.com/_/redis +# @see https://hub.docker.com/_/redis +# @dottie/validate required DOCKER_REDIS_VERSION="7.2" # Path (on host system) where the [redis] container will store its data # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_REDIS_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/redis" +# @dottie/validate required,dir +DOCKER_REDIS_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/redis" # Port that Redis will listen on *outside* the container (e.g. the host machine) -DOCKER_REDIS_HOST_PORT="${REDIS_PORT}" +# @dottie/validate required,number +DOCKER_REDIS_HOST_PORT="${REDIS_PORT:?error}" # The filename that Redis should store its config file within # @@ -1055,121 +1087,131 @@ DOCKER_REDIS_HOST_PORT="${REDIS_PORT}" # # Use a command like [touch "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis/redis.conf"] to create it. # -# Defaults to "" +# @default "" +# @dottie/validate required #DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" # How often Docker health check should run for [redis] service # -# Defaults to "10s" -DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" +# @default "10s" +# @dottie/validate required +DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" ################################################################################ -# Docker [db] service configuration +# docker db ################################################################################ +# Docker image for the DB service +# @dottie/validate required +DOCKER_DB_IMAGE="mariadb:${DB_VERSION}" + +# Command to pass to the [db] server container +# @dottie/validate required +DOCKER_DB_COMMAND="--default-authentication-plugin=mysql_native_password" + # Set this to a non-empty value (e.g. "disabled") to disable the [db] service #DOCKER_DB_PROFILE="" # Path (on host system) where the [db] container will store its data # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) -DOCKER_DB_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH}/db" +# @dottie/validate required,dir +DOCKER_DB_HOST_DATA_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/db" -# Port that the database will listen on *outside* the container (e.g. the host machine) +# Path (inside the container) where the [db] will store its data. +# +# Path MUST be absolute. +# +# For MySQL this should be [/var/lib/mysql] +# For PostgreSQL this should be [/var/lib/postgresql/data] +# @dottie/validate required +DOCKER_DB_CONTAINER_DATA_PATH="/var/lib/mysql" + +# Port that the database will listen on *OUTSIDE* the container (e.g. the host machine) # # Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL -DOCKER_DB_HOST_PORT="${DB_PORT}" +# @dottie/validate required,number +DOCKER_DB_HOST_PORT="${DB_PORT:?error}" + +# Port that the database will listen on *INSIDE* the container +# +# Use "3306" for MySQL/MariaDB and "5432" for PostgreeSQL +# @dottie/validate required,number +DOCKER_DB_CONTAINER_PORT="${DB_PORT:?error}" # How often Docker health check should run for [db] service -DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" +# @dottie/validate required +DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" ################################################################################ -# Docker [web] service configuration +# docker web ################################################################################ # Set this to a non-empty value (e.g. "disabled") to disable the [web] service +# @dottie/validate required #DOCKER_WEB_PROFILE="" # Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only +# @dottie/validate required,number DOCKER_WEB_PORT_EXTERNAL_HTTP="8080" # How often Docker health check should run for [web] service -DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" +# @dottie/validate required +DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" ################################################################################ -# Docker [worker] service configuration +# docker worker ################################################################################ # Set this to a non-empty value (e.g. "disabled") to disable the [worker] service +# @dottie/validate required #DOCKER_WORKER_PROFILE="" # How often Docker health check should run for [worker] service -DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" +# @dottie/validate required +DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" ################################################################################ -# Docker [proxy] + [proxy-acme] service configuration +# docker proxy ################################################################################ +# The version of nginx-proxy to use +# +# @see https://hub.docker.com/r/nginxproxy/nginx-proxy +# @dottie/validate required +DOCKER_PROXY_VERSION="1.4" + # Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service -DOCKER_PROXY_PROFILE="" +#DOCKER_PROXY_PROFILE= # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service -DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE}" +#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE?error}" # How often Docker health check should run for [proxy] service -DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL}" +# @dottie/validate required +DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" # Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTP traffic +# @dottie/validate required,number DOCKER_PROXY_HOST_PORT_HTTP="80" # Port that the [proxy] will listen on *outside* the container (e.g. the host machine) for HTTPS traffic +# @dottie/validate required,number DOCKER_PROXY_HOST_PORT_HTTPS="443" # Path to the Docker socket on the *host* +# @dottie/validate required,file DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH="/var/run/docker.sock" -# ! ---------------------------------------------------------------------------- -# ! STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP -# ! ---------------------------------------------------------------------------- -# ! Below this line is default environment variables for various [db] backends -# ! You very likely do *NOT* need to modify any of this, ever. -# ! ---------------------------------------------------------------------------- +# The host to request LetsEncrypt certificate for +# @dottie/validate required,fqdn +DOCKER_PROXY_LETSENCRYPT_HOST="${APP_DOMAIN}" -################################################################################ -# Docker [db] service environment variables for MySQL (Oracle) -################################################################################ -# -# See "Environment Variables" at https://hub.docker.com/_/mysql -# -# ! DO NOT CHANGE unless you know what you are doing +# The e-mail to use for Lets Encrypt certificate requests. +# @dottie/validate required,email +DOCKER_PROXY_LETSENCRYPT_EMAIL="${INSTANCE_CONTACT_EMAIL:?error}" -MYSQL_ROOT_PASSWORD="${DB_PASSWORD}" -MYSQL_USER="${DB_USERNAME}" -MYSQL_PASSWORD="${DB_PASSWORD}" -MYSQL_DATABASE="${DB_DATABASE}" - -################################################################################ -# Docker [db] service environment variables for MySQL (MariaDB) -################################################################################ +# Lets Encrypt staging/test servers for certificate requests. # -# See "Start a mariadb server instance with user, password and database" -# at https://hub.docker.com/_/mariadb -# -# ! DO NOT CHANGE unless you know what you are doing - -MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" -MARIADB_USER="${DB_USERNAME}" -MARIADB_PASSWORD="${DB_PASSWORD}" -MARIADB_DATABASE="${DB_DATABASE}" - -################################################################################ -# Docker [db] service environment variables for PostgreSQL -################################################################################ -# -# See "Environment Variables" at https://hub.docker.com/_/postgres -# -# ! DO NOT CHANGE unless you know what you are doing - -POSTGRES_USER="${DB_USERNAME}" -POSTGRES_PASSWORD="${DB_PASSWORD}" -POSTGRES_DB="${DB_DATABASE}" +# Setting this to any value will change to letsencrypt test servers. +DOCKER_PROXY_LETSENCRYPT_TEST="1" diff --git a/.env.example b/.env.example deleted file mode 100644 index 0a24d1dc1..000000000 --- a/.env.example +++ /dev/null @@ -1,80 +0,0 @@ -# shellcheck disable=SC2034,SC2148 - -APP_NAME="Pixelfed" -APP_ENV="production" -APP_KEY= -APP_DEBUG="false" - -# Instance Configuration -OPEN_REGISTRATION="false" -ENFORCE_EMAIL_VERIFICATION="false" -PF_MAX_USERS="1000" -OAUTH_ENABLED="true" - -# Media Configuration -PF_OPTIMIZE_IMAGES="true" -IMAGE_QUALITY="80" -MAX_PHOTO_SIZE="15000" -MAX_CAPTION_LENGTH="500" -MAX_ALBUM_LENGTH="4" - -# Instance URL Configuration -APP_URL="http://localhost" -APP_DOMAIN="localhost" -ADMIN_DOMAIN="localhost" -SESSION_DOMAIN="localhost" -TRUST_PROXIES="*" - -# Database Configuration -DB_CONNECTION="mysql" -DB_HOST="127.0.0.1" -DB_PORT="3306" -DB_DATABASE="pixelfed" -DB_USERNAME="pixelfed" -DB_PASSWORD="pixelfed" - -# Redis Configuration -REDIS_CLIENT="predis" -REDIS_SCHEME="tcp" -REDIS_HOST="127.0.0.1" -REDIS_PASSWORD="null" -REDIS_PORT="6379" - -# Laravel Configuration -SESSION_DRIVER="database" -CACHE_DRIVER="redis" -QUEUE_DRIVER="redis" -BROADCAST_DRIVER="log" -LOG_CHANNEL="stack" -HORIZON_PREFIX="horizon-" - -# ActivityPub Configuration -ACTIVITY_PUB="false" -AP_REMOTE_FOLLOW="false" -AP_INBOX="false" -AP_OUTBOX="false" -AP_SHAREDINBOX="false" - -# Experimental Configuration -EXP_EMC="true" - -## Mail Configuration (Post-Installer) -MAIL_DRIVER=log -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=2525 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="pixelfed@example.com" -MAIL_FROM_NAME="Pixelfed" - -## S3 Configuration (Post-Installer) -PF_ENABLE_CLOUD=false -FILESYSTEM_CLOUD=s3 -#AWS_ACCESS_KEY_ID= -#AWS_SECRET_ACCESS_KEY= -#AWS_DEFAULT_REGION= -#AWS_BUCKET= -#AWS_URL= -#AWS_ENDPOINT= -#AWS_USE_PATH_STYLE_ENDPOINT=false diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a1ddb073..6446fb6f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,19 @@ { "shellformat.useEditorConfig": true, + "[shellscript]": { + "files.eol": "\n", + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "[dockercompose]": { + "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.autoIndent": "advanced", + }, + "yaml.schemas": { + "https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json" + }, "files.associations": { ".env": "shellscript", ".env.*": "shellscript" diff --git a/Dockerfile b/Dockerfile index 1c8a30155..0e8e8a32f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,10 +20,8 @@ ARG FOREGO_VERSION="0.17.2" # See: https://github.com/hairyhenderson/gomplate ARG GOMPLATE_VERSION="v3.11.6" -# See: https://github.com/dotenv-linter/dotenv-linter -# -# WARN: v3.3.0 and above requires newer libc version than Ubuntu ships with -ARG DOTENV_LINTER_VERSION="v3.2.0" +# See: https://github.com/jippi/dottie +ARG DOTTIE_VERSION="v0.6.5" ### # PHP base configuration @@ -88,6 +86,13 @@ FROM nginx:${NGINX_VERSION} AS nginx-image # See: https://github.com/nginx-proxy/forego FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image +# Dottie makes working with .env files easier and safer +# +# NOTE: Docker will *not* pull this image unless it's referenced (via build target) +# +# See: https://github.com/jippi/dottie +FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image + # gomplate-image grabs the gomplate binary from GitHub releases # # It's in its own layer so it can be fetched in parallel with other build steps @@ -116,7 +121,6 @@ FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base ARG BUILDKIT_SBOM_SCAN_STAGE="true" ARG APT_PACKAGES_EXTRA -ARG DOTENV_LINTER_VERSION ARG PHP_DEBIAN_RELEASE ARG PHP_VERSION ARG RUNTIME_GID @@ -135,7 +139,6 @@ RUN set -ex \ WORKDIR /var/www/ ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} -ENV DOTENV_LINTER_VERSION="${DOTENV_LINTER_VERSION}" # Install and configure base layer COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh @@ -226,6 +229,7 @@ ENV RUNTIME_UID=${RUNTIME_UID} ENV RUNTIME_GID=${RUNTIME_GID} COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego +COPY --link --from=dottie-image /dottie /usr/local/bin/dottie COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www diff --git a/docker-compose.yml b/docker-compose.yml index 986bf351b..8b537db14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ --- -version: "3" +# Require 3.8 to ensure people use a recent version of Docker + Compose +version: "3.8" ############################################################### # Please see docker/README.md for usage information @@ -53,7 +54,7 @@ services: - ${DOCKER_PROXY_ACME_PROFILE:-} environment: DEBUG: 0 - DEFAULT_EMAIL: "${LETSENCRYPT_EMAIL}" + DEFAULT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" NGINX_PROXY_CONTAINER: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" depends_on: - proxy @@ -74,21 +75,21 @@ services: build: target: ${DOCKER_APP_RUNTIME}-runtime args: - PHP_VERSION: "${DOCKER_APP_PHP_VERSION}" + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" - APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" - PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" volumes: - "./.env:/var/www/.env" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" environment: - LETSENCRYPT_HOST: "${LETSENCRYPT_HOST}" - LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL}" - LETSENCRYPT_TEST: "${LETSENCRYPT_TEST:-}" + LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}" + LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" + LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}" VIRTUAL_HOST: "${APP_DOMAIN}" VIRTUAL_PORT: "80" labels: @@ -117,12 +118,12 @@ services: build: target: ${DOCKER_APP_RUNTIME}-runtime args: - PHP_VERSION: "${DOCKER_APP_PHP_VERSION}" + APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" PHP_DEBIAN_RELEASE: "${DOCKER_APP_DEBIAN_RELEASE}" - APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" - PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" + PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" + PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" volumes: - "./.env:/var/www/.env" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" @@ -133,23 +134,37 @@ services: - redis healthcheck: test: gosu www-data php artisan horizon:status | grep running - interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL}" + interval: "${DOCKER_WORKER_HEALTHCHECK_INTERVAL:?error}" timeout: 5s retries: 2 db: - image: mariadb:${DB_VERSION} + image: ${DOCKER_DB_IMAGE:?error} container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-db" - command: --default-authentication-plugin=mysql_native_password + command: ${DOCKER_DB_COMMAND:-} restart: unless-stopped profiles: - ${DOCKER_DB_PROFILE:-} - env_file: - - ".env" + environment: + TZ: "${TZ:?error}" + # MySQL (Oracle) - "Environment Variables" at https://hub.docker.com/_/mysql + MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:?error}" + MYSQL_USER: "${DB_USERNAME:?error}" + MYSQL_PASSWORD: "${DB_PASSWORD:?error}" + MYSQL_DATABASE: "${DB_DATABASE:?error}" + # MySQL (MariaDB) - "Start a mariadb server instance with user, password and database" at https://hub.docker.com/_/mariadb + MARIADB_ROOT_PASSWORD: "${DB_PASSWORD:?error}" + MARIADB_USER: "${DB_USERNAME:?error}" + MARIADB_PASSWORD: "${DB_PASSWORD:?error}" + MARIADB_DATABASE: "${DB_DATABASE:?error}" + # PostgreSQL - "Environment Variables" at https://hub.docker.com/_/postgres + POSTGRES_USER: "${DB_USERNAME:?error}" + POSTGRES_PASSWORD: "${DB_PASSWORD:?error}" + POSTGRES_DB: "${DB_DATABASE:?error}" volumes: - - "${DOCKER_DB_HOST_DATA_PATH}:/var/lib/mysql" + - "${DOCKER_DB_HOST_DATA_PATH:?error}:${DOCKER_DB_CONTAINER_DATA_PATH:?error}" ports: - - "${DOCKER_DB_HOST_PORT}:3306" + - "${DOCKER_DB_HOST_PORT:?error}:${DOCKER_DB_CONTAINER_PORT:?error}" healthcheck: test: [ @@ -159,7 +174,7 @@ services: "--connect", "--innodb_initialized", ] - interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL}" + interval: "${DOCKER_DB_HEALTHCHECK_INTERVAL:?error}" retries: 2 timeout: 5s @@ -169,9 +184,8 @@ services: restart: unless-stopped command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" environment: - - REDISCLI_AUTH=${REDIS_PASSWORD:-} - env_file: - - ".env" + TZ: "${TZ:?error}" + REDISCLI_AUTH: ${REDIS_PASSWORD:-} volumes: - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/redis:/etc/redis" - "${DOCKER_REDIS_HOST_DATA_PATH}:/data" @@ -179,6 +193,6 @@ services: - "${DOCKER_REDIS_HOST_PORT}:6379" healthcheck: test: ["CMD", "redis-cli", "-p", "6379", "ping"] - interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL}" + interval: "${DOCKER_REDIS_HEALTHCHECK_INTERVAL:?error}" retries: 2 timeout: 5s diff --git a/docker/nginx/root/docker/templates/etc/nginx/nginx.conf b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf new file mode 100644 index 000000000..4e87a4565 --- /dev/null +++ b/docker/nginx/root/docker/templates/etc/nginx/nginx.conf @@ -0,0 +1,41 @@ +# This is changed from the original "nginx" in upstream to work properly +# with permissions within pixelfed when serving static files. +user www-data; + +worker_processes auto; + +# Ensure the PID is writable +# Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged +pid /tmp/nginx.pid; + +# Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log /proc/self/fd/2 notice; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; + + # Write error log to stdout (/proc/self/fd/1 -> /dev/stdout) + access_log /proc/self/fd/1 main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + gzip on; + + # Ensure all temp paths are in a writable by "www-data" user. + # Lifted from: https://hub.docker.com/r/nginxinc/nginx-unprivileged + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/shared/root/docker/entrypoint.d/02-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh index 601cf153e..bbf7dd768 100755 --- a/docker/shared/root/docker/entrypoint.d/02-check-config.sh +++ b/docker/shared/root/docker/entrypoint.d/02-check-config.sh @@ -13,5 +13,9 @@ for file in "${dot_env_files[@]}"; do continue fi - run-as-current-user dotenv-linter --skip=QuoteCharacter --skip=UnorderedKey "${file}" + # We ignore 'dir' + 'file' rules since they are validate *host* paths + # which do not (and should not) exists inside the container + # + # We disable fixer since its not interactive anyway + run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --no-fix done diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 51e955682..190c04bc4 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -20,9 +20,10 @@ declare -g script_name= declare -g script_name_previous= declare -g log_prefix= +declare -Ag lock_fds=() + # dot-env files to source when reading config declare -a dot_env_files=( - /var/www/.env.docker /var/www/.env ) @@ -166,7 +167,7 @@ function log-error() log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" > /dev/stderr + echo -e "${error_message_color}${log_prefix}ERROR -${color_clear} ${msg}" >/dev/stderr } # @description Print the given error message to stderr and exit 1 @@ -197,7 +198,7 @@ function log-warning() log-error-and-exit "[${FUNCNAME[0]}] did not receive any input arguments and STDIN is empty" fi - echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" > /dev/stderr + echo -e "${warn_message_color}${log_prefix}WARNING -${color_clear} ${msg}" >/dev/stderr } # @description Print the given message to stdout unless [ENTRYPOINT_QUIET_LOGS] is set @@ -236,7 +237,7 @@ function log-info-stderr() fi if [ -z "${ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" > /dev/stderr + echo -e "${notice_message_color}${log_prefix}${color_clear}${msg}" >/dev/stderr fi } @@ -377,17 +378,20 @@ function acquire-lock() { local name="${1:-$script_name}" local file="${docker_locks_path}/${name}" + local lock_fd ensure-directory-exists "$(dirname "${file}")" + exec {lock_fd}>"$file" + log-info "🔑 Trying to acquire lock: ${file}: " - while file-exists "${file}"; do + while ! ([[ -v lock_fds[$name] ]] || flock -n -x "$lock_fd"); do log-info "🔒 Waiting on lock ${file}" staggered-sleep done - stream-prefix-command-output touch "${file}" + [[ -v lock_fds[$name] ]] || lock_fds[$name]=$lock_fd log-info "🔐 Lock acquired [${file}]" @@ -403,7 +407,11 @@ function release-lock() log-info "🔓 Releasing lock [${file}]" - stream-prefix-command-output rm -fv "${file}" + [[ -v lock_fds[$name] ]] || return + + # shellcheck disable=SC1083,SC2086 + flock --unlock ${lock_fds[$name]} + unset 'lock_fds[$name]' } # @description Helper function to append multiple actions onto @@ -450,14 +458,14 @@ function await-database-ready() case "${DB_CONNECTION:-}" in mysql) # shellcheck disable=SC2154 - while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent > /dev/null; do + while ! echo "SELECT 1" | mysql --user="${DB_USERNAME}" --password="${DB_PASSWORD}" --host="${DB_HOST}" "${DB_DATABASE}" --silent >/dev/null; do staggered-sleep done ;; pgsql) # shellcheck disable=SC2154 - while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" > /dev/null; do + while ! echo "SELECT 1" | PGPASSWORD="${DB_PASSWORD}" psql --user="${DB_USERNAME}" --host="${DB_HOST}" "${DB_DATABASE}" >/dev/null; do staggered-sleep done ;; diff --git a/docker/shared/root/docker/install/base.sh b/docker/shared/root/docker/install/base.sh index 4e97e82bb..a1a32a003 100755 --- a/docker/shared/root/docker/install/base.sh +++ b/docker/shared/root/docker/install/base.sh @@ -59,6 +59,3 @@ apt-get install -y "${packages[@]}" locale-gen update-locale - -# Install dotenv linter (https://github.com/dotenv-linter/dotenv-linter) -curl -sSfL https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh | sh -s -- -b /usr/local/bin "${DOTENV_LINTER_VERSION:-}" diff --git a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 6277ec080..0ca96819b 100644 --- a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -406,7 +406,7 @@ max_input_time = 60 ; Maximum amount of memory a script may consume (128MB) ; http://php.net/memory-limit -memory_limit = {{ getenv "PHP_MEMORY_LIMIT" "128M" }} +memory_limit = {{ getenv "DOCKER_APP_PHP_MEMORY_LIMIT" "128M" }} ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Error handling and logging ; @@ -570,8 +570,9 @@ report_memleaks = On ; Log errors to specified file. PHP's default behavior is to leave this value ; empty. ; http://php.net/error-log -; Example: -;error_log = php_errors.log +; +; NOTE: Write error log to stderr (/proc/self/fd/2 -> /dev/stderr) +error_log = /proc/self/fd/2 ; Log errors to syslog (Event Log on Windows). ;error_log = syslog diff --git a/goss.yaml b/goss.yaml index f558f788a..73f245c64 100644 --- a/goss.yaml +++ b/goss.yaml @@ -114,7 +114,7 @@ command: {{ end }} {{ if eq .Env.PHP_BASE_TYPE "apache" }} - nginx-version: + apache-version: exit-status: 0 exec: 'apachectl -v' stdout: From 143d5703dd7bb0e847e56a9c52a271bad14ba513 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 10 Feb 2024 23:08:22 +0000 Subject: [PATCH 384/977] update .env.docker --- .env.docker | 40 ++++++++++++++++++++++++---------------- Dockerfile | 2 +- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.env.docker b/.env.docker index 0fa9e1428..1158cace4 100644 --- a/.env.docker +++ b/.env.docker @@ -12,15 +12,16 @@ # app ################################################################################ +# The name/title for your site # @see https://docs.pixelfed.org/technical-documentation/config/#app_name-1 -# @dottie/validate required -APP_NAME="Pixelfed Prod" +# @dottie/validate required,ne=My Pixelfed Site +APP_NAME="My Pixelfed Site" -# Application domains used for routing. +# Application domain used for routing. (e.g., pixelfed.org) # # @see https://docs.pixelfed.org/technical-documentation/config/#app_domain -# @dottie/validate required,fqdn -APP_DOMAIN="__CHANGE_ME__" +# @dottie/validate required,ne=example.com,fqdn +APP_DOMAIN="example.com" # This URL is used by the console to properly generate URLs when using the Artisan command line tool. # You should set this to the root of your application so that it is used when running Artisan tasks. @@ -208,10 +209,12 @@ APP_TIMEZONE="UTC" # @dottie/validate required,boolean #INSTANCE_PUBLIC_HASHTAGS="false" +# The public e-mail address people can use to contact you by +# # @default "" # @see https://docs.pixelfed.org/technical-documentation/config/#instance_contact_email -# @dottie/validate required,email -INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" +# @dottie/validate required,ne=__CHANGE_ME__,email +INSTANCE_CONTACT_EMAIL="__CHANGE_ME__" # @default "false" # @see https://docs.pixelfed.org/technical-documentation/config/#instance_public_local_timeline @@ -237,7 +240,7 @@ INSTANCE_CONTACT_EMAIL="admin@${APP_DOMAIN}" # @default false # @see https://docs.pixelfed.org/technical-documentation/config/#media_exif_database # @dottie/validate required,boolean -MEDIA_EXIF_DATABASE="true" +#MEDIA_EXIF_DATABASE="false" # Pixelfed supports GD or ImageMagick to process images. # @@ -357,9 +360,12 @@ DB_HOST="db" # @dottie/validate required DB_USERNAME="pixelfed" +# The password to your database. Please make it secure. +# Use a site like https://pwgen.io/ to generate it +# # @see https://docs.pixelfed.org/technical-documentation/config/#db_password -# @dottie/validate required -DB_PASSWORD= +# @dottie/validate required,ne=__CHANGE_ME__ +DB_PASSWORD="__CHANGE_ME__" # @see https://docs.pixelfed.org/technical-documentation/config/#db_database # @dottie/validate required @@ -416,19 +422,21 @@ DB_APPLY_NEW_MIGRATIONS_AUTOMATICALLY="false" # @dottie/validate required_with=MAIL_DRIVER,number #MAIL_PORT="587" -# You may wish for all e-mails sent by your application to be sent from the same address. -# # Here, you may specify a name and address that is used globally for all e-mails that are sent by your application. # -# @default "hello@example.com" +# You may wish for all e-mails sent by your application to be sent from the same address. +# +# @default "bot@example.com" # @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_address -# @dottie/validate required_with=MAIL_DRIVER,email -MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}" +# @dottie/validate required_with=MAIL_DRIVER,email,ne=__CHANGE_ME__ +#MAIL_FROM_ADDRESS="__CHANGE_ME__" +# The 'name' you send e-mail from +# # @default "Example" # @see https://docs.pixelfed.org/technical-documentation/config/#mail_from_name # @dottie/validate required_with=MAIL_DRIVER -MAIL_FROM_NAME="Pixelfed @ ${APP_DOMAIN}" +#MAIL_FROM_NAME="${APP_NAME}" # If your SMTP server requires a username for authentication, you should set it here. # diff --git a/Dockerfile b/Dockerfile index 0e8e8a32f..efbe70510 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.6.5" +ARG DOTTIE_VERSION="v0.6.9" ### # PHP base configuration From e18d6083a209d5c17d87c6bcb19fb4cc6b9c8434 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 11 Feb 2024 01:24:26 +0000 Subject: [PATCH 385/977] bump dottie --- .env.docker | 8 ++++++-- .gitignore | 1 + Dockerfile | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env.docker b/.env.docker index 1158cace4..2b1ab66a6 100644 --- a/.env.docker +++ b/.env.docker @@ -1,12 +1,16 @@ #!/bin/bash # -*- mode: bash -*- # vi: ft=bash +# shellcheck disable=SC2034,SC2148 # Use Dottie (https://github.com/jippi/dottie) to manage this .env file easier! # -# @dottie/source .env.docker +# For example: # -# shellcheck disable=SC2034,SC2148 +# Run [dottie update] to update your [.env] file with upstream (as part of upgrade) +# Run [dottie validate] to validate youe [.env] file +# +# @dottie/source .env.docker ################################################################################ # app diff --git a/.gitignore b/.gitignore index 689c2e13a..f83ec13c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .bashrc .DS_Store .env +.env.dottie-backup .git-credentials .gitconfig /.composer/ diff --git a/Dockerfile b/Dockerfile index efbe70510..5b6f45426 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.6.9" +ARG DOTTIE_VERSION="v0.7.0" ### # PHP base configuration From fd62962d201f7bb052b1eeaac0182de313d1d3b6 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 11 Feb 2024 01:57:11 +0000 Subject: [PATCH 386/977] delete contrib --- contrib/docker-nginx.conf | 35 -------------------- contrib/nginx.conf | 67 --------------------------------------- 2 files changed, 102 deletions(-) delete mode 100644 contrib/docker-nginx.conf delete mode 100644 contrib/nginx.conf diff --git a/contrib/docker-nginx.conf b/contrib/docker-nginx.conf deleted file mode 100644 index 9d0a199e6..000000000 --- a/contrib/docker-nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ -upstream fe { - server 127.0.0.1:8080; -} - -server { - server_name real.domain; - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/real.domain/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/real.domain/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - - location / { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_x_forwarded_host; - proxy_set_header X-Forwarded-Port $http_x_forwarded_port; - proxy_redirect off; - proxy_pass http://fe/; - } -} - -server { - if ($host = real.domain) { - return 301 https://$host$request_uri; - } - - listen 80; - listen [::]:80; - server_name real.domain; - return 404; -} \ No newline at end of file diff --git a/contrib/nginx.conf b/contrib/nginx.conf deleted file mode 100644 index 0f86ea9e7..000000000 --- a/contrib/nginx.conf +++ /dev/null @@ -1,67 +0,0 @@ -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name pixelfed.example; # change this to your fqdn - root /home/pixelfed/public; # path to repo/public - - ssl_certificate /etc/nginx/ssl/server.crt; # generate your own - ssl_certificate_key /etc/nginx/ssl/server.key; # or use letsencrypt - - ssl_protocols TLSv1.2; - ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES; - ssl_prefer_server_ciphers on; - - #add_header X-Frame-Options "SAMEORIGIN"; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Content-Type-Options "nosniff"; - - index index.php; - - charset utf-8; - client_max_body_size 15M; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param REDIRECT_STATUS 200; - fastcgi_param HTTP_PROXY ""; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} - -server { # Redirect http to https - server_name pixelfed.example; # change this to your fqdn - listen 80; - listen [::]:80; - return 301 https://$host$request_uri; -} From 49a778d12889931af77612d6fe0242231cb7a17c Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 11 Feb 2024 02:00:09 +0000 Subject: [PATCH 387/977] add CODEOWNERS --- .env.docker | 11 +++++++---- CODEOWNERS | 18 ++++++++++++++++++ Dockerfile | 2 +- .../docker/entrypoint.d/02-check-config.sh | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 CODEOWNERS diff --git a/.env.docker b/.env.docker index 2b1ab66a6..9a27550d1 100644 --- a/.env.docker +++ b/.env.docker @@ -18,12 +18,14 @@ # The name/title for your site # @see https://docs.pixelfed.org/technical-documentation/config/#app_name-1 +# @dottie/example My Pixelfed Site # @dottie/validate required,ne=My Pixelfed Site -APP_NAME="My Pixelfed Site" +APP_NAME= # Application domain used for routing. (e.g., pixelfed.org) # # @see https://docs.pixelfed.org/technical-documentation/config/#app_domain +# @dottie/example example.com # @dottie/validate required,ne=example.com,fqdn APP_DOMAIN="example.com" @@ -368,8 +370,8 @@ DB_USERNAME="pixelfed" # Use a site like https://pwgen.io/ to generate it # # @see https://docs.pixelfed.org/technical-documentation/config/#db_password -# @dottie/validate required,ne=__CHANGE_ME__ -DB_PASSWORD="__CHANGE_ME__" +# @dottie/validate required +DB_PASSWORD= # @see https://docs.pixelfed.org/technical-documentation/config/#db_database # @dottie/validate required @@ -890,6 +892,7 @@ SESSION_DRIVER="redis" # 32 character string, otherwise these encrypted strings will not be safe. # # @see https://docs.pixelfed.org/technical-documentation/config/#app_key +# @dottie/validate required APP_KEY= # Prefix for container names (without any dash at the end) @@ -1226,4 +1229,4 @@ DOCKER_PROXY_LETSENCRYPT_EMAIL="${INSTANCE_CONTACT_EMAIL:?error}" # Lets Encrypt staging/test servers for certificate requests. # # Setting this to any value will change to letsencrypt test servers. -DOCKER_PROXY_LETSENCRYPT_TEST="1" +#DOCKER_PROXY_LETSENCRYPT_TEST="1" diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..69a06cf87 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,18 @@ +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +* @dansup + +# Docker related files +.editorconfig @jippi @dansup +.env @jippi @dansup +.env.* @jippi @dansup +.hadolint.yaml @jippi @dansup +.shellcheckrc @jippi @dansup +/.github/ @jippi @dansup +/docker/ @jippi @dansup +/tests/ @jippi @dansup +docker-compose.migrate.yml @jippi @dansup +docker-compose.yml @jippi @dansup +goss.yaml @jippi @dansup diff --git a/Dockerfile b/Dockerfile index 5b6f45426..77bb1da62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.7.0" +ARG DOTTIE_VERSION="v0.7.1" ### # PHP base configuration diff --git a/docker/shared/root/docker/entrypoint.d/02-check-config.sh b/docker/shared/root/docker/entrypoint.d/02-check-config.sh index bbf7dd768..627960352 100755 --- a/docker/shared/root/docker/entrypoint.d/02-check-config.sh +++ b/docker/shared/root/docker/entrypoint.d/02-check-config.sh @@ -17,5 +17,5 @@ for file in "${dot_env_files[@]}"; do # which do not (and should not) exists inside the container # # We disable fixer since its not interactive anyway - run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --no-fix + run-as-current-user dottie validate --file "${file}" --ignore-rule dir,file --exclude-prefix APP_KEY --no-fix done From 0faf59e3b7ed2f9162e7b093867c88ee34eb1dbe Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 14:26:45 -0700 Subject: [PATCH 388/977] Update ApiV1Controller, fix network timeline --- app/Http/Controllers/Api/ApiV1Controller.php | 40 ++++++++++++++------ config/instance.php | 2 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index d1bd9cac2..fed120b40 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2536,20 +2536,38 @@ class ApiV1Controller extends Controller AccountService::setLastActive($user->id); $domainBlocks = UserFilterService::domainBlocks($user->profile_id); - if($remote && config('instance.timeline.network.cached')) { - Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { - if(NetworkTimelineService::count() == 0) { - NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); - } - }); + if($remote) { + if(config('instance.timeline.network.cached')) { + Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() { + if(NetworkTimelineService::count() == 0) { + NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff')); + } + }); - if ($max) { - $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); + if ($max) { + $feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5); + } else if ($min) { + $feed = NetworkTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = NetworkTimelineService::get(0, $limit + 5); + } } else { - $feed = NetworkTimelineService::get(0, $limit + 5); + $feed = Status::select( + 'id', + 'profile_id', + 'type', + 'visibility', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->where('visibility', 'public') + ->whereLocal(false) + ->orderByDesc('id') + ->take(($limit * 2)) + ->pluck('id'); } + } if($local || !$remote && !$local) { diff --git a/config/instance.php b/config/instance.php index 03f666a79..dd645d931 100644 --- a/config/instance.php +++ b/config/instance.php @@ -33,7 +33,7 @@ return [ ], 'network' => [ - 'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', true) : false, + 'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false, 'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100), 'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6) ] From 7b8977e9cc816f569fa345fccea4b89dfe95dad9 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 14:27:38 -0700 Subject: [PATCH 389/977] Update changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5f73b61..be720cae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.11...dev) +### Updated + +- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) +- ([](https://github.com/pixelfed/pixelfed/commit/)) + ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) ### Fixes @@ -27,7 +32,6 @@ ### Federation - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) - Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) ### Updates - Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59)) @@ -112,7 +116,6 @@ - Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3)) - Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac)) - Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) From 8b4ac5cc0bd38831f40c4368c0d7e09409c4b123 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 20:21:46 -0700 Subject: [PATCH 390/977] Update public/network timelines, fix non-redis response and fix reblogs in home feed --- app/Http/Controllers/Api/ApiV1Controller.php | 91 ++++++++++++++----- config/instance.php | 1 + .../assets/components/sections/Timeline.vue | 21 +++-- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index fed120b40..6114c6566 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -2523,6 +2523,7 @@ class ApiV1Controller extends Controller $napi = $request->has(self::PF_API_ENTITY_KEY); $min = $request->input('min_id'); $max = $request->input('max_id'); + $minOrMax = $request->anyFilled(['max_id', 'min_id']); $limit = $request->input('limit') ?? 20; $user = $request->user(); @@ -2535,6 +2536,8 @@ class ApiV1Controller extends Controller $filtered = $user ? UserFilterService::filters($user->profile_id) : []; AccountService::setLastActive($user->id); $domainBlocks = UserFilterService::domainBlocks($user->profile_id); + $hideNsfw = config('instance.hide_nsfw_on_public_feeds'); + $amin = SnowflakeService::byDate(now()->subDays(config('federation.network_timeline_days_falloff'))); if($remote) { if(config('instance.timeline.network.cached')) { @@ -2554,35 +2557,79 @@ class ApiV1Controller extends Controller } else { $feed = Status::select( 'id', - 'profile_id', + 'uri', 'type', - 'visibility', + 'scope', + 'local', + 'created_at', + 'profile_id', 'in_reply_to_id', 'reblog_of_id' - ) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->where('visibility', 'public') - ->whereLocal(false) - ->orderByDesc('id') - ->take(($limit * 2)) - ->pluck('id'); + ) + ->when($minOrMax, function($q, $minOrMax) use($min, $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + return $q->where('id', $dir, $id); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->when($hideNsfw, function($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(false) + ->whereScope('public') + ->where('id', '>', $amin) + ->orderByDesc('id') + ->limit(($limit * 2)) + ->pluck('id') + ->values() + ->toArray(); } + } else { + if(config('instance.timeline.local.cached')) { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { + if(PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); - } - - if($local || !$remote && !$local) { - Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { - if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); + } else if ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); + } else { + $feed = PublicTimelineService::get(0, $limit + 5); } - }); - - if ($max) { - $feed = PublicTimelineService::getRankedMaxId($max, $limit + 5); - } else if ($min) { - $feed = PublicTimelineService::getRankedMinId($min, $limit + 5); } else { - $feed = PublicTimelineService::get(0, $limit + 5); + $feed = Status::select( + 'id', + 'uri', + 'type', + 'scope', + 'local', + 'created_at', + 'profile_id', + 'in_reply_to_id', + 'reblog_of_id' + ) + ->when($minOrMax, function($q, $minOrMax) use($min, $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + return $q->where('id', $dir, $id); + }) + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->when($hideNsfw, function($q, $hideNsfw) { + return $q->where('is_nsfw', false); + }) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(true) + ->whereScope('public') + ->where('id', '>', $amin) + ->orderByDesc('id') + ->limit(($limit * 2)) + ->pluck('id') + ->values() + ->toArray(); } } diff --git a/config/instance.php b/config/instance.php index dd645d931..7d5463055 100644 --- a/config/instance.php +++ b/config/instance.php @@ -29,6 +29,7 @@ return [ ], 'local' => [ + 'cached' => env('INSTANCE_PUBLIC_TIMELINE_CACHED', false), 'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false) ], diff --git a/resources/assets/components/sections/Timeline.vue b/resources/assets/components/sections/Timeline.vue index 6b064acea..ea215d895 100644 --- a/resources/assets/components/sections/Timeline.vue +++ b/resources/assets/components/sections/Timeline.vue @@ -187,7 +187,7 @@ forceUpdateIdx: 0, showReblogBanner: false, enablingReblogs: false, - baseApi: '/api/v1/pixelfed/timelines/', + baseApi: '/api/v1/timelines/', } }, @@ -204,7 +204,7 @@ } if(window.App.config.ab.hasOwnProperty('cached_home_timeline')) { const cht = window.App.config.ab.cached_home_timeline == true; - this.baseApi = cht ? '/api/v1/timelines/' : '/api/pixelfed/v1/timelines/'; + this.baseApi = cht ? '/api/v1/timelines/' : '/api/v1/timelines/'; } this.fetchSettings(); }, @@ -261,10 +261,19 @@ } } else { url = this.baseApi + this.getScope(); - params = { - max_id: this.max_id, - limit: 6, - '_pe': 1, + + if(this.max_id === 0) { + params = { + min_id: 1, + limit: 6, + '_pe': 1, + } + } else { + params = { + max_id: this.max_id, + limit: 6, + '_pe': 1, + } } } if(this.getScope() === 'network') { From 78da12004f6e0cf6baed641f35fccee389bb2f04 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 20:23:19 -0700 Subject: [PATCH 391/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be720cae2..d3331d10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Updated - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) +- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) From 97c131fdf2d3581130acaf917dbf2c49bcfd9b5e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 20:24:27 -0700 Subject: [PATCH 392/977] Update AccountImport component --- resources/assets/components/AccountImport.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/assets/components/AccountImport.vue b/resources/assets/components/AccountImport.vue index f7b7279f5..406c5c1ce 100644 --- a/resources/assets/components/AccountImport.vue +++ b/resources/assets/components/AccountImport.vue @@ -197,8 +197,17 @@

Media #{{idx + 1}}

- +
+

Caption

{{ media.title ? media.title : modalData.title }}

From 221fe43638f069a84bc85fb4f8c7445e3bc4a1f3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 11 Feb 2024 20:25:18 -0700 Subject: [PATCH 393/977] Update compiled assets --- public/js/account-import.js | Bin 27695 -> 27967 bytes public/js/home.chunk.ada2cbf0ec3271bd.js | Bin 0 -> 240067 bytes ...ome.chunk.ada2cbf0ec3271bd.js.LICENSE.txt} | 0 public/js/home.chunk.f3f4f632025b560f.js | Bin 240076 -> 0 bytes public/js/manifest.js | Bin 4006 -> 4006 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/js/home.chunk.ada2cbf0ec3271bd.js rename public/js/{home.chunk.f3f4f632025b560f.js.LICENSE.txt => home.chunk.ada2cbf0ec3271bd.js.LICENSE.txt} (100%) delete mode 100644 public/js/home.chunk.f3f4f632025b560f.js diff --git a/public/js/account-import.js b/public/js/account-import.js index b68bfb4953f60434fced647f83e429bf13ae9504..f1510f57ea8cea20b3183872e5d2c6a8327546f5 100644 GIT binary patch delta 458 zcmZ2~gK_^Y#tj$Q7{e!DVpCxZ*!-Q%ijmQOvJuB=#;nc%I8HGF#ZGe7umTmh2?Q|u zZ=No|#VnAeS5TB+l3!9;kgBPZqLDRuqp$^I*5p^h_CQ@)B1;)%{B^<+YHX{M^Ye;J zib|79@{6p(H@_DB#tt;anoI`YaI17q9D{0o$V2Bsw0mbd3Cx42o7q=?Wi%-?ijMXvH)B!TfG?WySG+TLI8HGF#ZGe7umTmh2?Q`Y zZk{f{#Vin@S5TB+l3!9;kgBPZq7g88qp$^Iz~ooL_CQ@)B1;)%9CiE=YHX{M^Ye;J zib|79@{6qeH@_DB#tt;x+utQ22AnJr9dvYrqB<_!rP zOq+Kl*|V`28d{iw)J&GnZ09gCH!v|UH?o+#DpQly(A>z_5-KG!nJY^HqE;_!GOMwL wp|SboV_Cke<|f8wmXjs2eGz(dv)4csOvq7(I$}?b1Dg&+Em#n2^q!n%0Q*i~^Z)<= diff --git a/public/js/home.chunk.ada2cbf0ec3271bd.js b/public/js/home.chunk.ada2cbf0ec3271bd.js new file mode 100644 index 0000000000000000000000000000000000000000..6b898fbc372e3da7f2f6c0da4a2f9cf953ffa89d GIT binary patch literal 240067 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�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 literal 0 HcmV?d00001 diff --git a/public/js/home.chunk.f3f4f632025b560f.js.LICENSE.txt b/public/js/home.chunk.ada2cbf0ec3271bd.js.LICENSE.txt similarity index 100% rename from public/js/home.chunk.f3f4f632025b560f.js.LICENSE.txt rename to public/js/home.chunk.ada2cbf0ec3271bd.js.LICENSE.txt diff --git a/public/js/home.chunk.f3f4f632025b560f.js b/public/js/home.chunk.f3f4f632025b560f.js deleted file mode 100644 index 76c5f028a02ab58d6d219fb46075ff11c339fddb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 240076 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 diff --git a/public/js/manifest.js b/public/js/manifest.js index 5f337f9b3cfc1e67d284e8ef56f45fcf66221dae..de48f35bf8f9b085ba3d10d64a58b001904be26e 100644 GIT binary patch delta 128 zcmZ1`zf69E8>>KKN}^G6Qkp?(vayl5VN%NGFxKtNs-?EY(K)f!nQ0oOdg-YpjwK~U znMtK3sTxYfMafE>K?ahge*nX!?9k!g~tnL*m-FxKtNs%f^x(K)f!nQ0nndg-YpjwK~U znMtK3sTxYfMafE`3Y|TLvnT1|1$^_H zim7y4;jg&HSD8>=7~7{AG8met+@#wuu|SR364$4Y~M1rkS!hIP6+ zdhs<=RDq$!ilprNe3#|huHUxzYbd#W+%DyxbvVnO$e?pV-m`1aS+GVyg$g~0V;oNN J<@iiLiyvG!H=h6i delta 185 zcmZw9I|{-;5P)H{kbtFyZ44Hv;yz|(cLeby7W>2pY7sn1E@3I8_7v8hB#k+M?a#+Q z)Iks= zb%sn@356a> Date: Thu, 15 Feb 2024 20:58:43 -0700 Subject: [PATCH 394/977] Update Federation, use proper Content-Type headers for following/follower collections --- app/Http/Controllers/FederationController.php | 4 ++-- app/Util/ActivityPub/Helpers.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php index 6faea7050..55c7b4393 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -253,7 +253,7 @@ class FederationController extends Controller 'type' => 'OrderedCollection', 'totalItems' => $account['following_count'] ?? 0, ]; - return response()->json($obj); + return response()->json($obj)->header('Content-Type', 'application/activity+json'); } public function userFollowers(Request $request, $username) @@ -269,6 +269,6 @@ class FederationController extends Controller 'type' => 'OrderedCollection', 'totalItems' => $account['followers_count'] ?? 0, ]; - return response()->json($obj); + return response()->json($obj)->header('Content-Type', 'application/activity+json'); } } diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 5819dc0bc..6f5b8ae11 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -372,6 +372,10 @@ class Helpers { $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); + if($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) { + return; + } + if(!self::validateUrl($id)) { return; } @@ -455,14 +459,21 @@ class Helpers { public static function storeStatus($url, $profile, $activity) { + $originalUrl = $url; $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']); $url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id); $idDomain = parse_url($id, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST); + $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST); if(!self::validateUrl($id) || !self::validateUrl($url)) { return; } + if( strtolower($originalUrlDomain) !== strtolower($idDomain) || + strtolower($originalUrlDomain) !== strtolower($urlDomain) ) { + return; + } + $reply_to = self::getReplyTo($activity); $ts = self::pluckval($activity['published']); @@ -763,7 +774,11 @@ class Helpers { if(!$res || isset($res['id']) == false) { return; } + $urlDomain = parse_url($url, PHP_URL_HOST); $domain = parse_url($res['id'], PHP_URL_HOST); + if(strtolower($urlDomain) !== strtolower($domain)) { + return; + } if(!isset($res['preferredUsername']) && !isset($res['nickname'])) { return; } @@ -831,6 +846,9 @@ class Helpers { public static function sendSignedObject($profile, $url, $body) { + if(app()->environment() !== 'production') { + return; + } ActivityPubDeliveryService::queue() ->from($profile) ->to($url) From 4c6ec20e36b24861b97e0c147ad6267e708b6394 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 20:59:08 -0700 Subject: [PATCH 395/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3331d10d..d01a049fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) +- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) From 1232cfc86a846807207fa26de81cb82bbc0d8e66 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 21:22:41 -0700 Subject: [PATCH 396/977] Update ActivityPubFetchService, enforce stricter Content-Type validation --- app/Services/ActivityPubFetchService.php | 81 +++++++++++++++--------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php index cbf153ecb..4b515859c 100644 --- a/app/Services/ActivityPubFetchService.php +++ b/app/Services/ActivityPubFetchService.php @@ -11,38 +11,61 @@ use Illuminate\Http\Client\RequestException; class ActivityPubFetchService { - public static function get($url, $validateUrl = true) - { + public static function get($url, $validateUrl = true) + { if($validateUrl === true) { - if(!Helpers::validateUrl($url)) { - return 0; - } + if(!Helpers::validateUrl($url)) { + return 0; + } } - $baseHeaders = [ - 'Accept' => 'application/activity+json, application/ld+json', - ]; + $baseHeaders = [ + 'Accept' => 'application/activity+json, application/ld+json', + ]; - $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); - $headers['Accept'] = 'application/activity+json, application/ld+json'; - $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; + $headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get'); + $headers['Accept'] = 'application/activity+json, application/ld+json'; + $headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'; - try { - $res = Http::withOptions(['allow_redirects' => false])->withHeaders($headers) - ->timeout(30) - ->connectTimeout(5) - ->retry(3, 500) - ->get($url); - } catch (RequestException $e) { - return; - } catch (ConnectionException $e) { - return; - } catch (Exception $e) { - return; - } - if(!$res->ok()) { - return; - } - return $res->body(); - } + try { + $res = Http::withOptions(['allow_redirects' => false]) + ->withHeaders($headers) + ->timeout(30) + ->connectTimeout(5) + ->retry(3, 500) + ->get($url); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if(!$res->ok()) { + return; + } + + if(!$res->hasHeader('Content-Type')) { + return; + } + + $acceptedTypes = [ + 'application/activity+json; charset=utf-8', + 'application/activity+json', + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + ]; + + $contentType = $res->getHeader('Content-Type')[0]; + + if(!$contentType) { + return; + } + + if(!in_array($contentType, $acceptedTypes)) { + return; + } + + return $res->body(); + } } From df5e61266c66b1b3537946723bf6d2feb0d4a551 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 21:23:00 -0700 Subject: [PATCH 397/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d01a049fb..e3ddb7c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) - Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3)) +- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) From 0f3ca194616d959807379a187d400643fabd5627 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 21:41:18 -0700 Subject: [PATCH 398/977] Update status view, fix unlisted/private scope bug --- resources/views/status/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/status/show.blade.php b/resources/views/status/show.blade.php index b6927157a..d9743c92f 100644 --- a/resources/views/status/show.blade.php +++ b/resources/views/status/show.blade.php @@ -4,7 +4,7 @@ ]) @php -$s = \App\Services\StatusService::get($status->id); +$s = \App\Services\StatusService::get($status->id, false); $displayName = $s && $s['account'] ? $s['account']['display_name'] : false; $captionPreview = false; $domain = $displayName ? '@' . parse_url($s['account']['url'], PHP_URL_HOST) : ''; From 70fc44dfe53beedd92301be11381998bc8a678e7 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 21:41:40 -0700 Subject: [PATCH 399/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ddb7c15..322e5ff49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) - Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3)) - Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8)) +- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) From bc4d223714be1c6d58fb4af258c4368c96dbf8e8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 15 Feb 2024 22:20:40 -0700 Subject: [PATCH 400/977] Update routes --- app/Providers/RouteServiceProvider.php | 17 +- routes/web-admin.php | 166 +++++ routes/web-api.php | 168 +++++ routes/web-portfolio.php | 23 + routes/web.php | 943 ++++++++----------------- 5 files changed, 677 insertions(+), 640 deletions(-) create mode 100644 routes/web-admin.php create mode 100644 routes/web-api.php create mode 100644 routes/web-portfolio.php diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 37aac4ac3..2452eb2a8 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -23,8 +23,6 @@ class RouteServiceProvider extends ServiceProvider */ public function boot() { - // - parent::boot(); } @@ -36,10 +34,7 @@ class RouteServiceProvider extends ServiceProvider public function map() { $this->mapApiRoutes(); - $this->mapWebRoutes(); - - // } /** @@ -51,6 +46,18 @@ class RouteServiceProvider extends ServiceProvider */ protected function mapWebRoutes() { + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-admin.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-portfolio.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web-api.php')); + Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); diff --git a/routes/web-admin.php b/routes/web-admin.php new file mode 100644 index 000000000..72572b5c0 --- /dev/null +++ b/routes/web-admin.php @@ -0,0 +1,166 @@ +prefix('i/admin')->group(function () { + Route::redirect('/', '/dashboard'); + Route::redirect('timeline', config('app.url').'/timeline'); + Route::get('dashboard', 'AdminController@home')->name('admin.home'); + Route::get('stats', 'AdminController@stats')->name('admin.stats'); + Route::get('reports', 'AdminController@reports')->name('admin.reports'); + Route::get('reports/show/{id}', 'AdminController@showReport'); + Route::post('reports/show/{id}', 'AdminController@updateReport'); + Route::post('reports/bulk', 'AdminController@bulkUpdateReport'); + Route::get('reports/autospam/{id}', 'AdminController@showSpam'); + Route::post('reports/autospam/sync', 'AdminController@fixUncategorizedSpam'); + Route::post('reports/autospam/{id}', 'AdminController@updateSpam'); + Route::get('reports/autospam', 'AdminController@spam'); + Route::get('reports/appeals', 'AdminController@appeals'); + Route::get('reports/appeal/{id}', 'AdminController@showAppeal'); + Route::post('reports/appeal/{id}', 'AdminController@updateAppeal'); + Route::get('reports/email-verifications', 'AdminController@reportMailVerifications'); + Route::post('reports/email-verifications/ignore', 'AdminController@reportMailVerifyIgnore'); + Route::post('reports/email-verifications/approve', 'AdminController@reportMailVerifyApprove'); + Route::post('reports/email-verifications/clear-ignored', 'AdminController@reportMailVerifyClearIgnored'); + Route::redirect('stories', '/stories/list'); + Route::get('stories/list', 'AdminController@stories')->name('admin.stories'); + Route::redirect('statuses', '/statuses/list'); + Route::get('statuses/list', 'AdminController@statuses')->name('admin.statuses'); + Route::get('statuses/show/{id}', 'AdminController@showStatus'); + Route::redirect('profiles', '/i/admin/profiles/list'); + Route::get('profiles/list', 'AdminController@profiles')->name('admin.profiles'); + Route::get('profiles/edit/{id}', 'AdminController@profileShow'); + Route::redirect('users', '/users/list'); + Route::get('users/list', 'AdminController@users')->name('admin.users'); + Route::get('users/show/{id}', 'AdminController@userShow'); + Route::get('users/edit/{id}', 'AdminController@userEdit'); + Route::post('users/edit/{id}', 'AdminController@userEditSubmit'); + Route::get('users/activity/{id}', 'AdminController@userActivity'); + Route::get('users/message/{id}', 'AdminController@userMessage'); + Route::post('users/message/{id}', 'AdminController@userMessageSend'); + Route::get('users/modtools/{id}', 'AdminController@userModTools'); + Route::get('users/modlogs/{id}', 'AdminController@userModLogs'); + Route::post('users/modlogs/{id}', 'AdminController@userModLogsMessage'); + Route::post('users/modlogs/{id}/delete', 'AdminController@userModLogDelete'); + Route::get('users/delete/{id}', 'AdminController@userDelete'); + Route::post('users/delete/{id}', 'AdminController@userDeleteProcess'); + Route::post('users/moderation/update', 'AdminController@userModerate'); + Route::get('media', 'AdminController@media')->name('admin.media'); + Route::redirect('media/list', '/i/admin/media'); + Route::get('media/show/{id}', 'AdminController@mediaShow'); + Route::get('settings', 'AdminController@settings')->name('admin.settings'); + Route::post('settings', 'AdminController@settingsHomeStore'); + Route::get('settings/features', 'AdminController@settingsFeatures')->name('admin.settings.features'); + Route::get('settings/pages', 'AdminController@settingsPages')->name('admin.settings.pages'); + Route::get('settings/pages/edit', 'PageController@edit')->name('admin.settings.pages.edit'); + Route::post('settings/pages/edit', 'PageController@store'); + Route::post('settings/pages/delete', 'PageController@delete'); + Route::post('settings/pages/create', 'PageController@generatePage'); + Route::get('settings/maintenance', 'AdminController@settingsMaintenance')->name('admin.settings.maintenance'); + Route::get('settings/backups', 'AdminController@settingsBackups')->name('admin.settings.backups'); + Route::get('settings/storage', 'AdminController@settingsStorage')->name('admin.settings.storage'); + Route::get('settings/system', 'AdminController@settingsSystem')->name('admin.settings.system'); + + Route::get('instances', 'AdminController@instances')->name('admin.instances'); + Route::post('instances', 'AdminController@instanceScan'); + Route::get('instances/show/{id}', 'AdminController@instanceShow'); + Route::post('instances/edit/{id}', 'AdminController@instanceEdit'); + Route::get('apps/home', 'AdminController@appsHome')->name('admin.apps'); + Route::get('hashtags/home', 'AdminController@hashtagsHome')->name('admin.hashtags'); + Route::get('discover/home', 'AdminController@discoverHome')->name('admin.discover'); + Route::get('discover/category/create', 'AdminController@discoverCreateCategory')->name('admin.discover.create-category'); + Route::post('discover/category/create', 'AdminController@discoverCreateCategoryStore'); + Route::get('discover/category/edit/{id}', 'AdminController@discoverCategoryEdit'); + Route::post('discover/category/edit/{id}', 'AdminController@discoverCategoryUpdate'); + Route::post('discover/category/hashtag/create', 'AdminController@discoveryCategoryTagStore')->name('admin.discover.create-hashtag'); + + Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages'); + Route::get('messages/show/{id}', 'AdminController@messagesShow'); + Route::post('messages/mark-read', 'AdminController@messagesMarkRead'); + Route::redirect('site-news', '/i/admin/newsroom'); + Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home'); + Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create'); + Route::get('newsroom/edit/{id}', 'AdminController@newsroomEdit'); + Route::post('newsroom/edit/{id}', 'AdminController@newsroomUpdate'); + Route::delete('newsroom/edit/{id}', 'AdminController@newsroomDelete'); + Route::post('newsroom/create', 'AdminController@newsroomStore'); + + Route::get('diagnostics/home', 'AdminController@diagnosticsHome')->name('admin.diagnostics'); + Route::post('diagnostics/decrypt', 'AdminController@diagnosticsDecrypt')->name('admin.diagnostics.decrypt'); + Route::get('custom-emoji/home', 'AdminController@customEmojiHome')->name('admin.custom-emoji'); + Route::post('custom-emoji/toggle-active/{id}', 'AdminController@customEmojiToggleActive'); + Route::get('custom-emoji/new', 'AdminController@customEmojiAdd'); + Route::post('custom-emoji/new', 'AdminController@customEmojiStore'); + Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); + Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates'); + + Route::get('directory/home', 'AdminController@directoryHome')->name('admin.directory'); + + Route::get('autospam/home', 'AdminController@autospamHome')->name('admin.autospam'); + + Route::redirect('asf/', 'asf/home'); + Route::get('asf/home', 'AdminShadowFilterController@home'); + Route::get('asf/create', 'AdminShadowFilterController@create'); + Route::get('asf/edit/{id}', 'AdminShadowFilterController@edit'); + Route::post('asf/edit/{id}', 'AdminShadowFilterController@storeEdit'); + Route::post('asf/create', 'AdminShadowFilterController@store'); + + Route::get('asf/home', 'AdminShadowFilterController@home'); + // Route::redirect('curated-onboarding/', 'curated-onboarding/home'); + // Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); + // Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); + // Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); + // Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); + + Route::prefix('api')->group(function() { + Route::get('stats', 'AdminController@getStats'); + Route::get('accounts', 'AdminController@getAccounts'); + Route::get('posts', 'AdminController@getPosts'); + Route::get('instances', 'AdminController@getInstances'); + Route::post('directory/save', 'AdminController@directoryStore'); + Route::get('directory/initial-data', 'AdminController@directoryInitialData'); + Route::get('directory/popular-posts', 'AdminController@directoryGetPopularPosts'); + Route::post('directory/add-by-id', 'AdminController@directoryGetAddPostByIdSearch'); + Route::delete('directory/banner-image', 'AdminController@directoryDeleteBannerImage'); + Route::post('directory/submit', 'AdminController@directoryHandleServerSubmission'); + Route::post('directory/testimonial/save', 'AdminController@directorySaveTestimonial'); + Route::post('directory/testimonial/delete', 'AdminController@directoryDeleteTestimonial'); + Route::post('directory/testimonial/update', 'AdminController@directoryUpdateTestimonial'); + Route::get('hashtags/stats', 'AdminController@hashtagsStats'); + Route::get('hashtags/query', 'AdminController@hashtagsApi'); + Route::get('hashtags/get', 'AdminController@hashtagsGet'); + Route::post('hashtags/update', 'AdminController@hashtagsUpdate'); + Route::post('hashtags/clear-trending-cache', 'AdminController@hashtagsClearTrendingCache'); + Route::get('instances/get', 'AdminController@getInstancesApi'); + Route::get('instances/stats', 'AdminController@getInstancesStatsApi'); + Route::get('instances/query', 'AdminController@getInstancesQueryApi'); + Route::post('instances/update', 'AdminController@postInstanceUpdateApi'); + Route::post('instances/create', 'AdminController@postInstanceCreateNewApi'); + Route::post('instances/delete', 'AdminController@postInstanceDeleteApi'); + Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi'); + Route::get('instances/download-backup', 'AdminController@downloadBackup'); + Route::post('instances/import-data', 'AdminController@importBackup'); + Route::get('reports/stats', 'AdminController@reportsStats'); + Route::get('reports/all', 'AdminController@reportsApiAll'); + Route::get('reports/get/{id}', 'AdminController@reportsApiGet'); + Route::post('reports/handle', 'AdminController@reportsApiHandle'); + Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll'); + Route::get('reports/spam/get/{id}', 'AdminController@reportsApiSpamGet'); + Route::post('reports/spam/handle', 'AdminController@reportsApiSpamHandle'); + Route::post('autospam/config', 'AdminController@getAutospamConfigApi'); + Route::post('autospam/reports/closed', 'AdminController@getAutospamReportsClosedApi'); + Route::post('autospam/train', 'AdminController@postAutospamTrainSpamApi'); + Route::post('autospam/search/non-spam', 'AdminController@postAutospamTrainNonSpamSearchApi'); + Route::post('autospam/train/non-spam', 'AdminController@postAutospamTrainNonSpamSubmitApi'); + Route::post('autospam/tokens/custom', 'AdminController@getAutospamCustomTokensApi'); + Route::post('autospam/tokens/store', 'AdminController@saveNewAutospamCustomTokensApi'); + Route::post('autospam/tokens/update', 'AdminController@updateAutospamCustomTokensApi'); + Route::post('autospam/tokens/export', 'AdminController@exportAutospamCustomTokensApi'); + Route::post('autospam/config/enable', 'AdminController@enableAutospamApi'); + Route::post('autospam/config/disable', 'AdminController@disableAutospamApi'); + // Route::get('instances/{id}/accounts', 'AdminController@getInstanceAccounts'); + // Route::get('curated-onboarding/show/{id}/activity-log', 'AdminCuratedRegisterController@apiActivityLog'); + // Route::post('curated-onboarding/show/{id}/message/preview', 'AdminCuratedRegisterController@apiMessagePreviewStore'); + // Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); + // Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); + // Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); + }); +}); diff --git a/routes/web-api.php b/routes/web-api.php new file mode 100644 index 000000000..f51762439 --- /dev/null +++ b/routes/web-api.php @@ -0,0 +1,168 @@ +middleware(['validemail', 'twofactor', 'localization'])->group(function () { + Route::group(['prefix' => 'api'], function () { + Route::get('search', 'SearchController@searchAPI'); + Route::post('status/view', 'StatusController@storeView'); + Route::get('v1/polls/{id}', 'PollController@getPoll'); + Route::post('v1/polls/{id}/votes', 'PollController@vote'); + + Route::group(['prefix' => 'compose'], function() { + Route::group(['prefix' => 'v0'], function() { + Route::post('/media/upload', 'ComposeController@mediaUpload'); + Route::post('/media/update', 'ComposeController@mediaUpdate'); + Route::delete('/media/delete', 'ComposeController@mediaDelete'); + Route::get('/search/tag', 'ComposeController@searchTag'); + Route::get('/search/location', 'ComposeController@searchLocation'); + Route::get('/search/mention', 'ComposeController@searchMentionAutocomplete'); + Route::get('/search/hashtag', 'ComposeController@searchHashtagAutocomplete'); + + Route::post('/publish', 'ComposeController@store'); + Route::post('/publish/text', 'ComposeController@storeText'); + Route::get('/media/processing', 'ComposeController@mediaProcessingCheck'); + Route::get('/settings', 'ComposeController@composeSettings'); + Route::post('/poll', 'ComposeController@createPoll'); + }); + }); + + Route::group(['prefix' => 'direct'], function () { + Route::get('browse', 'DirectMessageController@browse'); + Route::post('create', 'DirectMessageController@create'); + Route::get('thread', 'DirectMessageController@thread'); + Route::post('mute', 'DirectMessageController@mute'); + Route::post('unmute', 'DirectMessageController@unmute'); + Route::delete('message', 'DirectMessageController@delete'); + Route::post('media', 'DirectMessageController@mediaUpload'); + Route::post('lookup', 'DirectMessageController@composeLookup'); + Route::post('read', 'DirectMessageController@read'); + }); + + Route::group(['prefix' => 'v2'], function() { + Route::get('config', 'ApiController@siteConfiguration'); + Route::get('discover', 'InternalApiController@discover'); + Route::get('discover/posts', 'InternalApiController@discoverPosts')->middleware('auth:api'); + Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); + Route::get('profile/{username}/status/{postid}/state', 'PublicApiController@statusState'); + Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); + Route::get('status/{id}/replies', 'InternalApiController@statusReplies'); + Route::post('moderator/action', 'InternalApiController@modAction'); + Route::get('discover/categories', 'InternalApiController@discoverCategories'); + Route::get('loops', 'DiscoverController@loopsApi'); + Route::post('loops/watch', 'DiscoverController@loopWatch'); + Route::get('discover/tag', 'DiscoverController@getHashtags'); + Route::get('statuses/{id}/replies', 'Api\ApiV1Controller@statusReplies'); + Route::get('statuses/{id}/state', 'Api\ApiV1Controller@statusState'); + }); + + Route::group(['prefix' => 'pixelfed'], function() { + Route::group(['prefix' => 'v1'], function() { + Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials'); + Route::get('accounts/relationships', 'Api\ApiV1Controller@accountRelationshipsById'); + Route::get('accounts/search', 'Api\ApiV1Controller@accountSearch'); + Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses'); + Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById'); + Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById'); + Route::get('statuses/{id}', 'PublicApiController@getStatus'); + Route::get('accounts/{id}', 'PublicApiController@account'); + Route::post('avatar/update', 'ApiController@avatarUpdate'); + Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis'); + Route::get('notifications', 'ApiController@notifications'); + Route::get('timelines/public', 'PublicApiController@publicTimelineApi'); + Route::get('timelines/home', 'PublicApiController@homeTimelineApi'); + Route::get('timelines/network', 'PublicApiController@networkTimelineApi'); + Route::get('newsroom/timeline', 'NewsroomController@timelineApi'); + Route::post('newsroom/markasread', 'NewsroomController@markAsRead'); + Route::get('favourites', 'Api\BaseApiController@accountLikes'); + Route::get('mutes', 'AccountController@accountMutes'); + Route::get('blocks', 'AccountController@accountBlocks'); + }); + + Route::group(['prefix' => 'v2'], function() { + Route::get('config', 'ApiController@siteConfiguration'); + Route::get('discover', 'InternalApiController@discover'); + Route::get('discover/posts', 'InternalApiController@discoverPosts'); + Route::get('discover/profiles', 'DiscoverController@profilesDirectoryApi'); + Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); + Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); + Route::post('moderator/action', 'InternalApiController@modAction'); + Route::get('discover/categories', 'InternalApiController@discoverCategories'); + Route::get('loops', 'DiscoverController@loopsApi'); + Route::post('loops/watch', 'DiscoverController@loopWatch'); + Route::get('discover/tag', 'DiscoverController@getHashtags'); + Route::get('discover/posts/trending', 'DiscoverController@trendingApi'); + Route::get('discover/posts/hashtags', 'DiscoverController@trendingHashtags'); + Route::get('discover/posts/places', 'DiscoverController@trendingPlaces'); + Route::get('seasonal/yir', 'SeasonalController@getData'); + Route::post('seasonal/yir', 'SeasonalController@store'); + Route::get('mutes', 'AccountController@accountMutesV2'); + Route::get('blocks', 'AccountController@accountBlocksV2'); + Route::get('filters', 'AccountController@accountFiltersV2'); + Route::post('status/compose', 'InternalApiController@composePost'); + Route::get('status/{id}/replies', 'InternalApiController@statusReplies'); + Route::post('status/{id}/archive', 'ApiController@archive'); + Route::post('status/{id}/unarchive', 'ApiController@unarchive'); + Route::get('statuses/archives', 'ApiController@archivedPosts'); + Route::get('discover/memories', 'DiscoverController@myMemories'); + Route::get('discover/account-insights', 'DiscoverController@accountInsightsPopularPosts'); + Route::get('discover/server-timeline', 'DiscoverController@serverTimeline'); + Route::get('discover/meta', 'DiscoverController@enabledFeatures'); + Route::post('discover/admin/features', 'DiscoverController@updateFeatures'); + }); + + Route::get('discover/accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular'); + Route::post('web/change-language.json', 'SpaController@updateLanguage'); + }); + + Route::group(['prefix' => 'local'], function () { + // Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440'); + Route::get('exp/rec', 'ApiController@userRecommendations'); + Route::post('discover/tag/subscribe', 'HashtagFollowController@store'); + Route::get('discover/tag/list', 'HashtagFollowController@getTags'); + // Route::get('profile/sponsor/{id}', 'ProfileSponsorController@get'); + Route::get('bookmarks', 'InternalApiController@bookmarks'); + Route::get('collection/items/{id}', 'CollectionController@getItems'); + Route::post('collection/item', 'CollectionController@storeId'); + Route::delete('collection/item', 'CollectionController@deleteId'); + Route::get('collection/{id}', 'CollectionController@getCollection'); + Route::post('collection/{id}', 'CollectionController@store'); + Route::delete('collection/{id}', 'CollectionController@delete'); + Route::post('collection/{id}/publish', 'CollectionController@publish'); + Route::get('profile/collections/{id}', 'CollectionController@getUserCollections'); + + Route::post('compose/tag/untagme', 'MediaTagController@untagProfile'); + + Route::post('import/ig', 'ImportPostController@store'); + Route::get('import/ig/config', 'ImportPostController@getConfig'); + Route::post('import/ig/media', 'ImportPostController@storeMedia'); + Route::post('import/ig/existing', 'ImportPostController@getImportedFiles'); + Route::post('import/ig/posts', 'ImportPostController@getImportedPosts'); + Route::post('import/ig/processing', 'ImportPostController@getProcessingCount'); + }); + + Route::group(['prefix' => 'web/stories'], function () { + Route::get('v1/recent', 'StoryController@recent'); + Route::get('v1/viewers', 'StoryController@viewers'); + Route::get('v1/profile/{id}', 'StoryController@profile'); + Route::get('v1/exists/{id}', 'StoryController@exists'); + Route::get('v1/poll/results', 'StoryController@pollResults'); + Route::post('v1/viewed', 'StoryController@viewed'); + Route::post('v1/react', 'StoryController@react'); + Route::post('v1/comment', 'StoryController@comment'); + Route::post('v1/publish/poll', 'StoryController@publishStoryPoll'); + Route::post('v1/poll/vote', 'StoryController@storyPollVote'); + Route::post('v1/report', 'StoryController@storeReport'); + Route::post('v1/add', 'StoryController@apiV1Add'); + Route::post('v1/crop', 'StoryController@cropPhoto'); + Route::post('v1/publish', 'StoryController@publishStory'); + Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete'); + }); + + Route::group(['prefix' => 'portfolio'], function () { + Route::post('self/curated.json', 'PortfolioController@storeCurated'); + Route::post('self/settings.json', 'PortfolioController@getSettings'); + Route::get('account/settings.json', 'PortfolioController@getAccountSettings'); + Route::post('self/update-settings.json', 'PortfolioController@storeSettings'); + Route::get('{username}/feed', 'PortfolioController@getFeed'); + }); + }); +}); diff --git a/routes/web-portfolio.php b/routes/web-portfolio.php new file mode 100644 index 000000000..3785aaac1 --- /dev/null +++ b/routes/web-portfolio.php @@ -0,0 +1,23 @@ +group(function () { + Route::redirect('redirect/home', config('app.url')); + Route::get('/', 'PortfolioController@index'); + Route::post('api/portfolio/self/curated.json', 'PortfolioController@storeCurated'); + Route::post('api/portfolio/self/settings.json', 'PortfolioController@getSettings'); + Route::get('api/portfolio/account/settings.json', 'PortfolioController@getAccountSettings'); + Route::post('api/portfolio/self/update-settings.json', 'PortfolioController@storeSettings'); + Route::get('api/portfolio/{username}/feed', 'PortfolioController@getFeed'); + + Route::prefix(config('portfolio.path'))->group(function() { + Route::get('/', 'PortfolioController@index'); + Route::get('settings', 'PortfolioController@settings')->name('portfolio.settings'); + Route::post('settings', 'PortfolioController@store'); + Route::get('{username}/{id}', 'PortfolioController@showPost'); + Route::get('{username}', 'PortfolioController@show'); + + Route::fallback(function () { + return view('errors.404'); + }); + }); +}); diff --git a/routes/web.php b/routes/web.php index 6c765ba56..3947bf5da 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,186 +1,12 @@ prefix('i/admin')->group(function () { - Route::redirect('/', '/dashboard'); - Route::redirect('timeline', config('app.url').'/timeline'); - Route::get('dashboard', 'AdminController@home')->name('admin.home'); - Route::get('stats', 'AdminController@stats')->name('admin.stats'); - Route::get('reports', 'AdminController@reports')->name('admin.reports'); - Route::get('reports/show/{id}', 'AdminController@showReport'); - Route::post('reports/show/{id}', 'AdminController@updateReport'); - Route::post('reports/bulk', 'AdminController@bulkUpdateReport'); - Route::get('reports/autospam/{id}', 'AdminController@showSpam'); - Route::post('reports/autospam/sync', 'AdminController@fixUncategorizedSpam'); - Route::post('reports/autospam/{id}', 'AdminController@updateSpam'); - Route::get('reports/autospam', 'AdminController@spam'); - Route::get('reports/appeals', 'AdminController@appeals'); - Route::get('reports/appeal/{id}', 'AdminController@showAppeal'); - Route::post('reports/appeal/{id}', 'AdminController@updateAppeal'); - Route::get('reports/email-verifications', 'AdminController@reportMailVerifications'); - Route::post('reports/email-verifications/ignore', 'AdminController@reportMailVerifyIgnore'); - Route::post('reports/email-verifications/approve', 'AdminController@reportMailVerifyApprove'); - Route::post('reports/email-verifications/clear-ignored', 'AdminController@reportMailVerifyClearIgnored'); - Route::redirect('stories', '/stories/list'); - Route::get('stories/list', 'AdminController@stories')->name('admin.stories'); - Route::redirect('statuses', '/statuses/list'); - Route::get('statuses/list', 'AdminController@statuses')->name('admin.statuses'); - Route::get('statuses/show/{id}', 'AdminController@showStatus'); - Route::redirect('profiles', '/i/admin/profiles/list'); - Route::get('profiles/list', 'AdminController@profiles')->name('admin.profiles'); - Route::get('profiles/edit/{id}', 'AdminController@profileShow'); - Route::redirect('users', '/users/list'); - Route::get('users/list', 'AdminController@users')->name('admin.users'); - Route::get('users/show/{id}', 'AdminController@userShow'); - Route::get('users/edit/{id}', 'AdminController@userEdit'); - Route::post('users/edit/{id}', 'AdminController@userEditSubmit'); - Route::get('users/activity/{id}', 'AdminController@userActivity'); - Route::get('users/message/{id}', 'AdminController@userMessage'); - Route::post('users/message/{id}', 'AdminController@userMessageSend'); - Route::get('users/modtools/{id}', 'AdminController@userModTools'); - Route::get('users/modlogs/{id}', 'AdminController@userModLogs'); - Route::post('users/modlogs/{id}', 'AdminController@userModLogsMessage'); - Route::post('users/modlogs/{id}/delete', 'AdminController@userModLogDelete'); - Route::get('users/delete/{id}', 'AdminController@userDelete'); - Route::post('users/delete/{id}', 'AdminController@userDeleteProcess'); - Route::post('users/moderation/update', 'AdminController@userModerate'); - Route::get('media', 'AdminController@media')->name('admin.media'); - Route::redirect('media/list', '/i/admin/media'); - Route::get('media/show/{id}', 'AdminController@mediaShow'); - Route::get('settings', 'AdminController@settings')->name('admin.settings'); - Route::post('settings', 'AdminController@settingsHomeStore'); - Route::get('settings/features', 'AdminController@settingsFeatures')->name('admin.settings.features'); - Route::get('settings/pages', 'AdminController@settingsPages')->name('admin.settings.pages'); - Route::get('settings/pages/edit', 'PageController@edit')->name('admin.settings.pages.edit'); - Route::post('settings/pages/edit', 'PageController@store'); - Route::post('settings/pages/delete', 'PageController@delete'); - Route::post('settings/pages/create', 'PageController@generatePage'); - Route::get('settings/maintenance', 'AdminController@settingsMaintenance')->name('admin.settings.maintenance'); - Route::get('settings/backups', 'AdminController@settingsBackups')->name('admin.settings.backups'); - Route::get('settings/storage', 'AdminController@settingsStorage')->name('admin.settings.storage'); - Route::get('settings/system', 'AdminController@settingsSystem')->name('admin.settings.system'); - - Route::get('instances', 'AdminController@instances')->name('admin.instances'); - Route::post('instances', 'AdminController@instanceScan'); - Route::get('instances/show/{id}', 'AdminController@instanceShow'); - Route::post('instances/edit/{id}', 'AdminController@instanceEdit'); - Route::get('apps/home', 'AdminController@appsHome')->name('admin.apps'); - Route::get('hashtags/home', 'AdminController@hashtagsHome')->name('admin.hashtags'); - Route::get('discover/home', 'AdminController@discoverHome')->name('admin.discover'); - Route::get('discover/category/create', 'AdminController@discoverCreateCategory')->name('admin.discover.create-category'); - Route::post('discover/category/create', 'AdminController@discoverCreateCategoryStore'); - Route::get('discover/category/edit/{id}', 'AdminController@discoverCategoryEdit'); - Route::post('discover/category/edit/{id}', 'AdminController@discoverCategoryUpdate'); - Route::post('discover/category/hashtag/create', 'AdminController@discoveryCategoryTagStore')->name('admin.discover.create-hashtag'); - - Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages'); - Route::get('messages/show/{id}', 'AdminController@messagesShow'); - Route::post('messages/mark-read', 'AdminController@messagesMarkRead'); - Route::redirect('site-news', '/i/admin/newsroom'); - Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home'); - Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create'); - Route::get('newsroom/edit/{id}', 'AdminController@newsroomEdit'); - Route::post('newsroom/edit/{id}', 'AdminController@newsroomUpdate'); - Route::delete('newsroom/edit/{id}', 'AdminController@newsroomDelete'); - Route::post('newsroom/create', 'AdminController@newsroomStore'); - - Route::get('diagnostics/home', 'AdminController@diagnosticsHome')->name('admin.diagnostics'); - Route::post('diagnostics/decrypt', 'AdminController@diagnosticsDecrypt')->name('admin.diagnostics.decrypt'); - Route::get('custom-emoji/home', 'AdminController@customEmojiHome')->name('admin.custom-emoji'); - Route::post('custom-emoji/toggle-active/{id}', 'AdminController@customEmojiToggleActive'); - Route::get('custom-emoji/new', 'AdminController@customEmojiAdd'); - Route::post('custom-emoji/new', 'AdminController@customEmojiStore'); - Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); - Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates'); - - Route::get('directory/home', 'AdminController@directoryHome')->name('admin.directory'); - - Route::get('autospam/home', 'AdminController@autospamHome')->name('admin.autospam'); - - Route::redirect('asf/', 'asf/home'); - Route::get('asf/home', 'AdminShadowFilterController@home'); - Route::get('asf/create', 'AdminShadowFilterController@create'); - Route::get('asf/edit/{id}', 'AdminShadowFilterController@edit'); - Route::post('asf/edit/{id}', 'AdminShadowFilterController@storeEdit'); - Route::post('asf/create', 'AdminShadowFilterController@store'); - - Route::prefix('api')->group(function() { - Route::get('stats', 'AdminController@getStats'); - Route::get('accounts', 'AdminController@getAccounts'); - Route::get('posts', 'AdminController@getPosts'); - Route::get('instances', 'AdminController@getInstances'); - Route::post('directory/save', 'AdminController@directoryStore'); - Route::get('directory/initial-data', 'AdminController@directoryInitialData'); - Route::get('directory/popular-posts', 'AdminController@directoryGetPopularPosts'); - Route::post('directory/add-by-id', 'AdminController@directoryGetAddPostByIdSearch'); - Route::delete('directory/banner-image', 'AdminController@directoryDeleteBannerImage'); - Route::post('directory/submit', 'AdminController@directoryHandleServerSubmission'); - Route::post('directory/testimonial/save', 'AdminController@directorySaveTestimonial'); - Route::post('directory/testimonial/delete', 'AdminController@directoryDeleteTestimonial'); - Route::post('directory/testimonial/update', 'AdminController@directoryUpdateTestimonial'); - Route::get('hashtags/stats', 'AdminController@hashtagsStats'); - Route::get('hashtags/query', 'AdminController@hashtagsApi'); - Route::get('hashtags/get', 'AdminController@hashtagsGet'); - Route::post('hashtags/update', 'AdminController@hashtagsUpdate'); - Route::post('hashtags/clear-trending-cache', 'AdminController@hashtagsClearTrendingCache'); - Route::get('instances/get', 'AdminController@getInstancesApi'); - Route::get('instances/stats', 'AdminController@getInstancesStatsApi'); - Route::get('instances/query', 'AdminController@getInstancesQueryApi'); - Route::post('instances/update', 'AdminController@postInstanceUpdateApi'); - Route::post('instances/create', 'AdminController@postInstanceCreateNewApi'); - Route::post('instances/delete', 'AdminController@postInstanceDeleteApi'); - Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi'); - Route::get('instances/download-backup', 'AdminController@downloadBackup'); - Route::post('instances/import-data', 'AdminController@importBackup'); - Route::get('reports/stats', 'AdminController@reportsStats'); - Route::get('reports/all', 'AdminController@reportsApiAll'); - Route::get('reports/get/{id}', 'AdminController@reportsApiGet'); - Route::post('reports/handle', 'AdminController@reportsApiHandle'); - Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll'); - Route::get('reports/spam/get/{id}', 'AdminController@reportsApiSpamGet'); - Route::post('reports/spam/handle', 'AdminController@reportsApiSpamHandle'); - Route::post('autospam/config', 'AdminController@getAutospamConfigApi'); - Route::post('autospam/reports/closed', 'AdminController@getAutospamReportsClosedApi'); - Route::post('autospam/train', 'AdminController@postAutospamTrainSpamApi'); - Route::post('autospam/search/non-spam', 'AdminController@postAutospamTrainNonSpamSearchApi'); - Route::post('autospam/train/non-spam', 'AdminController@postAutospamTrainNonSpamSubmitApi'); - Route::post('autospam/tokens/custom', 'AdminController@getAutospamCustomTokensApi'); - Route::post('autospam/tokens/store', 'AdminController@saveNewAutospamCustomTokensApi'); - Route::post('autospam/tokens/update', 'AdminController@updateAutospamCustomTokensApi'); - Route::post('autospam/tokens/export', 'AdminController@exportAutospamCustomTokensApi'); - Route::post('autospam/config/enable', 'AdminController@enableAutospamApi'); - Route::post('autospam/config/disable', 'AdminController@disableAutospamApi'); - }); -}); - -Route::domain(config('portfolio.domain'))->group(function () { - Route::redirect('redirect/home', config('app.url')); - Route::get('/', 'PortfolioController@index'); - Route::post('api/portfolio/self/curated.json', 'PortfolioController@storeCurated'); - Route::post('api/portfolio/self/settings.json', 'PortfolioController@getSettings'); - Route::get('api/portfolio/account/settings.json', 'PortfolioController@getAccountSettings'); - Route::post('api/portfolio/self/update-settings.json', 'PortfolioController@storeSettings'); - Route::get('api/portfolio/{username}/feed', 'PortfolioController@getFeed'); - - Route::prefix(config('portfolio.path'))->group(function() { - Route::get('/', 'PortfolioController@index'); - Route::get('settings', 'PortfolioController@settings')->name('portfolio.settings'); - Route::post('settings', 'PortfolioController@store'); - Route::get('{username}/{id}', 'PortfolioController@showPost'); - Route::get('{username}', 'PortfolioController@show'); - - Route::fallback(function () { - return view('errors.404'); - }); - }); -}); - Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { - Route::get('/', 'SiteController@home')->name('timeline.personal'); - Route::redirect('/home', '/')->name('home'); - Route::get('web/directory', 'LandingController@directoryRedirect'); - Route::get('web/explore', 'LandingController@exploreRedirect'); + Route::get('/', 'SiteController@home')->name('timeline.personal'); + Route::redirect('/home', '/')->name('home'); + Route::get('web/directory', 'LandingController@directoryRedirect'); + Route::get('web/explore', 'LandingController@exploreRedirect'); - Auth::routes(); + Auth::routes(); Route::get('auth/raw/mastodon/start', 'RemoteAuthController@startRedirect'); Route::post('auth/raw/mastodon/config', 'RemoteAuthController@getConfig'); Route::post('auth/raw/mastodon/domains', 'RemoteAuthController@getAuthDomains'); @@ -203,489 +29,336 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); + // Route::get('auth/sign_up', 'CuratedRegisterController@index'); + // Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); + // Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); + // Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); + // Route::post('auth/sign_up/concierge', 'CuratedRegisterController@conciergeStore'); + // Route::get('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormShow'); + // Route::post('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormStore'); + // Route::get('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmail'); + // Route::post('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmailHandle'); + // Route::get('auth/sign_up/confirmed', 'CuratedRegisterController@emailConfirmed'); Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot'); Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail'); - Route::get('discover', 'DiscoverController@home')->name('discover'); + Route::get('discover', 'DiscoverController@home')->name('discover'); - Route::group(['prefix' => 'api'], function () { - Route::get('search', 'SearchController@searchAPI'); - Route::post('status/view', 'StatusController@storeView'); - Route::get('v1/polls/{id}', 'PollController@getPoll'); - Route::post('v1/polls/{id}/votes', 'PollController@vote'); + Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags'); + Route::get('discover/places', 'PlaceController@directoryHome')->name('discover.places'); + Route::get('discover/places/{id}/{slug}', 'PlaceController@show'); + Route::get('discover/location/country/{country}', 'PlaceController@directoryCities'); - Route::group(['prefix' => 'compose'], function() { - Route::group(['prefix' => 'v0'], function() { - Route::post('/media/upload', 'ComposeController@mediaUpload'); - Route::post('/media/update', 'ComposeController@mediaUpdate'); - Route::delete('/media/delete', 'ComposeController@mediaDelete'); - Route::get('/search/tag', 'ComposeController@searchTag'); - Route::get('/search/location', 'ComposeController@searchLocation'); - Route::get('/search/mention', 'ComposeController@searchMentionAutocomplete'); - Route::get('/search/hashtag', 'ComposeController@searchHashtagAutocomplete'); + Route::group(['prefix' => 'i'], function () { + Route::redirect('/', '/'); + Route::get('compose', 'StatusController@compose')->name('compose'); + Route::post('comment', 'CommentController@store'); + Route::post('delete', 'StatusController@delete'); + Route::post('mute', 'AccountController@mute'); + Route::post('unmute', 'AccountController@unmute'); + Route::post('block', 'AccountController@block'); + Route::post('unblock', 'AccountController@unblock'); + Route::post('like', 'LikeController@store'); + Route::post('share', 'StatusController@storeShare'); + Route::post('follow', 'FollowerController@store'); + Route::post('bookmark', 'BookmarkController@store'); + Route::get('lang/{locale}', 'SiteController@changeLocale'); + Route::get('restored', 'AccountController@accountRestored'); - Route::post('/publish', 'ComposeController@store'); - Route::post('/publish/text', 'ComposeController@storeText'); - Route::get('/media/processing', 'ComposeController@mediaProcessingCheck'); - Route::get('/settings', 'ComposeController@composeSettings'); - Route::post('/poll', 'ComposeController@createPoll'); - }); - }); + Route::get('verify-email', 'AccountController@verifyEmail'); + Route::post('verify-email', 'AccountController@sendVerifyEmail'); + Route::get('verify-email/request', 'InternalApiController@requestEmailVerification'); + Route::post('verify-email/request', 'InternalApiController@requestEmailVerificationStore'); + Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail'); - Route::group(['prefix' => 'direct'], function () { - Route::get('browse', 'DirectMessageController@browse'); - Route::post('create', 'DirectMessageController@create'); - Route::get('thread', 'DirectMessageController@thread'); - Route::post('mute', 'DirectMessageController@mute'); - Route::post('unmute', 'DirectMessageController@unmute'); - Route::delete('message', 'DirectMessageController@delete'); - Route::post('media', 'DirectMessageController@mediaUpload'); - Route::post('lookup', 'DirectMessageController@composeLookup'); - Route::post('read', 'DirectMessageController@read'); - }); + Route::get('auth/sudo', 'AccountController@sudoMode'); + Route::post('auth/sudo', 'AccountController@sudoModeVerify'); + Route::get('auth/checkpoint', 'AccountController@twoFactorCheckpoint'); + Route::post('auth/checkpoint', 'AccountController@twoFactorVerify'); - Route::group(['prefix' => 'v2'], function() { - Route::get('config', 'ApiController@siteConfiguration'); - Route::get('discover', 'InternalApiController@discover'); - Route::get('discover/posts', 'InternalApiController@discoverPosts')->middleware('auth:api'); - Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); - Route::get('profile/{username}/status/{postid}/state', 'PublicApiController@statusState'); - Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); - Route::get('status/{id}/replies', 'InternalApiController@statusReplies'); - Route::post('moderator/action', 'InternalApiController@modAction'); - Route::get('discover/categories', 'InternalApiController@discoverCategories'); - Route::get('loops', 'DiscoverController@loopsApi'); - Route::post('loops/watch', 'DiscoverController@loopWatch'); - Route::get('discover/tag', 'DiscoverController@getHashtags'); - Route::get('statuses/{id}/replies', 'Api\ApiV1Controller@statusReplies'); - Route::get('statuses/{id}/state', 'Api\ApiV1Controller@statusState'); - }); + Route::get('results', 'SearchController@results'); + Route::post('visibility', 'StatusController@toggleVisibility'); - Route::group(['prefix' => 'pixelfed'], function() { - Route::group(['prefix' => 'v1'], function() { - Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials'); - Route::get('accounts/relationships', 'Api\ApiV1Controller@accountRelationshipsById'); - Route::get('accounts/search', 'Api\ApiV1Controller@accountSearch'); - Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses'); - Route::post('accounts/{id}/block', 'Api\ApiV1Controller@accountBlockById'); - Route::post('accounts/{id}/unblock', 'Api\ApiV1Controller@accountUnblockById'); - Route::get('statuses/{id}', 'PublicApiController@getStatus'); - Route::get('accounts/{id}', 'PublicApiController@account'); - Route::post('avatar/update', 'ApiController@avatarUpdate'); - Route::get('custom_emojis', 'Api\ApiV1Controller@customEmojis'); - Route::get('notifications', 'ApiController@notifications'); - Route::get('timelines/public', 'PublicApiController@publicTimelineApi'); - Route::get('timelines/home', 'PublicApiController@homeTimelineApi'); - Route::get('timelines/network', 'PublicApiController@networkTimelineApi'); - Route::get('newsroom/timeline', 'NewsroomController@timelineApi'); - Route::post('newsroom/markasread', 'NewsroomController@markAsRead'); - Route::get('favourites', 'Api\BaseApiController@accountLikes'); - Route::get('mutes', 'AccountController@accountMutes'); - Route::get('blocks', 'AccountController@accountBlocks'); - }); + Route::post('metro/dark-mode', 'SettingsController@metroDarkMode'); - Route::group(['prefix' => 'v2'], function() { - Route::get('config', 'ApiController@siteConfiguration'); - Route::get('discover', 'InternalApiController@discover'); - Route::get('discover/posts', 'InternalApiController@discoverPosts'); - Route::get('discover/profiles', 'DiscoverController@profilesDirectoryApi'); - Route::get('profile/{username}/status/{postid}', 'PublicApiController@status'); - Route::get('comments/{username}/status/{postId}', 'PublicApiController@statusComments'); - Route::post('moderator/action', 'InternalApiController@modAction'); - Route::get('discover/categories', 'InternalApiController@discoverCategories'); - Route::get('loops', 'DiscoverController@loopsApi'); - Route::post('loops/watch', 'DiscoverController@loopWatch'); - Route::get('discover/tag', 'DiscoverController@getHashtags'); - Route::get('discover/posts/trending', 'DiscoverController@trendingApi'); - Route::get('discover/posts/hashtags', 'DiscoverController@trendingHashtags'); - Route::get('discover/posts/places', 'DiscoverController@trendingPlaces'); - Route::get('seasonal/yir', 'SeasonalController@getData'); - Route::post('seasonal/yir', 'SeasonalController@store'); - Route::get('mutes', 'AccountController@accountMutesV2'); - Route::get('blocks', 'AccountController@accountBlocksV2'); - Route::get('filters', 'AccountController@accountFiltersV2'); - Route::post('status/compose', 'InternalApiController@composePost'); - Route::get('status/{id}/replies', 'InternalApiController@statusReplies'); - Route::post('status/{id}/archive', 'ApiController@archive'); - Route::post('status/{id}/unarchive', 'ApiController@unarchive'); - Route::get('statuses/archives', 'ApiController@archivedPosts'); - Route::get('discover/memories', 'DiscoverController@myMemories'); - Route::get('discover/account-insights', 'DiscoverController@accountInsightsPopularPosts'); - Route::get('discover/server-timeline', 'DiscoverController@serverTimeline'); - Route::get('discover/meta', 'DiscoverController@enabledFeatures'); - Route::post('discover/admin/features', 'DiscoverController@updateFeatures'); - }); + Route::group(['prefix' => 'report'], function () { + Route::get('/', 'ReportController@showForm')->name('report.form'); + Route::post('/', 'ReportController@formStore'); + Route::get('not-interested', 'ReportController@notInterestedForm')->name('report.not-interested'); + Route::get('spam', 'ReportController@spamForm')->name('report.spam'); + Route::get('spam/comment', 'ReportController@spamCommentForm')->name('report.spam.comment'); + Route::get('spam/post', 'ReportController@spamPostForm')->name('report.spam.post'); + Route::get('spam/profile', 'ReportController@spamProfileForm')->name('report.spam.profile'); + Route::get('sensitive/comment', 'ReportController@sensitiveCommentForm')->name('report.sensitive.comment'); + Route::get('sensitive/post', 'ReportController@sensitivePostForm')->name('report.sensitive.post'); + Route::get('sensitive/profile', 'ReportController@sensitiveProfileForm')->name('report.sensitive.profile'); + Route::get('abusive/comment', 'ReportController@abusiveCommentForm')->name('report.abusive.comment'); + Route::get('abusive/post', 'ReportController@abusivePostForm')->name('report.abusive.post'); + Route::get('abusive/profile', 'ReportController@abusiveProfileForm')->name('report.abusive.profile'); + }); - Route::get('discover/accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular'); - Route::post('web/change-language.json', 'SpaController@updateLanguage'); - }); + Route::get('collections/create', 'CollectionController@create'); - Route::group(['prefix' => 'local'], function () { - // Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440'); - Route::get('exp/rec', 'ApiController@userRecommendations'); - Route::post('discover/tag/subscribe', 'HashtagFollowController@store'); - Route::get('discover/tag/list', 'HashtagFollowController@getTags'); - // Route::get('profile/sponsor/{id}', 'ProfileSponsorController@get'); - Route::get('bookmarks', 'InternalApiController@bookmarks'); - Route::get('collection/items/{id}', 'CollectionController@getItems'); - Route::post('collection/item', 'CollectionController@storeId'); - Route::delete('collection/item', 'CollectionController@deleteId'); - Route::get('collection/{id}', 'CollectionController@getCollection'); - Route::post('collection/{id}', 'CollectionController@store'); - Route::delete('collection/{id}', 'CollectionController@delete'); - Route::post('collection/{id}/publish', 'CollectionController@publish'); - Route::get('profile/collections/{id}', 'CollectionController@getUserCollections'); + Route::get('me', 'ProfileController@meRedirect'); + Route::get('intent/follow', 'SiteController@followIntent'); + Route::get('rs/{id}', 'StoryController@remoteStory'); + Route::get('stories/new', 'StoryController@compose'); + Route::get('my/story', 'StoryController@iRedirect'); + Route::get('web/profile/_/{id}', 'InternalApiController@remoteProfile'); + Route::get('web/post/_/{profileId}/{statusid}', 'InternalApiController@remoteStatus'); - Route::post('compose/tag/untagme', 'MediaTagController@untagProfile'); - - Route::post('import/ig', 'ImportPostController@store'); - Route::get('import/ig/config', 'ImportPostController@getConfig'); - Route::post('import/ig/media', 'ImportPostController@storeMedia'); - Route::post('import/ig/existing', 'ImportPostController@getImportedFiles'); - Route::post('import/ig/posts', 'ImportPostController@getImportedPosts'); - Route::post('import/ig/processing', 'ImportPostController@getProcessingCount'); - }); - - Route::group(['prefix' => 'web/stories'], function () { - Route::get('v1/recent', 'StoryController@recent'); - Route::get('v1/viewers', 'StoryController@viewers'); - Route::get('v1/profile/{id}', 'StoryController@profile'); - Route::get('v1/exists/{id}', 'StoryController@exists'); - Route::get('v1/poll/results', 'StoryController@pollResults'); - Route::post('v1/viewed', 'StoryController@viewed'); - Route::post('v1/react', 'StoryController@react'); - Route::post('v1/comment', 'StoryController@comment'); - Route::post('v1/publish/poll', 'StoryController@publishStoryPoll'); - Route::post('v1/poll/vote', 'StoryController@storyPollVote'); - Route::post('v1/report', 'StoryController@storeReport'); - Route::post('v1/add', 'StoryController@apiV1Add'); - Route::post('v1/crop', 'StoryController@cropPhoto'); - Route::post('v1/publish', 'StoryController@publishStory'); - Route::delete('v1/delete/{id}', 'StoryController@apiV1Delete'); - }); - - Route::group(['prefix' => 'portfolio'], function () { - Route::post('self/curated.json', 'PortfolioController@storeCurated'); - Route::post('self/settings.json', 'PortfolioController@getSettings'); - Route::get('account/settings.json', 'PortfolioController@getAccountSettings'); - Route::post('self/update-settings.json', 'PortfolioController@storeSettings'); - Route::get('{username}/feed', 'PortfolioController@getFeed'); - }); - }); - - Route::get('discover/tags/{hashtag}', 'DiscoverController@showTags'); - Route::get('discover/places', 'PlaceController@directoryHome')->name('discover.places'); - Route::get('discover/places/{id}/{slug}', 'PlaceController@show'); - Route::get('discover/location/country/{country}', 'PlaceController@directoryCities'); - - Route::group(['prefix' => 'i'], function () { - Route::redirect('/', '/'); - Route::get('compose', 'StatusController@compose')->name('compose'); - Route::post('comment', 'CommentController@store'); - Route::post('delete', 'StatusController@delete'); - Route::post('mute', 'AccountController@mute'); - Route::post('unmute', 'AccountController@unmute'); - Route::post('block', 'AccountController@block'); - Route::post('unblock', 'AccountController@unblock'); - Route::post('like', 'LikeController@store'); - Route::post('share', 'StatusController@storeShare'); - Route::post('follow', 'FollowerController@store'); - Route::post('bookmark', 'BookmarkController@store'); - Route::get('lang/{locale}', 'SiteController@changeLocale'); - Route::get('restored', 'AccountController@accountRestored'); - - Route::get('verify-email', 'AccountController@verifyEmail'); - Route::post('verify-email', 'AccountController@sendVerifyEmail'); - Route::get('verify-email/request', 'InternalApiController@requestEmailVerification'); - Route::post('verify-email/request', 'InternalApiController@requestEmailVerificationStore'); - Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail'); - - Route::get('auth/sudo', 'AccountController@sudoMode'); - Route::post('auth/sudo', 'AccountController@sudoModeVerify'); - Route::get('auth/checkpoint', 'AccountController@twoFactorCheckpoint'); - Route::post('auth/checkpoint', 'AccountController@twoFactorVerify'); - - Route::get('results', 'SearchController@results'); - Route::post('visibility', 'StatusController@toggleVisibility'); - - Route::post('metro/dark-mode', 'SettingsController@metroDarkMode'); - - Route::group(['prefix' => 'report'], function () { - Route::get('/', 'ReportController@showForm')->name('report.form'); - Route::post('/', 'ReportController@formStore'); - Route::get('not-interested', 'ReportController@notInterestedForm')->name('report.not-interested'); - Route::get('spam', 'ReportController@spamForm')->name('report.spam'); - Route::get('spam/comment', 'ReportController@spamCommentForm')->name('report.spam.comment'); - Route::get('spam/post', 'ReportController@spamPostForm')->name('report.spam.post'); - Route::get('spam/profile', 'ReportController@spamProfileForm')->name('report.spam.profile'); - Route::get('sensitive/comment', 'ReportController@sensitiveCommentForm')->name('report.sensitive.comment'); - Route::get('sensitive/post', 'ReportController@sensitivePostForm')->name('report.sensitive.post'); - Route::get('sensitive/profile', 'ReportController@sensitiveProfileForm')->name('report.sensitive.profile'); - Route::get('abusive/comment', 'ReportController@abusiveCommentForm')->name('report.abusive.comment'); - Route::get('abusive/post', 'ReportController@abusivePostForm')->name('report.abusive.post'); - Route::get('abusive/profile', 'ReportController@abusiveProfileForm')->name('report.abusive.profile'); - }); - - Route::get('collections/create', 'CollectionController@create'); - - Route::get('me', 'ProfileController@meRedirect'); - Route::get('intent/follow', 'SiteController@followIntent'); - Route::get('rs/{id}', 'StoryController@remoteStory'); - Route::get('stories/new', 'StoryController@compose'); - Route::get('my/story', 'StoryController@iRedirect'); - Route::get('web/profile/_/{id}', 'InternalApiController@remoteProfile'); - Route::get('web/post/_/{profileId}/{statusid}', 'InternalApiController@remoteStatus'); - - Route::group(['prefix' => 'import', 'middleware' => 'dangerzone'], function() { - Route::get('job/{uuid}/1', 'ImportController@instagramStepOne'); - Route::post('job/{uuid}/1', 'ImportController@instagramStepOneStore'); - Route::get('job/{uuid}/2', 'ImportController@instagramStepTwo'); - Route::post('job/{uuid}/2', 'ImportController@instagramStepTwoStore'); - Route::get('job/{uuid}/3', 'ImportController@instagramStepThree'); - Route::post('job/{uuid}/3', 'ImportController@instagramStepThreeStore'); - }); - - Route::get('redirect', 'SiteController@redirectUrl'); - Route::post('admin/media/block/add', 'MediaBlocklistController@add'); - Route::post('admin/media/block/delete', 'MediaBlocklistController@delete'); - - Route::get('warning', 'AccountInterstitialController@get'); - Route::post('warning', 'AccountInterstitialController@read'); - Route::get('my2020', 'SeasonalController@yearInReview'); - - Route::get('web/my-portfolio', 'PortfolioController@myRedirect'); - Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect'); - Route::get('web/username/{id}', 'SpaController@usernameRedirect'); - Route::get('web/post/{id}', 'SpaController@webPost'); - Route::get('web/profile/{id}', 'SpaController@webProfile'); - - Route::get('web/{q}', 'SpaController@index')->where('q', '.*'); - Route::get('web', 'SpaController@index'); - }); - - Route::group(['prefix' => 'account'], function () { - Route::redirect('/', '/'); - Route::get('direct', 'AccountController@direct'); - Route::get('direct/t/{id}', 'AccountController@directMessage'); - Route::get('activity', 'AccountController@notifications')->name('notifications'); - Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests'); - Route::post('follow-requests', 'AccountController@followRequestHandle'); - Route::get('follow-requests.json', 'AccountController@followRequestsJson'); - Route::get('portfolio/{username}.json', 'PortfolioController@getApFeed'); - Route::get('portfolio/{username}.rss', 'PortfolioController@getRssFeed'); - }); - - Route::group(['prefix' => 'settings'], function () { - Route::redirect('/', '/settings/home'); - Route::get('home', 'SettingsController@home') - ->name('settings'); - Route::post('home', 'SettingsController@homeUpdate'); - Route::get('avatar', 'SettingsController@avatar')->name('settings.avatar'); - Route::post('avatar', 'AvatarController@store'); - Route::delete('avatar', 'AvatarController@deleteAvatar'); - Route::get('password', 'SettingsController@password')->name('settings.password')->middleware('dangerzone'); - Route::post('password', 'SettingsController@passwordUpdate')->middleware('dangerzone'); - Route::get('email', 'SettingsController@email')->name('settings.email')->middleware('dangerzone'); - Route::post('email', 'SettingsController@emailUpdate')->middleware('dangerzone'); - Route::get('notifications', 'SettingsController@notifications')->name('settings.notifications'); - Route::get('privacy', 'SettingsController@privacy')->name('settings.privacy'); - Route::post('privacy', 'SettingsController@privacyStore'); - Route::get('privacy/muted-users', 'SettingsController@mutedUsers')->name('settings.privacy.muted-users'); - Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate'); - Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users'); - Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate'); - Route::get('privacy/domain-blocks', 'SettingsController@domainBlocks')->name('settings.privacy.domain-blocks'); - Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances'); - Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore'); - Route::post('privacy/blocked-instances/unblock', 'SettingsController@blockedInstanceUnblock')->name('settings.privacy.blocked-instances.unblock'); - Route::get('privacy/blocked-keywords', 'SettingsController@blockedKeywords')->name('settings.privacy.blocked-keywords'); - Route::post('privacy/account', 'SettingsController@privateAccountOptions')->name('settings.privacy.account'); - Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() { - Route::get('request/temporary', 'SettingsController@removeAccountTemporary')->name('settings.remove.temporary'); - Route::post('request/temporary', 'SettingsController@removeAccountTemporarySubmit'); - Route::get('request/permanent', 'SettingsController@removeAccountPermanent')->name('settings.remove.permanent'); - Route::post('request/permanent', 'SettingsController@removeAccountPermanentSubmit'); - }); - - Route::group(['prefix' => 'security', 'middleware' => 'dangerzone'], function() { - Route::get( - '/', - 'SettingsController@security' - )->name('settings.security'); - Route::get( - '2fa/setup', - 'SettingsController@securityTwoFactorSetup' - )->name('settings.security.2fa.setup'); - Route::post( - '2fa/setup', - 'SettingsController@securityTwoFactorSetupStore' - ); - Route::get( - '2fa/edit', - 'SettingsController@securityTwoFactorEdit' - )->name('settings.security.2fa.edit'); - Route::post( - '2fa/edit', - 'SettingsController@securityTwoFactorUpdate' - ); - Route::get( - '2fa/recovery-codes', - 'SettingsController@securityTwoFactorRecoveryCodes' - )->name('settings.security.2fa.recovery'); - Route::post( - '2fa/recovery-codes', - 'SettingsController@securityTwoFactorRecoveryCodesRegenerate' - ); - - }); - - Route::get('parental-controls', 'ParentalControlsController@index')->name('settings.parental-controls')->middleware('dangerzone'); - Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add')->middleware('dangerzone'); - Route::post('parental-controls/add', 'ParentalControlsController@store')->middleware('dangerzone'); - Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view')->middleware('dangerzone'); - Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update')->middleware('dangerzone'); - Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite')->middleware('dangerzone'); - Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle')->middleware('dangerzone'); - Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing')->middleware('dangerzone'); - Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle')->middleware('dangerzone'); - - Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone'); - Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone'); - Route::post('data-export/following', 'SettingsController@exportFollowing')->middleware('dangerzone'); - Route::post('data-export/followers', 'SettingsController@exportFollowers')->middleware('dangerzone'); - Route::post('data-export/mute-block-list', 'SettingsController@exportMuteBlockList')->middleware('dangerzone'); - Route::post('data-export/account', 'SettingsController@exportAccount')->middleware('dangerzone'); - Route::post('data-export/statuses', 'SettingsController@exportStatuses')->middleware('dangerzone'); - Route::get('developers', 'SettingsController@developers')->name('settings.developers')->middleware('dangerzone'); - Route::get('labs', 'SettingsController@labs')->name('settings.labs'); - Route::post('labs', 'SettingsController@labsStore'); - - Route::get('accessibility', 'SettingsController@accessibility')->name('settings.accessibility'); - Route::post('accessibility', 'SettingsController@accessibilityStore'); - - Route::group(['prefix' => 'relationships'], function() { - Route::redirect('/', '/settings/relationships/home'); - Route::get('home', 'SettingsController@relationshipsHome')->name('settings.relationships'); - }); - Route::get('invites/create', 'UserInviteController@create')->name('settings.invites.create'); - Route::post('invites/create', 'UserInviteController@store'); - Route::get('invites', 'UserInviteController@show')->name('settings.invites'); - // Route::get('sponsor', 'SettingsController@sponsor')->name('settings.sponsor'); - // Route::post('sponsor', 'SettingsController@sponsorStore'); Route::group(['prefix' => 'import', 'middleware' => 'dangerzone'], function() { - Route::get('/', 'SettingsController@dataImport')->name('settings.import'); - Route::prefix('instagram')->group(function() { - Route::get('/', 'ImportController@instagram')->name('settings.import.ig'); - Route::post('/', 'ImportController@instagramStart'); - }); - Route::prefix('mastodon')->group(function() { - Route::get('/', 'ImportController@mastodon')->name('settings.import.mastodon'); - }); - }); + Route::get('job/{uuid}/1', 'ImportController@instagramStepOne'); + Route::post('job/{uuid}/1', 'ImportController@instagramStepOneStore'); + Route::get('job/{uuid}/2', 'ImportController@instagramStepTwo'); + Route::post('job/{uuid}/2', 'ImportController@instagramStepTwoStore'); + Route::get('job/{uuid}/3', 'ImportController@instagramStepThree'); + Route::post('job/{uuid}/3', 'ImportController@instagramStepThreeStore'); + }); - Route::get('timeline', 'SettingsController@timelineSettings')->name('settings.timeline'); - Route::post('timeline', 'SettingsController@updateTimelineSettings'); - Route::get('media', 'SettingsController@mediaSettings')->name('settings.media'); - Route::post('media', 'SettingsController@updateMediaSettings'); + Route::get('redirect', 'SiteController@redirectUrl'); + Route::post('admin/media/block/add', 'MediaBlocklistController@add'); + Route::post('admin/media/block/delete', 'MediaBlocklistController@delete'); + + Route::get('warning', 'AccountInterstitialController@get'); + Route::post('warning', 'AccountInterstitialController@read'); + Route::get('my2020', 'SeasonalController@yearInReview'); + + Route::get('web/my-portfolio', 'PortfolioController@myRedirect'); + Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect'); + Route::get('web/username/{id}', 'SpaController@usernameRedirect'); + Route::get('web/post/{id}', 'SpaController@webPost'); + Route::get('web/profile/{id}', 'SpaController@webProfile'); + + Route::get('web/{q}', 'SpaController@index')->where('q', '.*'); + Route::get('web', 'SpaController@index'); + }); + + Route::group(['prefix' => 'account'], function () { + Route::redirect('/', '/'); + Route::get('direct', 'AccountController@direct'); + Route::get('direct/t/{id}', 'AccountController@directMessage'); + Route::get('activity', 'AccountController@notifications')->name('notifications'); + Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests'); + Route::post('follow-requests', 'AccountController@followRequestHandle'); + Route::get('follow-requests.json', 'AccountController@followRequestsJson'); + Route::get('portfolio/{username}.json', 'PortfolioController@getApFeed'); + Route::get('portfolio/{username}.rss', 'PortfolioController@getRssFeed'); + }); + + Route::group(['prefix' => 'settings'], function () { + Route::redirect('/', '/settings/home'); + Route::get('home', 'SettingsController@home') + ->name('settings'); + Route::post('home', 'SettingsController@homeUpdate'); + Route::get('avatar', 'SettingsController@avatar')->name('settings.avatar'); + Route::post('avatar', 'AvatarController@store'); + Route::delete('avatar', 'AvatarController@deleteAvatar'); + Route::get('password', 'SettingsController@password')->name('settings.password')->middleware('dangerzone'); + Route::post('password', 'SettingsController@passwordUpdate')->middleware('dangerzone'); + Route::get('email', 'SettingsController@email')->name('settings.email')->middleware('dangerzone'); + Route::post('email', 'SettingsController@emailUpdate')->middleware('dangerzone'); + Route::get('notifications', 'SettingsController@notifications')->name('settings.notifications'); + Route::get('privacy', 'SettingsController@privacy')->name('settings.privacy'); + Route::post('privacy', 'SettingsController@privacyStore'); + Route::get('privacy/muted-users', 'SettingsController@mutedUsers')->name('settings.privacy.muted-users'); + Route::post('privacy/muted-users', 'SettingsController@mutedUsersUpdate'); + Route::get('privacy/blocked-users', 'SettingsController@blockedUsers')->name('settings.privacy.blocked-users'); + Route::post('privacy/blocked-users', 'SettingsController@blockedUsersUpdate'); + Route::get('privacy/domain-blocks', 'SettingsController@domainBlocks')->name('settings.privacy.domain-blocks'); + Route::get('privacy/blocked-instances', 'SettingsController@blockedInstances')->name('settings.privacy.blocked-instances'); + Route::post('privacy/blocked-instances', 'SettingsController@blockedInstanceStore'); + Route::post('privacy/blocked-instances/unblock', 'SettingsController@blockedInstanceUnblock')->name('settings.privacy.blocked-instances.unblock'); + Route::get('privacy/blocked-keywords', 'SettingsController@blockedKeywords')->name('settings.privacy.blocked-keywords'); + Route::post('privacy/account', 'SettingsController@privateAccountOptions')->name('settings.privacy.account'); + Route::group(['prefix' => 'remove', 'middleware' => 'dangerzone'], function() { + Route::get('request/temporary', 'SettingsController@removeAccountTemporary')->name('settings.remove.temporary'); + Route::post('request/temporary', 'SettingsController@removeAccountTemporarySubmit'); + Route::get('request/permanent', 'SettingsController@removeAccountPermanent')->name('settings.remove.permanent'); + Route::post('request/permanent', 'SettingsController@removeAccountPermanentSubmit'); + }); + + Route::group(['prefix' => 'security', 'middleware' => 'dangerzone'], function() { + Route::get( + '/', + 'SettingsController@security' + )->name('settings.security'); + Route::get( + '2fa/setup', + 'SettingsController@securityTwoFactorSetup' + )->name('settings.security.2fa.setup'); + Route::post( + '2fa/setup', + 'SettingsController@securityTwoFactorSetupStore' + ); + Route::get( + '2fa/edit', + 'SettingsController@securityTwoFactorEdit' + )->name('settings.security.2fa.edit'); + Route::post( + '2fa/edit', + 'SettingsController@securityTwoFactorUpdate' + ); + Route::get( + '2fa/recovery-codes', + 'SettingsController@securityTwoFactorRecoveryCodes' + )->name('settings.security.2fa.recovery'); + Route::post( + '2fa/recovery-codes', + 'SettingsController@securityTwoFactorRecoveryCodesRegenerate' + ); + + }); + + Route::get('parental-controls', 'ParentalControlsController@index')->name('settings.parental-controls')->middleware('dangerzone'); + Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add')->middleware('dangerzone'); + Route::post('parental-controls/add', 'ParentalControlsController@store')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle')->middleware('dangerzone'); + Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing')->middleware('dangerzone'); + Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle')->middleware('dangerzone'); + + Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone'); + Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone'); + Route::post('data-export/following', 'SettingsController@exportFollowing')->middleware('dangerzone'); + Route::post('data-export/followers', 'SettingsController@exportFollowers')->middleware('dangerzone'); + Route::post('data-export/mute-block-list', 'SettingsController@exportMuteBlockList')->middleware('dangerzone'); + Route::post('data-export/account', 'SettingsController@exportAccount')->middleware('dangerzone'); + Route::post('data-export/statuses', 'SettingsController@exportStatuses')->middleware('dangerzone'); + Route::get('developers', 'SettingsController@developers')->name('settings.developers')->middleware('dangerzone'); + Route::get('labs', 'SettingsController@labs')->name('settings.labs'); + Route::post('labs', 'SettingsController@labsStore'); + + Route::get('accessibility', 'SettingsController@accessibility')->name('settings.accessibility'); + Route::post('accessibility', 'SettingsController@accessibilityStore'); + + Route::group(['prefix' => 'relationships'], function() { + Route::redirect('/', '/settings/relationships/home'); + Route::get('home', 'SettingsController@relationshipsHome')->name('settings.relationships'); + }); + Route::get('invites/create', 'UserInviteController@create')->name('settings.invites.create'); + Route::post('invites/create', 'UserInviteController@store'); + Route::get('invites', 'UserInviteController@show')->name('settings.invites'); + // Route::get('sponsor', 'SettingsController@sponsor')->name('settings.sponsor'); + // Route::post('sponsor', 'SettingsController@sponsorStore'); + Route::group(['prefix' => 'import', 'middleware' => 'dangerzone'], function() { + Route::get('/', 'SettingsController@dataImport')->name('settings.import'); + Route::prefix('instagram')->group(function() { + Route::get('/', 'ImportController@instagram')->name('settings.import.ig'); + Route::post('/', 'ImportController@instagramStart'); + }); + Route::prefix('mastodon')->group(function() { + Route::get('/', 'ImportController@mastodon')->name('settings.import.mastodon'); + }); + }); + + Route::get('timeline', 'SettingsController@timelineSettings')->name('settings.timeline'); + Route::post('timeline', 'SettingsController@updateTimelineSettings'); + Route::get('media', 'SettingsController@mediaSettings')->name('settings.media'); + Route::post('media', 'SettingsController@updateMediaSettings'); Route::group(['prefix' => 'account/aliases', 'middleware' => 'dangerzone'], function() { Route::get('manage', 'ProfileAliasController@index'); Route::post('manage', 'ProfileAliasController@store'); Route::post('manage/delete', 'ProfileAliasController@delete'); }); - }); + }); - Route::group(['prefix' => 'site'], function () { - Route::redirect('/', '/'); - Route::get('about', 'SiteController@about')->name('site.about'); - Route::view('help', 'site.help')->name('site.help'); - Route::view('developer-api', 'site.developer')->name('site.developers'); - Route::view('fediverse', 'site.fediverse')->name('site.fediverse'); - Route::view('open-source', 'site.opensource')->name('site.opensource'); - Route::view('banned-instances', 'site.bannedinstances')->name('site.bannedinstances'); - Route::get('terms', 'SiteController@terms')->name('site.terms'); - Route::get('privacy', 'SiteController@privacy')->name('site.privacy'); - Route::view('platform', 'site.platform')->name('site.platform'); - Route::view('language', 'site.language')->name('site.language'); - Route::get('contact', 'ContactController@show')->name('site.contact'); - Route::post('contact', 'ContactController@store'); - Route::group(['prefix'=>'kb'], function() { - Route::view('getting-started', 'site.help.getting-started')->name('help.getting-started'); - Route::view('sharing-media', 'site.help.sharing-media')->name('help.sharing-media'); - Route::view('your-profile', 'site.help.your-profile')->name('help.your-profile'); - Route::view('stories', 'site.help.stories')->name('help.stories'); - Route::view('embed', 'site.help.embed')->name('help.embed'); - Route::view('hashtags', 'site.help.hashtags')->name('help.hashtags'); - Route::view('instance-actor', 'site.help.instance-actor')->name('help.instance-actor'); - Route::view('discover', 'site.help.discover')->name('help.discover'); - Route::view('direct-messages', 'site.help.dm')->name('help.dm'); - Route::view('timelines', 'site.help.timelines')->name('help.timelines'); - Route::view('what-is-the-fediverse', 'site.help.what-is-fediverse')->name('help.what-is-fediverse'); - Route::view('safety-tips', 'site.help.safety-tips')->name('help.safety-tips'); + Route::group(['prefix' => 'site'], function () { + Route::redirect('/', '/'); + Route::get('about', 'SiteController@about')->name('site.about'); + Route::view('help', 'site.help')->name('site.help'); + Route::view('developer-api', 'site.developer')->name('site.developers'); + Route::view('fediverse', 'site.fediverse')->name('site.fediverse'); + Route::view('open-source', 'site.opensource')->name('site.opensource'); + Route::view('banned-instances', 'site.bannedinstances')->name('site.bannedinstances'); + Route::get('terms', 'SiteController@terms')->name('site.terms'); + Route::get('privacy', 'SiteController@privacy')->name('site.privacy'); + Route::view('platform', 'site.platform')->name('site.platform'); + Route::view('language', 'site.language')->name('site.language'); + Route::get('contact', 'ContactController@show')->name('site.contact'); + Route::post('contact', 'ContactController@store'); + Route::group(['prefix'=>'kb'], function() { + Route::view('getting-started', 'site.help.getting-started')->name('help.getting-started'); + Route::view('sharing-media', 'site.help.sharing-media')->name('help.sharing-media'); + Route::view('your-profile', 'site.help.your-profile')->name('help.your-profile'); + Route::view('stories', 'site.help.stories')->name('help.stories'); + Route::view('embed', 'site.help.embed')->name('help.embed'); + Route::view('hashtags', 'site.help.hashtags')->name('help.hashtags'); + Route::view('instance-actor', 'site.help.instance-actor')->name('help.instance-actor'); + Route::view('discover', 'site.help.discover')->name('help.discover'); + Route::view('direct-messages', 'site.help.dm')->name('help.dm'); + Route::view('timelines', 'site.help.timelines')->name('help.timelines'); + Route::view('what-is-the-fediverse', 'site.help.what-is-fediverse')->name('help.what-is-fediverse'); + Route::view('safety-tips', 'site.help.safety-tips')->name('help.safety-tips'); - Route::get('community-guidelines', 'SiteController@communityGuidelines')->name('help.community-guidelines'); - Route::view('controlling-visibility', 'site.help.controlling-visibility')->name('help.controlling-visibility'); - Route::view('blocking-accounts', 'site.help.blocking-accounts')->name('help.blocking-accounts'); - Route::view('report-something', 'site.help.report-something')->name('help.report-something'); - Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy'); - Route::view('labs-deprecation', 'site.help.labs-deprecation')->name('help.labs-deprecation'); - Route::view('tagging-people', 'site.help.tagging-people')->name('help.tagging-people'); - Route::view('licenses', 'site.help.licenses')->name('help.licenses'); - Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit'); - Route::view('import', 'site.help.import')->name('help.import'); - Route::view('parental-controls', 'site.help.parental-controls'); - }); - Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); - Route::get('newsroom/archive', 'NewsroomController@archive'); - Route::get('newsroom/search', 'NewsroomController@search'); - Route::get('newsroom', 'NewsroomController@index'); - Route::get('legal-notice', 'SiteController@legalNotice'); - }); + Route::get('community-guidelines', 'SiteController@communityGuidelines')->name('help.community-guidelines'); + Route::view('controlling-visibility', 'site.help.controlling-visibility')->name('help.controlling-visibility'); + Route::view('blocking-accounts', 'site.help.blocking-accounts')->name('help.blocking-accounts'); + Route::view('report-something', 'site.help.report-something')->name('help.report-something'); + Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy'); + Route::view('labs-deprecation', 'site.help.labs-deprecation')->name('help.labs-deprecation'); + Route::view('tagging-people', 'site.help.tagging-people')->name('help.tagging-people'); + Route::view('licenses', 'site.help.licenses')->name('help.licenses'); + Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit'); + Route::view('import', 'site.help.import')->name('help.import'); + Route::view('parental-controls', 'site.help.parental-controls'); + // Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues'); + // Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); + }); + Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); + Route::get('newsroom/archive', 'NewsroomController@archive'); + Route::get('newsroom/search', 'NewsroomController@search'); + Route::get('newsroom', 'NewsroomController@index'); + Route::get('legal-notice', 'SiteController@legalNotice'); + }); - Route::group(['prefix' => 'timeline'], function () { - Route::redirect('/', '/'); - Route::get('public', 'TimelineController@local')->name('timeline.public'); - Route::get('network', 'TimelineController@network')->name('timeline.network'); - }); + Route::group(['prefix' => 'timeline'], function () { + Route::redirect('/', '/'); + Route::get('public', 'TimelineController@local')->name('timeline.public'); + Route::get('network', 'TimelineController@network')->name('timeline.network'); + }); - Route::group(['prefix' => 'users'], function () { - Route::redirect('/', '/'); - Route::get('{user}.atom', 'ProfileController@showAtomFeed')->where('user', '.*'); - Route::get('{username}/outbox', 'FederationController@userOutbox'); - Route::get('{username}/followers', 'FederationController@userFollowers'); - Route::get('{username}/following', 'FederationController@userFollowing'); - Route::get('{username}', 'ProfileController@permalinkRedirect'); - }); + Route::group(['prefix' => 'users'], function () { + Route::redirect('/', '/'); + Route::get('{user}.atom', 'ProfileController@showAtomFeed')->where('user', '.*'); + Route::get('{username}/outbox', 'FederationController@userOutbox'); + Route::get('{username}/followers', 'FederationController@userFollowers'); + Route::get('{username}/following', 'FederationController@userFollowing'); + Route::get('{username}', 'ProfileController@permalinkRedirect'); + }); - Route::group(['prefix' => 'installer'], function() { - Route::get('api/requirements', 'InstallController@getRequirements')->withoutMiddleware(['web']); - Route::post('precheck/database', 'InstallController@precheckDatabase')->withoutMiddleware(['web']); - Route::post('store', 'InstallController@store')->withoutMiddleware(['web']); - Route::get('/', 'InstallController@index')->withoutMiddleware(['web']); - Route::get('/{q}', 'InstallController@index')->withoutMiddleware(['web'])->where('q', '.*'); - }); + Route::group(['prefix' => 'installer'], function() { + Route::get('api/requirements', 'InstallController@getRequirements')->withoutMiddleware(['web']); + Route::post('precheck/database', 'InstallController@precheckDatabase')->withoutMiddleware(['web']); + Route::post('store', 'InstallController@store')->withoutMiddleware(['web']); + Route::get('/', 'InstallController@index')->withoutMiddleware(['web']); + Route::get('/{q}', 'InstallController@index')->withoutMiddleware(['web'])->where('q', '.*'); + }); - Route::group(['prefix' => 'e'], function() { - Route::get('terms', 'MobileController@terms'); - Route::get('privacy', 'MobileController@privacy'); - }); + Route::group(['prefix' => 'e'], function() { + Route::get('terms', 'MobileController@terms'); + Route::get('privacy', 'MobileController@privacy'); + }); - Route::get('auth/invite/a/{code}', 'AdminInviteController@index'); - Route::post('api/v1.1/auth/invite/admin/re', 'AdminInviteController@apiRegister')->middleware('throttle:5,1440'); + Route::get('auth/invite/a/{code}', 'AdminInviteController@index'); + Route::post('api/v1.1/auth/invite/admin/re', 'AdminInviteController@apiRegister')->middleware('throttle:5,1440'); - Route::get('storage/m/_v2/{pid}/{mhash}/{uhash}/{f}', 'MediaController@fallbackRedirect'); - Route::get('stories/{username}', 'ProfileController@stories'); - Route::get('p/{id}', 'StatusController@shortcodeRedirect'); - Route::get('c/{collection}', 'CollectionController@show'); - Route::get('p/{username}/{id}/c', 'CommentController@showAll'); - Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed'); - Route::get('p/{username}/{id}/edit', 'StatusController@edit'); - Route::post('p/{username}/{id}/edit', 'StatusController@editStore'); - Route::get('p/{username}/{id}.json', 'StatusController@showObject'); - Route::get('p/{username}/{id}', 'StatusController@show'); - Route::get('{username}/embed', 'ProfileController@embed'); - Route::get('{username}/live', 'LiveStreamController@showProfilePlayer'); - Route::get('@{username}@{domain}', 'SiteController@legacyWebfingerRedirect'); - Route::get('@{username}', 'SiteController@legacyProfileRedirect'); - Route::get('{username}', 'ProfileController@show'); + Route::get('storage/m/_v2/{pid}/{mhash}/{uhash}/{f}', 'MediaController@fallbackRedirect'); + Route::get('stories/{username}', 'ProfileController@stories'); + Route::get('p/{id}', 'StatusController@shortcodeRedirect'); + Route::get('c/{collection}', 'CollectionController@show'); + Route::get('p/{username}/{id}/c', 'CommentController@showAll'); + Route::get('p/{username}/{id}/embed', 'StatusController@showEmbed'); + Route::get('p/{username}/{id}/edit', 'StatusController@edit'); + Route::post('p/{username}/{id}/edit', 'StatusController@editStore'); + Route::get('p/{username}/{id}.json', 'StatusController@showObject'); + Route::get('p/{username}/{id}', 'StatusController@show'); + Route::get('{username}/embed', 'ProfileController@embed'); + Route::get('{username}/live', 'LiveStreamController@showProfilePlayer'); + Route::get('@{username}@{domain}', 'SiteController@legacyWebfingerRedirect'); + Route::get('@{username}', 'SiteController@legacyProfileRedirect'); + Route::get('{username}', 'ProfileController@show'); }); From 40b45b2a115fa1fa6b923e55e55db7615ae2d472 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 02:15:39 -0700 Subject: [PATCH 401/977] Update Autospam, add live filters to block remote activities based on comma separated keywords --- app/Util/ActivityPub/Helpers.php | 17 +++++++++++++++++ app/Util/ActivityPub/Inbox.php | 16 ++++++++++++++++ config/autospam.php | 5 +++++ 3 files changed, 38 insertions(+) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 6f5b8ae11..e25fd93b7 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -315,6 +315,23 @@ class Helpers { return; } + if(config('autospam.live_filters.enabled')) { + $filters = config('autospam.live_filters.filters'); + if(!empty($filters) && isset($res['content']) && !empty($res['content']) && strlen($filters) > 3) { + $filters = array_map('trim', explode(',', $filters)); + $content = $res['content']; + foreach($filters as $filter) { + $filter = trim($filter); + if(!$filter || !strlen($filter)) { + continue; + } + if(str_contains($content, $filter)) { + return; + } + } + } + } + if(isset($res['object'])) { $activity = $res; } else { diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 5c9959e17..b6ae80893 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -197,6 +197,22 @@ class Inbox public function handleCreateActivity() { $activity = $this->payload['object']; + if(config('autospam.live_filters.enabled')) { + $filters = config('autospam.live_filters.filters'); + if(!empty($filters) && isset($activity['content']) && !empty($activity['content']) && strlen($filters) > 3) { + $filters = array_map('trim', explode(',', $filters)); + $content = $activity['content']; + foreach($filters as $filter) { + $filter = trim($filter); + if(!$filter || !strlen($filter)) { + continue; + } + if(str_contains($content, $filter)) { + return; + } + } + } + } $actor = $this->actorFirstOrCreate($this->payload['actor']); if(!$actor || $actor->domain == null) { return; diff --git a/config/autospam.php b/config/autospam.php index 39975ec9b..bc0ce4681 100644 --- a/config/autospam.php +++ b/config/autospam.php @@ -33,5 +33,10 @@ return [ 'nlp' => [ 'enabled' => false, 'spam_sample_limit' => env('PF_AUTOSPAM_NLP_SPAM_SAMPLE_LIMIT', 200), + ], + + 'live_filters' => [ + 'enabled' => env('PF_AUTOSPAM_LIVE_FILTERS_ENABLED', false), + 'filters' => env('PF_AUTOSPAM_LIVE_FILTERS_CSV', ''), ] ]; From 83eadbb811c098e84ac108188f9262cfeced4e40 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 02:20:11 -0700 Subject: [PATCH 402/977] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 322e5ff49..ab6220584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.11...dev) +### Features +- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) + ### Updated - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) From b0fb1988299ec690bcfe97f159d8f6a59a578343 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 02:58:27 -0700 Subject: [PATCH 403/977] Add Software Update banner to admin home feeds --- .../Controllers/SoftwareUpdateController.php | 21 ++++++ .../Internal/SoftwareUpdateService.php | 68 ++++++++++++++++++ config/instance.php | 6 +- resources/assets/components/Home.vue | 71 ++++++++++++++++++- routes/web-api.php | 6 ++ 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/SoftwareUpdateController.php create mode 100644 app/Services/Internal/SoftwareUpdateService.php diff --git a/app/Http/Controllers/SoftwareUpdateController.php b/app/Http/Controllers/SoftwareUpdateController.php new file mode 100644 index 000000000..e29359830 --- /dev/null +++ b/app/Http/Controllers/SoftwareUpdateController.php @@ -0,0 +1,21 @@ +middleware('auth'); + $this->middleware('admin'); + } + + public function getSoftwareUpdateCheck(Request $request) + { + $res = SoftwareUpdateService::get(); + return $res; + } +} diff --git a/app/Services/Internal/SoftwareUpdateService.php b/app/Services/Internal/SoftwareUpdateService.php new file mode 100644 index 000000000..40aaf867e --- /dev/null +++ b/app/Services/Internal/SoftwareUpdateService.php @@ -0,0 +1,68 @@ + $curVersion, + 'latest' => [ + 'version' => null, + 'published_at' => null, + 'url' => null, + ], + 'running_latest' => $hideWarning ? true : null + ]; + } + + return [ + 'current' => $curVersion, + 'latest' => [ + 'version' => $versions['latest']['version'], + 'published_at' => $versions['latest']['published_at'], + 'url' => $versions['latest']['url'], + ], + 'running_latest' => strval($versions['latest']['version']) === strval($curVersion) + ]; + } + + public static function fetchLatest() + { + try { + $res = Http::withOptions(['allow_redirects' => false]) + ->timeout(5) + ->connectTimeout(5) + ->retry(2, 500) + ->get('https://versions.pixelfed.org/versions.json'); + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + if(!$res->ok()) { + return; + } + + return $res->json(); + } +} diff --git a/config/instance.php b/config/instance.php index 7d5463055..d1566da4a 100644 --- a/config/instance.php +++ b/config/instance.php @@ -140,5 +140,9 @@ return [ 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 1), 'auto_verify_email' => true, ], - ] + ], + + 'software-update' => [ + 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false) + ], ]; diff --git a/resources/assets/components/Home.vue b/resources/assets/components/Home.vue index c75ed0a13..dac188281 100644 --- a/resources/assets/components/Home.vue +++ b/resources/assets/components/Home.vue @@ -9,6 +9,48 @@
+ + + @@ -59,7 +101,6 @@ "rightbar": Rightbar, "story-carousel": StoryCarousel, }, - data() { return { isLoaded: false, @@ -67,7 +108,10 @@ recommended: [], trending: [], storiesEnabled: false, - shouldRefresh: false + shouldRefresh: false, + showUpdateWarning: false, + showUpdateConnectionWarning: false, + updateInfo: undefined, } }, @@ -84,10 +128,33 @@ this.profile = window._sharedData.user; this.isLoaded = true; this.storiesEnabled = window.App?.config?.features?.hasOwnProperty('stories') ? window.App.config.features.stories : false; + + if(this.profile.is_admin) { + this.softwareUpdateCheck(); + } }, updateProfile(delta) { this.profile = Object.assign(this.profile, delta); + }, + + softwareUpdateCheck() { + axios.get('/api/web-admin/software-update/check') + .then(res => { + if(!res || !res.data || !res.data.hasOwnProperty('running_latest') || res.data.running_latest) { + return; + } + if(res.data.running_latest === null) { + this.updateInfo = res.data; + this.showUpdateConnectionWarning = true; + return; + } + this.updateInfo = res.data; + this.showUpdateWarning = !res.data.running_latest; + }) + .catch(err => { + this.showUpdateConnectionWarning = true; + }) } } } diff --git a/routes/web-api.php b/routes/web-api.php index f51762439..e19c36b6c 100644 --- a/routes/web-api.php +++ b/routes/web-api.php @@ -1,5 +1,7 @@ middleware(['validemail', 'twofactor', 'localization'])->group(function () { Route::group(['prefix' => 'api'], function () { Route::get('search', 'SearchController@searchAPI'); @@ -7,6 +9,10 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('v1/polls/{id}', 'PollController@getPoll'); Route::post('v1/polls/{id}/votes', 'PollController@vote'); + Route::group(['prefix' => 'web-admin'], function() { + Route::get('software-update/check', [SoftwareUpdateController::class, 'getSoftwareUpdateCheck']); + }); + Route::group(['prefix' => 'compose'], function() { Route::group(['prefix' => 'v0'], function() { Route::post('/media/upload', 'ComposeController@mediaUpload'); From 56b736c325fcc960e68952dca1c5ea3582ecc0ca Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 03:00:51 -0700 Subject: [PATCH 404/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6220584..00a1b6677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features - Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) +- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988)) ### Updated From f07843a0f2424f574271e5b33be429ce9fd41e80 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 03:01:51 -0700 Subject: [PATCH 405/977] Update compiled assets --- public/js/home.chunk.88eeebf6c53d4dca.js | Bin 0 -> 243768 bytes ...ome.chunk.88eeebf6c53d4dca.js.LICENSE.txt} | 0 public/js/home.chunk.ada2cbf0ec3271bd.js | Bin 240067 -> 0 bytes public/js/manifest.js | Bin 4006 -> 4006 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/js/home.chunk.88eeebf6c53d4dca.js rename public/js/{home.chunk.ada2cbf0ec3271bd.js.LICENSE.txt => home.chunk.88eeebf6c53d4dca.js.LICENSE.txt} (100%) delete mode 100644 public/js/home.chunk.ada2cbf0ec3271bd.js diff --git a/public/js/home.chunk.88eeebf6c53d4dca.js b/public/js/home.chunk.88eeebf6c53d4dca.js new file mode 100644 index 0000000000000000000000000000000000000000..1f6666262bbfa8b8cd1b892145acde29e1e0fa55 GIT binary patch literal 243768 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�?aEg_g#yJDIs#5o~V@S>xW9zxouSQeu&r5RFc(dv7IkHMPkK`Ie|UlxXWi z1=u`V(v$P=xU^Fn@#kIt=rt#JATB{p3?7UmWvi$jfH_v}{qta>hkfu-?0GDVUu zqLz-pE8nTX3@ibWe~V}!!BJ`+a1(J6DCsyCuJR|UX`WcK*(-g}9)zVkQjrukUUlJ$ znc8o?1y%5iZia$(9$hhA#^ly_CuZJJdEO9Ytz-ahj=|ub91<^ zB?L->7A)kG3$56cuQ`xygZuZkG+HAcq48CJ<6dFAEc~)JPJX2+BITkacl()?SLbi^ zSe%E4_NfMkUQdb?thCrriBPcSw17JgMbYSsOs9PFv(p?ox1UK8(TO9FLnthjAic6q z11dbxsQ&nosr~pLPWkZ{Ok{VDtog1}0V-OLAFKGKo5>ye@~4O#HR6R1<|f*p?F7>; zDmyoM8#_fENyLhVzTpKbeRF--PwUe$p)3;Wkoo2p_lH@JUVC&HNG-Skhn!01bjkrZ zuK%FG$PI%ZH- zO5HG;3kCM19j-7exk)>w$X54GMT}P;3`d`G=_$YHYS$%FV9?`e2V5p7=7G zll$q)yMhvA2n;5^{xc7s`{tKDevqSoh2zIjKhyDZVEW|;5Z;GhZ~)yrepB1kaKNrV zf?sNb>JK4z#?=8d4pKOY!pO|Mtb7K^>yjXgvK-zhW(jEdn}tXj86$Vf2j4G$ zCK4Ayy}FmjX)+nJ;wP&>x#%D7hrwy937u(h#akeV_pLy>f%};@;I6b@5SuiF*`}2! zdQnt!aa~)VvXq*@Gv`*SUk-tM)jg-1a{$Aiq^1tgUw%5CV6Iw-^9q41*c=Grl4G9Z zM_q<_>0~pzp}V!S^YDgE8sOSuQa1`9GhU5-#zSC#B$jGe6pLf^ z4_VEwe>mDROJ>rJn7z@x)B&Xo(I5gc^v)Md=U5;&4KYfN>h0G*FD^0`cj+BZR;ash zw(RZj#rmgFu%Y?pXg2AcjeGZ7{9|(B$H{;pM=^At%Xqm&jjBuL4I+8bs5mTn(|niw z`f=OHf{G#G$a zE_$OFMOgvle4;kPV6F(E!>1D#Q4424El@(kyO(GL$;3=XjCO6km0`LXVDBDt7+PAd z0>)atd7g*C3)+^8xetpZ)WHrW(;LEW+(W62q&4Oq;_QyzIUqX3Sg=vN?q~t`GT`+) z#L3T)j%9nUpM;KB0M~dCp94Q-D+l!(I0il@A6oxdVbEjj4IN@g?P3{7l=?u4kQ1o} za+cX8)HarL^_qELgG(*VgR;f(WYk8O&HK|B)p*lB60@d*+1iDI4B(GYRC@0$0N73-Ao?v25r(<@F*xlr`-L^lFU-fIlF(_eUm5SPO9<+{9CbL!W zxB7;ph%gkgi0HrILCj&Ej@Vk@Cc`Rp*Fs-6sCDC+8W6U&4GPyTEq~$m-w-Gt0MaN^ z*xh-w{qTlOia+fLeiCbSM7~E#gA66O#B&={H%}wR6grQdaY-QBk$>zCnNE4D8qpta)mo|uhHZ0ER&GV z#Y&Q!OTdWCa#A@`GJB)hx1N4+G|o|Du>KM+D8$Iv?i!+!oK&H^QU*np_r_xcTH`VK zSC>p@AFl8pEYE)FA4pn)4x^(k&?|&Hk7wM~g7>ByiPz9&dI>w%q$p&yN&Zst)@!-e zjh3^MlSx8+q7N8ESl=|~X4Z9hH>{s)}gh}Z=u zffS(x-`3kRY2daSY2uMW1n5qu2|?$?eE#mj#V!u|-UydZkh+I3)zD?njR9MfeJjEJ z2dd0B#D2=825$bD3lk3>@9aMKHHC>!qGV7H4C*%_VrR3X6zHkg0I3OinG5h@6%EG_ zDO6sB=CFGcPE{DLW*jLH?F5fT(Lg9G{|8Tw5u|81x+!vgYrgDlx8_HX8IEQjdN_NK zeMa{8|BNnUSB((4#?3E)&xy=pmg?Hb$> zn@1w+Rz7zO)e@b!m~6&>Cv`#&-mNTFX4IRo0;$cE@u>KSN{!gD13_q~{)R%;VNG6Ccx)}iG6AClw0(HYaClt_t#*YlL;cpk^K1OPK1d+Qgn!B7kb)n1PVUFa5UEv@*@~XB6*jdm^F``Y<$>;e6<&oegU^bFHM;ol%CYS0-+&k{h zX(!udHK0Wg1}7B#5f&;~D*2cFWqc%A_Fg=r$>MaoOp=8Mm`$XTM1Xx3vW)-9kSFyg zgfgg<1c_RNC1f6zHQNf&557Q(UmlRk0?RcWr;D>!GU^*d{v*@9I5GrJk(`-)see^+ zFr8A;@k=gppHP(&`;=q(lM@q3QJ|)G-`G-(iNXsD&5uYKFuR?ep`B)N3Sxw?&o)RC z-i!iM69zKIj6Q6T4sGGiZKswEwC5B1#r@6PdcnV~Is9NjOE zJc`y8pC_pd%nTl8<%C_!4Pa89)Z~(?oAv>hH{k4wn3&x$h4hd(ombMkze$X%ELoC< z8khC=WqDoU;FWdZajcA{&NbhmCh{G^e>t6kSXG3PLid)R=SylVkIPtH*-#YLg7xpB zbq0H$Cw{CoHi-TW8%=BdXaY-b?lOVBnIS$>%G0_Yh=ts(i19CEJBlK&u}+k-LI+sD zM4|GFmfO!(M-bwmaBkh-1n65Vgjll$Um6DVUKC((jLnv4GA|)ei8N$ghw}4NH2+$Q z^A7oo1nj&AQ@GcaKseW1*~J%Bw5x|&{=aQ*f>6Oy`~i%RE&=LMVuv*mqlSbAM7vfi zGir!w{e|fb1-GfFBgLqsMiiZI=sV)L4&j?97x(Z=1C>j9& zxqF-ETExTOl1T04AkWtWaa~n}SI7uPY4eWfY$ajIEvrcKLBpL)ynYWAorqX!*mIPc zrmd>bCz$JEnsXHMZp%DqCO61(`Xbq!AtAR83q_fLD1iGV{Q*jmXN*1xjT`wJS(D3) z3l|nW8LuX&*12GZms|zYMdmMXrM1-Jx3M&qphZKeaAD=e_(w{c_IbA_q+~ZhGrBCO z8m*hSd&b;f6|DQB3qSzLg|LQoEAS7YxbXrZ>zYJZK0RM_9qAcDk#O=Q3w7OEN)LWk zWC9hW#ia>{a^^4$h4jCop$xoQPC6fKH}jDidjqWzYNS^K4RvJhL>_k%Xu7yjkq)vm z6h8}uO+-8eN6$<-poXkDskRqk4yyiu@rfg9uygB4+eB|?ZQ?vdZow4m=Bd+lZ26j% z3wJTI7G)cok!p!W{s!*_Bn!kS;g9@Bpd^RBChzyQV6O>n57?2K zH^n@h+sgITZN5rLP`5p=kf0FGw>XG9b`VWKTz?K3R+I7g{-(~MK}r(D{y2te zI$Vn$G>UwR0{bP7A;=+=^E^sp2bJTuzHF36$@8QJea&Bx%mz1B(JZFS_>&bg0gd*{R8I-* zDOjU@u@4Of(s?N~k-cyepNLsTtI?_rA<5*j>beV8TJ+%+B^S`sstd4%Ow??)=*?$R zd`tz_F`8XKMbx!?iyvKtOr6Zo@VR)kYL)kJ2q*P=namkyY}>Tn&3VULe4n4L)LHflLNL33q3wm;R@@U`J(MQVvRw0WHI635K>-b7l~4zxa!aGy^q5 zg*&khJ@iP<8B^7ijWMQDQyV6KgaY3QlbNe=@{OZ~rynqKR-+1V6`)np0QQ|5_-2R_ zLw&qKwAp_s^gtxg=&81O3t^@3tZ@~r^$!%oz18bbaSh;r2=S9pF7KJ$qH7k_W5Nj* z9kmiDq5D+!6hJGeQT1*Lx(d*+844H+mMNZ?TLNW!yAT6!Kg<4-H9#r(t3yn+_FMvv z{5a^|Rn?Ii5Uod4!||J#yvi^XTezAmDX=%G__dVqDag)37=qgexl%>X^%L{vZ*!ck zo8l6#qrfI#YsrFI#&`;4s)gz4C8a0cY)H+*C9mu}-r9WpYf3WzjO3M4Y^M?e#F?s% zq3DowVVwE#X8wwkK&mkuI8&PG<|Gj3ktnz^cQ)lH1w?{=i6EK-WlYXaHeebD`_KYw z2)=mNgOv;U8a>p_L?F?0HbRGEln2S)Bfhe1T&U1hu?~n79gvfpC%$2|5!SzG;a|x!C1n$ywi?oyr_FfLr0Kc(1S%$N zszbWzJtgII5pJ8$zr1`{fO_J5hg`qI)@d#QNYI{D z9$mWTS`grnDNc7k3q0lUG=b!ogR&tZEeb&?KyaYsk2+A6uU9}RIeE7PrB7Ycl&aka zyN|wbaAghZ`;@Ands3r-x|S{ANW;8;gClLM;S>a*NTvd+OGYE|uL@`X-~aRf7JYbb z+Wmam{qtl3n^Do2c@wi5J_VtrIB^jm*u@Zsahr#clh(PoNQ;6`v2H$O)Xy_^jAT^) zcj^&$@pe@Sr^1dxa1^vN%)^C~)_oVv5P#0TM>|V=`a9(ORP2n){xmtCTzn_lWXYIJ zW9`QsQN(i36^BL6uFjK(B{HMyS^L9F1^hNbtuH#um08;X-5ft~`I>e5lA^g+#5 z1hIg0+SdWaGQ3N>pizN=99ICCtv;}|k!KAInoqI<47f-7dg zcs0L8_SPWN7{qeXrZD6{@1ZMmU3I7ML6^bEN2%P1&kPh3Wj!caII(({9VNe@fK|VV zmSrhfgD#6s_aO~UZ?vO5*9%H!Zf|r;8m(^5EcKk`IaN<8qHYZ$*G#=f;a25c{$&BW zj2I7oely%^kTO9gwvocc{(Xo^H+(z-SY~r1e<4Rn?T~!p1t0LzJe$R$l}%8qPt&e^ zTIM8%FA;&*Y%+*4xQMuBL1j&P(~C&LsVVol7BQ+n!uP3u5^K}nb?YD~f zs#_uqN-7*g-Q%EEvpdBl;;$jV5=ZO#02h|wIsPklr2~e-(v88KQsYE59!<&efG_kt z>|Q-WO5=WU7Np7JhYL22I_R>1ZWq;wwjMp)+(gIJ`;TxqY+5#W4s1mp1Tpc`B= zefNDbQ1hstpVz0Z3oZ7lyXMKh#sb%xFViIwnH<+puK8pl`B+8zTH!k51W6m|oV%-d zNTC(4=1@da2V0?m-7j}94=S1jP5rEnobJ&LL!2f%+ob z73tv=txU8b#lKFT5vgH>Y%FVhgFi~*gM@##G`oeIZq_WmG*n#sOjw`_E$~pi zEj|SN&?HBAm2rN56bND+Px;APv=>um$rg6@r@i#?~k27@;7G9@jzI`pZw1El8VAx^VJmf-?q>-B|Alp=?b2qtEV6D zCNirBP0zQvm*ZbW9P=<@*-Zjha*^~hNJpnP&#|RM(-J#tE}XoH#mD$Q^|{uyECv%n zjyv3i?3Yx6JciA)yp&2p!zx&-gB{%m8X|PUvzFp6hREU1Y6b3gRKMBUkMb9+;)0}w z@l3)ZA~adji_9=tvxmW0%o%D0<~1ZESsAF%w13+%wq*=bbB#bi)>X|AL?vlfbbq#T(soS@y+r4umId(I$$G!>6V=tES8%WwB=E0ch(ePWPTxhQI#?MFnImSL zW@!BmgTTomg>gXs>9J}$+%_oU!IZDb>I_21Nj#Uj9qt*FJHw01gjL3o4@s`MGo_C#YZS!gXk9T zwNbx=D~G4Q>XNRkQd4WQEC1>5c5nj9SSqhOfws&)tdtDTY-^5;VGVFTKZWb9k+;1^ z@q}4Gd0j<&@!>um=lGBpQ>^w{Nj9zZ(}bn~FAopm6&d-o2kSeVjNR))$* zoY(vNrTmW2c1fDo>QNAj@OD`2ZS9e)75|toEoG-9)m&*$Q0Yp#V#?N46As!gdeIF*hP{bd=AM61Ri05x8H#q2;gP0TYonT-*wMdO%6v0`I|GO568kQ<|tu} z$j9OWkrRYQA{ojV>(e-RBT4%i;M_{FP82;uRKo1cYw$|Gec?8r08TG26k9Ochr7so zQDIi=w4gK;=Hd+WHX(0-cbdtsY={Sr6%MW!zM!a^XJJ5@{9Ow+I`cRZq@ z>~CAyBSWm^RNw+7$qa>up>s@{; z$NP~B<25ga1j0hcT1y|b9_CY3_8l*3K2n|lhTkPK8$%Z7zx5?KO6Mnai}*(v!q1_Z^! zbs(5cCl@X29G&-L0)Shy<3dxI>%dY53J{qoAXRgGjv0kby9U$nR)y|Yj;U;Xam_;X zvrjb_szc0K z+C^(qoR2Y#(DjU7fU^m?s_1P9r4!%IfHrAo1!lVt6fQ8i3AOh=e)MGw8lNc&H=jWD zZn6Pvuuk7BJh%ndM!m1YeBau_uEax(eyx>W@`#ICwMS+E?MuwcWele6`X>M99pN`w z&$s5^VWp@+B)=N?laJByC*l)%(M@(mb>|uiv8M2rdBL>Q`JKv0v@$4nfCV)_*(+%WlG3OCh_bn~VGe9c`YY~RCNk;fz!b$0^H`a_eP z!9A@2lqiM9Xz7Lz-x%yp#B|*JmkkauYqg9D;^#%p|QvQ&D9;_x#d=&a|edJWhc%7h|-qs@KA z(yRja>w?GeblvNl`K5R8q0W+y@>TpdBtwcX2ky+;>2Lz`3>YAeP-WWq&(U1y>7=D* zr(`SY6Oto=R_F61oqlM7J5SL3?0dv}gZwd04wxmR)S44F|F8e?zwG|!&nlEv@Vq}? zT`%r_R3U=YZFKaz+-f<(B^0B`)OtZI+NPc6*2_Y@ffb@1jDA=E5}-=l4#?JdvkNDR~0Y0Xgm~= zVGg755^RS*vX+Ztls*dj$YVxsZ6ZD!5^CVC(JyWXqr)TsbRiS0E%y)v&f4jU3W*dhr# zGJXQ#K(3y@$$X$^i}k5cGTp{U{DiewT~;O7p7>WJtFWKrf4+#B@6y# zLU33oK}oajHsXWVgCVhsbuCYZAiM?$t>z~YiOcC2z{Gy_-)9U24lKWc!PUSl5uwLz zh(3(wM6FT(7|nkGGsR=cY3?3s!|4@}P}lIJ#a2Mo6M?x0k|aTyM@CJkQ7}9@x%l;0 z1h@P(LVsLyjXYS?6b)$Yg49+~+9yK$Ahd;Cu%}O3frByS<$%JXaEhR585S^}ntOJ4 z!a2zikYO(+;s&Yzk!53BDy3Eh0c2c3D*6)PC)mEiu>WHXQQe@zH~uW#eYKR9r65z= zS}) zyrXRwhpi#WV$33p0hdQJ3tS4mZf`&vFN<5PipAgycY9G9-IZWL)5&Vj>}66PYxwd7T6r>ctVn<7&I4J6}aVR#wx)k0GQK-?e78gY2ITR`@=G# zjfC~TlE!3d@GqO^Nn?TKq1B708J#g}QFa}B%BCrrZe=l7-v^yk2-~o~K%WSDsi51n zeqqa6;S0a!m1-jBSQRa6MOBBEr+ByF+J(v^nGb4^rVt3~29gXBptKnBWNX{$`&Xu2 zi35FRe+uEA_x@^fI^UV+*c0oi;pXHhqPZHYbbpL?wU}O+Ligc#9p(v?8UBJk849{$ zJRLyD8SY?LIS+6nA1-=B%FM$x~BpS6*!jNZBT!(jK(x%z{&5l)iz}kEu1`@fK#)z zL!G99!+jBIsUegu<;q@9hONk{W{d~u>oBGP!zIv9csEHNuvSstLz2fJ4Qo`r+Gf!n z_u8$GS*FCbmydPU2#~JCT7v6D6AUX(i9`1x99hfZKV>bD1=iY@k$>+ zi#UuT?k0UIjl+IK_eJ)+7Oj@werf478zfKV0dgt)$Ws3k$ZSf(km+9zRb<2EE~hEI z#{Q*FHIF@d@VEk1H_bos)o(fq6D@d2Hi8KMdJM5i-ZIkSmt6HD1?9^7`6y^mIt?8qb z_KBe;q^vC=*h>v=V?wV2V3X&2Cjf)W1&)fmG8#WB7a{ss@as?KcXcjGHjWD#Sav@D z|G-;2y~uHVpuAV1t@DRv19Tw_ou`iRbd04e!c!Jz7x>PQD{ip5F$R|N>ZN}Ca@;G` zf1)Ts0Zdx=p9Qyx3($2Xvnn?Kt7yqImTiAi^_!wJl?%U-6@b$gu*a8i0IHo|`d|3a zqbNyT0lMA!kI7;s=0qGIP95E>L}*yblu9}CJs8vV9Gn^dTh4zag(R0$RE+{MKWKf@ z4_8!EbFZO2tX-ZId;9vKc6iWfe&nVRF-O6jwqe3CjQkq$=l-5?B-ot>6nc zA*Y3V5zYdmOs)_YjTSW%Ed>8!Akv=@%Fy^9&2;+gCH&lJe~4V1k1iTF(v~EdHBd+3 z&DlUA>2Jo#D0LST-b#IzC>A^=$DzA6>LKPnn}!#WJ?U-P9(dKk448vk zq-j3$`n-DSpK)Q9K2Yntn;~Rc=;Tg$)YF@)$bgPvK~W*}Us;Qb8D&~8C9e=WLL*=R z#l&uqv6{T&Et!0A9*;a$;0e!KYXGb}alPv|Joi_Z5aZ=$O@AQk1@MkiGbYG_6$ptd zH>ldrKSwTv`vd^w8vw^g6NKuY-g%e~c_fHODz`pz9Kfc~gO@OipQh6>I}Z9)6ym4C zUngBLFPrk2zg$u|d?}w<$`(Y_XoJFyS;UWG2(HN_Ah=*XuDuu*ip`u-yGY*BJT}#A zwOodtWuffY%X3w8_hYV@x1_rgJ6kQ-&1AbJbN82AG@~jFP8-)Z30YK*Ss~xdTO=Ee z9Sxr}-66~^^;3xy6z*F@bbrQ$sYh!SXTsy$X*5imNv`g@_)6_R>7`O9VDyBJ=Hl`L zmY(j-2*;gYk0vNLYf3m)!jCLNVeJznA+XlEY=zYnV=lt^tj@v@)OS_wGD%rX9uSq? z1?E8W)0dpnL%#5hk37YeeHEHGk2hM24kCvp+cbTU`t8tgIT!9JLS57Jr*03M$VKNo*+B=<}!N z&{X=5jIG>xSZRA1l`#nzQLU<#aH>V@J2>yM-g?6~XutLoK$luLCx05eSy>5@g2W4$ z-Q!MlmRy~oa+g%-EMsxvdK7AV`R|!s6bTk*){i@WWQMIZeq0|NytgwSV;T@jsQ7@! z4y88&xu|cyn!mF5p??g%RHwSLv5 zFK%+L+MRsaK*iEtt3p-*HUB&zr>syA!E=$-nj))Hj#CFqZgs3T)*!mU^brf){uc@xV{ z?9W%H$*^EnBJC4ipcBeR47SmP))e!`@Dw8X9;dos%+UiKDkk~kEQ=67&N6*_oO9=O zy};EKy9We;D>NXC#;FSG2ubY(ZI+}5nrv~4KtG6ErvAg=j!8v95r@ogx#|-^9S0lc z)jdTVVhXkL43!y3*rPL>1jY)^?3qx%!*NH3)-XZW1g-53M8oxabjur$hLz8fbN++H z5>uB^KO!Ea2QacUQEEk#;`k*oGc~BX7b6tSXjYn)LrJIe%R{(ev=(_d@)Gfx@tZH} zkeT|Ll^rs7$BR3J6dB>m=~lCbz3q`)lxSGU-gTFl@XXx$4t08*S=)?pld2zfdW^ih z)Yx^DZoJ_dXqU6tsZUJO`GEu>>D37Abam4Yv_+46g!Z;DFiEX^Fc zGp`8DQ?i3l@}qY|L#oF#98}Tak}Y_QQz88l3eSZ6`h-q|j3;^?bRMLn$Z3!t*E$RO zxJNz-@{+RWz+Sw`DbRaA-Wibh@8SgPP$YPKIZFQC?;9V0br`<)z8ryjzr`57_j73? z_wIR2hVQ+m455dYuS@Fw-5-b8&r)GuNbXsK{W9m}bnr_pG7X6~y&fe)(SFhNEz-0d z1}c;$Q^q6?&J^uLx(RId0H^jiH76LCAv~s_swzSwR7RYwh!RXjuBg0(a)f_zlFy(g z^VUlG3sy3_X1UOm0{Y*xhc(^7Mzu$QInxpwt%yAfIJFg#Cx_O%SH>|+YhFChxt#jv zsCSY)M|;;R?I3`M1yAJ94gJ~pm&=3-6#(N_mUUgAkg=2kAugH9E*}Vbm|NOMi~40Z z#36ff+a1x0ylpm-M)A6Z!MyAncJ53K{Z?YGM8^79u1%Cz8Y&4%($JM3Z^=y^p%omY zGE4hGhU@B0c`>5gnA3}%%)keyH*;WZmo2rseZYZNY9K41sb09}`7xUw1rT(_=k@QL?YQ5fN8pLj1^5MRm^ z>@{C$hwI%QvMk7Er9a&tJ@m0VEk(P86}yYRot)1AWs{b9PiPXOt7~*Mnoa~Ry^J|$ zF8ru0+TVrw8Rc_|m&nON{QkGuH?zTwiEpg*8KwQp#^5kLit;U zHg=Yi6UhfP%4od4AM@s{TU+?5Un-J$v}T49-T8?v%}AeF<0rzA%|J}&S>~kD7L?#i z`d3}8L|e?V>DE_qr6yFPPIqU5YE0y%bUSjUr_IVUF>YHIl2uHZQ$3x5AEF7IBv|VQ zZReK$sD(OdT4cg5==eNE-D@<~Mc?LDyIpJddi#D~au*;`_q!RWZ+^N=@iZ$bnz-*T z_Dey0mRB~y)3?!TVf}-E)5n^*TEPC)VStNOtPW}rAZmn24nEShkB*v0Pa(^Xk)bp; zY7#hoM1rQ@F^V}q1A)Vv5AbxZI;t4eXyU` zb%TJxDXO@;`xb)>J{>O1IizVad^MgfSnmGK&E7BfZxd97ykcG=!i5}8wRS#9BQ|r5 zvyA9NV{G|E+gAN@J4FPtDP7XQK2U~M+e1mi$6_|&QnQh+n`Km|$>Lp-Oe*=hp~m+b zK=8t1tftX*CeCt+V&a*4o~SZgf0wYvD?smKwA3&)(y$n}dsSkFDu2=GQp^mch)m0`ks*187;_^;cnmEdrQ)b=vD8$>SE!u(K%UI9 zIsJmjx8cT{Epz31p-nl3z_*$!_+OOq`K0nfNk|lr_)5&V5{N(D&Zd((S-dWYt&N2o+S?}LeECCvoU}JmJrcRebIpX(GV}& zsM}PZu!d&qAgz_lztI?CHly5=Y8vRu{^E=0JkBsGp6i|S=fmWDa5G*s{&9Wr#fe;J z?s~zf*9m5xIe5WIH4;2$DL~xIsWbX!eDJoqWuZ0_JR=}4M26F6nnd$@>&vR?83~Ta zEF*$;d?*!>0D}dw=WyGm_v1DG!(RuTCq+}r>PjDoTgVw%!7W~+lkI-CWcz6TH~49a z%CzgRF900y3$p9CLg~sd>9l4C5R>LpfV^K?Rpi#DP?lbkm7vXGJFT0XC2(uj zbuSc4U@(z`nHnmA>InXg>5T&dsO>rBZeANXR=(=Fn#o#z0gMpLtiU7kn8>OWj&{YU z)?-&iMSk1eltJ|VtHkNW~-L)a><6F5?Dyz715hu4c%N# z0)~*&RaM=mUMfX|*q|F}q|hFJ0y1lF|4UVE`mPB<_-j^q3w2p(EMWQD>I%28B^QH{ z8?S3_zF4rCxz@<vZS|F$aLl2O~|Y4bJW&n{Hc=tqTV_5=5MiDe$M%JGl3efqB&{b{# zy1Wr?3+psl42FXR7~OcvUJzFL&!;ytqGxRpdaTO^1dU~^iATrh)T=j@XW;Pn+D-{UCdl~Tx?elmn*s4fBEVA?>G>qqAfX_ zBzv4_xP3WxcGr}Dfr_lVrPpm89`^SZwFD&l;*a-L9J9(C9rDf=)e|&|-b?mt^Tu#5 zVZ#`PA^Ll`Y-Eh1ALXlWCUg0A@UVYE+~??pQ=<0owR|7W?s?3gZfL2mY84L3RMKw`i`+8A@syv1L`(IaR`Fnv4DwbM&qV{#>v;ohj5d&&OTS1NG;5GMfzGfkKvp zs1{T@;hr`S!Kfl2P10Lt+|em{*a=ZKxGMVGe=+$3V&g7iuau3DY>5{L1e_zNfkv>u z{>oYGciSE|zyW9S;} z^A;Ar!-%H!zMPt@?Qfmpo72&5S5{~Cao1HBRW&A>h$#mFczQ6Q!hX$%YRN z{2`)^hbo8Fdrj|am9jaOnq@;y<&|~Bzig=wI(-_G>33B66qRJORNs{9beqRdKv@ml zUouz6T!RoXrNMS`;hPlvN)DE-!0xmJgFIbBSbk58iO{d@1};k!uBk~e84 zh_SQ9=xxyyjp$my^wmk zbMgufBuA)5qj$68=5Uka*?}vDB$F~HPEh*6)av{>RWAyMWf%N*&+!^G1Q{0R3b+A^*-eh0PvBJ-GJK#*JIRr9*;vHyd07CYj*_U>4HL*XuHC zO$_a>Ye(3Xm|xB2#ivQ})&E3hG!zn8EiDk-QoSb{%Ftc-&NTgPV3gy#Jw~-_$d$hl zr?={h6pvmw-ng058KiaKt%}<0{ZqQeUF~0+Z4Hb>f1q^yNocoQ7*+I9Aypq}oY9zK z7&WOl%1JVx?=>48K-0JpduV>UpJw*g1&CnxTK%ER_EKHQt0;>Qp+y7#Hf-EDaxEdG zG{#psF7MWXRq$gA<<0xpbop)eW!mKJMkU)Fy_$fgUennn9YuX571S}=+hgcU&>>-T^iHq#_{lJJ>a!-$uhy;C2cO1QoCWu<( zXaVQ&eKJ(_1YdfesJ&FLsfhHwfL8uB5#>M>8isTToHfy^P*=*Yxra59mtrpwMML)r zqjg~hD^;En?+Bd8S-vHD9mp%yo zHcB)`iJ}*r8@Q^>FOF9h7zt{4A~e&1mC$oeCc-Dh;dQl_wM0`MYb2Ym-i`r^&HD$f z+Ng^6E6cHwN^G?1m5QhL4l^*9)@vu~>BVVj?*;;~PF=R%?{smzAN$px5G||FB&%Ev z?!^2cQot>sHAr9stTS9^3$N6ft%Js-hnLln)7jW{eesO6V*QoWj5#Z-8G~yGuP^c# zpw*fhVAfHDE=$EAkho(Zqq>Hgsd0&@mlT7!+H3oA9_y4NF(E6D2Hc`5MTIsKas(#R zMFoyO3MrYpe?bW|$8-J~vn`}x|8a11TEj6si~vdsC_!vW^V4XUsV-wTg_5_xHsIcE z;v}FuVzs@`fsM}0NtReU&lmTA_XcwE!|^u-JuG8zp&!K`_qOVZs9I4uT6E9?>A9{> z;}5+^LHWQ7zqai6=b+1*K)eC+}N{p+zrMp;8$~W z;;2BLr%pd*IqF@q(Nhn97c#3anNHIL!@4Hk;OZ?%9Uk3C=C)}6Lf)(b6~`*I68)4O8F@Y1@P3!2W}Vr#XidZ!lTTL* zHB-%7RlPB6EMQh3#{gs%p-K_DF1W{G;s z_hLzm=C5u}!Q2@=m$bz+nIx48UJSc$&=RSt%x6_30H4h5w^#~6Qhvyk8$l5x-7{1n z)tywI0G%V@GV4*TPNot=!=i!^*#|O3)asM;=gi}_G?>C99*EO4iKmzaW!a)JT|93^ z-F)RC_cNB7N{U><1}$$rgFg+BpYLOMOm*ED4=Q{! z;=O<^ZSZE`qiO6bEBqb_goCx-+uiiN9sj*rMaWQ49#|1O#6C+^-vy3;dJ4mx5N?>G z_bj*8dUq+E7w$vkd@F>}x*+0eNDk>2c}`mX+2|bwrFAn|-=cXd22C9!Lt&F8=5l+J zP#Xm<$+I1%5({ERz4_@?B2r~eQo-c-x-H6&pO2F9ka~fJocx5$@TY=mp>Rne!^I|O zxQ@Sou0s->PtO4Qw6Z}|GZKc9XDhf(qq7V$f2{*03w-fO&|4>c*+9o&x6hzR5+_9+nc zZ20%z0JKMbgg%9gOIEg8b63k4dE{0*MlX}`wV0%S{YQzJDM&-f%#yk=4*ZMoq1?pa zNn5)Kwn~zWw=rMXbM4dYT-wk7`sy2WsmeNtBZtvA-USaLdT7?~YR>2>h;DYp#Qw9l zi1|3VYI&5$Xp~9eKBK@A6>AEgI~{-iZiH+{3Y=_;v!N2_U?@s<&X9w<9s~h1J96IA zE6fQG%pK!;l%Dj$UOc{nvj`Xb9IY0`i9fG--uUUPf===8Qm;Hz1W^(~>qffB3vMD6 zsJbqaLRLQq{$513CGeG<(NZo-6tT^LDaT4969lTL;xIW`y4ejc^kX2=4f4VuOSxi( z^R+Xa@2lakGMH49!4JlE+nyUYR^{DbHUUq`a6~{0moPkk?8@~vWLy4&qP#1ln;Z++ zoO9WO<*hP+Taj-KXYM!2+ORC-Bd(=7{$#@fPZ2As9^<8PxCBh|HqjDRWok%`n`C&! zOiG(lX*1Q#X-!|Dx~fX<2$$Q$WAjUz2t02HdQ#xF9-hdIBFix=#tVssR%k_NAWu*K z0km%YoE}P~><~@lSfwUzFuBn{Xqc&yhOKa6YV=#(`a6SzqTDkeIh35nkf&#mymm=al>`G{jpWx z%XhWmxZj|VTuKfu5NxGgWT4Tf6I4LDGx;HQ^X^>|grrY7s<-tFYT4gYVlTT18pHXX zHs0S@Rt_$JSpxfaq^1}~UxY^*prXz@KtC4s8x-BjsZKkXH0nyETCnVdS^0Wv$x8A(6i_x zT>HBE_pSA0nPZViipruKZFyze#G0*2L)pBAxKiJ=nu0zhFVzCs7Rs4lMz4M0ohqNQ z@R?r&CDcJ%VonRp6|43}9UBo@XhtMTtRPFh!^_WFcII`zlh?QMxoT@5|7n-aT^p}+Ta@W9# z!YUxB16LRC2rH1_yC3DNf1^)F$!ab^75q4zXne&^Bs>9bKkZmNK5oB4$Y%bxcN1h| zB6nbM2Z4)XZ3UVI4OiphvXDPSJvtWWm5n6WI$O&9R&UNST6H94G$A*9TIHakTH2^H zd&L>b%PeMh8B)t{`Eohtm3dN0#R%oJmX^>KN#AVsH z7|&#%OINa3oQC!A0^?tb2n*qKu_^%J0IbEQ3DX5y_Z8aLI8TU3iG}GsYl_D2AlLot zyKjeZdTL8s`&gg{C7wcU$+L#7C(tZTx+D@cwBk(DTfzs1g$kC}k_(DhO5qtiaY^~h zYs87AbOG}fvl@@TLw5LqEvRidJ+4o4l2v-m7DtQ;(H5b@pe(K_eQNwoY{v)9%_)pZ zuILk7Ilc4x2n8?!(Fa&d_M>jJ$~IeYAWIG?S!&7;z&8{z2;v%Ta31wIJ%MAGJ!+OH zO!|%{-k7AF&}MUBIa|S%Sy~%-5&Y^0>m9QIYN5vrqW+a6#C#>CKwzg?w-OvGmdSMS zcUTPM(ax9=Il=`MuTWN$vseWsbCEt>cu;tpZoE2>aILX#*5G=2{f0V#`F%9pa_a5QtI+di+QkCr2YmiBAll#yjH8gkNQXL#uU21H}+35 za7NDXztHH;X@Wux2Gb#6kF0zKy2t<=;iPjLHTG!?^POt^^7#jpPK{kMP3 zQ$%b4&>Bp^nH=Wj|Nb97qJV|x{9jVOcrm!T)`+QnnjNuGNX%ACxjr_iNO#{zl*_`(#ZYxFJ!i8dI$9 zdsn${Q@WxzXQSN;4?=#E=ue1lP4zQS>H3xi+l0|$C8g3<@;qM2THS?IwSzgw9sxhp z=yWt5E$-sPX^W-_DEBAQWl9j~y%4|RW}}j)(Fafsdog?qH$?mf%+%WvM`XfVJqnP| zIYU|Go0!V6Ic>mfR-ScltOW#U&Ejw!({r^5?Lv;>6KKd zwR%$pDwM=7&XZ(lmMKp}A9}Tv!D>(|d)i|wCyJ(7J5@US)vs~tcq#+t7~?T3RuArB zm#s^eLxvG6;G|yMmlcp#Is&iIc5kvQ7xu&>xHF(S>P0 zqq&SKh^@6SEk z+OwG@G+R&88@~DZ8w-*2t=kpaW;xQ;P8v=aqFO8h0s7<>^b7F?f$-9d&=75${Df%C zTnqtb8cHy_h;okG6HWS<9TQFZ;E-s{w6b(#ch2tbj!DP8RsBC1n;*NdI}Yb!&;fHx zLAxmi;{t~1N}^GC-;V;?aGcJfIQRsQNnjq!@_$W)9R>i?dnJuk6LlSP2#N&EzIyY-2WVgOAX;bSMnCrN`#tHbjLFDCt{AGWNJ;{y#zC>Fug@*L!cUf8#$J^_ z1WxK^XvPh7GWcd9obbh4-394l~! z!1zj5I1)4N>Boo2Kvm4?cme-mZ8muKSNZO??!Il9Xfr$!BjD=T?M}Wyt8EltF#4N? z34j`i?-q==xJs65Uq^ipBjbP0T8Kso?68+ymIXhj0>2v%qGpP=<8*e%}FHm(|* z_G>t7L~6Kl6=WL`!%8&aPi>$iMr8pmOB&AEY_v~ppk_tw#3Mwu8uul^PZ3w2+CaUN zrW)e(^s5wX*aqrT*C(sAP&;^_q#nsp&4@??tZbPcS4M}m);hF- zXrHE)I*1xcpSnH)bNeVO`>E>_5TIT<1DZHQ0G8AaB5fVvNP~~^X?K09{&mWNjun_# ztkq9PKeGlZcgfD}%@>**=!5oE18c_VT*e=Xpt?C4Z@$8x`Vkgx*w ziSEW3PdgJkZPCBBsTJvR#SJesh)^@Q9VDlCHkp@(VvI-Ibl1FG@$mNDVZuqTnS$Z@ z_hW(KUCTwLf!m>I14h|Iy6khYpsawhu_1|iqiRp9q)n{xzUQc+M8wVf`Q?;gdgoCh zQnT`dBD90BoEMUD<9`;6pMhl`)Z;7+xc2?;4YJ#~Tb9RLtT^6uwt?I=wft7fTI4G- z&)NxFO~0VzHc#Np(pYoEAzAEx)Znd38?1m?Vrz?KS8A60_ZwhvG_1IMrQ8ac@b4;nHvYfrjEOdD%@fa{RHjER___uj3sItV|9*fprDE^{y_UUgf(y z+e>)i688y`IR2#1qeE?PAF^tx?*MToIOBR4PluGB7|G>`} zndgBI25DtO)-83m|3FEjJ?2KI zi!8iBH9>C@u@|0>rl|N^>?FkRbP%q13e7hAYn4GrwN_P$XoE+gCPMpp1d$<9PM0tq^TZX=gzE<0JDB>Lr(*YZO)$V^lQ&9;y}3P<#q|5TA56D+sw5enwp@+U zNSE1oA)Zu-rAmR2gB8sgFec`dd*(-yEHccs#EQi~Wwe_QD_*=| zmy|93uvNzJ-&S}6HykH4o`eekd#@!9EPpY-ieC$BB+Kg>p}7SAi|gecYcQqv7h+^R zM6hsaa1qO=vei=11*Mc0idVT5|BV>EdrJbeCK(`Y@1!b=2+ zk&g7zU6UbJwsx4mT1;o7WKNxW>(L-nozHqcN#5m$!50Cj?-XutS>r_K)&dJG`C?g) zwB>k<{4iggi7r~P#mG5k{F6)Ti0r#nC{&F|RRVJ~ZkYH%7UyCgAi#>>fprqj5aIlXB7<*Mr67mpX-fn?)aNY`Mt1WJ_ z!tzjmiH%1myV8F}37_fg?GD%aVmv)X`qj?JH(hM=&T8Q-9?xY2txt`t zcUJPF{C$Q$5e$$IG|T`EHGQTTo=O1q z)>Lm+z8F)nsZg(zI!|2yWsXG$Hw0<50o9(O8iX53>Xk89M`gz@7Gy>CS&Zdd4je2Y z@q<>WGOn@$HFn*lOeVL<7`c?iYw00peHN^s#_v%(8l_E%_)oJ|yj9K0WaZ9~Hm?@Y ze6B>Nc{&R{#=wx%%Yi-4YGWS>5xgBu?jYaNGZ<3u-xuGd0||dh6`edk21Pubr40R0 z#va%$>7F9DciAFFnLs$Ml2lt}Wh0DvQJ=IjoH^jatn@2<+EkJPZzgBXL#NwroF1#5~j!_6jhl;z6KX`SeRucAOEq|fn=wzK!l?C|ye24;} zr&G2uGhySay{_HT%vrVxTs~Wshr|~tlJ)s93A6Ode2EajS@H&1$!~5(FHqZ275l5b z-Wuz2Iyig#(-fVxQNiP8j_TT@3zQGe2pz*6Q@(2Qeq#!-@hj6a%9d9!&*3CnSyVSB zu~n}L^^dP01R^0hY*l~%Ci>__7pvDeX0f_Au89YBd7mM^9!0o|(XMPwIQ4KcSb zg^n8wc7P+}IUAj&(O$E&mm=rg2=*eo zQm@$Lg(#!)E&*(c$^UlGbbT)u3xNHfKffNmN5BKc-lsE!+pKY0{Xy?zb<-cr6%m$* zgKnJL>W+pHAk``KlU99q?1F_Lh?S$d>fsRWh)41 z&g#?2SSv38xh7Qrev$`Q{$wj$QO`%juJl4g_(oK}#+pTrv!?#SQ=$1qA}>GK%O$}{ zb=i;cdYL?CAb>3k*V?nGw3{uODiiUZ>T4df`=i zd>NR8P)){ZlvRBd>@|#>Wd~IY9is*fNsn^u5%qVo!8O2Q84b9~I;!VD9n|k|tXc0L zr#)MP2J9{;SKvplcTs0)%zjKA`Q!Z<#a963a@qN_x2BpA?JUV+GX+A`qiajv_&Cf~ zJ1D&Ew7I;aobhV$kP|}UK3MDsfff;dp{rw-LI?Nm5&}1MJjY_u7AD_p{!1{Qsnh@D^XQl!Q2%`Q#{d(E2s(}0&S_}ty!uT@Zw zXRpepmp~9eCc9bw(tO<*BT_LZ!~;55^6m|bl~(tm1f*|#CHY?J?(x1xZ!gXwld`)7Ao1f)aUA%&ttuDF$-z z=7M*Y2Pw6E{(1HK5&`H!MSstwdP4O>CZvd$>agMzv~0z zhPb0MQi7!*xOUZ*)8EM#Tb(aF9WAV>6U<%E`61{HR#o-rSCy! z?dh213#rRtg1J*5-A=@*trrx6T^#8UwT{~5g5|K2?ck-hbUXjvHPGcB*Gn2E3E)!eGQ%dqxPki6g#KX7 z+N-^G?U~4SW<9ZUlv1jSLZ?*3^eFjGWtY*c{<+QK8RNa4FU$5}{w{82lS-U!mq{`e z4H-TrJ=bh%j!_8u?;*5~YWFjzqx}APf?kuFfGaV3`V^!^IRaXB)tdZ$nb+ylE}hvj zHf(ZNPQI4AYNK6(qXxa&j-LV!KB&btPbRdXZ;K2cQ#nQ(9XiiTp#)27_1nVLrbQy| zfUwO2D0A%UvYXd#vGQw05*r_@?F;&BC={8Ms1K#N*wBd!Hc z;G8!na!HRN%pek+l0XFy5w+mnd(Sg15eo*&@Q;{ptjZQOFmfqe&|Pza!sFG#lcS@N za8YuX)aT$Em!4w&0D;_Yw!0rFwQdb{EC3RPffKBFMA==Pfo3BkXf`(*4r!G~nM>sv z`i4(uivg1{3q`!}HmT`xEzT!l4v}H4QPV<2yT&W@y|HMSY`7K(Dz=;c8l)+P5+8yB z&~7>c_+J}ev5QKdVTiQ|SUS#_0U zBm&juM)N;kT`!=0SXzk2BL`TeiBzbAp%1o+hJo1tkons8ZMbH~5;_BpgT1NLBFQC+ zqPt*Sy%vz(a9gB}9g9|pP(|fx8JM%bM3_~Md%_vsHs0UtVbUk1XOcQZK4HQ?XN?Jx zJHKIQP=P#P@YL%hUc^=Q9c;DH3KtuM;L5Dm{8n&`67&V#B_z=S}K_yGFm@P@U9F-hLH*i7SXUjD{5eUiRvB?HdF)r9IkIj*u^Nb>YxClH94P zSz~^C;esa}L_Vyi3V$1|a^10JLl&CGlnq^kui8E%_lpybH?*lDk|3=+7?^g-bX*1l z@`O!S7>UW*U6bd<<2iSXeTewU#q{Ge%}&l0ua>IaEeR2-lCN3AKO$Mto1dN23aAf(*|J> z^o3z5_T}fF+rM(r(VU{K?Zi~9ay3`3n4>6fabN-I%IN#|rIqjb?WA7)1M3@Zn*Tz} z3d>?1>Y0nQN<1sK5nmAmr6hOtw73CpI!0TdwYszeFU-Jd?UW1+Eo=H!6tY0{*NJ7X z+LbXgvql+^C?PLcfJLVD69jFVyxW_*hS19F9V4*3pQoowAl+AC}LD?lg1 zt5a6J`q0bKi$*B_zCd{wG~r7q!3<9KH?d?=BkvSJv&s#c8MFLUl;FJsVP+Q-?vs}B zd8+LWxMMbMA&ggLy(raXS!u`^Htb`|jul_Q+d>NX7OkeE$Nwd(EL1Lme)Z=J(0Ao( zI!n+;@O%olnAns1{VH;|=y~{E3lFmf5FkLEOOr)qjlT){FQj&$2&z(dp#N!)t8fSl zoo~Cx^M)+x8_HNh?wmfOOD!&G@7OKf9Etn)k=}-6LSN%v&Bn`UBFdwF3%Z-E04tnM zw>W&Z37+ObK3_op8Vr$cmFkyX3%sf<{d)WK=@9Mg5Iy2vQI;Wh=y!ddHWXfF=>k(_ z=lZUB2}?z!VMU1CrHoR`4E?V6RF^- zC71C{sd0+*L+R7?yWS1}T9{v~Ct^vME$g^--UcsNR!%b>UU`za=(NDO=B!JpuFH7$ z4Dr%+56?<}6rKxrhNO*hh7K$!sBuJNa2M3|LQ`FcMO@vFq*F}=+sTG z5!OOqZ8mGeLoFbRR*cQadAW7OS%IMkeY@zj!=Wi0F$Gm%KltH$xSt6&7!d?8X9=y^ zJ>d?Jae@|<|9)$K4S2(_>~62NL8%BGp-_b;2L@;u8{yU_QV$Z<5)hbM>JW0_6FIA`1D7FxQAfiqlCqYhwX-#_EY#nxOCw1k5E7vH*1UVdgD8I{3D!(le@=-!G6&u7ejQer?-3yYBb;T>_*t3C(r~WcDsjexzl(4B0KuPdJ!ocin8Sk z5aKhJF9ec`dn*e1M{5uRMVp&EN=;S&g73M-_MbRju>>qmp)g<#Zxg^zuFv1_Q1=yO z^M`7GG<=T_Bm8xKGs0g`npg=vfP|t;^*8Tgaa+b8D?fRcqL~i8yzz_8kKX0*x!=UEg|L`N($D+F_qa#~4MYIi z%CJ2KK<%iH7QfS6^irMu2XoQ}O#Rm^{VmJs|+ps#-MqkIMo zYWf!XX+gUSz4U8yjlLc0hQt@vNQzHN@4>=pYj+SBl%lLzK!k{|Ye(2(CIf)%W@(dI z+j?LrQ}Fo#eZF@;P3+kydv zw}j^cui>88xe;cM%$)d*c_8&kJzLnDt7LXzG?O9cgXb+T9JxlSl$e3LF#d9;h4Hwd zE?h>)fY_ku{n!8e-+5I&_YkqCXtTv1LM!8Pa7*(-SgDv4;I5VFxjOZD$W(wZTdi~0 z>y)hXH`Ii_{@(tJKTaR^Pgs?6^kTl(>a~vd;c%EYCXm#7`+Iv%;a@m~fALG}HH7iW zpmBXOzucphKdesX`LN3&)S{W_km5_Z=qz|g9EnbuMX-`_vz_+XwV!H zD=vR|HmSA$+4=eTiOIrfxg?Cw&O7Jb^Mi+m>A{kJ5Ik%BW8~93AoZ>KdjAB>4H?(d z7lVh|!9lB4((|(Wb^W4YWd@uAOe;=3EcfABwIH@1im$G48T;qcvzs}#Q~oG7w2{~ADxa+b@%RG(;GiK?hZPq zM>?Jlyhse-U^wIC-Y{z+_N7a~l6D9;T-rJoLY5%|Yo27u6uBHBk>ZQ>-U3zscEDpf zgX&Y)eE$COO%vq{ZkzD{8Gd%SUI|W3B^gX+!Nz#?=pYz#HIdf=I;rlx0cMb zRdi0Yh#!k2EuB?#MD&tjYDn ztG#q`)<9M3$r6r!lH$L1vng&<_HJipE|g|-@hg|UT@!)9saGTcYyU*vA{^JPfZJ876kQ%Mdw{6RERmG?WsR;%|B^>G zSR0n}=zn&4K0G~7)_=w2Xgs8EXMBgQ%(JWA@1-3?OOoV+;qNIwI(t-1j_^{hx4R+X~l&?ja zMjOrQppDKdEi_N%^{8TbgwYuVR}X3nQ(2R^CFDni8GhkKelayQ+x%QVy~uL@#xW*y zVwg_9(Z#1qBxBBbp(OLWKEL|*r(RsEqFZC`ozfcMY>7+j`U2w1Z6AsrJ3XgYV)atTY zf>AHk5MOfU3!j%7Y@3fSu4uag@kecPap) zYJb^#$92i8Dh+jREWZa;7g(n-&*wcddRXz^1~h9%1gU-ot!kG(^+b(b<S#0W&{Tue+ zI2+89m{0BaxK*;U)?*N#V|!_QsA41Ebx54Vdj3EW#3)9!MGf)PjQd zQ24s&6Y3b$jpGbBBx}823N!VKc*JOeG6Dz}m3yY3fdhrd&Qji@;%mR;!-mQHY&N>4 z3vT5j{i7LCKT5t)7oh@UbOG;}&%DBE=yhf`0UHpXVyWe~?m|pek#NcKL&6fe#d}kFg#zIrgErxS{sMW?6&?*=*7QhJ* zXQyvrJkiX7%bWcLGtr0>F|d~WzT_wCM}zA>LZ-p~#x})U$;!y`KAjO90oy+l${?Ue z=IDLa0n6|8RzH=H7xC1LlKH)7{m?fNY7ks?&j8^rtoL}>a}l3uaJb@j-Ky30=O87| zoOfPJ?kYN5$08#V)Z<l%dW-Se?Wy`snBm!ij>mGGZN55?)FE1i zeBIRP^t72ESl7+EY^jQv$TNW;nE;}=^2s}_cY$Kk9UZ5+2xk}RXJdCuc#g1~n(&=M zl_svpoMhFPf|a?gYoqAIH$?OPoWtYeUi)Jij#XhH6jp3buojpjZvuCk?T_b}0?yW= zgIcFaH#r^*fL+XerBc!;D5_eELZ;u;Qyd4xhIMM?AL5%{LUmH|G?@jyeQI|Z^*ep0 zae6I(v~@`Zk2xYNM0~k*rd`!x@Iw(-11$eIk+BL4U?NSUH?Jgg?f6dsob5I#{e1|2ax#dyQVLRcjmoa2z~ThLsynE&txdGYt0g9glqI%8!z@L3ad zME5SeNfjT2(dv2bQ>&#L^Hgq$ztor~btV%A2Xu$HEImVM)X6`YI8xao=4o%f>CrgK z%*u1-h_Dg+&1!q_eWi7p-I z>o1+>SGN0nK-0@JV(EAeS0tk(Mdj~ftdO0Mgb4!wS;)lA;MKfjh*o~iI6~r^;9j9w zJM%j7BJcHi0$HK=Gji-=w^c-^PT@}K&+DtuvfkUgONb#=Up|GdLHWgqsmF~#WGSVW zwcuA`!NYtgz);3$kH}cNww|-mr<4?| z5L!GzOi6t%F>Xh&a-QAEw86xZpjuhd4=EK2vW6o9GxPglI)7-8%qOb!_ z61;dYY9o6eTuacJdi;EzCmLAnULds@h=gltSZ9Xred>BXqQuRs%)RzOxB2mue?{lyl6o7%D1oxe1 zpNG;#>MrDfRk~pI5$>rtmwXs|qKP|B@-bpBhe37kcpIH&F0sQOBF!!oOagkq8eoPQ zf95m(#}Zz~=S*o8@;dQ37$B>{M@3-eRzrs!PM`?;sq9vaN27&9o|jw{FnQSRaWA|` zk-!t8ia+CDD;{46r0{hLcjA1uNLA2!tjjJ?(c54#E%q8R4TXL0Ta!edjFk`Cw7Btv z@85j3W^rS4PA!9_E7?CE%@}2;mpP?m$S@YPxX-NH22b*zZ`TF~Foz=F8rEa?nEBGTy~z$4kOE>Zb}nnEgJ z8WI^P#=OLyCQ1r{C6U-#^!4tBj6vN`Q`@YjC8;Au*UHE*avY;O9(sb+Q+G_XfWMX- zM_to5>kxV#@9$rti{4(d*8Ha$Ve+-cvGvb-1O9B7B<*lcwWW3*)NL~}(_HU$)=e`f9Zh?BC^0rM!_*X@>@O^+qBQ}e{oPbc{a2g0bOYtGDWmzq!4rBPpe^%I7r5=>Cw$`|7 ziQN@>3N?760eDO;P-v}!5Zx8;{$d=d2}!{Xkt|a1WDj8|Nph~D(C3;m)Jj{N81~`bxwl`r_c@o~XNrwi z)5v-CqAQbshz0AaX7pLR)$X;A^-)H17KoX>F)|<)G#sIxOA4Ed42V+090pEWT?>J7 zw{qg%d$Lnc*}~FU2TwqN9hhgyGj`#LMlXWTK0Io7KVmw+^bH}ypMph?Q@hILn%1x8 zb!&vd47?jB8lC`{5oR~_D76*HTChBe=?sZ0g^%m^E$4Lin{cPce2X7LpAKJOuw35p zMP6LH+dFQbq(*q3JRIY@pvHmIrYN17xv=)xd8>DR7*kzJ>RJ{h6(kQT1~FvJat7@h z_Ddz;7Oi7u0X!jXP8h=BrI#g-U^X@)*CCj?z%k`B2Rc1rzMndg0wD6q0Wkr)DIU|Mc@K8+>H9Vn9(-qd7qUdoH)i1FqUP7U}H?h9!_OT1$QG^hgO<{hD1?SOYarvun*YCJN3Gq z*1-lj5Hqh{%WU?1y5!#+?NgMQabm7AAA-Pp!mC`D^9xGs;tqla{%CRzEYS&|h z+OtWv$I7uXy=K>A?KXQy2kXZeR*h8>UUoj#UbDRci(EZcV61jM*52XaBP=UHRJ$JO zF(77-Fw_IWZFW7@PP=!wK}2j71xTQ6*JJJWnvcM+Bxtnjp>~cAIvb>?ty)$wc6L41 z-of#~BancoU%MXb@lp3^1JL8dQw9zWkB&Q{4D127-)`+g9k_phO+DK~fCtsg7SiZw zziOm@^%+X3fByO2^q0l!7pw#~z#sc3^Tu#5sZH_6UaQ?{!Vc2ACokaHo$sMST(h}v zHHZDZf&H^KLM=grtY`Rx&Y#iQkMi-G$-MTgRh!(5$F-_03s;rT`;Fv1s}If}YF%g; z8?csvH~-^K@33V@JBW|={-Mk;Wvu*{H^AfL_9hyTzd^^H=E0$EP`8Qg-hP8RU7SWc zo>?PtEbqZr%i!-MW7$ZLPXh)|;OO8G)c%yu^wH6_Lgo12pbd8h=i6$ww%2SQcYoX6Ejd`Pdw676cB^;P zgHFHQ_Tp5u+pWjf6OVeW4a_ZHh3_=GILL<6?p&DmI-B@}{Dap$gvPCec+>-{*>-0h zmk>LLN8Jsy8-K#>57>A9up=sBdCK`V&5a24ZAruhoO~yXAzT_aC5j zo2BVp*l1OomzXo654)Ytmb?ez)6h50;u9C7N3GT-elvf&58!vP+uiOppSmQ4)w`{e z2>ODNEuCKTsjCj17F3<>To2vO@v+ej!Mu@JxaEO$S3aKJ1=$D7CAYQ#*o-RdULfU1~vfkSxWQcS{6 z>oaq|6VdTeuPYPIN3ce@2o?iZ62TgI5v+mT*6E9pjZq=a#K6mTLugAZfKBB6iPcEngkiP8(I$(Au92%SB>uW?S|Gl?5eiDd+KHF|+2d%@dgh6&pYdw0twi{aKpz|m!SA1044Go9{8ihOGQ1K>hH?+>dLGLkG zF8w~+jSSU99!DqIw&lc82hBJiWj8xpo=#XSa2M^K&cvy)-EJPjWPFT8gPUy=?@eIm zb=&I0*fCEN|G;*uh7@+D>g=3xhylOd(BQTJ{B8F`6A0<@p{2cok$bW&y+70hv{~|v z0MKo1-tvt=VU~O&pebA1Uk^1wKbCys0TyIi%?Mr&!VXKmvEAwPw%AmwX{|Z+$sDe9SI08l~`^IA^pj#6T;2$O5*8#iP{`3p} zqU0M74{#c{o*_MDWx4p_5sFZ3{YF6`l$~R@%~ZH8S)4!sO1=?Z+}8GUl-PU8H&SeD z&u}E(UGj}kz*;+CI1DtGd?U`0B6PPfYoMw!z+3W-IF1?{-Rg}JVJ-Q_HWaY!_wksK z&5~~fkZx|lM@iEv`9{gK-};S`A64>=k}tpY8zo7iBO78mBZxk4H**7u+ed{+0c(mjjnMc0$3kB>|@`aKPzV#agW>xZylGMHR8wJ@^ z@{LT*-ui_CC@T3vNwD7jjl{K-e50gEZ~ewD92el@NFzp$ZLf29fCd`+;phm?IQs#` qU5NrRmmM85HAFvj+EA422S8KVfy4X)mq_}6&LL+ezdRsa`Tqgmi)%&z diff --git a/public/js/manifest.js b/public/js/manifest.js index de48f35bf8f9b085ba3d10d64a58b001904be26e..7e718244da63b48e887e0c55f9477f66b0ded65a 100644 GIT binary patch delta 29 kcmZ1`zf69E8>@hYMQUnlQkq$^sd0))N^;`nFxCJr0FL>KKN}^G6Qkp?(vayl5VN%NGFxCJr0FX)v3;+NC diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 5162e3d9600a6bfa0576e8587bebea95b11e3e69..5a2fbe031e14a9659da9ded1c10cc4e775b7b7cb 100644 GIT binary patch delta 147 zcmeyZ@mpg9BZq>ynSqf>qLG1#S+a3rN}5Tcv2jwGSz?;0fkAR|nuXb9MGo;;3yaj$ zRG@IOsd0))N^+uJR@!nrlafr$EiF@$5)+dx h&CD$=j4ezJj1A2V%oB}MER)SACoqX@Ucqsh8vv@KECv7o From 66f6640072a45d077c76d79a45f610fc27c1a44e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 03:08:50 -0700 Subject: [PATCH 406/977] Bump version to 0.11.12 --- CHANGELOG.md | 6 ++++-- config/pixelfed.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a1b6677..cbbea5c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Release Notes -## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.11...dev) +## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev) +- ([](https://github.com/pixelfed/pixelfed/commit/)) + +## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) ### Features - Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) @@ -13,7 +16,6 @@ - Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3)) - Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8)) - Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11) diff --git a/config/pixelfed.php b/config/pixelfed.php index 9ed7fc616..50164218c 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your Pixelfed instance. | */ - 'version' => '0.11.11', + 'version' => '0.11.12', /* |-------------------------------------------------------------------------- From d835e0adaaa16db3bfb0560851f917574e78fb91 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 16 Feb 2024 06:57:04 -0700 Subject: [PATCH 407/977] Update Inbox, cast live filters to lowercase --- app/Util/ActivityPub/Helpers.php | 4 ++-- app/Util/ActivityPub/Inbox.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index e25fd93b7..bcf4f359c 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -321,11 +321,11 @@ class Helpers { $filters = array_map('trim', explode(',', $filters)); $content = $res['content']; foreach($filters as $filter) { - $filter = trim($filter); + $filter = trim(strtolower($filter)); if(!$filter || !strlen($filter)) { continue; } - if(str_contains($content, $filter)) { + if(str_contains(strtolower($content), $filter)) { return; } } diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index b6ae80893..62ab3be75 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -203,11 +203,11 @@ class Inbox $filters = array_map('trim', explode(',', $filters)); $content = $activity['content']; foreach($filters as $filter) { - $filter = trim($filter); + $filter = trim(strtolower($filter)); if(!$filter || !strlen($filter)) { continue; } - if(str_contains($content, $filter)) { + if(str_contains(strtolower($content), $filter)) { return; } } From 9978b2b9591c584e8e85b3ad03460415494db60a Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Fri, 16 Feb 2024 17:56:13 +0100 Subject: [PATCH 408/977] Update .gitattributes to collapse diffs on generated files --- .gitattributes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitattributes b/.gitattributes index 967315dd3..25c1b1b65 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,10 @@ *.scss linguist-vendored *.js linguist-vendored CHANGELOG.md export-ignore + +# Collapse diffs for generated files: +public/**/*.js text -diff +public/**/*.json text -diff +public/**/*.css text -diff +public/img/* binary -diff +public/fonts/* binary -diff From 6edd71258196ce929acaa11c5741aba2fba93461 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 17 Feb 2024 01:19:59 +0000 Subject: [PATCH 409/977] bump dottie --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 77bb1da62..e82de9d45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.7.1" +ARG DOTTIE_VERSION="v0.8.0" ### # PHP base configuration From 9a1c4d42b5f528aef09486ddca906815ef84ba55 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 17 Feb 2024 01:23:12 +0000 Subject: [PATCH 410/977] drop php 8.1 support --- .github/workflows/docker.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ea119bec3..9a451937c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -70,7 +70,6 @@ jobs: # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs matrix: php_version: - - 8.1 - 8.2 - 8.3 target_runtime: From 011834f473d97ce10389629c3bcd12dbfecb183a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 13:05:49 -0700 Subject: [PATCH 411/977] Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 --- config/federation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/federation.php b/config/federation.php index 773d3d16b..21415dad3 100644 --- a/config/federation.php +++ b/config/federation.php @@ -49,7 +49,7 @@ return [ ], 'network_timeline' => env('PF_NETWORK_TIMELINE', true), - 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2), + 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 90), 'custom_emoji' => [ 'enabled' => env('CUSTOM_EMOJI', false), From 17027c34875128f154f2f4534cb1a56b69ebc69e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 13:07:26 -0700 Subject: [PATCH 412/977] Update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbea5c31..13cd2813b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Release Notes ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev) + +### Updates + +- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad)) +- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) @@ -9,7 +14,7 @@ - Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) - Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988)) -### Updated +### Updates - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) From 4dc15bb37d161695e46d12dc9c6600d6f01940f1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 19 Feb 2024 00:48:36 +0000 Subject: [PATCH 413/977] fix validation --- .env.docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.docker b/.env.docker index 9a27550d1..fb15956b4 100644 --- a/.env.docker +++ b/.env.docker @@ -819,7 +819,7 @@ SESSION_DRIVER="redis" # # @default the value of APP_DOMAIN, or null. # @see https://docs.pixelfed.org/technical-documentation/config/#session_domain -# @dottie/validate required,domain +# @dottie/validate required,hostname #SESSION_DOMAIN="${APP_DOMAIN}" ################################################################################ From 9117df186c3a1c3bcd6e96941c5a668f559d1195 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 19 Feb 2024 00:52:12 +0000 Subject: [PATCH 414/977] more validation fixes --- .env.docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.docker b/.env.docker index fb15956b4..a38c6b61c 100644 --- a/.env.docker +++ b/.env.docker @@ -47,7 +47,7 @@ ADMIN_DOMAIN="${APP_DOMAIN}" # # @default "production" # @see https://docs.pixelfed.org/technical-documentation/config/#app_env -# @dottie/validate required,oneof='production,dev,staging' +# @dottie/validate required,oneof=production dev staging #APP_ENV="production" # When your application is in debug mode, detailed error messages with stack traces will From 4c26f59cd09d8e33a1a713b668021c656a47812a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 18:00:43 -0700 Subject: [PATCH 415/977] Update composer deps, add php 8.3 support --- composer.json | 2 +- composer.lock | 1078 ++++++++++++++++++++++++------------------------- 2 files changed, 524 insertions(+), 556 deletions(-) diff --git a/composer.json b/composer.json index 285d38ccd..08c0af239 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "AGPL-3.0-only", "type": "project", "require": { - "php": "^8.1|^8.2", + "php": "^8.1|^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", diff --git a/composer.lock b/composer.lock index 12c097343..bfd4b920b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "74351c8a36870209c9bbfb76d158a10c", + "content-hash": "00f283796faf6517a8d98f9080440b7f", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.293.2", + "version": "3.299.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4" + "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4", - "reference": "1d3e952ea2f45bb0d42d7f873d1b4957bb6362c4", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0f87b8e8bfb9afd0ffd702fcda556b465eee457", + "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457", "shasum": "" }, "require": { @@ -151,9 +151,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.293.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.299.1" }, - "time": "2023-12-01T19:06:15+00:00" + "time": "2024-02-16T19:08:34+00:00" }, { "name": "bacon/bacon-qr-code", @@ -426,16 +426,16 @@ }, { "name": "carbonphp/carbon-doctrine-types", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "67a77972b9f398ae7068dabacc39c08aeee170d5" + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/67a77972b9f398ae7068dabacc39c08aeee170d5", - "reference": "67a77972b9f398ae7068dabacc39c08aeee170d5", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", "shasum": "" }, "require": { @@ -475,7 +475,7 @@ ], "support": { "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.0.0" + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" }, "funding": [ { @@ -491,7 +491,7 @@ "type": "tidelift" } ], - "time": "2023-10-01T14:29:01+00:00" + "time": "2023-12-11T17:09:12+00:00" }, { "name": "cboden/ratchet", @@ -843,16 +843,16 @@ }, { "name": "doctrine/dbal", - "version": "3.7.2", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" + "reference": "a19a1d05ca211f41089dffcc387733a6875196cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/a19a1d05ca211f41089dffcc387733a6875196cb", + "reference": "a19a1d05ca211f41089dffcc387733a6875196cb", "shasum": "" }, "require": { @@ -868,14 +868,14 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.42", + "phpstan/phpstan": "1.10.57", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.13", + "phpunit/phpunit": "9.6.16", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4|^6.0", - "symfony/console": "^4.4|^5.4|^6.0", + "squizlabs/php_codesniffer": "3.8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" }, "suggest": { @@ -936,7 +936,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.7.2" + "source": "https://github.com/doctrine/dbal/tree/3.8.2" }, "funding": [ { @@ -952,20 +952,20 @@ "type": "tidelift" } ], - "time": "2023-11-19T08:06:58+00:00" + "time": "2024-02-12T18:36:36+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", "shasum": "" }, "require": { @@ -997,9 +997,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" }, - "time": "2023-09-27T20:04:15+00:00" + "time": "2024-01-30T19:34:25+00:00" }, { "name": "doctrine/event-manager", @@ -1094,16 +1094,16 @@ }, { "name": "doctrine/inflector", - "version": "2.0.8", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "shasum": "" }, "require": { @@ -1165,7 +1165,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.8" + "source": "https://github.com/doctrine/inflector/tree/2.0.10" }, "funding": [ { @@ -1181,31 +1181,31 @@ "type": "tidelift" } ], - "time": "2023-06-16T13:40:37+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/lexer", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "84a527db05647743d50373e0ec53a152f2cde568" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/84a527db05647743d50373e0ec53a152f2cde568", - "reference": "84a527db05647743d50373e0ec53a152f2cde568", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1242,7 +1242,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1258,7 +1258,7 @@ "type": "tidelift" } ], - "time": "2022-12-15T16:57:16+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "dragonmantank/cron-expression", @@ -2567,20 +2567,20 @@ }, { "name": "laravel/framework", - "version": "v10.34.2", + "version": "v10.44.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "c581caa233e380610b34cc491490bfa147a3b62b" + "reference": "1199dbe361787bbe9648131a79f53921b4148cf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c581caa233e380610b34cc491490bfa147a3b62b", - "reference": "c581caa233e380610b34cc491490bfa147a3b62b", + "url": "https://api.github.com/repos/laravel/framework/zipball/1199dbe361787bbe9648131a79f53921b4148cf6", + "reference": "1199dbe361787bbe9648131a79f53921b4148cf6", "shasum": "" }, "require": { - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.3.2", @@ -2609,7 +2609,7 @@ "symfony/console": "^6.2", "symfony/error-handler": "^6.2", "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.3", + "symfony/http-foundation": "^6.4", "symfony/http-kernel": "^6.2", "symfony/mailer": "^6.2", "symfony/mime": "^6.2", @@ -2622,6 +2622,9 @@ "voku/portable-ascii": "^2.0" }, "conflict": { + "carbonphp/carbon-doctrine-types": ">=3.0", + "doctrine/dbal": ">=4.0", + "phpunit/phpunit": ">=11.0.0", "tightenco/collect": "<5.5.33" }, "provide": { @@ -2677,7 +2680,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.15.1", + "orchestra/testbench-core": "^8.18", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", @@ -2733,6 +2736,7 @@ "files": [ "src/Illuminate/Collections/helpers.php", "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Support/helpers.php" ], @@ -2765,28 +2769,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-11-28T19:06:27+00:00" + "time": "2024-02-13T16:01:16+00:00" }, { "name": "laravel/helpers", - "version": "v1.6.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/laravel/helpers.git", - "reference": "4dd0f9436d3911611622a6ced8329a1710576f60" + "reference": "6caaa242a23bc39b4e3cf57304b5409260a7a346" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/helpers/zipball/4dd0f9436d3911611622a6ced8329a1710576f60", - "reference": "4dd0f9436d3911611622a6ced8329a1710576f60", + "url": "https://api.github.com/repos/laravel/helpers/zipball/6caaa242a23bc39b4e3cf57304b5409260a7a346", + "reference": "6caaa242a23bc39b4e3cf57304b5409260a7a346", "shasum": "" }, "require": { - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "php": "^7.1.3|^8.0" + "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "php": "^7.2.0|^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^7.0|^8.0|^9.0|^10.0" }, "type": "library", "extra": { @@ -2819,42 +2824,42 @@ "laravel" ], "support": { - "source": "https://github.com/laravel/helpers/tree/v1.6.0" + "source": "https://github.com/laravel/helpers/tree/v1.7.0" }, - "time": "2023-01-09T14:48:11+00:00" + "time": "2023-11-30T14:09:05+00:00" }, { "name": "laravel/horizon", - "version": "v5.21.4", + "version": "v5.23.0", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "bdf58c84b592b83f62262cc6ca98b0debbbc308b" + "reference": "0b1bf46b21c397fdbe80b1ab54451fd227934508" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/bdf58c84b592b83f62262cc6ca98b0debbbc308b", - "reference": "bdf58c84b592b83f62262cc6ca98b0debbbc308b", + "url": "https://api.github.com/repos/laravel/horizon/zipball/0b1bf46b21c397fdbe80b1ab54451fd227934508", + "reference": "0b1bf46b21c397fdbe80b1ab54451fd227934508", "shasum": "" }, "require": { "ext-json": "*", "ext-pcntl": "*", "ext-posix": "*", - "illuminate/contracts": "^8.17|^9.0|^10.0", - "illuminate/queue": "^8.17|^9.0|^10.0", - "illuminate/support": "^8.17|^9.0|^10.0", - "nesbot/carbon": "^2.17", - "php": "^7.3|^8.0", + "illuminate/contracts": "^9.21|^10.0|^11.0", + "illuminate/queue": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", "ramsey/uuid": "^4.0", - "symfony/error-handler": "^5.0|^6.0", - "symfony/process": "^5.0|^6.0" + "symfony/error-handler": "^6.0|^7.0", + "symfony/process": "^6.0|^7.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^6.0|^7.0|^8.0", + "orchestra/testbench": "^7.0|^8.0|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^9.0|^10.4", "predis/predis": "^1.1|^2.0" }, "suggest": { @@ -2897,22 +2902,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.21.4" + "source": "https://github.com/laravel/horizon/tree/v5.23.0" }, - "time": "2023-11-23T15:47:58+00:00" + "time": "2024-02-12T18:36:34+00:00" }, { "name": "laravel/passport", - "version": "v11.10.0", + "version": "v11.10.5", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "966bc8e477d08c86a11dc4c5a86f85fa0abdb89b" + "reference": "4d81207941d6efc198857847d9e4c17520f28d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/966bc8e477d08c86a11dc4c5a86f85fa0abdb89b", - "reference": "966bc8e477d08c86a11dc4c5a86f85fa0abdb89b", + "url": "https://api.github.com/repos/laravel/passport/zipball/4d81207941d6efc198857847d9e4c17520f28d75", + "reference": "4d81207941d6efc198857847d9e4c17520f28d75", "shasum": "" }, "require": { @@ -2977,20 +2982,20 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2023-11-02T17:16:12+00:00" + "time": "2024-02-09T16:27:49+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.13", + "version": "v0.1.15", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "e1379d8ead15edd6cc4369c22274345982edc95a" + "reference": "d814a27514d99b03c85aa42b22cfd946568636c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/e1379d8ead15edd6cc4369c22274345982edc95a", - "reference": "e1379d8ead15edd6cc4369c22274345982edc95a", + "url": "https://api.github.com/repos/laravel/prompts/zipball/d814a27514d99b03c85aa42b22cfd946568636c1", + "reference": "d814a27514d99b03c85aa42b22cfd946568636c1", "shasum": "" }, "require": { @@ -3006,7 +3011,7 @@ "require-dev": { "mockery/mockery": "^1.5", "pestphp/pest": "^2.3", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.11", "phpstan/phpstan-mockery": "^1.1" }, "suggest": { @@ -3032,9 +3037,9 @@ ], "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.13" + "source": "https://github.com/laravel/prompts/tree/v0.1.15" }, - "time": "2023-10-27T13:53:59+00:00" + "time": "2023-12-29T22:37:42+00:00" }, { "name": "laravel/serializable-closure", @@ -3098,25 +3103,25 @@ }, { "name": "laravel/tinker", - "version": "v2.8.2", + "version": "v2.9.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3" + "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/b936d415b252b499e8c3b1f795cd4fc20f57e1f3", - "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "php": "^7.2.5|^8.0", - "psy/psysh": "^0.10.4|^0.11.1", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0" + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -3124,13 +3129,10 @@ "phpunit/phpunit": "^8.5.8|^9.3.3" }, "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0)." + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0)." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - }, "laravel": { "providers": [ "Laravel\\Tinker\\TinkerServiceProvider" @@ -3161,34 +3163,34 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.8.2" + "source": "https://github.com/laravel/tinker/tree/v2.9.0" }, - "time": "2023-08-15T14:27:00+00:00" + "time": "2024-01-04T16:10:04+00:00" }, { "name": "laravel/ui", - "version": "v4.2.3", + "version": "v4.4.0", "source": { "type": "git", "url": "https://github.com/laravel/ui.git", - "reference": "eb532ea096ca1c0298c87c19233daf011fda743a" + "reference": "7335d7049b2cde345c029e9d2de839b80af62bc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/eb532ea096ca1c0298c87c19233daf011fda743a", - "reference": "eb532ea096ca1c0298c87c19233daf011fda743a", + "url": "https://api.github.com/repos/laravel/ui/zipball/7335d7049b2cde345c029e9d2de839b80af62bc0", + "reference": "7335d7049b2cde345c029e9d2de839b80af62bc0", "shasum": "" }, "require": { - "illuminate/console": "^9.21|^10.0", - "illuminate/filesystem": "^9.21|^10.0", - "illuminate/support": "^9.21|^10.0", - "illuminate/validation": "^9.21|^10.0", + "illuminate/console": "^9.21|^10.0|^11.0", + "illuminate/filesystem": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "illuminate/validation": "^9.21|^10.0|^11.0", "php": "^8.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0", - "phpunit/phpunit": "^9.3" + "orchestra/testbench": "^7.0|^8.0|^9.0", + "phpunit/phpunit": "^9.3|^10.4" }, "type": "library", "extra": { @@ -3223,9 +3225,9 @@ "ui" ], "support": { - "source": "https://github.com/laravel/ui/tree/v4.2.3" + "source": "https://github.com/laravel/ui/tree/v4.4.0" }, - "time": "2023-11-23T14:44:22+00:00" + "time": "2024-01-12T15:56:45+00:00" }, { "name": "lcobucci/clock", @@ -3366,16 +3368,16 @@ }, { "name": "league/commonmark", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5" + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", "shasum": "" }, "require": { @@ -3388,7 +3390,7 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", + "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", @@ -3398,10 +3400,10 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -3468,7 +3470,7 @@ "type": "tidelift" } ], - "time": "2023-08-30T16:55:00+00:00" + "time": "2024-02-02T11:59:32+00:00" }, { "name": "league/config", @@ -3608,16 +3610,16 @@ }, { "name": "league/flysystem", - "version": "3.22.0", + "version": "3.24.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "d18526ee587f265f091f47bb83f1d9a001ef6f36" + "reference": "b25a361508c407563b34fac6f64a8a17a8819675" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d18526ee587f265f091f47bb83f1d9a001ef6f36", - "reference": "d18526ee587f265f091f47bb83f1d9a001ef6f36", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b25a361508c407563b34fac6f64a8a17a8819675", + "reference": "b25a361508c407563b34fac6f64a8a17a8819675", "shasum": "" }, "require": { @@ -3637,7 +3639,7 @@ "require-dev": { "async-aws/s3": "^1.5 || ^2.0", "async-aws/simple-s3": "^1.1 || ^2.0", - "aws/aws-sdk-php": "^3.220.0", + "aws/aws-sdk-php": "^3.295.10", "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", @@ -3648,7 +3650,7 @@ "phpseclib/phpseclib": "^3.0.34", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", - "sabre/dav": "^4.3.1" + "sabre/dav": "^4.6.0" }, "type": "library", "autoload": { @@ -3682,7 +3684,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.22.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.24.0" }, "funding": [ { @@ -3694,24 +3696,24 @@ "type": "github" } ], - "time": "2023-12-03T18:35:53+00:00" + "time": "2024-02-04T12:10:17+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.22.0", + "version": "3.24.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "9808919ee5d819730d9582d4e1673e8d195c38d8" + "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/9808919ee5d819730d9582d4e1673e8d195c38d8", - "reference": "9808919ee5d819730d9582d4e1673e8d195c38d8", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", + "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", "shasum": "" }, "require": { - "aws/aws-sdk-php": "^3.220.0", + "aws/aws-sdk-php": "^3.295.10", "league/flysystem": "^3.10.0", "league/mime-type-detection": "^1.0.0", "php": "^8.0.2" @@ -3747,8 +3749,7 @@ "storage" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.22.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.24.0" }, "funding": [ { @@ -3760,20 +3761,20 @@ "type": "github" } ], - "time": "2023-11-18T14:03:37+00:00" + "time": "2024-01-26T18:43:21+00:00" }, { "name": "league/flysystem-local", - "version": "3.22.0", + "version": "3.23.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "42dfb4eaafc4accd248180f0dd66f17073b40c4c" + "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/42dfb4eaafc4accd248180f0dd66f17073b40c4c", - "reference": "42dfb4eaafc4accd248180f0dd66f17073b40c4c", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00", + "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00", "shasum": "" }, "require": { @@ -3808,7 +3809,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.22.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1" }, "funding": [ { @@ -3820,7 +3821,7 @@ "type": "github" } ], - "time": "2023-11-18T20:52:53+00:00" + "time": "2024-01-26T18:25:23+00:00" }, { "name": "league/iso3166", @@ -3882,16 +3883,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.14.0", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "b6a5854368533df0295c5761a0253656a2e52d9e" + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/b6a5854368533df0295c5761a0253656a2e52d9e", - "reference": "b6a5854368533df0295c5761a0253656a2e52d9e", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", "shasum": "" }, "require": { @@ -3922,7 +3923,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.14.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" }, "funding": [ { @@ -3934,7 +3935,7 @@ "type": "tidelift" } ], - "time": "2023-10-17T14:13:20+00:00" + "time": "2024-01-28T23:22:08+00:00" }, { "name": "league/oauth2-server", @@ -4496,16 +4497,16 @@ }, { "name": "nesbot/carbon", - "version": "2.72.0", + "version": "2.72.3", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "a6885fcbad2ec4360b0e200ee0da7d9b7c90786b" + "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/a6885fcbad2ec4360b0e200ee0da7d9b7c90786b", - "reference": "a6885fcbad2ec4360b0e200ee0da7d9b7c90786b", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83", "shasum": "" }, "require": { @@ -4599,35 +4600,35 @@ "type": "tidelift" } ], - "time": "2023-11-28T10:13:25+00:00" + "time": "2024-01-25T10:35:09+00:00" }, { "name": "nette/schema", - "version": "v1.2.5", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", - "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", "shasum": "" }, "require": { - "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": "7.1 - 8.3" + "nette/utils": "^4.0", + "php": "8.1 - 8.3" }, "require-dev": { - "nette/tester": "^2.3 || ^2.4", + "nette/tester": "^2.4", "phpstan/phpstan-nette": "^1.0", - "tracy/tracy": "^2.7" + "tracy/tracy": "^2.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -4659,22 +4660,22 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.5" + "source": "https://github.com/nette/schema/tree/v1.3.0" }, - "time": "2023-10-05T20:37:59+00:00" + "time": "2023-12-11T11:54:22+00:00" }, { "name": "nette/utils", - "version": "v4.0.3", + "version": "v4.0.4", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015" + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/a9d127dd6a203ce6d255b2e2db49759f7506e015", - "reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015", + "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", "shasum": "" }, "require": { @@ -4745,31 +4746,33 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.3" + "source": "https://github.com/nette/utils/tree/v4.0.4" }, - "time": "2023-10-29T21:02:13+00:00" + "time": "2024-01-17T16:50:36+00:00" }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -4777,7 +4780,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -4801,9 +4804,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2024-01-07T17:17:35+00:00" }, { "name": "nunomaduro/termwind", @@ -5174,32 +5177,32 @@ }, { "name": "pbmedia/laravel-ffmpeg", - "version": "8.3.0", + "version": "8.4.0", "source": { "type": "git", "url": "https://github.com/protonemedia/laravel-ffmpeg.git", - "reference": "820e7f1290918233a59d85f25bc78796dc3f57bb" + "reference": "b16fd89fab3a37727711d3abc3e89229e3c4f14d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/820e7f1290918233a59d85f25bc78796dc3f57bb", - "reference": "820e7f1290918233a59d85f25bc78796dc3f57bb", + "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/b16fd89fab3a37727711d3abc3e89229e3c4f14d", + "reference": "b16fd89fab3a37727711d3abc3e89229e3c4f14d", "shasum": "" }, "require": { - "illuminate/contracts": "^9.0|^10.0", - "php": "^8.1|^8.2", - "php-ffmpeg/php-ffmpeg": "^1.1", - "ramsey/collection": "^1.0|^2.0" + "illuminate/contracts": "^10.0", + "php": "^8.1|^8.2|^8.3", + "php-ffmpeg/php-ffmpeg": "^1.2", + "ramsey/collection": "^2.0" }, "require-dev": { "league/flysystem-memory": "^3.10", "mockery/mockery": "^1.4.4", "nesbot/carbon": "^2.66", - "orchestra/testbench": "^7.0|^8.0", - "phpunit/phpunit": "^9.5.10", + "orchestra/testbench": "^8.0", + "phpunit/phpunit": "^10.4", "spatie/image": "^2.2", - "spatie/phpunit-snapshot-assertions": "^4.2" + "spatie/phpunit-snapshot-assertions": "^5.0" }, "type": "library", "extra": { @@ -5240,7 +5243,7 @@ ], "support": { "issues": "https://github.com/protonemedia/laravel-ffmpeg/issues", - "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.3.0" + "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/8.4.0" }, "funding": [ { @@ -5248,29 +5251,29 @@ "type": "github" } ], - "time": "2023-02-15T10:10:46+00:00" + "time": "2024-01-02T23:13:28+00:00" }, { "name": "php-ffmpeg/php-ffmpeg", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", - "reference": "eace6f174ff6d206ba648483ebe59760f7f6a0e1" + "reference": "785a5ba05dd88b3b8146f85f18476b259b23917c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/eace6f174ff6d206ba648483ebe59760f7f6a0e1", - "reference": "eace6f174ff6d206ba648483ebe59760f7f6a0e1", + "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/785a5ba05dd88b3b8146f85f18476b259b23917c", + "reference": "785a5ba05dd88b3b8146f85f18476b259b23917c", "shasum": "" }, "require": { "evenement/evenement": "^3.0", - "php": "^8.0 || ^8.1 || ^8.2", + "php": "^8.0 || ^8.1 || ^8.2 || ^8.3", "psr/log": "^1.0 || ^2.0 || ^3.0", "spatie/temporary-directory": "^2.0", - "symfony/cache": "^5.4 || ^6.0", - "symfony/process": "^5.4 || ^6.0" + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "mockery/mockery": "^1.5", @@ -5335,9 +5338,9 @@ ], "support": { "issues": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/issues", - "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.1.0" + "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.2.0" }, - "time": "2022-12-09T13:57:05+00:00" + "time": "2024-01-02T10:37:01+00:00" }, { "name": "phpoption/phpoption", @@ -5416,16 +5419,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.45", + "version": "2.0.46", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "28d8f438a0064c9de80857e3270d071495544640" + "reference": "498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/28d8f438a0064c9de80857e3270d071495544640", - "reference": "28d8f438a0064c9de80857e3270d071495544640", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d", + "reference": "498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d", "shasum": "" }, "require": { @@ -5506,7 +5509,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/2.0.45" + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.46" }, "funding": [ { @@ -5522,7 +5525,7 @@ "type": "tidelift" } ], - "time": "2023-09-15T20:55:47+00:00" + "time": "2023-12-29T01:52:43+00:00" }, { "name": "pixelfed/fractal", @@ -6275,25 +6278,25 @@ }, { "name": "psy/psysh", - "version": "v0.11.22", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "128fa1b608be651999ed9789c95e6e2a31b5802b" + "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/128fa1b608be651999ed9789c95e6e2a31b5802b", - "reference": "128fa1b608be651999ed9789c95e6e2a31b5802b", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/750bf031a48fd07c673dbe3f11f72362ea306d0d", + "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d", "shasum": "" }, "require": { "ext-json": "*", "ext-tokenizer": "*", - "nikic/php-parser": "^4.0 || ^3.1", - "php": "^8.0 || ^7.0.8", - "symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4" + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -6304,8 +6307,7 @@ "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", "ext-pdo-sqlite": "The doc command requires SQLite to work.", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", - "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ "bin/psysh" @@ -6313,7 +6315,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-0.11": "0.11.x-dev" + "dev-main": "0.12.x-dev" }, "bamarni-bin": { "bin-links": false, @@ -6349,22 +6351,22 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.11.22" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.0" }, - "time": "2023-10-14T21:56:36+00:00" + "time": "2023-12-20T15:28:09+00:00" }, { "name": "pusher/pusher-php-server", - "version": "7.2.3", + "version": "7.2.4", "source": { "type": "git", "url": "https://github.com/pusher/pusher-http-php.git", - "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9" + "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/416e68dd5f640175ad5982131c42a7a666d1d8e9", - "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/de2f72296808f9cafa6a4462b15a768ff130cddb", + "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb", "shasum": "" }, "require": { @@ -6410,9 +6412,9 @@ ], "support": { "issues": "https://github.com/pusher/pusher-http-php/issues", - "source": "https://github.com/pusher/pusher-http-php/tree/7.2.3" + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.4" }, - "time": "2023-05-17T16:00:06+00:00" + "time": "2023-12-15T10:58:53+00:00" }, { "name": "ralouphie/getallheaders", @@ -7083,16 +7085,16 @@ }, { "name": "react/socket", - "version": "v1.14.0", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "21591111d3ea62e31f2254280ca0656bc2b1bda6" + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/21591111d3ea62e31f2254280ca0656bc2b1bda6", - "reference": "21591111d3ea62e31f2254280ca0656bc2b1bda6", + "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", "shasum": "" }, "require": { @@ -7104,7 +7106,7 @@ "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", "react/promise-stream": "^1.4", "react/promise-timer": "^1.10" @@ -7112,7 +7114,7 @@ "type": "library", "autoload": { "psr-4": { - "React\\Socket\\": "src" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7151,7 +7153,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.14.0" + "source": "https://github.com/reactphp/socket/tree/v1.15.0" }, "funding": [ { @@ -7159,7 +7161,7 @@ "type": "open_collective" } ], - "time": "2023-08-25T13:48:09+00:00" + "time": "2023-12-15T11:02:10+00:00" }, { "name": "react/stream", @@ -7302,21 +7304,21 @@ }, { "name": "spatie/db-dumper", - "version": "3.4.0", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "bbd5ae0f331d47e6534eb307e256c11a65c8e24a" + "reference": "59beef7ad612ca7463dfddb64de6e038eb59e0d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/bbd5ae0f331d47e6534eb307e256c11a65c8e24a", - "reference": "bbd5ae0f331d47e6534eb307e256c11a65c8e24a", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/59beef7ad612ca7463dfddb64de6e038eb59e0d7", + "reference": "59beef7ad612ca7463dfddb64de6e038eb59e0d7", "shasum": "" }, "require": { "php": "^8.0", - "symfony/process": "^5.0|^6.0" + "symfony/process": "^5.0|^6.0|^7.0" }, "require-dev": { "pestphp/pest": "^1.22" @@ -7349,7 +7351,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.4.0" + "source": "https://github.com/spatie/db-dumper/tree/3.4.2" }, "funding": [ { @@ -7361,7 +7363,7 @@ "type": "github" } ], - "time": "2023-06-27T08:34:52+00:00" + "time": "2023-12-25T11:42:15+00:00" }, { "name": "spatie/image-optimizer", @@ -7420,44 +7422,44 @@ }, { "name": "spatie/laravel-backup", - "version": "8.4.1", + "version": "8.6.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "b79f790cc856e67cce012abf34bf1c9035085dc1" + "reference": "c6a7607c0eea80efc2cf6628ffcd172f73a2088f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/b79f790cc856e67cce012abf34bf1c9035085dc1", - "reference": "b79f790cc856e67cce012abf34bf1c9035085dc1", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/c6a7607c0eea80efc2cf6628ffcd172f73a2088f", + "reference": "c6a7607c0eea80efc2cf6628ffcd172f73a2088f", "shasum": "" }, "require": { "ext-zip": "^1.14.0", - "illuminate/console": "^10.10.0", - "illuminate/contracts": "^10.10.0", - "illuminate/events": "^10.10.0", - "illuminate/filesystem": "^10.10.0", - "illuminate/notifications": "^10.10.0", - "illuminate/support": "^10.10.0", + "illuminate/console": "^10.10.0|^11.0", + "illuminate/contracts": "^10.10.0|^11.0", + "illuminate/events": "^10.10.0|^11.0", + "illuminate/filesystem": "^10.10.0|^11.0", + "illuminate/notifications": "^10.10.0|^11.0", + "illuminate/support": "^10.10.0|^11.0", "league/flysystem": "^3.0", "php": "^8.1", "spatie/db-dumper": "^3.0", "spatie/laravel-package-tools": "^1.6.2", - "spatie/laravel-signal-aware-command": "^1.2", + "spatie/laravel-signal-aware-command": "^1.2|^2.0", "spatie/temporary-directory": "^2.0", - "symfony/console": "^6.0", - "symfony/finder": "^6.0" + "symfony/console": "^6.0|^7.0", + "symfony/finder": "^6.0|^7.0" }, "require-dev": { "composer-runtime-api": "^2.0", "ext-pcntl": "*", - "laravel/slack-notification-channel": "^2.5", + "larastan/larastan": "^2.7.0", + "laravel/slack-notification-channel": "^2.5|^3.0", "league/flysystem-aws-s3-v3": "^2.0|^3.0", "mockery/mockery": "^1.4", - "nunomaduro/larastan": "^2.1", - "orchestra/testbench": "^8.0", - "pestphp/pest": "^1.20", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^1.20|^2.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.1" @@ -7503,7 +7505,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/8.4.1" + "source": "https://github.com/spatie/laravel-backup/tree/8.6.0" }, "funding": [ { @@ -7515,7 +7517,7 @@ "type": "other" } ], - "time": "2023-11-20T08:21:45+00:00" + "time": "2024-02-06T20:39:11+00:00" }, { "name": "spatie/laravel-image-optimizer", @@ -7587,20 +7589,20 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.16.1", + "version": "1.16.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "cc7c991555a37f9fa6b814aa03af73f88026a83d" + "reference": "e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/cc7c991555a37f9fa6b814aa03af73f88026a83d", - "reference": "cc7c991555a37f9fa6b814aa03af73f88026a83d", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15", + "reference": "e62eeb1fe8a8a0b2e83227a6c279c8c59f7d3a15", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0", + "illuminate/contracts": "^9.28|^10.0|^11.0", "php": "^8.0" }, "require-dev": { @@ -7635,7 +7637,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.1" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.2" }, "funding": [ { @@ -7643,7 +7645,7 @@ "type": "github" } ], - "time": "2023-08-23T09:04:39+00:00" + "time": "2024-01-11T08:43:00+00:00" }, { "name": "spatie/laravel-signal-aware-command", @@ -7721,16 +7723,16 @@ }, { "name": "spatie/temporary-directory", - "version": "2.2.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/spatie/temporary-directory.git", - "reference": "efc258c9f4da28f0c7661765b8393e4ccee3d19c" + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/efc258c9f4da28f0c7661765b8393e4ccee3d19c", - "reference": "efc258c9f4da28f0c7661765b8393e4ccee3d19c", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", "shasum": "" }, "require": { @@ -7766,7 +7768,7 @@ ], "support": { "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.2.0" + "source": "https://github.com/spatie/temporary-directory/tree/2.2.1" }, "funding": [ { @@ -7778,7 +7780,7 @@ "type": "github" } ], - "time": "2023-09-25T07:13:36+00:00" + "time": "2023-12-25T11:46:58+00:00" }, { "name": "spomky-labs/base64url", @@ -7913,16 +7915,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a" + "reference": "49f8cdee544a621a621cd21b6cda32a38926d310" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/ac2d25f97b17eec6e19760b6b9962a4f7c44356a", - "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a", + "url": "https://api.github.com/repos/symfony/cache/zipball/49f8cdee544a621a621cd21b6cda32a38926d310", + "reference": "49f8cdee544a621a621cd21b6cda32a38926d310", "shasum": "" }, "require": { @@ -7989,7 +7991,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.0" + "source": "https://github.com/symfony/cache/tree/v6.4.3" }, "funding": [ { @@ -8005,7 +8007,7 @@ "type": "tidelift" } ], - "time": "2023-11-24T19:28:07+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/cache-contracts", @@ -8085,16 +8087,16 @@ }, { "name": "symfony/console", - "version": "v6.4.1", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd" + "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a550a7c99daeedef3f9d23fb82e3531525ff11fd", - "reference": "a550a7c99daeedef3f9d23fb82e3531525ff11fd", + "url": "https://api.github.com/repos/symfony/console/zipball/2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", "shasum": "" }, "require": { @@ -8159,7 +8161,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.1" + "source": "https://github.com/symfony/console/tree/v6.4.3" }, "funding": [ { @@ -8175,20 +8177,20 @@ "type": "tidelift" } ], - "time": "2023-11-30T10:54:28+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/css-selector", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "d036c6c0d0b09e24a14a35f8292146a658f986e4" + "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/d036c6c0d0b09e24a14a35f8292146a658f986e4", - "reference": "d036c6c0d0b09e24a14a35f8292146a658f986e4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ee0f7ed5cf298cc019431bb3b3977ebc52b86229", + "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229", "shasum": "" }, "require": { @@ -8224,7 +8226,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.0" + "source": "https://github.com/symfony/css-selector/tree/v6.4.3" }, "funding": [ { @@ -8240,7 +8242,7 @@ "type": "tidelift" } ], - "time": "2023-10-31T08:40:20+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/deprecation-contracts", @@ -8311,16 +8313,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788" + "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/c873490a1c97b3a0a4838afc36ff36c112d02788", - "reference": "c873490a1c97b3a0a4838afc36ff36c112d02788", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/6dc3c76a278b77f01d864a6005d640822c6f26a6", + "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6", "shasum": "" }, "require": { @@ -8366,7 +8368,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.0" + "source": "https://github.com/symfony/error-handler/tree/v6.4.3" }, "funding": [ { @@ -8382,20 +8384,20 @@ "type": "tidelift" } ], - "time": "2023-10-18T09:43:34+00:00" + "time": "2024-01-29T15:40:36+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6" + "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d76d2632cfc2206eecb5ad2b26cd5934082941b6", - "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", "shasum": "" }, "require": { @@ -8446,7 +8448,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3" }, "funding": [ { @@ -8462,7 +8464,7 @@ "type": "tidelift" } ], - "time": "2023-07-27T06:52:43+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -8606,16 +8608,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "5c584530b77aa10ae216989ffc48b4bedc9c0b29" + "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/5c584530b77aa10ae216989ffc48b4bedc9c0b29", - "reference": "5c584530b77aa10ae216989ffc48b4bedc9c0b29", + "url": "https://api.github.com/repos/symfony/http-client/zipball/a9034bc119fab8238f76cf49c770f3135f3ead86", + "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86", "shasum": "" }, "require": { @@ -8679,7 +8681,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.0" + "source": "https://github.com/symfony/http-client/tree/v6.4.3" }, "funding": [ { @@ -8695,7 +8697,7 @@ "type": "tidelift" } ], - "time": "2023-11-28T20:55:58+00:00" + "time": "2024-01-29T15:01:07+00:00" }, { "name": "symfony/http-client-contracts", @@ -8777,16 +8779,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "44a6d39a9cc11e154547d882d5aac1e014440771" + "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/44a6d39a9cc11e154547d882d5aac1e014440771", - "reference": "44a6d39a9cc11e154547d882d5aac1e014440771", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5677bdf7cade4619cb17fc9e1e7b31ec392244a9", + "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9", "shasum": "" }, "require": { @@ -8834,7 +8836,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.0" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.3" }, "funding": [ { @@ -8850,20 +8852,20 @@ "type": "tidelift" } ], - "time": "2023-11-20T16:41:16+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.1", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "2953274c16a229b3933ef73a6898e18388e12e1b" + "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2953274c16a229b3933ef73a6898e18388e12e1b", - "reference": "2953274c16a229b3933ef73a6898e18388e12e1b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9c6ec4e543044f7568a53a76ab1484ecd30637a2", + "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2", "shasum": "" }, "require": { @@ -8947,7 +8949,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.1" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.3" }, "funding": [ { @@ -8963,20 +8965,20 @@ "type": "tidelift" } ], - "time": "2023-12-01T17:02:02+00:00" + "time": "2024-01-31T07:21:29+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba" + "reference": "74412c62f88a85a41b61f0b71ab0afcaad6f03ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", - "reference": "ca8dcf8892cdc5b4358ecf2528429bb5e706f7ba", + "url": "https://api.github.com/repos/symfony/mailer/zipball/74412c62f88a85a41b61f0b71ab0afcaad6f03ee", + "reference": "74412c62f88a85a41b61f0b71ab0afcaad6f03ee", "shasum": "" }, "require": { @@ -9027,7 +9029,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.0" + "source": "https://github.com/symfony/mailer/tree/v6.4.3" }, "funding": [ { @@ -9043,20 +9045,20 @@ "type": "tidelift" } ], - "time": "2023-11-12T18:02:22+00:00" + "time": "2024-01-29T15:01:07+00:00" }, { "name": "symfony/mailgun-mailer", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", - "reference": "72d2f72f2016e559d0152188bef5a5dc9ebf5ec7" + "reference": "96d23bb0e773ecfc3fb8d21cdabfbb3f4d6abf04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/72d2f72f2016e559d0152188bef5a5dc9ebf5ec7", - "reference": "72d2f72f2016e559d0152188bef5a5dc9ebf5ec7", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/96d23bb0e773ecfc3fb8d21cdabfbb3f4d6abf04", + "reference": "96d23bb0e773ecfc3fb8d21cdabfbb3f4d6abf04", "shasum": "" }, "require": { @@ -9096,7 +9098,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.0" + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.3" }, "funding": [ { @@ -9112,20 +9114,20 @@ "type": "tidelift" } ], - "time": "2023-11-06T17:20:05+00:00" + "time": "2024-01-29T15:01:07+00:00" }, { "name": "symfony/mime", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ca4f58b2ef4baa8f6cecbeca2573f88cd577d205" + "reference": "5017e0a9398c77090b7694be46f20eb796262a34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ca4f58b2ef4baa8f6cecbeca2573f88cd577d205", - "reference": "ca4f58b2ef4baa8f6cecbeca2573f88cd577d205", + "url": "https://api.github.com/repos/symfony/mime/zipball/5017e0a9398c77090b7694be46f20eb796262a34", + "reference": "5017e0a9398c77090b7694be46f20eb796262a34", "shasum": "" }, "require": { @@ -9180,7 +9182,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.0" + "source": "https://github.com/symfony/mime/tree/v6.4.3" }, "funding": [ { @@ -9196,20 +9198,20 @@ "type": "tidelift" } ], - "time": "2023-10-17T11:49:05+00:00" + "time": "2024-01-30T08:32:12+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -9223,9 +9225,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9262,7 +9261,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -9278,20 +9277,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", "shasum": "" }, "require": { @@ -9302,9 +9301,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9343,7 +9339,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" }, "funding": [ { @@ -9359,20 +9355,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", "shasum": "" }, "require": { @@ -9385,9 +9381,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9430,7 +9423,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" }, "funding": [ { @@ -9446,20 +9439,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:30:37+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", "shasum": "" }, "require": { @@ -9470,9 +9463,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9514,7 +9504,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" }, "funding": [ { @@ -9530,20 +9520,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -9557,9 +9547,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9597,7 +9584,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -9613,20 +9600,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", "shasum": "" }, "require": { @@ -9634,9 +9621,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9673,7 +9657,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" }, "funding": [ { @@ -9689,20 +9673,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -9710,9 +9694,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9756,7 +9737,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -9772,20 +9753,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", "shasum": "" }, "require": { @@ -9794,9 +9775,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9836,7 +9814,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" }, "funding": [ { @@ -9852,20 +9830,20 @@ "type": "tidelift" } ], - "time": "2023-08-16T06:22:46+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e" + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e", - "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", "shasum": "" }, "require": { @@ -9879,9 +9857,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -9918,7 +9893,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" }, "funding": [ { @@ -9934,20 +9909,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/process", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa" + "reference": "31642b0818bfcff85930344ef93193f8c607e0a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/191703b1566d97a5425dc969e4350d32b8ef17aa", - "reference": "191703b1566d97a5425dc969e4350d32b8ef17aa", + "url": "https://api.github.com/repos/symfony/process/zipball/31642b0818bfcff85930344ef93193f8c607e0a3", + "reference": "31642b0818bfcff85930344ef93193f8c607e0a3", "shasum": "" }, "require": { @@ -9979,7 +9954,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.0" + "source": "https://github.com/symfony/process/tree/v6.4.3" }, "funding": [ { @@ -9995,7 +9970,7 @@ "type": "tidelift" } ], - "time": "2023-11-17T21:06:49+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -10088,16 +10063,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.1", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40" + "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/0c95c164fdba18b12523b75e64199ca3503e6d40", - "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40", + "url": "https://api.github.com/repos/symfony/routing/zipball/3b2957ad54902f0f544df83e3d58b38d7e8e5842", + "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842", "shasum": "" }, "require": { @@ -10151,7 +10126,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.1" + "source": "https://github.com/symfony/routing/tree/v6.4.3" }, "funding": [ { @@ -10167,25 +10142,25 @@ "type": "tidelift" } ], - "time": "2023-12-01T14:54:37+00:00" + "time": "2024-01-30T13:55:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.0", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", - "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^2.0" + "psr/container": "^1.1|^2.0" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -10233,7 +10208,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" }, "funding": [ { @@ -10249,20 +10224,20 @@ "type": "tidelift" } ], - "time": "2023-07-30T20:28:31+00:00" + "time": "2023-12-26T14:02:43+00:00" }, { "name": "symfony/string", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809" + "reference": "7a14736fb179876575464e4658fce0c304e8c15b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/b45fcf399ea9c3af543a92edf7172ba21174d809", - "reference": "b45fcf399ea9c3af543a92edf7172ba21174d809", + "url": "https://api.github.com/repos/symfony/string/zipball/7a14736fb179876575464e4658fce0c304e8c15b", + "reference": "7a14736fb179876575464e4658fce0c304e8c15b", "shasum": "" }, "require": { @@ -10319,7 +10294,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.0" + "source": "https://github.com/symfony/string/tree/v6.4.3" }, "funding": [ { @@ -10335,20 +10310,20 @@ "type": "tidelift" } ], - "time": "2023-11-28T20:41:49+00:00" + "time": "2024-01-25T09:26:29+00:00" }, { "name": "symfony/translation", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37" + "reference": "637c51191b6b184184bbf98937702bcf554f7d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", - "reference": "b1035dbc2a344b21f8fa8ac451c7ecec4ea45f37", + "url": "https://api.github.com/repos/symfony/translation/zipball/637c51191b6b184184bbf98937702bcf554f7d04", + "reference": "637c51191b6b184184bbf98937702bcf554f7d04", "shasum": "" }, "require": { @@ -10371,7 +10346,7 @@ "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", "symfony/config": "^5.4|^6.0|^7.0", "symfony/console": "^5.4|^6.0|^7.0", @@ -10414,7 +10389,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.0" + "source": "https://github.com/symfony/translation/tree/v6.4.3" }, "funding": [ { @@ -10430,20 +10405,20 @@ "type": "tidelift" } ], - "time": "2023-11-29T08:14:36+00:00" + "time": "2024-01-29T13:11:52+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.4.0", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5" + "reference": "06450585bf65e978026bda220cdebca3f867fde7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dee0c6e5b4c07ce851b462530088e64b255ac9c5", - "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/06450585bf65e978026bda220cdebca3f867fde7", + "reference": "06450585bf65e978026bda220cdebca3f867fde7", "shasum": "" }, "require": { @@ -10492,7 +10467,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.1" }, "funding": [ { @@ -10508,20 +10483,20 @@ "type": "tidelift" } ], - "time": "2023-07-25T15:08:44+00:00" + "time": "2023-12-26T14:02:43+00:00" }, { "name": "symfony/uid", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92" + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/8092dd1b1a41372110d06374f99ee62f7f0b9a92", - "reference": "8092dd1b1a41372110d06374f99ee62f7f0b9a92", + "url": "https://api.github.com/repos/symfony/uid/zipball/1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", "shasum": "" }, "require": { @@ -10566,7 +10541,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.4.0" + "source": "https://github.com/symfony/uid/tree/v6.4.3" }, "funding": [ { @@ -10582,20 +10557,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T08:18:17+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.0", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6" + "reference": "0435a08f69125535336177c29d56af3abc1f69da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c40f7d17e91d8b407582ed51a2bbf83c52c367f6", - "reference": "c40f7d17e91d8b407582ed51a2bbf83c52c367f6", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da", + "reference": "0435a08f69125535336177c29d56af3abc1f69da", "shasum": "" }, "require": { @@ -10651,7 +10626,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.0" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.3" }, "funding": [ { @@ -10667,20 +10642,20 @@ "type": "tidelift" } ], - "time": "2023-11-09T08:28:32+00:00" + "time": "2024-01-23T14:53:30+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.1", + "version": "v6.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "2d08ca6b9cc704dce525615d1e6d1788734f36d9" + "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/2d08ca6b9cc704dce525615d1e6d1788734f36d9", - "reference": "2d08ca6b9cc704dce525615d1e6d1788734f36d9", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", + "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", "shasum": "" }, "require": { @@ -10726,7 +10701,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.1" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.3" }, "funding": [ { @@ -10742,7 +10717,7 @@ "type": "tidelift" } ], - "time": "2023-11-30T10:32:10+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "tightenco/collect", @@ -10800,23 +10775,23 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.6", + "version": "v2.2.7", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/83ee6f38df0a63106a9e4536e3060458b74ccedb", + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" @@ -10847,9 +10822,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.2.7" }, - "time": "2023-01-03T09:29:04+00:00" + "time": "2023-12-08T13:03:43+00:00" }, { "name": "vlucas/phpdotenv", @@ -11312,20 +11287,20 @@ }, { "name": "web-token/jwt-util-ecc", - "version": "3.2.8", + "version": "3.2.10", "source": { "type": "git", "url": "https://github.com/web-token/jwt-util-ecc.git", - "reference": "b2337052dbee724d710c1fdb0d3609835a5f8609" + "reference": "9edf9b76bccf2e1db58fcc49db1d916d929335c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-util-ecc/zipball/b2337052dbee724d710c1fdb0d3609835a5f8609", - "reference": "b2337052dbee724d710c1fdb0d3609835a5f8609", + "url": "https://api.github.com/repos/web-token/jwt-util-ecc/zipball/9edf9b76bccf2e1db58fcc49db1d916d929335c0", + "reference": "9edf9b76bccf2e1db58fcc49db1d916d929335c0", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11", + "brick/math": "^0.9|^0.10|^0.11|^0.12", "php": ">=8.1" }, "suggest": { @@ -11373,7 +11348,7 @@ "symfony" ], "support": { - "source": "https://github.com/web-token/jwt-util-ecc/tree/3.2.8" + "source": "https://github.com/web-token/jwt-util-ecc/tree/3.2.10" }, "funding": [ { @@ -11381,7 +11356,7 @@ "type": "patreon" } ], - "time": "2023-02-02T13:35:41+00:00" + "time": "2024-01-02T17:55:33+00:00" }, { "name": "webmozart/assert", @@ -11607,16 +11582,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.23.0", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01" + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", "shasum": "" }, "require": { @@ -11642,11 +11617,6 @@ "ext-mbstring": "Required for multibyte Unicode string functionality." }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "v1.21-dev" - } - }, "autoload": { "psr-4": { "Faker\\": "src/Faker/" @@ -11669,22 +11639,22 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" }, - "time": "2023-06-12T08:44:38+00:00" + "time": "2024-01-02T13:46:09+00:00" }, { "name": "fidry/cpu-core-counter", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077" + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/85193c0b0cb5c47894b5eaec906e946f054e7077", - "reference": "85193c0b0cb5c47894b5eaec906e946f054e7077", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", "shasum": "" }, "require": { @@ -11724,7 +11694,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.0.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" }, "funding": [ { @@ -11732,7 +11702,7 @@ "type": "github" } ], - "time": "2023-09-17T21:38:23+00:00" + "time": "2024-02-07T09:43:46+00:00" }, { "name": "filp/whoops", @@ -11917,16 +11887,16 @@ }, { "name": "laravel/telescope", - "version": "v4.17.2", + "version": "v4.17.6", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "64da53ee46b99ef328458eaed32202b51e325a11" + "reference": "2d453dc629b27e8cf39fb1217aba062f8c54e690" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/64da53ee46b99ef328458eaed32202b51e325a11", - "reference": "64da53ee46b99ef328458eaed32202b51e325a11", + "url": "https://api.github.com/repos/laravel/telescope/zipball/2d453dc629b27e8cf39fb1217aba062f8c54e690", + "reference": "2d453dc629b27e8cf39fb1217aba062f8c54e690", "shasum": "" }, "require": { @@ -11982,22 +11952,22 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v4.17.2" + "source": "https://github.com/laravel/telescope/tree/v4.17.6" }, - "time": "2023-11-01T14:01:06+00:00" + "time": "2024-02-08T15:04:38+00:00" }, { "name": "mockery/mockery", - "version": "1.6.6", + "version": "1.6.7", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e" + "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e", - "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e", + "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", + "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", "shasum": "" }, "require": { @@ -12010,9 +11980,7 @@ }, "require-dev": { "phpunit/phpunit": "^8.5 || ^9.6.10", - "psalm/plugin-phpunit": "^0.18.4", - "symplify/easy-coding-standard": "^11.5.0", - "vimeo/psalm": "^4.30" + "symplify/easy-coding-standard": "^12.0.8" }, "type": "library", "autoload": { @@ -12069,7 +12037,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2023-08-09T00:03:52+00:00" + "time": "2023-12-10T02:24:34+00:00" }, { "name": "myclabs/deep-copy", @@ -12331,23 +12299,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -12397,7 +12365,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -12405,7 +12373,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -12650,16 +12618,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.15", + "version": "9.6.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", "shasum": "" }, "require": { @@ -12733,7 +12701,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" }, "funding": [ { @@ -12749,7 +12717,7 @@ "type": "tidelift" } ], - "time": "2023-12-01T16:55:19+00:00" + "time": "2024-01-19T07:03:14+00:00" }, { "name": "sebastian/cli-parser", @@ -12994,20 +12962,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -13039,7 +13007,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -13047,7 +13015,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -13321,20 +13289,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -13366,7 +13334,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -13374,7 +13342,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -13772,7 +13740,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1|^8.2", + "php": "^8.1|^8.2|^8.3", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", From ea6b162340a32b90a78930287b85251d2a4de7e6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 18:01:14 -0700 Subject: [PATCH 416/977] Update cache config, use predis as default redis driver client --- config/cache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cache.php b/config/cache.php index 452e3dd4d..88129848e 100644 --- a/config/cache.php +++ b/config/cache.php @@ -74,7 +74,7 @@ return [ 'redis' => [ 'driver' => 'redis', 'lock_connection' => 'default', - 'client' => env('REDIS_CLIENT', 'phpredis'), + 'client' => env('REDIS_CLIENT', 'predis'), 'default' => [ 'scheme' => env('REDIS_SCHEME', 'tcp'), From 147113cc95c155db6d6dceeb8fe6cdbd6c872191 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 18:02:08 -0700 Subject: [PATCH 417/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cd2813b..ab92cff4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad)) - Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4)) +- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 545f7d5e705547f94c928c40057f618fdbb032d3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 22:14:23 -0700 Subject: [PATCH 418/977] Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 --- app/Http/Controllers/Api/ApiV1Controller.php | 2 +- app/Http/Controllers/Api/ApiV2Controller.php | 124 ++++++++++--------- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 6114c6566..a4908b0f0 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1612,7 +1612,7 @@ class ApiV1Controller extends Controller 'short_description' => config_cache('app.short_description'), 'description' => config_cache('app.description'), 'email' => config('instance.email'), - 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')', + 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')', 'urls' => [ 'streaming_api' => 'wss://' . config('pixelfed.domain.app') ], diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 2380588cf..23a122397 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -71,72 +71,76 @@ class ApiV2Controller extends Controller ->toArray() : []; }); - $res = [ - 'domain' => config('pixelfed.domain.app'), - 'title' => config_cache('app.name'), - 'version' => config('pixelfed.version'), - 'source_url' => 'https://github.com/pixelfed/pixelfed', - 'description' => config_cache('app.short_description'), - 'usage' => [ - 'users' => [ - 'active_month' => (int) Nodeinfo::activeUsersMonthly() - ] - ], - 'thumbnail' => [ - 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - 'blurhash' => InstanceService::headerBlurhash(), - 'versions' => [ - '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), - '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')) - ] - ], - 'languages' => [config('app.locale')], - 'configuration' => [ - 'urls' => [ - 'streaming' => 'wss://' . config('pixelfed.domain.app'), - 'status' => null + $res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) { + return [ + 'domain' => config('pixelfed.domain.app'), + 'title' => config_cache('app.name'), + 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')', + 'source_url' => 'https://github.com/pixelfed/pixelfed', + 'description' => config_cache('app.short_description'), + 'usage' => [ + 'users' => [ + 'active_month' => (int) Nodeinfo::activeUsersMonthly() + ] ], - 'vapid' => [ - 'public_key' => config('webpush.vapid.public_key'), + 'thumbnail' => [ + 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + 'blurhash' => InstanceService::headerBlurhash(), + 'versions' => [ + '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), + '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')) + ] ], - 'accounts' => [ - 'max_featured_tags' => 0, + 'languages' => [config('app.locale')], + 'configuration' => [ + 'urls' => [ + 'streaming' => null, + 'status' => null + ], + 'vapid' => [ + 'public_key' => config('webpush.vapid.public_key'), + ], + 'accounts' => [ + 'max_featured_tags' => 0, + ], + 'statuses' => [ + 'max_characters' => (int) config('pixelfed.max_caption_length'), + 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), + 'characters_reserved_per_url' => 23 + ], + 'media_attachments' => [ + 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), + 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'image_matrix_limit' => 3686400, + 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, + 'video_frame_rate_limit' => 240, + 'video_matrix_limit' => 3686400 + ], + 'polls' => [ + 'max_options' => 0, + 'max_characters_per_option' => 0, + 'min_expiration' => 0, + 'max_expiration' => 0, + ], + 'translation' => [ + 'enabled' => false, + ], ], - 'statuses' => [ - 'max_characters' => (int) config('pixelfed.max_caption_length'), - 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), - 'characters_reserved_per_url' => 23 + 'registrations' => [ + 'enabled' => null, + 'approval_required' => false, + 'message' => null, + 'url' => null, ], - 'media_attachments' => [ - 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), - 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'image_matrix_limit' => 3686400, - 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, - 'video_frame_rate_limit' => 240, - 'video_matrix_limit' => 3686400 + 'contact' => [ + 'email' => config('instance.email'), + 'account' => $contact ], - 'polls' => [ - 'max_options' => 4, - 'max_characters_per_option' => 50, - 'min_expiration' => 300, - 'max_expiration' => 2629746, - ], - 'translation' => [ - 'enabled' => false, - ], - ], - 'registrations' => [ - 'enabled' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, - 'message' => null - ], - 'contact' => [ - 'email' => config('instance.email'), - 'account' => $contact - ], - 'rules' => $rules - ]; + 'rules' => $rules + ]; + }); + $res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration'); return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } From ce9c0e0b248c24fdee43ddd1df6683746dbaba46 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Feb 2024 22:16:46 -0700 Subject: [PATCH 419/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab92cff4a..f13c984e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad)) - Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4)) - Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623)) +- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9)) +- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 1f74a95d0c799f11ba4b6ea4933a8a8f99082e6d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 01:36:09 -0700 Subject: [PATCH 420/977] Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max --- app/Http/Controllers/Api/ApiV1Controller.php | 169 ++++++++++++++----- 1 file changed, 127 insertions(+), 42 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index a4908b0f0..55ecb25e2 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -496,9 +496,12 @@ class ApiV1Controller extends Controller abort_if(!$account, 404); $pid = $request->user()->profile_id; $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' + 'limit' => 'sometimes|integer|min:1' ]); $limit = $request->input('limit', 10); + if($limit > 80) { + $limit = 80; + } $napi = $request->has(self::PF_API_ENTITY_KEY); if($account && strpos($account['acct'], '@') != -1) { @@ -594,9 +597,12 @@ class ApiV1Controller extends Controller abort_if(!$account, 404); $pid = $request->user()->profile_id; $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:80' + 'limit' => 'sometimes|integer|min:1' ]); $limit = $request->input('limit', 10); + if($limit > 80) { + $limit = 80; + } $napi = $request->has(self::PF_API_ENTITY_KEY); if($account && strpos($account['acct'], '@') != -1) { @@ -698,7 +704,7 @@ class ApiV1Controller extends Controller 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|min:1|max:100' + 'limit' => 'nullable|integer|min:1' ]); $napi = $request->has(self::PF_API_ENTITY_KEY); @@ -713,7 +719,10 @@ class ApiV1Controller extends Controller abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); } - $limit = $request->limit ?? 20; + $limit = $request->input('limit') ?? 20; + if($limit > 40) { + $limit = 40; + } $max_id = $request->max_id; $min_id = $request->min_id; @@ -959,12 +968,16 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $this->validate($request, [ - 'id' => 'required|array|min:1|max:20', + 'id' => 'required|array|min:1', 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX ]); + $ids = $request->input('id'); + if(count($ids) > 20) { + $ids = collect($ids)->take(20)->toArray(); + } $napi = $request->has(self::PF_API_ENTITY_KEY); $pid = $request->user()->profile_id ?? $request->user()->profile->id; - $res = collect($request->input('id')) + $res = collect($ids) ->filter(function($id) use($pid) { return intval($id) !== intval($pid); }) @@ -989,8 +1002,8 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'q' => 'required|string|min:1|max:255', - 'limit' => 'nullable|integer|min:1|max:40', + 'q' => 'required|string|min:1|max:30', + 'limit' => 'nullable|integer|min:1', 'resolve' => 'nullable' ]); @@ -1000,22 +1013,23 @@ class ApiV1Controller extends Controller AccountService::setLastActive($user->id); $query = $request->input('q'); $limit = $request->input('limit') ?? 20; - $resolve = (bool) $request->input('resolve', false); - $q = '%' . $query . '%'; + if($limit > 20) { + $limit = 20; + } + $resolve = $request->boolean('resolve', false); + $q = $query . '%'; - $profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { - return Profile::whereNull('status') - ->where('username', 'like', $q) - ->orWhere('name', 'like', $q) - ->limit($limit) - ->pluck('id') - ->map(function($id) { - return AccountService::getMastodon($id); - }) - ->filter(function($account) { - return $account && isset($account['id']); - }); - }); + $profiles = Profile::where('username', 'like', $q) + ->orderByDesc('followers_count') + ->limit($limit) + ->pluck('id') + ->map(function($id) { + return AccountService::getMastodon($id); + }) + ->filter(function($account) { + return $account && isset($account['id']); + }) + ->values(); return $this->json($profiles); } @@ -1033,20 +1047,25 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', - 'page' => 'nullable|integer|min:1|max:10' + 'limit' => 'sometimes|integer|min:1', + 'page' => 'sometimes|integer|min:1' ]); $user = $request->user(); $limit = $request->input('limit') ?? 40; + if($limit > 80) { + $limit = 80; + } - $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') + $blocks = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') ->whereUserId($user->profile_id) ->whereFilterableType('App\Profile') ->whereFilterType('block') ->orderByDesc('id') ->simplePaginate($limit) - ->pluck('filterable_id') + ->withQueryString(); + + $res = $blocks->pluck('filterable_id') ->map(function($id) { return AccountService::get($id, true); }) @@ -1055,7 +1074,23 @@ class ApiV1Controller extends Controller }) ->values(); - return $this->json($blocked); + $baseUrl = config('app.url') . '/api/v1/blocks?limit=' . $limit . '&'; + $next = $blocks->nextPageUrl(); + $prev = $blocks->previousPageUrl(); + + if($next && !$prev) { + $link = '<'.$next.'>; rel="next"'; + } + + if(!$next && $prev) { + $link = '<'.$prev.'>; rel="prev"'; + } + + if($next && $prev) { + $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"'; + } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res, 200, $headers); } /** @@ -1247,13 +1282,16 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'sometimes|integer|min:1|max:40' + 'limit' => 'sometimes|integer|min:1' ]); $user = $request->user(); $maxId = $request->input('max_id'); $minId = $request->input('min_id'); $limit = $request->input('limit') ?? 10; + if($limit > 40) { + $limit = 40; + } $res = Like::whereProfileId($user->profile_id) ->when($maxId, function($q, $maxId) { @@ -1620,7 +1658,7 @@ class ApiV1Controller extends Controller 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'languages' => [config('app.locale')], 'registrations' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, + 'approval_required' => false, // (bool) config_cache('instance.curated_registration.enabled'), 'contact_account' => $contact, 'rules' => $rules, 'configuration' => [ @@ -2049,18 +2087,23 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40' + 'limit' => 'sometimes|integer|min:1' ]); $user = $request->user(); $limit = $request->input('limit', 40); + if($limit > 80) { + $limit = 80; + } $mutes = UserFilter::whereUserId($user->profile_id) ->whereFilterableType('App\Profile') ->whereFilterType('mute') ->orderByDesc('id') ->simplePaginate($limit) - ->pluck('filterable_id') + ->withQueryString(); + + $res = $mutes->pluck('filterable_id') ->map(function($id) { return AccountService::get($id, true); }) @@ -2069,7 +2112,23 @@ class ApiV1Controller extends Controller }) ->values(); - return $this->json($mutes); + $baseUrl = config('app.url') . '/api/v1/mutes?limit=' . $limit . '&'; + $next = $mutes->nextPageUrl(); + $prev = $mutes->previousPageUrl(); + + if($next && !$prev) { + $link = '<'.$next.'>; rel="next"'; + } + + if(!$next && $prev) { + $link = '<'.$prev.'>; rel="prev"'; + } + + if($next && $prev) { + $link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"'; + } + $headers = isset($link) ? ['Link' => $link] : []; + return $this->json($res, 200, $headers); } /** @@ -2181,7 +2240,7 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:100', + 'limit' => 'sometimes|integer|min:1', 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, @@ -2191,6 +2250,9 @@ class ApiV1Controller extends Controller $pid = $request->user()->profile_id; $limit = $request->input('limit', 20); + if($limit > 40) { + $limit = 40; + } $since = $request->input('since_id'); $min = $request->input('min_id'); @@ -2200,6 +2262,10 @@ class ApiV1Controller extends Controller $min = 1; } + if($since) { + $min = $since + 1; + } + $types = $request->input('types'); $maxId = null; @@ -2261,7 +2327,7 @@ class ApiV1Controller extends Controller 'page' => 'sometimes|integer|max:40', 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'sometimes|integer|min:1|max:40', + 'limit' => 'sometimes|integer|min:1', 'include_reblogs' => 'sometimes', ]); @@ -2270,6 +2336,9 @@ class ApiV1Controller extends Controller $min = $request->input('min_id'); $max = $request->input('max_id'); $limit = $request->input('limit') ?? 20; + if($limit > 40) { + $limit = 40; + } $pid = $request->user()->profile_id; $includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false; $nullFields = $includeReblogs ? @@ -2515,7 +2584,7 @@ class ApiV1Controller extends Controller $this->validate($request,[ 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', + 'limit' => 'sometimes|integer|min:1', 'remote' => 'sometimes', 'local' => 'sometimes' ]); @@ -2525,6 +2594,9 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $minOrMax = $request->anyFilled(['max_id', 'min_id']); $limit = $request->input('limit') ?? 20; + if($limit > 40) { + $limit = 40; + } $user = $request->user(); $remote = $request->has('remote'); @@ -3043,10 +3115,13 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:80' + 'limit' => 'sometimes|integer|min:1' ]); - $limit = $request->input('limit', 10); + $limit = $request->input('limit', 40); + if($limit > 80) { + $limit = 80; + } $user = $request->user(); $pid = $user->profile_id; $status = Status::findOrFail($id); @@ -3485,7 +3560,7 @@ class ApiV1Controller extends Controller 'page' => 'nullable|integer|max:40', 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, - 'limit' => 'nullable|integer|max:100', + 'limit' => 'sometimes|integer|min:1', 'only_media' => 'sometimes|boolean', '_pe' => 'sometimes' ]); @@ -3518,6 +3593,9 @@ class ApiV1Controller extends Controller $min = $request->input('min_id'); $max = $request->input('max_id'); $limit = $request->input('limit', 20); + if($limit > 40) { + $limit = 40; + } $onlyMedia = $request->input('only_media', true); $pe = $request->has(self::PF_API_ENTITY_KEY); $pid = $request->user()->profile_id; @@ -3547,7 +3625,7 @@ class ApiV1Controller extends Controller ->whereStatusVisibility('public') ->where('status_id', $dir, $id) ->orderBy('status_id', 'desc') - ->limit($limit) + ->limit(100) ->pluck('status_id') ->map(function ($i) use($pe) { return $pe ? StatusService::get($i) : StatusService::getMastodon($i); @@ -3565,6 +3643,7 @@ class ApiV1Controller extends Controller $domain = strtolower(parse_url($i['url'], PHP_URL_HOST)); return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks); }) + ->take($limit) ->values() ->toArray(); @@ -3584,7 +3663,7 @@ class ApiV1Controller extends Controller abort_unless($request->user()->tokenCan('read'), 403); $this->validate($request, [ - 'limit' => 'nullable|integer|min:1|max:40', + 'limit' => 'sometimes|integer|min:1', 'max_id' => 'nullable|integer|min:0', 'since_id' => 'nullable|integer|min:0', 'min_id' => 'nullable|integer|min:0' @@ -3593,6 +3672,9 @@ class ApiV1Controller extends Controller $pe = $request->has('_pe'); $pid = $request->user()->profile_id; $limit = $request->input('limit') ?? 20; + if($limit > 40) { + $limit = 40; + } $max_id = $request->input('max_id'); $since_id = $request->input('since_id'); $min_id = $request->input('min_id'); @@ -3758,11 +3840,14 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $this->validate($request, [ - 'limit' => 'int|min:1|max:10', + 'limit' => 'sometimes|integer|min:1', 'sort' => 'in:all,newest,popular' ]); $limit = $request->input('limit', 3); + if($limit > 10) { + $limit = 10; + } $pid = $request->user()->profile_id; $status = StatusService::getMastodon($id, false); From eb0e76f8e279f80fd4fb760b5d39ae18abbf7cff Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 01:39:52 -0700 Subject: [PATCH 421/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f13c984e0..b4a2cb6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623)) - Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9)) - Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e)) +- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From b6c97d1d2663ca831d78dd63500c6422ce3361b1 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 01:46:45 -0700 Subject: [PATCH 422/977] Update npm deps --- package-lock.json | 1543 ++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 836 insertions(+), 709 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15443b198..0b34d7765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ }, "devDependencies": { "acorn": "^8.7.1", - "axios": "^0.21.1", + "axios": ">=1.6.0", "bootstrap": "^4.5.2", "cross-env": "^5.2.1", "jquery": "^3.6.0", @@ -83,11 +83,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -151,28 +151,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -196,11 +196,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -232,13 +232,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -255,16 +255,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", + "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -309,9 +309,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -377,9 +377,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -479,9 +479,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } @@ -495,9 +495,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } @@ -516,22 +516,22 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -598,9 +598,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "bin": { "parser": "bin/babel-parser.js" }, @@ -609,9 +609,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -623,13 +623,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -638,6 +638,21 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", @@ -727,9 +742,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -741,9 +756,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -886,9 +901,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -900,9 +915,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", - "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", @@ -917,13 +932,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -933,9 +948,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -947,9 +962,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -961,11 +976,11 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -976,11 +991,11 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -992,17 +1007,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1014,12 +1028,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1029,9 +1043,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1043,11 +1057,11 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1058,9 +1072,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1072,9 +1086,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -1087,11 +1101,11 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1102,9 +1116,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -1117,11 +1131,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1131,12 +1146,12 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1147,9 +1162,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -1162,9 +1177,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1176,9 +1191,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -1191,9 +1206,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1205,11 +1220,11 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1220,11 +1235,11 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1236,12 +1251,12 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, @@ -1253,11 +1268,11 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1283,9 +1298,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1297,9 +1312,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -1312,9 +1327,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -1327,15 +1342,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dependencies": { - "@babel/compat-data": "^7.22.9", + "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1345,12 +1360,12 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1360,9 +1375,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -1375,9 +1390,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -1391,9 +1406,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1405,11 +1420,11 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1420,12 +1435,12 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1437,9 +1452,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1451,9 +1466,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "regenerator-transform": "^0.15.2" @@ -1466,9 +1481,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1480,15 +1495,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz", - "integrity": "sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", + "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "engines": { @@ -1507,9 +1522,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1521,9 +1536,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" @@ -1536,9 +1551,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1550,9 +1565,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1564,9 +1579,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1578,9 +1593,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1592,11 +1607,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1607,11 +1622,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1622,11 +1637,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1637,24 +1652,25 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", - "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", "dependencies": { - "@babel/compat-data": "^7.23.2", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1666,59 +1682,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.23.2", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.23.0", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.23.0", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-modules-systemjs": "^7.23.0", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.23.0", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1756,9 +1771,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1767,32 +1782,32 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1800,11 +1815,11 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -1859,9 +1874,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } @@ -1889,9 +1904,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1952,9 +1967,9 @@ } }, "node_modules/@peertube/p2p-media-loader-core": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz", - "integrity": "sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.15.tgz", + "integrity": "sha512-RgUi3v4jT1+/8TEHj9NWCXVJW+p/m8fFJtcvh6FQxXZpz4u24pYbMFuM60gfboItuEA7xqo5C3XN8Y4kmOyXmw==", "dependencies": { "bittorrent-tracker": "^9.19.0", "debug": "^4.3.4", @@ -1964,11 +1979,11 @@ } }, "node_modules/@peertube/p2p-media-loader-hlsjs": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz", - "integrity": "sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.15.tgz", + "integrity": "sha512-A2GkuSHqXTjVMglx5rXwsEYgopE/yCyc7lrESdOhtpSx41tWYF7Oad37jyITfb2rTYqh2Mr2/b5iFOo4EgyAbg==", "dependencies": { - "@peertube/p2p-media-loader-core": "^1.0.14", + "@peertube/p2p-media-loader-core": "^1.0.15", "debug": "^4.3.4", "events": "^3.3.0", "m3u8-parser": "^4.7.1" @@ -1988,9 +2003,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", - "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -2000,100 +2015,100 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", - "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", - "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", - "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", - "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bonjour": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.12.tgz", - "integrity": "sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/clean-css": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.9.tgz", - "integrity": "sha512-pjzJ4n5eAXAz/L5Zur4ZymuJUvyo0Uh0iRnRI/1kADFLs76skDky0K0dX1rlv4iXXrJXNk3sxRWVJR7CMDroWA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", + "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "node_modules/@types/connect": { - "version": "3.4.37", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", - "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz", - "integrity": "sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", - "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", - "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", - "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/express": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", - "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2112,9 +2127,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.17.39", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", - "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2132,46 +2147,46 @@ } }, "node_modules/@types/http-errors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", - "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/http-proxy": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.13.tgz", - "integrity": "sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/imagemin": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.3.tgz", - "integrity": "sha512-se/hpaYxu5DyvPqmUEwbupmbQSx6JNislk0dkoIgWSmArkj+Ow9pGG9pGz8MRmbQDfGNYNzqwPQKHCUy+K+jpQ==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz", + "integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/imagemin-gifsicle": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-gifsicle/-/imagemin-gifsicle-7.0.3.tgz", - "integrity": "sha512-GQBKOk9doOd0Xp7OvO4QDl7U0Vkwk2Ps7J0rxafdAa7wG9lu7idvZTm8TtSZiRtHENdkW88Kz8OjmjMlgeeC5w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-gifsicle/-/imagemin-gifsicle-7.0.4.tgz", + "integrity": "sha512-ZghMBd/Jgqg5utTJNPmvf6DkuHzMhscJ8vgf/7MUGCpO+G+cLrhYltL+5d+h3A1B4W73S2SrmJZ1jS5LACpX+A==", "dependencies": { "@types/imagemin": "*" } }, "node_modules/@types/imagemin-mozjpeg": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-mozjpeg/-/imagemin-mozjpeg-8.0.3.tgz", - "integrity": "sha512-+U/ibETP2/oRqeuaaXa67dEpKHfzmfK0OBVC09AR4c1CIFAKjQ5xY+dxH+fjoMQRlwdcRQLkn/ALtnxSl3Xsqw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-mozjpeg/-/imagemin-mozjpeg-8.0.4.tgz", + "integrity": "sha512-ZCAxV8SYJB8ehwHpnbRpHjg5Wc4HcyuAMiDhXbkgC7gujDoOTyHO3dhDkUtZ1oK1DLBRZapqG9etdLVhUml7yQ==", "dependencies": { "@types/imagemin": "*" } }, "node_modules/@types/imagemin-optipng": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/imagemin-optipng/-/imagemin-optipng-5.2.3.tgz", - "integrity": "sha512-Q80ANbJYn+WgKkWVfx9f7/q4LR6qun4NIiuV1eRWCg8KCAmNrU7ZH16a2hGs9kfkFqyJlhBv6oV9SDXe1vL3aQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@types/imagemin-optipng/-/imagemin-optipng-5.2.4.tgz", + "integrity": "sha512-mvKnDMC8eCYZetAQudjs1DbgpR84WhsTx1wgvdiXnpuUEti3oJ+MaMYBRWPY0JlQ4+y4TXKOfa7+LOuT8daegQ==", "dependencies": { "@types/imagemin": "*" } @@ -2186,14 +2201,14 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/mime": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.3.tgz", - "integrity": "sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz", + "integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==" }, "node_modules/@types/minimatch": { "version": "5.1.2", @@ -2201,27 +2216,35 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" }, "node_modules/@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dependencies": { - "undici-types": "~5.25.1" + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dependencies": { + "@types/node": "*" } }, "node_modules/@types/parse-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", - "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/qs": { - "version": "6.9.9", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", - "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==" + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" }, "node_modules/@types/range-parser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", - "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/retry": { "version": "0.12.0", @@ -2229,31 +2252,31 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, "node_modules/@types/send": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", - "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/send/node_modules/@types/mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", - "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/serve-index": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.3.tgz", - "integrity": "sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", - "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -2261,9 +2284,9 @@ } }, "node_modules/@types/sockjs": { - "version": "0.3.35", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.35.tgz", - "integrity": "sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dependencies": { "@types/node": "*" } @@ -2274,9 +2297,9 @@ "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==" }, "node_modules/@types/ws": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", - "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dependencies": { "@types/node": "*" } @@ -2296,13 +2319,16 @@ } }, "node_modules/@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", "dependencies": { - "@babel/parser": "^7.18.4", + "@babel/parser": "^7.23.5", "postcss": "^8.4.14", "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" } }, "node_modules/@vue/component-compiler-utils": { @@ -2543,9 +2569,9 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "node_modules/@zip.js/zip.js": { - "version": "2.7.30", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.30.tgz", - "integrity": "sha512-nhMvQCj+TF1ATBqYzFds7v+yxPBhdDYHh8J341KtC1D2UrVBUIYcYK4Jy1/GiTsxOXEiKOXSUxvPG/XR+7jMqw==", + "version": "2.7.34", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.34.tgz", + "integrity": "sha512-SWAK+hLYKRHswhakNUirPYrdsflSFOxykUckfbWDcPvP8tjLuV5EWyd3GHV0hVaJLDps40jJnv8yQVDbWnQDfg==", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -2565,9 +2591,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -2724,9 +2750,9 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "node_modules/array-union": { "version": "2.1.0", @@ -2774,10 +2800,16 @@ "inherits": "2.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "funding": [ { "type": "opencollective", @@ -2793,9 +2825,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -2811,12 +2843,14 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-helper-vue-jsx-merge-props": { @@ -2843,12 +2877,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -2864,23 +2898,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", - "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3089,12 +3123,10 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -3205,19 +3237,22 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { @@ -3242,9 +3277,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -3260,9 +3295,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -3319,13 +3354,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3360,9 +3400,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001553", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", - "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", + "version": "1.0.30001588", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", + "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", "funding": [ { "type": "opencollective", @@ -3441,15 +3481,9 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3462,6 +3496,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -3507,9 +3544,9 @@ } }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dependencies": { "source-map": "~0.6.0" }, @@ -3605,6 +3642,18 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -3770,9 +3819,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/core-js": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.1.tgz", - "integrity": "sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -3780,11 +3829,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.1.tgz", - "integrity": "sha512-6pYKNOgD/j/bkC5xS5IIg6bncid3rfrI42oBH1SQJbsmYPKF7rhzcFzYCcxYMmNQQ0rCEB8WqpW7QHndOggaeQ==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", + "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.3" }, "funding": { "type": "opencollective", @@ -3928,20 +3977,20 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "peer": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -3951,7 +4000,16 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-loader/node_modules/lru-cache": { @@ -3968,9 +4026,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "peer": true, "dependencies": { @@ -4138,9 +4196,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/custom-event-polyfill": { "version": "1.0.7", @@ -4200,16 +4258,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -4255,6 +4316,15 @@ "node": ">=8" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4317,11 +4387,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -4454,9 +4519,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.563", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.563.tgz", - "integrity": "sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw==" + "version": "1.4.673", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz", + "integrity": "sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -4519,9 +4584,9 @@ } }, "node_modules/envinfo": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", - "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", "bin": { "envinfo": "dist/cli.js" }, @@ -4542,10 +4607,29 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" }, "node_modules/es6-object-assign": { "version": "1.1.0", @@ -4553,9 +4637,9 @@ "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -4772,11 +4856,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4810,9 +4889,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4838,9 +4917,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -4994,9 +5073,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -5012,6 +5091,20 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5106,15 +5199,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5235,11 +5332,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5308,9 +5405,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", "dependencies": { "function-bind": "^1.1.2" }, @@ -5327,9 +5424,9 @@ } }, "node_modules/hls.js": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.12.tgz", - "integrity": "sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.6.tgz", + "integrity": "sha512-rmlaIEfLuSwqRtYLeTk30ebYli5qNK2urdkEcqYoBezRpV+MFHhZnMX77lHWW+EMjNlwr2sx2apfqq54E3yXnA==" }, "node_modules/hmac-drbg": { "version": "1.0.1", @@ -5597,9 +5694,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "engines": { "node": ">= 4" } @@ -5660,9 +5757,9 @@ } }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/import-fresh": { @@ -5739,9 +5836,21 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } }, "node_modules/ipaddr.js": { "version": "2.1.0", @@ -5964,6 +6073,11 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6180,9 +6294,9 @@ } }, "node_modules/laravel-mix/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6568,9 +6682,9 @@ } }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { "node": "*" } @@ -6593,9 +6707,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -6665,9 +6779,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", - "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -6730,9 +6844,9 @@ } }, "node_modules/node-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6763,9 +6877,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -6841,12 +6955,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -7184,9 +7298,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -7202,7 +7316,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7359,9 +7473,9 @@ } }, "node_modules/postcss-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -7481,9 +7595,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^6.0.2", @@ -7497,9 +7611,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -7694,9 +7808,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7743,7 +7857,6 @@ "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, "optional": true, "bin": { "prettier": "bin-prettier.js" @@ -7801,6 +7914,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -8019,9 +8138,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -8032,9 +8151,9 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexpu-core": { @@ -8280,9 +8399,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.69.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.4.tgz", - "integrity": "sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==", + "version": "1.71.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz", + "integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -8357,10 +8476,11 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -8418,9 +8538,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dependencies": { "randombytes": "^2.1.0" } @@ -8510,14 +8630,16 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8591,13 +8713,17 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8782,23 +8908,18 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", + "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -8870,6 +8991,11 @@ "node": ">= 6" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -8885,9 +9011,9 @@ } }, "node_modules/std-env": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", - "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" }, "node_modules/stream-browserify": { "version": "2.0.2", @@ -9075,9 +9201,9 @@ } }, "node_modules/terser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", - "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", + "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -9092,15 +9218,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -9241,9 +9367,9 @@ } }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -9282,9 +9408,9 @@ } }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -9340,9 +9466,9 @@ } }, "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } @@ -9427,11 +9553,12 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dependencies": { - "@vue/compiler-sfc": "2.7.14", + "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" } }, @@ -9617,9 +9744,9 @@ } }, "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, "dependencies": { "de-indent": "^1.0.2", @@ -9693,18 +9820,18 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz", + "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==", "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -9718,7 +9845,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -10017,9 +10144,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 6598175d9..8ecb08ae7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "acorn": "^8.7.1", - "axios": "^0.21.1", + "axios": ">=1.6.0", "bootstrap": "^4.5.2", "cross-env": "^5.2.1", "jquery": "^3.6.0", From 9409c569bd2e61a26b2b4950adb3219271ea7543 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 01:47:57 -0700 Subject: [PATCH 423/977] Update compiled assets --- public/js/account-import.js | Bin 27967 -> 27966 bytes public/js/activity.js | Bin 11252 -> 11256 bytes public/js/admin.js | Bin 216009 -> 216055 bytes public/js/admin_invite.js | Bin 15750 -> 15753 bytes public/js/app.js | Bin 19690 -> 19692 bytes .../js/changelog.bundle.742a06ba0a547120.js | Bin 30605 -> 0 bytes .../js/changelog.bundle.bf44edbbfa14bd53.js | Bin 0 -> 30583 bytes public/js/collectioncompose.js | Bin 10483 -> 10481 bytes public/js/collections.js | Bin 21570 -> 21569 bytes public/js/components.js | Bin 1619 -> 1619 bytes public/js/compose-classic.js | Bin 17119 -> 17120 bytes public/js/compose.chunk.1ac292c93b524406.js | Bin 93554 -> 0 bytes public/js/compose.chunk.ffae318db42f1072.js | Bin 0 -> 93529 bytes public/js/compose.js | Bin 68959 -> 68958 bytes public/js/daci.chunk.34dc7bad3a0792cc.js | Bin 0 -> 126628 bytes public/js/daci.chunk.8d4acc1db3f27a51.js | Bin 126677 -> 0 bytes public/js/developers.js | Bin 23310 -> 23314 bytes public/js/direct.js | Bin 40995 -> 40990 bytes public/js/discover.chunk.b1846efb6bd1e43c.js | Bin 71802 -> 0 bytes public/js/discover.chunk.c2229e1d15bd3ada.js | Bin 0 -> 71788 bytes public/js/discover.js | Bin 8949 -> 8948 bytes ...over~findfriends.chunk.941b524eee8b8d63.js | Bin 125576 -> 0 bytes ...over~findfriends.chunk.b1858bea66d9723b.js | Bin 0 -> 125531 bytes ...iscover~hashtag.bundle.6c2ff384b17ea58d.js | Bin 50871 -> 0 bytes ...iscover~hashtag.bundle.a0f00fc7df1f313c.js | Bin 0 -> 50856 bytes ...iscover~memories.chunk.37e0c325f900e163.js | Bin 0 -> 125868 bytes ...iscover~memories.chunk.7d917826c3e9f17b.js | Bin 125914 -> 0 bytes ...cover~myhashtags.chunk.8886fc0d4736d819.js | Bin 0 -> 172753 bytes ...cover~myhashtags.chunk.a72fc4882db8afd3.js | Bin 172802 -> 0 bytes ...cover~serverfeed.chunk.262bf7e3bce843c3.js | Bin 0 -> 125109 bytes ...cover~serverfeed.chunk.8365948d1867de3a.js | Bin 125166 -> 0 bytes ...iscover~settings.chunk.65d6f3cbe5323ed4.js | Bin 0 -> 129394 bytes ...iscover~settings.chunk.be88dc5ba1a24a7d.js | Bin 129441 -> 0 bytes public/js/dms.chunk.2b55effc0e8ba89f.js | Bin 0 -> 32273 bytes public/js/dms.chunk.53a951c5de2d95ac.js | Bin 32296 -> 0 bytes .../js/dms~message.chunk.76edeafda3d92320.js | Bin 69671 -> 0 bytes .../js/dms~message.chunk.976f7edaa6f71137.js | Bin 0 -> 69626 bytes public/js/error404.bundle.3bbc118159460db6.js | Bin 4900 -> 0 bytes public/js/error404.bundle.b397483e3991ab20.js | Bin 0 -> 4901 bytes public/js/hashtag.js | Bin 9036 -> 9037 bytes public/js/home.chunk.264eeb47bfac56c1.js | Bin 0 -> 243730 bytes ...ome.chunk.264eeb47bfac56c1.js.LICENSE.txt} | 0 public/js/home.chunk.88eeebf6c53d4dca.js | Bin 243768 -> 0 bytes public/js/i18n.bundle.47cbf9f04d955267.js | Bin 26154 -> 0 bytes public/js/i18n.bundle.93a02e275ac1a708.js | Bin 0 -> 26130 bytes public/js/landing.js | Bin 184645 -> 184610 bytes public/js/manifest.js | Bin 4006 -> 4010 bytes .../notifications.chunk.0c5151643e4534aa.js | Bin 0 -> 50404 bytes .../notifications.chunk.3b92cf46da469de1.js | Bin 50423 -> 0 bytes public/js/portfolio.js | Bin 45145 -> 45149 bytes public/js/post.chunk.5ff16664f9adb901.js | Bin 0 -> 221675 bytes ...ost.chunk.5ff16664f9adb901.js.LICENSE.txt} | 0 public/js/post.chunk.eb9804ff282909ae.js | Bin 221711 -> 0 bytes public/js/profile-directory.js | Bin 3388 -> 3388 bytes public/js/profile.chunk.7a6c846c4cb3cfd4.js | Bin 0 -> 222806 bytes public/js/profile.chunk.d52916cb68c9a146.js | Bin 222906 -> 0 bytes public/js/profile.js | Bin 114769 -> 114781 bytes ...ofile~followers.bundle.5d796e79f32d066c.js | Bin 0 -> 27172 bytes ...ofile~followers.bundle.5deed93248f20662.js | Bin 27232 -> 0 bytes ...ofile~following.bundle.7ca7cfa5aaae75e2.js | Bin 0 -> 27144 bytes ...ofile~following.bundle.d2b3b1fc2e05dbd3.js | Bin 27203 -> 0 bytes public/js/remote_auth.js | Bin 357562 -> 357565 bytes public/js/search.js | Bin 22369 -> 22368 bytes public/js/spa.js | Bin 203755 -> 203758 bytes public/js/status.js | Bin 136372 -> 136375 bytes public/js/stories.js | Bin 29421 -> 29424 bytes public/js/story-compose.js | Bin 22462 -> 22464 bytes public/js/timeline.js | Bin 140280 -> 140281 bytes public/js/vendor.js | Bin 3599650 -> 3656495 bytes public/js/vendor.js.LICENSE.txt | 4 ++-- public/mix-manifest.json | Bin 5243 -> 5243 bytes 71 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 public/js/changelog.bundle.742a06ba0a547120.js create mode 100644 public/js/changelog.bundle.bf44edbbfa14bd53.js delete mode 100644 public/js/compose.chunk.1ac292c93b524406.js create mode 100644 public/js/compose.chunk.ffae318db42f1072.js create mode 100644 public/js/daci.chunk.34dc7bad3a0792cc.js delete mode 100644 public/js/daci.chunk.8d4acc1db3f27a51.js delete mode 100644 public/js/discover.chunk.b1846efb6bd1e43c.js create mode 100644 public/js/discover.chunk.c2229e1d15bd3ada.js delete mode 100644 public/js/discover~findfriends.chunk.941b524eee8b8d63.js create mode 100644 public/js/discover~findfriends.chunk.b1858bea66d9723b.js delete mode 100644 public/js/discover~hashtag.bundle.6c2ff384b17ea58d.js create mode 100644 public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js create mode 100644 public/js/discover~memories.chunk.37e0c325f900e163.js delete mode 100644 public/js/discover~memories.chunk.7d917826c3e9f17b.js create mode 100644 public/js/discover~myhashtags.chunk.8886fc0d4736d819.js delete mode 100644 public/js/discover~myhashtags.chunk.a72fc4882db8afd3.js create mode 100644 public/js/discover~serverfeed.chunk.262bf7e3bce843c3.js delete mode 100644 public/js/discover~serverfeed.chunk.8365948d1867de3a.js create mode 100644 public/js/discover~settings.chunk.65d6f3cbe5323ed4.js delete mode 100644 public/js/discover~settings.chunk.be88dc5ba1a24a7d.js create mode 100644 public/js/dms.chunk.2b55effc0e8ba89f.js delete mode 100644 public/js/dms.chunk.53a951c5de2d95ac.js delete mode 100644 public/js/dms~message.chunk.76edeafda3d92320.js create mode 100644 public/js/dms~message.chunk.976f7edaa6f71137.js delete mode 100644 public/js/error404.bundle.3bbc118159460db6.js create mode 100644 public/js/error404.bundle.b397483e3991ab20.js create mode 100644 public/js/home.chunk.264eeb47bfac56c1.js rename public/js/{home.chunk.88eeebf6c53d4dca.js.LICENSE.txt => home.chunk.264eeb47bfac56c1.js.LICENSE.txt} (100%) delete mode 100644 public/js/home.chunk.88eeebf6c53d4dca.js delete mode 100644 public/js/i18n.bundle.47cbf9f04d955267.js create mode 100644 public/js/i18n.bundle.93a02e275ac1a708.js create mode 100644 public/js/notifications.chunk.0c5151643e4534aa.js delete mode 100644 public/js/notifications.chunk.3b92cf46da469de1.js create mode 100644 public/js/post.chunk.5ff16664f9adb901.js rename public/js/{post.chunk.eb9804ff282909ae.js.LICENSE.txt => post.chunk.5ff16664f9adb901.js.LICENSE.txt} (100%) delete mode 100644 public/js/post.chunk.eb9804ff282909ae.js create mode 100644 public/js/profile.chunk.7a6c846c4cb3cfd4.js delete mode 100644 public/js/profile.chunk.d52916cb68c9a146.js create mode 100644 public/js/profile~followers.bundle.5d796e79f32d066c.js delete mode 100644 public/js/profile~followers.bundle.5deed93248f20662.js create mode 100644 public/js/profile~following.bundle.7ca7cfa5aaae75e2.js delete mode 100644 public/js/profile~following.bundle.d2b3b1fc2e05dbd3.js diff --git a/public/js/account-import.js b/public/js/account-import.js index f1510f57ea8cea20b3183872e5d2c6a8327546f5..1acfa82455f0e1c7bd5da57e6b214790be128bd7 100644 GIT binary patch delta 357 zcmYk2PfNov7{(bc9%L6`f-=y0(F8(Cnl?!(bn%%LY8 z={N7#S zwT0fw2D{QD!a=wUb32DA!f|J3e(mfM80uO+D*h#+j0$4jpvNfJ@7dX&U_?47xGwT( zWfdVLIEU1ACD-`=YNM0sM|IG3JM5dHtr}~fhQOdYlVjkUcCDIhV#Df=aFw`K=bUj) lA`Br4PiAFO)n#rap;-V|u(GFNvKlOD5DF^$0-`2D!4JuDYIFbq delta 353 zcmYk2u}i~16vin{K(vdHK|yRLy@MlO?{b$rLL|D2;@+WX#I{EYY3fokIJ&6taQ7c@ z5H}Uw-Q1-Ag7`mpjS7zM@xAZ+eeY$~c%3z_FIu5uqD!Vibd-QAvktdIZ+TMT_^em> z{Uk?>QO)l^KGSBzIb52yRV+Mw?YwQckx-F@=TqWoCaA)}ady(1m=aeclo6Z{)28TX zNc%U{%HOiFLo>WS!EfkJAV&+#mgVYM_f zH#D4Fu4y1zwE-$7fxZSfr2-GyrTnP~KO{ e*v!lzR!2ia)7GxKD7B=tC{H2L7UG;6+A;vEA~wPR delta 212 zcmewn{v~{ZBaflEnMJHlwVA1znfc^(T1FG=c~}iC%}q@=P7vl{HL^6fGy@8&Z9d1d zM26Me!q~`Ua=E5~kg1uurd~>FT4HHViDs=1kPFo;U}|n;Xs)SKY@4cKWNu+%Hkn(? zm(|qJ(!gMHtd j==p0H=tNssSOT4^p`i&BDoQOWEy`0!w1qh9j7KQB9n(%EBOxxJMd||wE*ud^-oqXdQZ7h2b0JzsL`aAWH&6~peDz*WRWCn#8146} z-m6#dxBC4dKmE`A^iLly#ZpCYweH6eURzoUx?=fir>Ub@d-QaSRgWQ8pd#6AD zKcs$g;qdsqr)y_+JF_dv)mb;4&n8!rgQZ7b{_LQcF2DQka|i0t4-WE2d(CdEk)*T1 zq~BO>HM{GLWN*AN9W~b12hC*pTC(;%T5KkV*MD^UVqN+=jB)ZytnFd%@h8vLRUB%0 z^s`>|sY?Fpyz)%`!w5hB>Q+COU%Fj+@$l%wi|^NU;Q1=Yru#>4yk7bHuj*0=5gV>I zD*4&xtLO98Pb+U8RsK<_eXSB6JomVH)V@-E`{lZd!ic2*F?|{4jQq7`+UmAC%j+-Q z?=;D1#;S4b`OB@lgUQzP?#kQion|0KXbHEcJ3HMOs6uO*|MpMSwXZT~tJ9Q0B!%7C zxi^?5*r=5VWWMxK^}Br)>NvI=dV^6n8SHnlC-(i&E!>z+hl8%+^@G5d%Mh)7FNn-P zN(miU5jVCwvlRPOAR=0ZQ3HAO`;V&cS59e9g!z->>X-73EIxm8=LM+(82|7N9jryj~s>ip7b?LrnvKZ;t7mDNsf8e9LdEw>4c zZ7GxH6rM673uX{IUxa8u>oRsHBa}jqBeu(7WFedyqD<3iVXsADR79EO^ru8)m|#CEh)I4+i+%qUR$>^egr zyp0U<{m3T)+nhcQ!+#>wiY&0r?sFI}2vo=svCRe5VRNXbxTS6v`R@pO?G8Etj#aFL z!KAe%4U7qer%10sVM2=$*E$X(GS7fx*kQ!5M&>YQ3O7SfUk4d^V_*n$l%W+Q!j|Wb ziKVbuyu+?_7=;mSGoT>4!-dh}AqPcFdJSz*FwTZU$VEw|m>mvT35#KmSZQugLpFxP zrhf||$q&P#cUz1+!AqKZ(2(=QVdDqw8%t#?tHXJFIHt#rW>JJm5#=vP1PdOpAPFKZ z*wjN#0*gne&=IsGCyuy0I}E*i^B^+$JDRR!=FQ~@@RE>`qV_l9Qsc1cKKSe)Bae#i zv%q6StkfPqy~Z4|4x5PewICahi-={6`;XxP)H817WA=cOW)DX;?H40&c~G+2W2wn0QDPSSmspq9Q(KOF3-Z zgGU5K+_-Sh0#D$cAPCrZlZ^f22(Y8ZThyE}VKZ823UeU2flo$l^WqbzlXMDUQn5rT zuCQQ%weSvO=T6vKGFuR)o9)qL(|9=K=EVdOVQD)Zv|*?pYe8luOj{huXo>Gj^7T&9 zJuEg>i#6BhowU7S-hV>F+vdJR;5#@HVaqc!9T+)A;X5nZuf<3!4a-s%&A6mmoS1hj zzMafLN%3X{B>yL{WEhHwbWYgn4kLK|2rnn%Ecnbz%Zik;}h82AYJq$Q0EofWS<`b$d4#u^QZe+ z`ElUL+-fZAyx-nL-U*GbU85yo_P8a%4!%Oepmnx`!?=W8eE-e)5{%9F-yH8~6n%ac z%=hWFVhWhx>Q+$bxDt`DW!puT1Q=P+O?NpiSN`hh(_(J6{#0=;^ugNMK3lI}ci)g-?zxPV*TE6QAZ@zQXTdC9~ldGSQXjE*GZ5HCErN&`z;t-%rbxm&d> uCBY^Z&EoavAGA`8Fw>RS;h#4#+4SCOHJfItSt@`U|N7gtOaBJAnz4fb delta 5727 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 VF~yiTsK}~Wk$aixKJsApegUaXTJit@ delta 326 zcmeCIZmXW)$Zcd`W)!PaZE0y{HqpnP)zH%1)O6#7{hX{82IeLP6MfY-S8)|dsh5(PmROooqFJkBVrpq>F!`*t9gCrP-LrNpp&Tqw9vp{a*eeQD?}a06(FBX=C=u9 z1)DO_SB?*EF4#+x1#IluYjwcJp~zZ-Tn&;{udUT7v9+r%(enq|5^Z5&X%VZVp`i&B TDoQOWEy`0!wFQSquB{9JSMFL5 diff --git a/public/js/app.js b/public/js/app.js index 86bae4e30373de99ef95882235b961e65d6b05b7..9438095182daff00a17cadfbdbe41735db5771bc 100644 GIT binary patch delta 372 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 diff --git a/public/js/changelog.bundle.bf44edbbfa14bd53.js b/public/js/changelog.bundle.bf44edbbfa14bd53.js new file mode 100644 index 0000000000000000000000000000000000000000..455a31a10c592a4eb94265243e4af1aa2573d501 GIT binary patch literal 30583 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 literal 0 HcmV?d00001 diff --git a/public/js/collectioncompose.js b/public/js/collectioncompose.js index d729c91323961ab3f52e08f633380a55ee794f69..ae3e62289a04be32aaf746d742f46573d0de8a49 100644 GIT binary patch delta 224 zcmewy_%U#TBe#i(xk;=}wS|G9>EwMpYLhRh^K7iQ;$by0wXiVWypLz2B#VimrRC%z zHA68ALsMgOO}&)Vw8YY!63tp2umDWk=_w3hoWjjA4bTK8(8aap%C3ic#o4jndvE zcmUKR7(aChyF$37*13!CN7Kg~7KkY9k{aC7 z9|PMGTqwD`xqM!0D#8t9K5psl&~8f)3u=s<1WX`GO2_)%4X-VO{wIBg_KqhoCv`3& z6&Ct8yl6|Ns0aPqNs{HgQ2;P18H`D;@nB%pI#aVsv(+@USJA27j9QifmJUaz-6}fU zKn7Q>EMd~FqCR&y$3p}F3{GZgoKMrF7YF7ACc4JN>o zoHB^!W+ciJ;_D+ivk6twEyIgVzyEn@Q`b|T{tf9V=U5QvJZ4Yh#d85yNMVA#GNgJN zcPt`^+T4%c_AHlSR-xUf(^HIjsy1+PFoh^LPW73l(VbN(R>|}>>Kt(*SwjVi32wN0 zmmKTA2=%C-Qb7Mwzrj@^CU z=f9ax@CojVKqraLuyZ6WJyz43A?p<2(U-y~vKFx5sEA?$tb7?3q5!P_bxe^2-1e*n z+%O}ZSOa+A7_*os=e|Aw?wDC;$pP+rwgWtQ!EUYoZf+wPPxxRd5^Ws4zU^@Aq*U&G s89fI^853nF1o(MZ<;}S&W{vOv#$4oK?Hfqjbv0|Q0BIgB1za;of0-OTj{pDw delta 231 zcmW-bONzo!5JeGnB3XeWhgDF7m(=?bUN510E;tZ7p#@DKi6V%&1$z-LKzb>zr@=C_ z%emaoGkGSr&m<`2S9H#qIs0ACuTB~ENW0iTZcHY!7Rid0MQUS{)=(pL!ZK@(M*1FD zhqMtE^57lP_8oCr1*F4ZZ;>itPJ5@2eg;+{?ISecEnAssKwHN%NlSHb#ytLyS22WQM(?q3XN)7wRHH64ORD1;Q-)TM+@-77acIyi~s z1Na5R-AO-+NvqKPd4KQkdA^s8pJih;X!*XxLo{bX5YM>!&_tbG^VDxWE?SnL+$V3V zmv)m8>6sIKR7Rbzo<)30usK!7dkka7pmUqt#j`Yr1)^L~yv;GMsseH0%ko{7jfzP; zxfR&&O6Cdz(Nv9`48#zP)d*}bpZ&|7>Vt8#{jYV97BF~-Cw5$=eBxnueF+$q9x?Z! z?n*8QgTc)>o91?^fQv=|Q!2Un2(PS7ZmVbQ*R0!{YaN>BLEBnWE5n{uw`L~Np@nK{ vH30bYkMueBICqB#0I1I9lO&%_GCNW0X;2PwXR4BoX;MHKbF5QjOqfJN*)@jxVnL3wehg_Ew+IQ)FvdU}NAi+5iLy&rt<`^?9+7H=|^eV%{ zprffE#1#-#GP4ViK(v$@=qX>pwb2C(dB=GJ{fDHq5dR7)@tXFb4hG zWl_~uE`ftGpz|F^eTFwiKQ}Pd2kwtM{WuEL%Us*;s5(=NcHVIm8fZsVP6Yl9;^>ll np73G>0F)Q&GObr-VWqPB4XQzHS2FD{O&bXP+P(s?iJ|`o0+eba diff --git a/public/js/compose.chunk.1ac292c93b524406.js b/public/js/compose.chunk.1ac292c93b524406.js deleted file mode 100644 index ed1b9307165c18197ef96a8506c9df975246e5c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93554 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 diff --git a/public/js/compose.chunk.ffae318db42f1072.js b/public/js/compose.chunk.ffae318db42f1072.js new file mode 100644 index 0000000000000000000000000000000000000000..4c9c21f2da782e433f3f458647e63570c2e036b8 GIT binary patch literal 93529 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 AZ2$lO literal 0 HcmV?d00001 diff --git a/public/js/daci.chunk.8d4acc1db3f27a51.js b/public/js/daci.chunk.8d4acc1db3f27a51.js deleted file mode 100644 index 75059480aa1c39f8621555114995e122c1617242..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126677 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 diff --git a/public/js/developers.js b/public/js/developers.js index b76d009578d6c0ccb378b58a92eb36ea654dc5ea..eb6231ca52a833b399a93821dc80d1324d2b840f 100644 GIT binary patch delta 818 zcmZ`%PiqrF9A(LlR*4u;D)AIlaEC5&{_Olq*qR1C32C;AX$(Y(xL1$+gK{3aD`+INR@AuxjWADSU_r86ev0-??5hqxm^vKs2 zhn_FEHK3ORP5phMnom=tdz0zXxxo6U@p8UXdEkRcaL&x< zTaE5s)E_H-{iS(0-&ozb0hCfq&W{?OE&uR9Gu2vbzQ>0H-8J6Bhw zK$OG!)9l%3?|BYqArq1sOU27vD5-~=`+x`)vA?i;I_s{P2`uF^BN&e3Y2fs|bgRS=Fy&+tN{I#7n!U+ZD2}(sD&n zPNg*vn5`typVM}g6M`^oKG_{l^V%d1P&f^66qzu7rKxYuR2<&%i9{%Je1Vn1hJWXZ z;A#n5X>q>_>`DJfD2X|964ZEL)`H?D%;h3IBVz3qF}Tts{oA_SzYLrW7mwsfFL0+_ z;I29g++ILG8r(dsPtxB2cB=yOiw+hc^=esL%ZnLtf@4A+;`B>r#^ z0L6;@3OT!sW7?pSeoKEJT;P;*Gz^0P#_I=@EZ>`qYgugj80JA9WPRv`LoX9-{>7@Phsm%Zs;=?MUoktmwW_`cu^E`KW_{%8<95 z`Bu_Gx4Nb%6$*xj@whe&`Y;^EwxId~U P6A0^RZ99OI9<2WX7xE1V diff --git a/public/js/direct.js b/public/js/direct.js index 704d14c089627fda070701420cabf845e27f096e..c965d2872ac79da0ca8d7948b1c3a8ece50c0392 100644 GIT binary patch delta 528 zcmXw$!D`zu9LAxsde~A@Mrqgv#Vxdvh6t8y$x?&c^fDMEw97pQ(|ItqIa}=P67mK+ zbQ9#TG;yHx0ZLw==RU*k+xirfWODugKYic#|LNv_V{yN6bynrnV|_gKsqneFTI~J! zxL;2Kgg-0LpX+Ez$LxAef0#;PzHV}+)>}Jw;Z!s)t~-(;c!W;g=%!Zu6`b6 zhYsi5YFCZU-#0bMBsqpTPH>E(ld*a`-F1sn=7yD||vi)O?q` zk4K+!G{IbOLdsYdgk1MpJxu>qfD8o_>pG9Ws?P=ULTc*A%@k$`r%?>c$F3DX$ADHv(N@3bJC3y|p*xoAvN$yhU~B2zMnyx_^0Q5Blg$WFi*;GF zH?jU8#_X^Ju`IgG=iKXK2o2+5l8=U|n?!~^&SBonq6D?R#X~olAk@m6=MYW$sP!MU Cm7)g# delta 467 zcmX|-y-LGS7=~$09BPUV3RV%in4U<`BwCWMIq&z}_tTI4^wp~6(;(P(eQd1KqqY+KjOY)&#t z9rqwa5n^02;mh zt{Xn)9*nn^3`#kMwU#@Hna7y3xL4VaP7WK+xyuEIotRuVEyJh2?7gkFsTI^1m?4)B>U1U4i$` zq>*qSig9v}mKAqP$%Co8!Ev`RaHSSXqw087K!-u%Z4m13b(m;iDlF%OkYd|K$O)Iv RYL&)Gt(vcd+HUG$?iZN#i*^72 diff --git a/public/js/discover.chunk.b1846efb6bd1e43c.js b/public/js/discover.chunk.b1846efb6bd1e43c.js deleted file mode 100644 index fb2ff14820035305710357fe3640f6a67c237aa9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71802 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μIqzG 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 fEYZ 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 diff --git a/public/js/discover.chunk.c2229e1d15bd3ada.js b/public/js/discover.chunk.c2229e1d15bd3ada.js new file mode 100644 index 0000000000000000000000000000000000000000..be3e462802bb48e350796a8f96728997516eedbd GIT binary patch literal 71788 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 a*jJCiQLlRs!UlEqARN28?;tk_^!z`$(YlcU literal 0 HcmV?d00001 diff --git a/public/js/discover.js b/public/js/discover.js index 4102b71f751e18fa8ba19eb7b32c4b7d16071df7..35f72887a4378611923beb2ffd3b312927e092e9 100644 GIT binary patch delta 209 zcmezB`o(pEBbSk(WvouMiG_jXL|;2ra}zU5qm2{V*jPOXXI++9q>u>I3$`05XRlFPt5afY nS6!m#uVJ7QZER*{5UZo1p$QZ!N-ZfZ%2Pg;}glwWYCziQzDHrulsO0t-n znOICNQ7{lSH8(W0)YMB!O-n4zDbcLe0SQd>GM@ZKfrsD7+}K1@C)2iA!_vsaz?i9eiLiilP4t%Ivo1?4Qb+`823ZZ1_LZ`* eu(XKP(a_MewW}^lEh#O^Q%JQn0z2l0k_-UQv^ALk diff --git a/public/js/discover~findfriends.chunk.941b524eee8b8d63.js b/public/js/discover~findfriends.chunk.941b524eee8b8d63.js deleted file mode 100644 index cbf5bdb24da2938a5103871970dca374574fcf27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125576 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 diff --git a/public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js b/public/js/discover~hashtag.bundle.a0f00fc7df1f313c.js new file mode 100644 index 0000000000000000000000000000000000000000..d49e228c06560b067f2dfc1d7ec451bad1c38f43 GIT binary patch literal 50856 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 literal 0 HcmV?d00001 diff --git a/public/js/discover~memories.chunk.37e0c325f900e163.js b/public/js/discover~memories.chunk.37e0c325f900e163.js new file mode 100644 index 0000000000000000000000000000000000000000..b4c14378c947ae953fad9cc5268ae6a2213c9f56 GIT binary patch literal 125868 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

s>{b=NGK~c#6^f^u7sJEN?`g5! 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�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 S3hIL!4MIe{@Gl1hi~bA5@RP#; literal 0 HcmV?d00001 diff --git a/public/js/discover~myhashtags.chunk.a72fc4882db8afd3.js b/public/js/discover~myhashtags.chunk.a72fc4882db8afd3.js deleted file mode 100644 index adab1bc44e95f1788926635ae358caa0f877a457..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172802 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

t>fn}HSYeJh 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

Kf!vJl7Vk3~pueBg5vroc;19>0 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 RzO?sSJ$P>UmpjBr{Qph{St9@d literal 0 HcmV?d00001 diff --git a/public/js/discover~serverfeed.chunk.8365948d1867de3a.js b/public/js/discover~serverfeed.chunk.8365948d1867de3a.js deleted file mode 100644 index fba06dbcabb3fc7afe1c302b8364b4bc51fb0c10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125166 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 K&^~t1cK-*3B+Tsq diff --git a/public/js/dms~message.chunk.76edeafda3d92320.js b/public/js/dms~message.chunk.76edeafda3d92320.js deleted file mode 100644 index 17a5262d935226cbeb690e562d3f416dfef17d38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69671 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 BTEGAR literal 0 HcmV?d00001 diff --git a/public/js/hashtag.js b/public/js/hashtag.js index 5ef001f9f30557f795f133195d526ed9c7cd80c1..6ae64fc6922c88e804fa52cfe2335bcaf578f765 100644 GIT binary patch delta 354 zcmYL_y-EW?6osqNENmhEVMLN~MVXNy&i~93HlRgnu@o#8T|tlyY&JF_SlLIW)rSyE zUqLH-#W!&?YfO9Zx!*bGzV|=+)8kMvMlZ;ial?&c!GY_w!^is&7^!3aGJR?QON9}y zEO_1kN^vgTXX|J)*1|H}xyr87(QSz*gh|T8A7Bd@ZDQ-r+Ydll#qAutUrccKE-he| zq{u32I3_nq+R0IdNnXurN*A62Z!o{B0*`X?cE)XjBafMdg<-5twTY3Lq4DI23U;i9mgc6W8z;=+U^O$gFtglj$T?S()zH+! z+;H+`Iejrh3kxFyO}&)Vw8YY!63tp23jpkS<8 zRI6!SmRO{aYMZEGX>4q6si~7`o2Zwkk*cYa2NZ*7wNVIQ1)B=8Uckc8z|0V=PQ%F3 z*wkS1eFY~;3lmEd3r*|f{Ji24g?w8L1D!+-plt>QldTkkSRv{rD%!C^d~r(2ezLHV z2rJmA$%dSAllLfbLVPp%l#&CR4%j%j1jxCDKnY(d3kyq&SRD-wOUm1O`ew^C*R diff --git a/public/js/home.chunk.264eeb47bfac56c1.js b/public/js/home.chunk.264eeb47bfac56c1.js new file mode 100644 index 0000000000000000000000000000000000000000..a789de6afdb35e2c4323c5851ae15a067796e529 GIT binary patch literal 243730 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 T;r7rkt&=8M_4t?fNLl`W^|o8u literal 0 HcmV?d00001 diff --git a/public/js/home.chunk.88eeebf6c53d4dca.js.LICENSE.txt b/public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt similarity index 100% rename from public/js/home.chunk.88eeebf6c53d4dca.js.LICENSE.txt rename to public/js/home.chunk.264eeb47bfac56c1.js.LICENSE.txt diff --git a/public/js/home.chunk.88eeebf6c53d4dca.js b/public/js/home.chunk.88eeebf6c53d4dca.js deleted file mode 100644 index 1f6666262bbfa8b8cd1b892145acde29e1e0fa55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243768 zcmeFa>vkJQvM%_4o&u`&k%27$BzTc1&@fw;rMAvUwnmcMYcDm2s|XayYJmVY3J}FZ z@lECd<_+fmJjy)Dd|yOlR@Mc)$a34>t=+p@L{(N^A|vA-k!$~Pr}1<;Z;aDnGRcxg zIysuoPvb>8oit|SB*vpGNg5~9)1*5*Sx(+{cXpB_IXt>Q+}<2*jfQdepIP_$v&T<< zeEp=m_^@dF$J+na%#!g@_bfS_#lv@x`PD4_fJez_u=35vk8k%|-PtlbX}*2CvGrhm zzkTs=XM1a_-(0klcGenvdl4-;oy9yIE~4G6J8vefcGev=llH|ZIf|F#MZbx5W(e{OI09zJNb=Yu95Z*FZrM3>F` z_aFAQTdm#UbdoI^LyMZHuD7w6C52vTI=_Hvf z(0(+J&ysmR?OLmLmX4Ccc;1iwlSO)(jMGWdpZG`f^!Q}KR#X2d$`;f4d1n~Ur+~pY z>d)P)%l0T<#Qme?WXMfxwl3z$VmY5&q}lUnJW59WJH0lhf0T}s{`+YGwG@qEJX3J)Jo zCzC|x61-kY`)5Edn*#uKI$cf{7$cba;v~(w=_Fk=TbJ##criTbUwnlnFOq)5#}U3t z7AMmYV9Vd(x7_i?uJmBb9h{|;(e$i)ke$TyWb{4Y)CKhBZ5_toGq|&^4~k14+-Xjh zE!NRS!OmpTAX3|x?_(|lHof7 zS2qMB3SRgvShOw*5d7)zpV-T8oMq|pq#3?yFVIU(=anrP@T=YULpsg6$H}4@t;Mr+ z4aA`%(_C|XbaYBvrGJ6ySLTbdT@K{IS;7k#{KP0qn;18HaB;adTu`0*}AXPbEnr^-*R+v zp3}(%Fdb)92&LlCr6)qgr}HEhV(}g5oxb^QItAmIc*=Tb9q5`R-{W-PG*4b6ljW+G zy%PP6l+Gu@EGvMD?m%r>{W6&o*o|?ZA~+P1#lI# zloP~1jD6$JdcrEiQBR^AVrvf~cSnAMLISq0W!t)^$taBv;>98!o?!Be?BmCqwCawN z$?@U@C+-Gac0qR*$vnVpkRE^H%Jk5t%hSB*+K=GR3K2+0QH!VNG@h*(!8$>sw~#bs z!U9Z3>dQRClT~diJ0S1`5ePsR4~OJx-E=fq-P6eUP59wFnT^j6 zkI~;GIeTRkXXVi*nhe>>9os2AQgb=b_*DORq-e70d)?#k%f0ZCAz7cp zpvw>$KCwrgRX-Sn48#O5?k$P=C`o302umqMM-T|eWcYCsPex-9BhRNHteXr`AX@`c zra*dwq@$GFe`sHQm7GHSi%2wahC$fgC3)w?Hhjl6%gI%(^ex+wT|dV+*;O58ozv+k z9=rChDLStF0HDX);ur3`RtDklrL(61FF)r=oSi>UC-1^OlJ_9R-QjW$X?^h~p5t70 z568=Sv(=_-xLl;;?r@yW4yW-P!ikUxMWN+tYhWPmg1qV#pd3 zS?*r`NEX<6Z!+n>Xw1H(iIM3xo{=B-iHDY@`rmqqLD(}2BGi}rXD?%$>)zNf3 zo}T4j6c3=j)%jfAatY0kJ%w|jh!gf)mQfs)r5F?_X(UEnC0NJFNC~5X$1hlswFoo_ zUMrYlaiAXf;kNKYg@=>9R!sGZV<|gVqJpMt+SCqBu8UxFSUo#81A#+6JrDrh|3~kk z5Eag}G3gchuSh2$fykwny;YyYI!4`B`s%VRBz-!11Mz(&!31sXEZ6{upgPLN&L`+7 zDRdOlhy`ps7c1-^)b#r+mLp?tu5WFw-(Ei|j18q*>_WyO1AWJoM6x+ov7uY!*# zxPgzX#6dT&K`tyhiV|njU_ziM948UsM~yN5+l{sw0jOWW3XrZjLUWOh3fv_fK}-@# zwP*`OMfHlR90#K%UElxa3Z4=)*+vuD`|YirhufcJ@88lWIgj>NZ81f7HKge|Q9F#= zn#qOU?qr*$t2(?BE3j8jl#^o5y7&3D7BQr#%>!NSpAjEM{PoEquWh z4b)r)NnR+3F8?tU`~t!H41K;{Ks@!*q-Sk{<>A<{WKvF|8O-#)U}CzIg=3hHf>(Lp z;aO@Y1LXjqz?8^nPRn*5&FArX7sO#PB?5P0Q-1>%O^PLUWce8Km+L#ewH@kPQg>E9kBcj?`)8E@{zYCMbNa2~T zYy-GH$_N(B5`ErDTgu;dlPciScJbj=fJ2xczY!PZo{Ew4s7A-_09}+l|=?4#OT6ZVUn{H62f<-veme$Qc;hs2LroeIGLU z%SUe>{{X=#f2Tb`Fr{u&J~Hs}h>6*_3vJBrdut}yWNT`}J;2!+LHJH%NFY#DYmCe* zBnBJL48ZoQbnb+jg7?;|HVX)?89YOoGhmk@%dBHaKP)=Y3Nwt+&W5N z&wp9$f#RBXYt=PaaU#n;73XAKgCTG@eV@#8^tUdjnit{lf7yeOJ>xu&2l}9eR zCkM6E`P;=l^pAVdno|hNXHbN?0zmKG0}(JoBW#pnAJX&uDuJ5^UeD|#oki`pO853j zGn_^!e#ee&anuF}qZ$fLN^PzEgj|}m8a%r2<{tQ`K)|W4`6hfm7`VQx`%;BMr}H~N zp<_^>4)gH-#=}q0mqKrRZ+pY}lXli0K7g;C-uMkUyW)-C+<`twZ#+Hs_jlZZ4m|Kx zO$$>4R(PNL-Um+(~2Pjhd3 zJ9q^L?G1cDhF+^Q(RK=7HYmtZ0>VN6J4`PkhEN5>-*_2;eu5w3NYJ6#hB74d2Py}& zr1AM7a`S7^td zX{LgK{alKa0Pr1yp=FroSmG6?AIus>vouvn4g<4s@?i!Wv8q6!@9PqA2eM7CebdSG z1>K!*lG7R8w_@PN-j2r_PtbpRflrQ)q1Q)$jl{3^8nb_qPM(YBslU;FACH%zxmK*v zWjZO?k|k^J*Vi)WA;Xh38stDmKp$rDJU+#&|Es@%|Fw=bnP|L<%~Ox2X~G#m#aamz z&8PmC&pLI8mnVz(_&1#TzFN+ooX!^K#1!B?bSUh-M1ugBgr1;~BCcWHC>bY2f$$;V za3y0DT>c=@AKawR=8P)yR~siM=>@+Sng;M_8l0HmMVh5io|6Ka!zYmY7$OeWL#sRVygC*(1< zC&X6k!$(fAL(CxTSv|OP_lieP46WWTiLe!md8k1JPErL13OGV;ye@9Qg{n+}pq&k% zDldsNnTf04uHU_@SaWBPzl31A=Ud`Oe{)wfopLI@N9$bl_5Plofxy6AGG2?X#PCXQnL#><1XiG=ZB zi1SJRXbqD5t|UMs0BX;#0*m2@@$ocA1K^r@6TFuZ07ZmomrWlXPn!$`gEeWffGC2H zft!3P4mA1jc=~2ak)-gIoHUL5n=1i>m+}-bfK=|I_AFkU^l3mw5ZM|ZtwjutL#UKu zKN=_#r(GGxp)I>HGVx?#KTZDA6K+x(w(GJjQ(6@mS34_hT%MRdz1iISWNdOj(|I9a z2xlg-7AAq_04wk=`6kT>Qt)2k%JiM-`6a9@{tmVg$J4;L^iLg-?92(3?R0V=zYiAE zgQ}H27w42VnjqKp&#Mv?uwM;`dTbfRtL$?C;g@!bd~mZI<7s(4q^h2}+4(QCyo?;s zw7{~z)`x|q6BvZZiQtYRsaPQBYMhW7=HSHvFnpwwg{5dqQ;t$Pwps(}Fo*$fjb zphvoqR`q&TO@ZX@J z{4BP-ZYM`cxRYhMU8Js19(B0Ip-o3D{!WXYcVOtc!8ll2w>clsTqyYsz z0E@Bvb^982P4&C6rZPTXFfZ?C1ErY8=0=fAz4^xb$oSUB-ExoO9{fX?%>Z z?5j=giZBo>KtOy);#;uuV$>yE8Fd!)^+CkO<@C48NAqTt|;eoCS1+N1AyZ z(+>rbqKDS&IZh+$)oMh&P$TN?_t*9Cx{hCqaSVR6N(ArQ#DZLP!`%l(&oKSF&nt5@ z1aqI*7-HlJkkh>mJ;j%8#;4Ih@k=^_$mWkGj<9;ns`Fgm$K)5WxyKqC+m5Q$tjXGT zk+FgpA-K5XYz9lwuF0!ZjrKOS`^mSxJp_7eAdKA2$HG#7#|dq$_ct*ixHez#47>(~ z9tPnWfY7f)3{d`hG;q*?tw7@n4iL=Oh3_rIvx5+GGl=xda|`XOrWowU;sK zuV47g2~4jjwLK9#U%tuUxvL`<1#|oXb|ykcfSnxap1X03AIfm~Ku$m#mMu|E@*+VG zK85{CRFCT8LZ84;nWbZZWj`({2)aU`IK$d3XxlXoUpiba^cFUtnSkaG9k03!HI0}V zqizIB_|kjz+fewdfq_PBdkY>-+To#C(WzPZu38Y(ZVFb#Aq#!y(n=*csK&bBmMtQK z0q;f@HxWnO*Y@E6NrTRsCb9&MAqC|}8srv2z-Wmq)l@NvEsSTlMbMmYt4?d0B#L2` z{v$(>gCNQ{(15M!~4z%(BI^|?;LL@~bzJqv{Vo9Pe0BiRn+E`@;vvSGPYQKrl5TLY&x4Kd z!T%09R!~jwlRE0*tkp#=)16u`#JJA+!jfh*+oI+L(6DYKA={vvCfu(A2G^6FrJ!U8 zv$_}}9lFMLP@V1dliEE*6xX}mLXWu)d;1=-wG!C4v(wvp`0GL`)H7@GwRtFoYHr0y z3UynmnLuLRAmxGwd0(t|yLwOT%BTveLE>Os_lbfl$cJ;8ngO`p(}{^#`U-L4_Q@+Y z9omrTv~m%b#?+8B_#rS66)bS72!W$GOWTIGw0-d&QCOQX9hT!IFZCJ3M>qh`c{P8|NgF=g2nZ7IBf*XpX_##0E2=r1+fkkobzl4 zsJXH?lmfnZSiX2z&MoHfi-$#O;=m^_2Juhxu&6ElU+zmWxpIYfL*!`Z8@R$f;c9Sv zaZk`)^5=0+)cF|J_nnV{!S&)oxT%jJ5g$Y0GiUpM^+)wFh}i|P@#~)u8=r=G%rGxA zml(lI2uMLru$oo0%p7bGR0?xTn~Tc2K;RRqj`b;V>z@eMrY+k=|1f}C>|}@nVX32j zgg7OOZdzqv$-S&vPsmw6{uuE`4nUzZn6VRn*43C4(a=LExcHCy*rID>OIn1s)*s&g zgv9kQWeIf$QJS!9XM2m{Z!B@ZK!1xX+rMV-Z5sm=7%o0ANJ$AUF~A;#`7-wS$?0J- zdKsTf7y?^PXK6B$ryg})TYW(7NZ!4sPc4>|0hWP5|NgN^c46Ji3|NvD&m> z>q7uBh+Ygiv?3k+nFFg3;mF2QBn*W$1FVC}7xvrUvDXt+viJOibmPPQBg17?faUn{y^iB#f6&lFY z1@IZS$|m->rWO=^=L=7iI8oh#-AP`|$(4fYo-y|C4Ght+5eJ}U>4(Z-f2)LJhrSIG z$zU+qBHgo}=VQcF*ru^(+ks#m(zOX5o-SS2yRpCG&cDNtfK3@!%L7<~cf!KLV=7Y!sJ(yZK^DMTSEi>3l>!^pU%u}M8bL}F;JOTwn`GPq(Z~B;f{kQ3NsA(Qf zL8xGD2erGZT@~oQ|G0u>!E`>$;5xun^P*{%L>tN!YJN)*CQqyjQA!Lj3|O?%NjNxm zrSinZLIit-jmtx$gic=F=CazITj_U zR+UKc6k(=&B`f({*0hGuG5xDwC^3c5f__$2AF*&G?iJWtTb7AB18#BJ9W*6Qx%Rm& zC$c(-l5$dKA`g0oa&bDHbf+*3tm^fNmM#oSc%Z03ak64`mq&sCL5yFG;5xQbA>JjrqIgq3IejNHsBiFCRZ4rQaxf@^qOGi}a;5`-xkMj-QiJb2 zoe*jjXs);F(9O@CKi$Av_GSl6mLr<=psZQzkqx?~4(<79_`TP=2U2FXpp)BV!v*FN zDGLQjqMl_SN%be86s2KXrK5Rm)=};Bs;AY5V<1vOTv|wKD3+Q=3*$YEoNr zGEBzde&|NmeRm3SIY)JcqNuEQY)H^K3UOh(aAgMc1sB`N8h1>=#GK*7jxfoov;ws+ z1jogr(eMma)0H=P;)pRHjYf}uF~q^=A(x2UW910(r&D~yX(B1QfLBroYGch-J!MTC zEA^m%^b^Tzlk7&WjY)0?g}k>ahJhc*{L^p)Jy~z-<5NfrtjYNJm)BbOmOEQ$WR;F> zKVa}v=mEBhbT26QK^5e0Rm#!jWA14^3SwF}_)ca{`0`NpjZlWNPi#TCv?;fOow5OhGn zQi~EdIJI>w=XW^o*(^q^4e!OMQur6RpyE;XT3h`0-hKy$+w6Y%6>6#1$ElqRI z!I-ODSR^hj1fD0QNa(Cs5G2V{u2Dhh5EQiWCBs)7CE@5<$k82JtUj!;MQY4q_=cr2 z7sGHsAq@8@U^+o+JVg!ILPnTYl;~>Bib}Om-ej^T$zlAGl1ujlb~^M(Q?ACos|qdG zWc|y_WvGB!TNMMQ*m6k>Ji1vu4ArlooNMWhmu5GXm6}ahYIWrH;R4iGL&RLFr5Ymo zG@P2-FGf-q%8BGJ%5!sjd}T&@EH8KGqj`FauS!SlO1-Q0dJg+=?CHV@H${DFpQ<^8hIFt0jwYAz{h zN7aXwQwKF(ZbAwzRI3v9f-?wWVmBxG54}Q2mm!r^Lo9#oj}2wOFCbeuqRao~OFr()64 zx7G#k;jkBlZO1kx*@wi!%r6+;* z{8Nh7xTLD9FHy+zqGx1%-uCa_Rbr(B;z#FAjv8kA1* zcf=wEqi-tVPWT>SA#IvzDk6QEK4M_6EB>yl1!v(U}jNB{5tgU!ETW8e9%t7fi~ zNoh(1=S(NK(C>#gFP=-aIuutUk{1v_3Fr5)+fh=41qj~uY=Cm9C*7m*6qV|mEcU|p zouswK``iHR;B~fke{=o82A;e9Out#*i`H?EVfyW$x0l{)g7u*gPvaaVQL$n2FT4)- zq6X^0Mg3;EyY;Od9I;mMQB)iqH8e{<-4I&HL34aY3!jnEzh! z>HW%VAx5FZCsXeLMb&6m zyDJo4=pc(Y1+4UQUp~9#N zBWV_{$vb>xVN!XI{0*z~f9n#B6B{3ufQ+0fg*aU;;Sq9CEd=k`Co5IBVqc?!L+}M( zc3Eq$ZmqWu5ZZ}Y&N}Xj147Z0rOia!Uwz3Ztp;Qdx=d{jYO8ms$l%VhT`1Tk{@FNCe6JnLlj-aU=WDfHNo0h4{n_ zXF;G6mbtoDV+Eg+4B zKQXv-BL0y}tZHo(CFD7&Gdb5RG}qO&D${)zWmL;3KpTX=TR4qi1}ZmRR|JUE`nrlg zt}naVBBQc}vOPJI60YR5E!t~p7@2OqD;o3jL_hx%faaQNK2_lKJ{dR^rZ!MEz}P`2~fk;C75{+>`O3B24)-5lAC}s03{*9wCl(4yhK^;`MT`cJv;V{ZP*h zN0Yvrse7?fP|}ObeL{_XV?%UQk}|pECq~;CAm7uci8)<5!6Y?hkvtosHX@k(Pnr zzVYxrl8*$}6zz81vKp-D?MV7`fV7s0Mod7hL9o#DsPX#z^l&=9dpFAD4+QuWUhuLw z1ei?cd+yVqLuIEzsI@21`D(3; zn#PM}7H$w$TG}E~f%){TLHo!X?2QCWHlt?~*n84ZBVHijXC}ASWjL_$2vFQN0YXE5c9^z#`N%?P*bzU=@M7dp2WChI&M^U zzS(Lh<>NKjuvI!_vW>d1+bxohACqsPyND)FrzxZl_iGm-sJn;}b{;NGkrnXIzDe}< zMO;1zyvuwxZkmrYZniGQ12-^!*a1u~1x`jjBr62GnoE=VE5N#BEP2kNTdTl|H60A51(s0Wt+Ar9H%}I6I#VpBcH4_k|}> zdxS;~!{5Zm(j@pDEW|<(2P6fO9=PMB|IM@;dk1qg z7k{~!pICRGgGc|FCS&+)-VdVWLo!^_3KO-D2a=YF`-gZb{2_2T-{%8hd%gt3x(B~L@E0L_ z&QF%j^Y%QrtJR|hB?K52WO|Psgef05*#30Xem8i0_wM87 z#-WH_D9U0eFOrXKJ#MMho2%f@b9AEd6*nBWz?wwR$eM9`0vL@IABO{A#M^kky=YIA z59o}r?4ej8wb=;2fEexNV5G!g*T!Fh{ZEC0f)h-eOMu5w2{5LY!&o z!2ZRcyK)CGn=6`x?HycBY&r4ZSZBJ6>A?iTvPVxo3=>I@$8ykJOU!-lXa=1T$K%8K za)wYr-8;C+cWcUW{o44nhkglnkDJV`t$Bj{I<4PaF;_WU*#-v-gbi>9bIY`ZC0 z29AUgJaS=(eTrjAibj#sp@xSQJjB3|MO!Bj^x&F8Tz8E@h|q7)iBSOUqa+}{IWZZJ zNIse-8RWc01N@W9G~~LkqkHMS=xdYZ;0tUY%5F`uTQPllHu<*n9@0ZBZ>sdr(G0(; z!i1(=8a^+=NJp8V3u37|?l|nS{ml$FDS(PC$BQ2!j_*VM5pf0GkQ!n?_|Nm{upI!! zfvNx-C7uReI2+5;X!R%k8-mh-qM;Kb$sjCa2-HgM>$=Zr)hlv2)wFSo;yEVYU%;_- zx!Z%p(_jS$_=|bw@CFiZZ@<;v$lsXPUEgi&V`t(cY>ocjL|b8d==aG48dL%^KFS-T z+HXGon|ypKJh+?^7aL9|A3s7!NMs`i?ois5J`D_ltqWTZlDxx5?65TKfR|g!+M@Y% zx`-~pMm+<6C6lzN0*ELs%lq~mv2HwLbz)Ef=nN6HDEuP4;39cDms6KH^$UeAsOOoU z<~qPPrnV}6xYj~s@1)s}@sEJ9sUq5UJV1Tw$-wmhSwOFc-@SJ){>HW4uiFq7yB*Sg$e?iXdDLzgAZ*=p7k)GlTO+Z3!Ox5I?HRk<;qU~>pzq#W_lRa%TRh@4Iw)qp5^t5mw-=e-|cNWce{)hqyyBhrcy z*dbUD-tK!_MM|2Sw2Z()TC-L03OL-s%&exiCntad?&_X5;b&E{V-g^R3tWKQ6n<(w zdgutJ8>B!cM^P5;)VoJ;iULg%Ws2G0S^H6rF9R0Sat6l!Bb3bos^@k}C9y)bY1ti^ ze^uD3{Vn37v`|UXZr|{;p_8=8&84xEv6SV?BNb}o&ww>K&*Pg``%cpGMj&B!=(`oE zt*tvmp#Gypb3NCf*b@Xan3_xyN1h&ocRU4zyC8(>)Dv#vz zVw6eF7ez?*LNm>1n;WuZAAKY%*tU>zkQYFWmCN1C{a!Ts(5}UI!UN(ia{MJB^ci zaL$Lp<6Pv?zF?~#ihLj-C-QzX@e)%$c>b&n>4K98+vlBzUj$DzpaEa03Ko9r<{5tJ zo^dSslmg71q0Mf65>gX~sklYx?96LEvM7xlJ)sQP8g5fajLqxo=v2~p^Cm=2EM+w{jRIY!nBpAV( zWoG4t%R{|ZM5ZIertj1of=XNHB1m|2(}dKjydJJjamMb+lMeN8_MNQndxWZLBveUD z0#y(pQ|Tnd73q#RQDAWdTjL072}+`9{=te&wBlue5=%@iCeWvg5cj}`vI4T_ z)Sg!R4FnEM5&J={>9?Aq;-E}VP?)ScM>H-Y-}F?rMhP5E8DtZjrDNZ5DC-+tNXIk; zjg1nR9|v;2=wP%EiETJvof*8Gw7|XgyqI}rV$SMQl_KXi1sZ04?mgh#Phq#`-*U=w zOmSN;Xry2bRJ|dDKpB{FBTZ_6ia=uyO?A4DQ_&s|=H?WU?Z_qDhDe!mS=n4AzXY_k zZ^EXxRgO(!B=5U)=+?k(VxAMahmfgMjV7KzIKz}B?%V5K6kM(Qz{)60)fgdTsS(0X zV^3~|7GLTRCPTCogRrx_2J@lw^rFzl_%h7Q6N{V`1Lma&VT~oFvHT{PN6W?3Qx6&e zk7Df5$^ZuR!K$d$%9!f!&qFtZY=|&P;`Q?IG+oe}z=%-zaWsGv0ci2$2{JnPRHm0n zWwqzxr0dJ;*KqsswY!z@DLoXZ>pHY*nW{Rp-=|r6n2yuMxtolSL2wVomhtC)XR|48 z?wlVmgx5Wjs?W&YAieq5j3XmvZv1$>U3>mstkb-h7{Dk~qNa*W!NDL#hxrMLzqmHpP5UpOb=oWq zy@=u8^5L?dbK3h1$#Hik87_^IWJcd+illGcQpvF9b-O++jDw+5NLi1b4o4DkSa=$g z%@)rCOtJs-^-n)`HEJO}I#;f&NDO@zb5St^s8iCzY%D#=E_zM*q@7cznz=|=SLRb^ z=cAu12iSe*=FOu|LSLLZ2-9zTlx0u_#lL7UJ=6tYmQQx!Wt!y_k`aP(X?A|5aWHa5 zX#sAswbx&Cp3%Khu)}lfQzw!Vy=WoJNyRelrxzh}%p}gG-o~zcn`c zat9pszI+{2!Np+*Hj9=m{Eb_Y!FD{=EJ9ciM(x68Brc(B>8yDS7V9B{=Y*+*ObJFN z&!NIf+j&e}+w&(F@0f_uE-)xu&>Zk9oMsRZ1Ov#w2awj{;R)$5Jn4GK%J}|An3~Sa zD2*9&?5NbNL|a#>N^vDH-%($4FgJ}7vV&}*BU!L_ufir1K@h?S&@t1?#X-J_4+{t) z9Xi>?1tG6E)~=7g`%SNQr8^Yh#Q*}BLxR`BS_>aR!gmo%-?$t_*aPjfUeBgo*EeB! z)~TLQ92p5#ejLzlOo0mc$$L?^>Sn1c^pk>4lE5WZSvD-yx!QKae*C4OA_pnWB2HZ$sO$r-tAWb4 zU!#$2S+LOSQKE8WaqKj`w!9k!wtGd1wL)3I#`$#FIEyC|1F8LrR|KAIkn9FyP}yYO zVAdn5^Sc6^U?Ng=E_y8g`m+7VNvDG1s=75^F%y{4U?Q#4Aj3UFa~6i7_o*LosvYgO z)6uG)5k$a>Kn{7peGa>VWN?Le8zP?200p~fWFBP|MwZNEl%Q5Lfj`1giW<=b=qdgQ zFPiABCFWo+izZj4j6khWF`42fWQ9<&V#0>lp0@F7c#;g?$(HMhhs#xEi0!_W^jIQ@ z#9bw%Tr0_>`h)%u!AJiDCpO5LFk5Ec_O`&oa}Cvh?H?(FM;W~=?^+ND5??6Nh2&TB z^Bk$mzjIHj2tY*xAYwHG5#wEHt|c=tfm52H1;-N*s%b@NBgXAF6l5&#AjtWjz0gY)Drt1NUE+Ka% zXRfQiU^W**&KaxhiU}nG2HhKhiBRgqKhgfbhYBvzML75viI3#k%a7WS< zF-Pd64MsPbD@QaZ^tYlrWT@UM_vzG!ref8zt!O|= zm2I_4iUE!LAo?vJPU($5@!EjekrF0zOBf|}PfZ7toB~H_>c|05!%Jg&`iw)u3=8mk z0#{Lz>fvALB8PoWbbfnEuV8UXC^DvUk}LqQYCP?WKo-Lp3x4rZC{+zTP|Fd5SUpa! zwcETQcG9SFzxQBk=k}>X(=c^tV|! z;!v~{(+>|a$aMx*uLVkgpz=lmiPfm$uMvU(avH;{g_e(wncn0??J$)LMJ*WK0|P_$ z3w)=|=(POcghADP+WwLIJ??%Fn#lSiPo;M=&9^UD7X-d9raYuW*fpWUo`5f&;NlGA zS;XhTI~L*yerj=z`~ldy&fM25!PFsLGtXVnAduBWoOks%0Z96HGTc#>JR@874ROHD zLWIABR?L=%fNRuuNTf$8Is#!miaW>S>0uJdF+=heOPR7tO|cC);kyp!WbMFl-&4q_ zX$CH=v(o#lZZItyO4Mzn86l;JF)-(da9A~~A5v7Ye~?wd)no1djaNd z%O9&*?YGSXX5M(d=hrLmhEjRK6FVeY9gegIc~%xHVRTL(Icr0aB7(dHNVd{O81NjL z=gsD<8cC(Y6}^D5z(Je<*m5H!t7Jivn`aBF4<;q7x`*TC9Oh|QB5BnYKfrS-mlGh| zRoX9)SFrBP7_N0CcSZybzU$#_eM$Y;QfH6#p72|B$NL!UyYJ`mSu#f{S)mB!=M*t! z=d2iWL+tQeeCTfKDiA@IcO^Ih`!`d$*dpvVxO@r512)>rAIV3}3-nN-UKNXq7r!aR zLF|H2^SBvFkFW`4ja)ruwraf()TGo(4{-c~*W#E`>*mL%pZ$o~|7{OUS3?+R$$|*Vzd8C2Q6^hPCH7IGZ>sO5XISS*s3WBhgG| zB-m=ZTSD5($8yDdhey+471yL2WXveQ_;6s^4$3XdiTrtWYiq>zYQ55{P4dkz%}g70 z^!gcHRk=h*5f1uR=W}(-yn;qP7m>k(g_0N*3j?W~evC~j7g_KXm`fzk0Hnwoo?qS* zQSSwz5hxr;NSwGr6dq6^-Z6|l2U~&dT}P18%ykeh!=`pl5A3>Ic zKplY6j*;164g@MA2;;F7&VlXDZ2HDga?a{ZH3Ki&|nO;L|h|dTT1x!Vtst zcYsrT;5EsD0*RSgnHJu{^q8~}Ixp}p+$_Vj+A1yiXV|qEaaXhh8kM|VPvwu%AGFL@ ziQc@jvMcQ_7_w^je*5{>AkYjGb>k_fd&la`h`DMoKFsMt1#pe_-j$rlxMUj-5dv zLtliglDPKoAZ$5-=!+17`!!U+xup=o0>Ba)@uwC-C{7@VYW?Bu4cRG;>te7@uQpg~ zrOD?h!hp?|{Nob0w$Hs)dSe~s3SjtL#36B4ZU9qw@`@!(e34lC7tKE5FS2J0kp0lI zmfGd1s560K$>jHWA~!ix#gzM@Jx^ls1u0b<2$RAEbqyLPp?}}Q5H3tfVvsCMB%fV3 zTOJ}P6Lr^uE`T%P=;4BM1pq~ZLf#7~bAbpmFO}Rbl@~`P!--YDmh7zmTEY(c8f5`k z=Iar1mA{VPiUajO29b+^-Tzul$WIVM7FgE6jLkNpT{19vH~I@4pO7Qms(MU!OY$ek ze}ijXV-g-MENmH>31a%d=|UJ=vIt(#dGzTB-elP%M(?)# zVkl2C3VFZXh*vbBVY-uzm&Z~4is3!uidhO*fG8_wV#QyvXc$cki_Y>VTr@+3N;Wo$ zXhO2s9q!3+yo67RUA1(S)y3OmCSU2jG)WhdW*J;e1kLWDr+a7xvoCGJmvrSa5F}{q zfll{Ur56u$S4R|3m+;6ULL*!kK5eO{t}oIFi*h|iNv`SXlhf%xQ;nu9J*>y+-4LTO z-G9Pb1p%*wczzqc{w6&=Za_ykK4$4Bl(@nLeo532{6&c(b^sGUj2)azM}DKe`gk#j zzIK*yterlUAI;xK37Y5>3udI7T?0`!|zC#IxY%ybqflcX{iT&c3>Ty zrK1IkF!gq##z~SMpJ1;18_S??eSjPuMo8N)MynajFt2}ny+$cMK2FwTscT4*&S+6F#h-?hmg~7n~3-L0Donuf1z3 zO6JqET)4?)WU%es>+IlV!tWy8YdHnX{ktlSibx`pb+=IF{nu48$K5;+w{NdmkAn~~D_<@lV(a%e9tOF>|2Ea7nZTgO!73!NS`K5h zpr6?js7gPbO%Smn6?3)ftre6=T@|F-HtdFhy(y9{uDIR4c9`!i1q3B7m@bRJFjvJE z;xwdcsefi^-mDPmQt-1pw_Lu2{B5>xsMFr^;OR^Lgn*zHL zTD{pO6{F&x&7mRr3v`3)yP)LuAlcI@+I=+=wP^SUcwJF$7ntX&l4IE{Q--KJ{fxn` z_-~8l>|8-DEC&_E9dbw+0pJr;nQm2i7^U)9b{S=lpk*OWbL`Dt=C`^kQWCBj;jnph zWzARelWTX#UhepEkBPr@ zhtET^9xN!1em7WwP3$c+x2`q6iT7H2{ePU@>o@-~y6Ej(e%snxI}R$tdCmq~6n)Q4 z3LILd4YvlMNc@6lEm&riNde~^q;h)~Y?c6_3t~>K%e~KGRZCf;>}tbWwMoggCJnAL zuT>gVEo?fF7~aZ8w)=sR4Szec+OIaU^^*WMGw}fZT)D|9-x8Yi97O^dL%yMUwM&lk zd=poDzgNVd3Kjn~VX)y8%a?Q)-<1PPgSUmCQa&ePXHe-a+#L>1F!Cb<{)@BYkL&Dk zJ0$_%zsk$u1CYBU2mb=+1_UTrK)hoEH;^WC9?7S2nOCmfhnzy~@Lt~X(RdbAq1xhA z{^L;MFy+#P!<9=~Xc+>VRP~auTTClgc2~##t(utT&swgH#y!i#=r6{Nq`l4CYqbS` zF+;0Bk|m%DXr3OWNL_$KC+la;2b){l4_nt3rZOCam{c$53SG2DgB78G{Pf|@_U6{@ zmlG=K8={#@9@m5WTVIrPIs~Pp?H45-*I0c~(*F=jx~SD*Dy5G<3A6*1q%SPoH}Xzb zeUPo0Wu(iS=9qJ-H+Vw-wi4i?IP{~)9DN}28u*B!G|DBQJ-a6)K4XTUnB73`omxGq zx0W{qy=KU4%cz(Hh6&hXO|w5j$^p{zNN73_i`&D9u~frhp4{Kb%yl*XQ?poNF%C)@Lsn0qE6TA_rHNU zjXPVeQ>RyiWHE{vRu5o3B5|HG=Z{IQKxHdxm;IP5&ZhHs`BQBP>m!VGNqm~wx33Y> z0ILIj)wA$Q(aG*n665|7TrGi-1A{N3jBEx7X?2xp%99&VwSwsmv!*Z_lJK8n^s_aUEc@4PIA+%`?Od4X)pY=x8Fx`>(1Z4i*VR{_pXDD+733t z4mQw%Fzw+Ya`ro4P(XbmPqmyt0+v3d?F;GqnQ&NSgP7IRl@rJd1TcTXYI;=irLJ>c z>oxZ(11b0JS~^l&uNL_+Ib)rjPiA_?#24E_rbHf9(ewJNH*=^G-5uq|oG? znb|$$BbdTPY=sp=uT9PG_{H6PgFp5iznC^05Eg!6Vi&9kdKt360Tn?rF!2Am z@A%b~{%P;{)#?9+cl^Sa15YyC_#+fVM2#UWq0?*)zGWt*`hfFVJ!WYt`+!%7&@5Dk z3^U5XCfA|zILvh8BZ`2oka@jSEVS_`K?!gZ7a4`^Wy0`9PFNi`de?~5uhT;ZGn(~% zMLdRd0MkltKGq&Uj~I$ClJy{Fr9TBUO}2O9k00;28N=ahbN^<$CHHUJS18DWTkGRk|OLz-+bkPsZ8f6&C>HIushgkOAcmcYJ-1Ynv&NsDJALBB9aS zYgeLx1Ekr@jhBTdDEUv_QHQL@`Eu04SGDC}ATFY`GizvsCqi@GaNOJ~Jp(J83OQCe zEWDAwxB?A_@L)mA57QGyyo$!zhFEvfN;0^BisG7A_90Wq`<`vg?%thrp9pxsVJqF< z01Cxg*S28ueD7KT4#5_j-$^DBLKssb69!GYo=VZFT5lRcGn8#FOv@E|nEk|(%} zgH+Gs;k#u1Xfi>Z%rNP{Ep33k-)|#{#E!uQob<1gId56RtqSm2;h)l1XtA$WuT1W* zb2|uTq8d_RhYv={;qn+Z3#NXczGQ-HDR7`CFrkN6$mQY}PrENKQfw~jlBQrSl1HNv zgkVkU3H#Rinspa0*YvE7A;?KQ8z-10$4s|6P$N)biVROP0$=E=_=(irLLkkathJG6 zrL_9d4{zSQMCA?o0y!k&h(P&NeV{K2l}T9X7TBOc%81!>hwgi2&yUr-qZ=&z#O{+) z6-G>wxJOKO>jyWmsOVIc=hReB4G+G^zRw4VBsVyrN{?G%NfCp1{ofhM!sU zqMkI$6l!0g5IC2Ldo%z5T*FG1jqsO=I}HCbBdt#6EnH#K9>Z=Gk6xqh3xY@H{2fa{ z_!lfbNtQ7tTKU;@pS-8jt^4B9kI$YydHv?#<)c@xpL`!gq{zh|C5(0`8ty9YBn*Ed zX2)b^1nW!&m==`EF^XWWELp25P*`L6P}pc;@3`A%(q%oD^oV!yNpfJ!8(#)qE?Y-E z4fP;S7-BJ`ffCOqHMMLUj&@Za{y&z|71b3~q3Nc*oyp+hzx3hr_%IpwFV_1Jugzh7 zw|aO!fAaSy&ktVz^6bsyAFANg;8mZzSx9VbMkx7zK0i2M0gD43{RPUYCiBKyvg;1t z4ZgP7e7*lx;uZ&AuYbMY!0p%*glJ;NF}3Fe_Y-ACdyGzUiJ&s^21Y^8EDz^)<}Krl z@XdZ#H=-TQT2PoPpYnpr-2nYHSIiLJK$jC>kSQ)!^H+Cyt>?nkRKbyA&H-!X%4Bit zt;=}gE5jFTT`46{?pBAvS2O0&A+t_-Kp*Q_uR)jSDd_pAVNh$Jvg7bXuDfG-dT*qy zOej^s0ajJoDsTe5ppY-CmJ3Qk1xgDu21%mE!XlBADJl%36jO30UL$bKJ1brzmk*(2 zPDB>%9Hqc|8@G2(hmz%k&ECe278%vly!^JQFoHZ>>lIo(FQ_W1jDH5L9@G9e(du=j zr7dFKdrh$RvkAxvuHOaAz-3ZWwLSx`#v@!RKIym%pK&KDSS946?ms5aaM3ug7hsvq zL%Y=-@^4!{i0AY8+%6)|H1=BxgU1aQhwko%+i$k)ws!`BgP%L>Yv}ZUL#>`{Wzq1; z5=-q$mCMjG3^E@&EK}A*%OH^DwQJiFo*B z1s67NDia*fC{{2%56!VC0Ly$+>sm$XaIjFzs#P>pcmX+a<&g^0a->eB4Z0G!IIk1f z3i(?gcFkNWW{og{ybxm)1dflTD#b%cZS~fUJ4lBBxh5<2e}Gj@ju$6>_6Yd}{2i=e zk|_Mf1sI$?%n~8w1}mzuq0MrR)CI4|wtFbg@gJo=y{xs$P~x?PJ9i+lGe;M3D0*op z0`XxWp?!cio?B!+eWq**tfz2)v&L)*P5)+%eGR4?G;c;0QAhg@Z%zfv45x~To=!(7 zuL#3`&_JI-Yk>9-2o(%eyecS6b6l_FJ+@0bL^u^TQvAxrN4Vvzh?IprruqW;7bo5& z85E|}>Fv|Ig=+914ooYdPdh&XD-Z;jfpUG~-jE1F*2C*CUZ}+a^a|8MNyMNk+hi|AlLxsqA#}N zNxv!$WW)4)aN5`uRqozjhmQu^}!Bi%v3--$K^@)RP#w!nk~Pa#`0;zS-bXN zh*6ejqhyhtHd8rS^(G^p509hngdk(3bpCN{cwQEB*nQxe%#qpe6=<;1`ELfj?Y#v$ z&;*Tj9JkiJsN3x#CW>x8)QhkCJctBYF48f>dWse1X|ops`ZKD_`ZMaskJe^#zkw?= z>4MGfp=dbn*n+tmr+t*dSRc4u@wJUhKO?EL){;TmL3nsxsyEEPg+ zN-s|jJL~d1w{(2{&CHbtioX3N zY2d^(Xd1z>OGb_C94h^3M#beM8P8B1d4Z#Yo48mdC&lI0@u7SGy8#!_ZAdL;^@TOQ zi5pVvzJMfO%Q3u%3PDzs=m@r(&Y9RLI)_jVqHnqV->k*oN&)DjF^U;sCZGfjGYB-k zIh><(Pa~Z9H?-@l?(+NTGt->k9A zZ&4fzi(8x|=h_(7VH~q0zmcqw9_eesW{$fisNgl?88#WIyMSW%o3+`um}|B;my%3e zZ)m#gub@dYk<0PaaovLq338>d_gL6M;kz&M`@*tnaHOkpa^JZ(#l0=Eg+3^C&@ z#)7#&G17Z+!4WQF0TA3QbVSvBYrx$7+AN@}uzZxh2cF~l_!(;Xj-=?jIGDp5wthLm z?qKDo3MoG}Zi}O75V(q19}TlTi{}$?I2h=}RJ76SJ@^DC4ojPy*g$9PH z`AKUS_#A*p!rYLeX8gWjqjJigMVwq)cssXZO`X?_RTXFnfR_Q64y!dQEXb{I1FIJd z1fW4+%Me|cQGb$mB&Ac_6;Jd0xmJ{NFUI)`bgq?`esvI>KldkB{ccA$I&d4VQ$l{%dkFie3E8JR@N8G3R zg&H(6;Kp1@hM=wL)^l)q1!flfv{_u%f-pX;Iq{8Y(;~~QJCWvW052^q&s!u@k#*%9!49CFb zo`pL_B@bc_BHy{qs9sSCfL|4SfA_9$3TwcwGCq2bx#xXMtd;ivwU}&oKd+f-(fqT~@Wek4Pjb^6R={AV>>t+hTvE z#h_zxkaJ4f-QES1E}Ae&(6wV3`Lcgu45)5wZ6bGbJ4#c98(iir61+f(KbUok^Kxeg zlK+QvD)5H1dSQ|4sCQBJjX-EOFTVzXt_<>^i=j-S!<2Jw2`;S?JrstB)n#|y(iopt z?b)YkT_ZjjUq%kBj-z$DIpmC}@eT(J4Mr&Kg2)bq;ot@G3AzBZ#dz{(d&ZhkXd2$g{fwG4|t?qzX`*HJ=SpR9zyv9cF&!$r9q6|XnKwK<7CBFQ- z9LGQ0DJ!5L_9p|$3WY>&4lB2X1w8Sfyn_fVUg=H}9EUhpin_rO@4#s*hX>BcQS#v@ zhE~IVDd#|s(zkpN{Kna%fm1({E3VxGo+}BghW6gpK+%#_IXvEHt758E@4y ztN;0%sXMe4{w1_g&j)2YO7>1{zjsgFWaa1I4mJS=IXp#r0KnHh)rW;`GhdtQncNlL z{8et^vNr>?syHy%Pg}f#q8XUL?**Hje&f6ww-(aJBBPKNS;dM1LhAxf82_W|eYhhr zbftLZA6Ztr!i~GlGMd^;rO_aTQ(WM(sv&{^g1$!*?s*YDkqMwEW~J}q1j^GJ#O0BF zi}H}l3aNqRw@#}*oIqE(M;wPM#(!QF`v7E$RdB-;u2x7P&>CR&z?4@(z~DKM%a0%} zu4;#A_aJHH4g8E&_23)Grv}JXCQBOTeGNoEbX&Jbe35-#X-*zHl@ANEn2vDGk*;1pQ&PhhP*pDr5` zY=rP7s?YjVhUhj5<5#WpFKOzn++`1Ax0ucCA2seyuc=BkEf{wZev4`_5Mxv9c!q*E z8Opp_?1dt3DUTeU#DJ2GE^kJAP~Rzjj}rpme+u=v zd)Fd^$mM582Mo0NBTys|O}!iOvDzBN=vB|Ul-PSKnX9QyR>-eJ)uBXNA1c7hqa{5# zAIGINa|)U^gi}$v(Xqfhf14R#P|n}fZKhfW*1j8pLI}Gm;>J7(2@k7%oq5r2=y^RN z$dz})*^~<~td1>I;)a0DI}hQ?<%9;t&$^zHsq*Rt1+%!}ih}OLC8_2L(13$6=pvUX z3&@f~znsnvfA{dr>emXVPf%jhX_^aCX!nW$U3_6yJ=aDIrv=Oy*2wwS4 z4Q5~oi2R#JeF=_I^MIR(i$F<7xp0-YsHS;h&1SFkL3uIz`-owgI{fm2r?XvL8&M5hfrihe_lHBcQQeK^Z(P42O9@@tm z9C|$|Qn1ouLnT7Nn$rUAJQPKvFEX9-%g;`8RD$%$RT@yC#Y*+Z z_e|}_|8UBWe_$fJYjVZ7P6eoFJ-)BvNH>!^^y5zvIcmfU?aWPdg|-t+x2Wvg~;XesO=8_2{)nhk?|B3vkG(bWW!nfaCfB1xBtL z{J60MsssNialu`IN;mWhhOLk+J4waLr6g*VU|IYGa;cLw^@Rvgp#Y(CahFoBm`xcm z-G0#k=W)Q<@X0x0xJcLh^``9$C<>=c@EcVXJXj@i6M}B)Uim@Sb;Sdf1;6@v%T-P4 zEFm~`0L$U~H&X;!p|T48w;7pN)%9aRUPw)X^okq&PLnROH%x2(&TejPJmLL$)P51A zh5)H(6i<#>Ad%63KfsapJzNLpeb~bgDn-#=(+gIBx$gMv)lo*1i)alhF;%iXms(+V zBB?H1JtuPQ%uW0ss6qlaA^aYwLof9cEhe*x3kSyIDZB`-(j-b_Xt+pTXjtW!d(0Kc zx5`M3lkQ68DdDHg<(R^HN)6d-)yjo+ski*6=`6bgkNBfWLvA^2OovE6fPLl+J`$IB zhRM_9qdn_ZyJ_Xl^y<_@lX1RS5PQt1f14e1gQ-dHkGEBBekITcd$gj(@3T3%pRT+s zC_#q6VB+h)^6yHhc}8@0$ToMAyP*A$er?mxpja@I(-X|y!+d{ z^6r-G(3fe?imol|uk0z4*r<{h&H3$+r)QH2WA5m{m&awFsy+6`2lshySF{!~vA`&)_>!>bnin4d?Dv@8r;FQb+tdo&2hSv{X1 zhBDt>2x_tbHJ38JQUzEph|$;x-$Ev#MgxvGcHvWt6oLGfOIEqD0YMqFAXFl+AcNXJ zBX}F|zmRWHPXe?PxT}z?UlJsiLR8|tdxGp51oTR55rvy-ZSM;)inIe(B!@7OxDe{q zzBo#g@rV^aSp~{P|9C$PPFqdr%nDb$1%i0r3ZxsjpJ@Yb%hd~FlZG(6Y9)$Z6xCc@ zSMO7nQWJRQ+)DM!A&@WIXLNJ+Vfd5O)IRQ)pNz(ss}|zCL?8<`2ZFfdnCJLWmtkHy z+01Te-$x;|o0Y+UXNx)cEYp{wwjS)U!&ma?DST#p8vBHY!2C!gmB_Vr+K$^(RInj- zpi0BS3+?+b&diQE($wCfSdT>|?bWvzZyD>n zbdJVLlwderbT;{6eQVTiSp9N19d}MgovjA{7$5t7G9b%g3^nK?UMx_q>VheRh}kxR z4x`>A-!Kmg9QFx=2~aG}b`NKQKEZw1U{QtV;C(Y{AoHqzqBfTuH<$a3*#{(Q2}FpX zN+f>K%oP?lQzpZ7vEr|4*w}KyY?lM@H6T&c+FJptA>zUtDEJC27y%j$_@lQPN61rJ zj8KwS@ZmMnm--inDE`I5_Y)l8za+ihXlo~GA4++PezZCJfQ#QVdXoQgbacev>V9!t56)2zeNizu(b_GmX=o=)^{ z>?@I=xPs$&){lCyW?wPATaSI+7-<>xM;%lPNgAV0cG>~?8NS2419E{t@&UeQWZ%If z0>3j<^W$;CI|;TwG$e%J?1ZjM(6=g95!NTY@<=C}&=HF{cG?pFc(UMD8>$gWQIoi# z=+?kp1L>F)I%@5gMw^w#U?<`9iV#O-0?T(CXYml>5&fw9AefCL<1s2iPgq-} z`cqVdLt?F_+u_Ox60CTRA}qIsi_Qd_PE1`nq3x($!BGZPL=I+;wIEc3_4MdCGs;3P zf!q#jMH<}9YZCG=#8f~i2+0S6n^gqFt7K3Agkj2R9Uy8=AF*aAvbm$5C+{ZHGt|$* z%FAmn%<#F;n_El}MgMGq>BR68-4la2!wG1&H`}RKBu< zNHjwS2T4=2Hjr@BN{hrDfB>JzXM+@Y| zCXVtyK1sljo`U={O1gAF4o+E1FlRpa>$F)Ga;{?#w;;(?Vt21V+G0;RrYkAO)Debh z9chdA;(Efc>Zoa@fRy$VHHQK`89(o6s1Dk4SLPqZ~jG9neOPq^|^@ z;+k$Wk)$g}LMq*~HT}^wbNWL`1XWqpF68wV33RTs=%tz{Tt;MA!m3W3g+u!i;_ODa z_n3L@IWo}(;+KjxB^i58!9#`Yf;s>}Z}<*bj8i1bn;6a^dqouFp!4_)DucZ1ubM=y z^k|Y|Rd_6MV=bJBOC{p*u!$h(_a}``V;iDN%Snxgoops$6m%N<(CFjATdxs>#vN7; zu4qIF%GQ=y3dE>Ta4F5HDn_*qk*7y$h-@K>JTAOWz5I{OR}wBKKt?Yn@2xBesb{or>AdzdGzYZ!Ot(hfAr?b z!S~NzKl<+ZlY^&^o;`o^{Q+M6`0U5O4N$kIyl`eWy|>B`RvAIpypKCB?(gh8_$|!` zVoE9-3iKg)%uxg)>;j!QfciyDC@ti!8+6u*_I*OwRXfnOQqZvyOV~5zPtXqFRC5 zkv{8~dy4P$$)ln0Iu)D{?YP7n2Jtbj*u_7<2Oz%K+$oF7>*k-W94VQ-S*~t9{bFyN zqh@-17ti+`WZW~bJtuVPu9QLHWXK$3@d)XhXeR&ak{MEnC;SJ?vtRlLN+Uo+&|c@b zk&ru&HtuO5MvP(!qMltn2}`r$Bh=22Dhb6`ujN@gT1<~&?Bea4j5Gxi)-NrDGJ~0M z`i^Ce@VprN12R1wL2jIm@bk{dp;3xkJp|r>g1Sf&zT2b8A+cBy3wi{_;zCr}(&d@A^ z&N&nKyNy6z2?Op7@%R|GfFT(!3=HPRfGx_tmEitdHqHVNuEBl^>jrL>+l7gPlHGr} zxqbf+N6BEwGpG~kt|MZn(?e*)h7FMZQT=5OUaTsP7;;7Lfm34pCY-9!T}?l74UH2# zR*D8fSp`dJP4_suE-r+|Y|+_h%nl(l98N!UaP}bkl zFgy!almAWUs`}uZEUDs>%%nqe5F+hXc2g>y#?niiQ!WjM3Bv!n%W?BhBir=q2C zj+{{k)^TY2x;1A0RH^3&UbF`uF8`qis2~je=BxJgVeBeC$4ajT0?fy_W%hG|;W=-) z{XO9zhZLcF4a zrBdakyNC~^mX{aLNXAb#%FIB_BhCw4a(0qH3;o6aWT?jQ6#}MJN`gc!!V>Bmlr`H5 z(GNzb>yXL=@H&k#ICa%PDa{j1`#cS=diFS*ESLsd%T4JVeu zZLi!KWiUHi#+Fuzw9fTjFZD{XVe271g-fwG1u;U{uq&hqZ$=SsufrVN_6+IJ7Vd%r z)v|$0z{P&CwVqoq_&1#7j8pGNVi>YtV)EC(C|)4_^_nYvtbJu`?t-g-&W`uq57xil z1xl5;a1CT+**+=(ps8TN$+pY?uGlx}NEe@q^7?;F{;>%E^HhyAGr~(X*u-THc;<+M~NJG}EP=0oTD~7McdBC*hpr3W%(($?y2ES=gFppAb z`%+>Q1wnw8<~b>l+S=mrh-&~*tfw>KA+Dzih`jTmL)fb?M^;>Jx1~_|k=^-opIB$~ z@zx(87XM?<1721~`$f1Cudv`0{|AEML}EM9UW>PsrI2F$4I(Bt8?$qmJ48Au6LSg@ zj$Fg^#nLUF2pM0dTu4(Ku77|(ZV1i3EWKSj6%>4jeG)~;{bZ{1AX!U?7EKxDbVO|| zILd$7{BCFC>HU=)23db56b}M!Qe6a8o*_yD=qh9{P?`*mX!o_3FaJm)wU>iDUk}7} zRS{mn%?p2V%X7ApuvAa-LBpL)d=qqHV3!*99Hpjdt19#f=DN(u9L2oHI1kOw4YHiR zu=-P!{i(x3Q6?Y?;C)GdfKuccqc3&Ajr@bG$yMJ77ZyDlEyuVMV9t9aa}`V%nSa2Q z)>4Z<#?pAv1+J|P7gk=3Z&KQ{&$~S#CA$HdNl*oXSFf75Yh&)O3fBFu3qWDxb72iv zt-v=zO#B=H?^>#)e0qM@b)=`ra=^*gXqPJveyQPSMJ7-|T3lA)P|h3%p^*MJTw95- zeMVN!CKVW+o(zs?>EM{27h!x;MnZn7t z>_x^qek&cHAO!H5p#lgJ3@b*Vx&z<(BYMPjtb0*=Rg7C%q;qzHl8K;L->;6%J7JSH zgdZd*Em)$2VO2-Fm~bqhrj-Ts{qqk(>kItz7(RWX8vaK6Ps z+_Zye0^-%@kYP0$T@8Ae{vhYjASFiFFUL?#hgYHp?WLch!2TY`5adv@N?er2Chn)| z{h>ThYS7mtG0C6yR94wy=!FWLPLW28<&GvY&o)9G3eG25?W8J>Z>NUA!f78f6SC}5 za^Edt+@(XCxfR#>dB!|krp_S<**RO&$!v)$4%L+wE(ocj&V6EW6q*(2*U0Lk{fIYh z;}LlI3^`b^FyM|yI5yh)mtOHGs;7kZ6s*y{q@V@^>AV!0$dSH@PsA*v)o9g9VrDMg zsp~FWY0-yQlw3eht1iF>QqZ#Lyfd3hiDMO5N4QcCDx$9Cd;Dl4Wa=0J%=6;Ys#V^@ zA)M6fMKWWYu~})aJk@5|SuIMbKs25%N1df20Vw&V3Pd-TQFwpe$O|eFLeq{LV&s;n zb?VTvwn_~94d{|e(Sq8i2&zHyv?YFuOccF9nk}ypj7{wE$%OivSQf}%ROTfEpwtX# z88#CPMJ^>{>^EL~rwT-0%~0V^T!kKbBxj7NS`|?}qEb_v?y)*qcy({#=?9FQ)u@6V z3eYNPC>Z#Bn(4abnJPFfsVRh&!n4LzaHW5s819X0&n0NROtnjZ z1srARysD1Wfaq#OHLSde$*T-Q$wgI@B?b0XDt;{`dca`L3Q6IV~no4?Hr ze$^Bg$T|;f^0k&MsAY_&P^Ma#o<34~;?0KCEW9N1u($s3x0Gc570D~5*iIz`NL^7I zL(w7W!Z`Ee&HNQ(Z8cN{kFAkkzxJeeaRvYWj}d}Z0VP@$`09S|uxASXN+N;==LrXj2BF@a6< zE3ogHFR{IiOH+T#UB4z@0=9XU8t|T#lAu%0MUq9%cn}Iaqkb!WDw168+>biFE!$wg zh51q%S5ZT9kdCq<2dXh+WW#L+ujWC?=uFX+qvy|Ym?~GveT1j2ex8OS*+||NjY7F8|L#bFCP}5jyT^T*KhKIW*3;mA9rAzMKnFiW#&Oj<>!^%+u{)t zvUY){6r($PZbdh{LK7T6NWXXt3P@0K??XnY$=Afq-H1vJ&Aqe_9bnym<80qL}_ z1Bz8DDD8qq1qO0l5p8PDi>7#D2gYy(&8D;EIG&sLGHW+I6T)qp{#u24jl~!a|A7v9 zUnQ%nPhKvDS#nGsNk$A^rKILLIy3Lcv8Z%bjzFC)Dg6-5j)u`aSA@;XLZSovEwZor znZ_WNOT&gC2l}p=f;qb!#d`aYd&{O&Zp3E>iizTZlq{TBz0G^;e@6kUeiJRrQnChJ z7M<=x!SqHu+B3DqDVe#w(JfhNb#rE^XEe{LdQuT}YY@3+>KzKVD(~_y3(#f6c<}Sv zL2reW2{N&P6fWKpj+k`A$3uW+Iz#doib$y)k}Y1aftTh%W_e*|P!U|yu6$Z1YxaQ< z3qi~aH)tI*eTXEiI$P;vN(@gJr-=DT?UK>!@pQp_-GgQGts=hamI#BAI&V??D7X>E zonq4gieYvVN9$utxUdZE_^;TN_8AIGHwJS`&1_ZU(UdF?_(I>Kh*pP?YDnBK&VpPe z|KXh1)9$xfrmBq^sJ)#B>+8ME`&&CW95yZMJO{R>vb7N=%yNepOy7N<^wm7-=jZjQ z>q3jY>aKaRud%=@&6nu{iA;`bC+h8|h5Sa`?sJ(;SMpdX75X z1ZjcCDU!TknPh^t2(87=uLFh}13wQia9@Q59R18pa^HlvxEZ}DSdkm z{|VwaRuH#UY1#!{twX)^DfgON4w>sD20XSH>Qo`FdWEw6s# zGk=cRP6l5;bLH|CluJe_?`E{vl2%2%EAd$^l1JHbsh6+C;pzVG|HiOlLj)3K6K zh6Po`F%Kh_y-MIp&XZ0C>FDI)f5*pTJ$i;&l-P683Ybowxh#dZ`R^V<&_1oTFlz(6q7bGo=XA%|>p~;e7 zWQNI_Jq-F{&QK#Tuc4@em4OOP`?u|5L;4^!*9Zh;UDb3!RFW%E9jSUN*P*Xkx<6Yv zX}czdUW%=vmId(I$$EeA6V=vCEO4+FB=E0ch{9EpoW6@pb+9rVGDi&R49|E0Q*=B} zVH}WuI=q_+ZW|QwV9FN@UAtPrv{Y*wR{R@7P+=V`>T7TDZ;wG`^TmE+8@Qnl5HE`i zxU-G&n~e_M>{mL|(WF29v)HA3#$NUBV#n^8^=+0D@2s+#&?5EJpj9_RjAVku#;sN$ z4Wa0~(+e9OhBw9^e0*yRswKvo>yG&1uWM;-oYUI>zrA-|ZsW+-L|+Ah&vuj10E8g{Pq*xe4xJv(;T3I#|MMYKSG1AvscX+_L~%Q z|7#5naRRbfDvw)%Hp@RuDH)#G+6^*>{Q~3&1&WrIxkK@UdG2vtym(O6u!jxXJGSrg9x!m7R`;bv9B(hGceo0*^zs_Rm zk&KFeOedAHQxfv0Hp$WgKBx3 zx@O@9MiPbYS*q1g>JDfq1_;|zN#Dwfk0W9?CQ$!I7chjsQ7sTPP|x9WNInefbck-g z0~G}DRN(eM6${_~UG{U{ zFg!bnFNNZWnvwzdR49K7dOMGqg}wdjET>)qtP}BHs)A`M$-{?bJ!kf5`!~OnIVV*- zV9uRbYltp2gR-kqzQTeB602(InrRwVg@8xYm;KFF776;Fx~oH>jc94B)E0%3 z9WjPEbAm2k?4P7R(|H2#Ng%e0&q#z(QhQ76>9rWvCCZD|UPgJ@+1Jnu#(I^3CQ7J_ zEDLx@yXvskGjy#(#OW_4Fd!yR4+x5fWgwW(ruQgQER zOaX~zUT=YEz^5GD&mB|Q_;Afa^p_&5mY>F`yr>Q_b3~B>I=x?iPUW`Hkw{*aIXUV1 z2vB#;c>_@A2cHKNBDmq)SOdvgLWP`PB%UFS0@CgUh%izEV1$k{dI3%wa#iSU@TC)1 zUzj#&W4M*3geY8Koe#<`FqV-TpsMUfV+>hlRx792ST8CP3~2(c5*pJ1<7ya$qKndJ|o*&4uuF zcKbuYQ|q~9G{6RZ?GS_b$D56+_LxgpKHY^FrX-yO`hjp_IZ75UB^mQz(nx4#j#Rc0 z$B02mSu#mGfLi-)~YD11?9DO!RV}b@;PaT^w^Wy zgDWfvd6$MTIP;#TyvDaCi>3D{4!=MIowZ!mZxHr|)|YVLz&eYP!y?<(5%=qy$MJB? zo7?4;ckm(35|8p#{I^6yvM(3hnbp%_1SYzfKper!w7s~)xzN%Xi_2k1jyPJK&69Zg zsS)lnL5t<@;qMLd$GAAeETIdQIdRMX^FRJyyZ?Dz2*N68-oK%f*8PvlM3B0TzTsCp zfg=Q=C`HEO0bjIDJI%q%hI%7b2=?~`eccd9fSPkT{_n?lJN0^(vn!3l&B6^dW5?Gx zLYDWl3rV!`*u8l;m8*(DZfRQxq=-|B(Mv*jf)340X7RL5`%!%*iEILg>w^V2Icq%qhK(Lol8*pc}&5DulrJ$sY6qAPGr zR)U0sxf|}Q(ZktUlP*hW@nSx$oxer@2KGL-M+7#*6dUYC7W~V&-~cCyl4jj)_y=!B zW5y~rwLBT(EYv}0B|q^Eoef)~+B!`84beUcZXp-!>El+Q$ELI#p|B{NBGI(-85&%vrRIU%op4Ta2xQ1h z1;0URe`KZBj&iATv!gjWK`MF)^OM-V!jQJdnxdLPg>L+tQ1|s*T0lXfw$I5HIr{Dq zxaU(>HS=UrN&Kn+U?I{Hj2fpb>#xdN)|md$y<~E;MDMpNG5lu&A5@t0UsoW-Wkrdm z;cQf(P)$g&HcgwCK9MFl+Xc%3N6tksLE|lSUq$3v$|#rbCy-4gCq{T&O5q)DJ3nj# zNfuKUVGO7|npxmh&~^I&+IZQx)w*a5K6AG>rLnEzEXZ1jodGfC4-P!QBE5lM!~|wJ z#>RV09m4;f4Qvi&JV8lQ44QyeQKS6KSRvRr0CTdi{XM`w&3jCJe*h!;I9vNGaZEsi ze{7y7nK#Qrs}&Eh5REaveA#vEDVwHfx|NN&`abBS!mthd3*?EQm&!BP+2w*QYlRD{ z9%#)gl|&F$6fJ87YsiY4r_fB8b|Lae=7TDvX$S;m14)JmqO|DpWQ*qchgYUui3|G7 z{uG9LKKRh&?7uVbu_x9O!_CRjh~|2%()}sgE@OF_YBngVwTORLz=OX`xCxBI2-=CT7v5D5M(H(i9`1wdbEB+e$f9kqf6n z;+4LD8gb}F+)w&c>WBS|=8NokErON=0~wxQde0WgQ~3b7H2lb7{}ae;@*R=(vx_RS z?sAvYWba7-s!`1&k6w6OfvB72pZMapvo8OZ%>=T~yd#5{Id`XFYyiN~4tW011k$rjC!4S|g;yZ;L_zL&bDzyiuXo1z?lrdnW*s$~lgT ztTO69s^%g3T;bQho!|AjC|N%)a3GJ4slfApN4&L@iyXyAl=r%5>-=HaBDxTY&Qp)^ zbcCg9gr@*zAMu?bRonu)F#?ve>ZNx4YSb&0f1*)>5-@4ae>S*HOn}kc8I6|AlXMlo z$x2hc@DBn37-mdLedNfK>AwaV`|1(*vj5!elh!aP*D-jx&GNn+?d=L6` zJp*UP{+6>}Ng>ISimK6oOw>_D8gsLQ7K4_n7u^TPZct*y#&uM$Px|SKYHIE^q=${m zlVWdQe{38d_1d4gX++FWaIbBca10~AM(nx&%yA@Y2h0{8U`2hH-}#5Nl2~&9p$e%z zIdB;O&LqcAEU*JLFkN6rh9<0u0m^uyUeXgHtn&4#Pll)C`sQk+T?_ygaB_$*Mn0^W2IdV z?Ct7G{g~E0aop{9pPqX{+^O0d%esV7$H#NAUaUUky{our?+V?k@ZyB-Rb24htHx7I zZ;yIfSs4>@gx5f5I$5r?h#V{Fql6LV5c8Z(!;A2q^s#IYy!vQ?n1ef{X+HD%yn5-M zab}jjQ0u&#DP&scoKjBp`=BydJ>B9A?f8e#hx&j-owrXpx;uDot%peO^ASCNBQMF%wiChTx z4FQmE5I8=W!BzkC&cl4nBf)s2YVeWc05X+mbBv$n@E~stCxw`oP5I0RmsAcfr8A{u zL0FA8DBPMv{3wdxhC~943)Boea z2hx49ot*%7JKGIp?)i#9GosSyymfP%kVNH(mE)w%k(s&1}uhdUvq@YmWBBc8_ z+?ZOlD9+~jwSC8zU*dJ{G#aAKBv*GWenJnQG^o@F7(JnrrI`E>OHcD=gyJr5CNq?q zH6E8Xag1Qn& zW`S@VYmGc8|NzMRI+C%3V^S zvx>%v`%wn&<-cckQ8-wfSU>Lkkr}qp{BeJD^4`vTj%9!;q2dD?J5=5X)O4(po*1DddnUrCh)UM}`qReQK;MlZKV%-lJdb&q!M(4?(Vf?LCXt&}9IekE#@Q*E{2N|?K) znigCo=#Px5u?P|R+f1H?OH-l5`apTnPrMzZc*RXQh<|oH>pi zOVCflC1_i`8j4@a4*e4Oa41nsX~W+>c?`*aUoRG;dpNJ;FSwC?%$w06yT(l7ze!`k z!#^}TAa{{RO@x9J6$?_p*^zt+cTnV+#>8PkWAx(Lvq{U$^=xu^5Keb_sKI!1ya_X0 z>TJ&36JO2$wJrAE$S^1<46`|lvHi^Eh_4S1hjXl>|-b`?+nQJ z`#1r6WC0w6N2xJmqT#xw-~|qey)t<-ZPI$@V#NGAbNQFrl{^e{dti6 zB4zf4=$=*BuX0vS7k;Uwi@?$5H!exl?&a!(;)gRYmm8wQ2ee--kyE*Wdrap`o7sPz{}F}D z$|vjsHGtRE39dk+fa*3C2EWl>%L*g(>^IsA6S+rwZ&Ahx9mGqCg1zVK3vD0xbVxDe zv3o5=TSC*^P2bKg7YJpOmU;W76xm3jwN3$l3nte$=xQ{d!GMcS5hV-B-1u2Zw7-Dm z1^IItFOlPg`2DYoZx*9lDvlgfPdg{`dlkl4cK2g6mZdsZNw{!d)q9(lauzzQ^@k8XDC&uO#-JMk)Y{! z^kObAfZ(v^BX~Mj9aWTSH1Pnw+M1_|lv&p&cB?J{67D{!Xzwe4^nLXMhrv0jxVz_8 zqqnea+`xpn1UF5_ucq@A%iX`ZJ^1V1;TB4r4S?KKF&3yZOuM%Nj$ z#}$f+XX<&PCT8<}!Wyp#dY_`DhAoUht4f_QL$yEaa`K@==S_Fo%&w=2mhfs#Ml-C{m#lEf=OY;EKeCLv8MM!$SXT7y~}JrWYn zw#!3$2R z;ovzT8|)q=$k{UEleg6^3#F0Z9RYfQGNtb{iRR7DS9Q}f5)_eHMhx2VL#Y@EFj_J8 z9A?|}c?{#f{B_WIvcLqe(+R3dE#!=>p%$;v$@Z`T8lOzllI@e_Uty;yD$}mNzChrB zUyxm|3Z~1}iKfmDASTVHAo6}wVKpP3)R9}8hO!LWtORWi+j-OEEP-0Hu6rR{0)dGf z%v4bcL`M(>EN>bhK;ys(cgx1ak@9u#)lAmv05D821A#}RF%hT~&UVeLHX~O>MSi>e zltA(G6CqIPbT=VM6Us*^71@Rgg-vcFN?E>c;tFyG*yb2bgJvkpir}|qu4jc|9P8R^ zt>@ws%fR=ENm;gM%GFv~Ky^4-SY8;Q+NveIT(V&(1Qz0VP2?tML$}v(bo#nvR#sJU zpTdI_5n_XGq>(~;{0Wg+2Zvv&Xft$82*O{p!docIN@D>_-_}>Sg)Yhd0(QQxx%q0v zX69NWzt^?(@$u>Ac7`S%oyOH@c@+u`Sso{4RMA^`HM+P$kn3tmv4TZ+`KYOGn<96P zMMfXLT}&Y^s1REpP0w$ybHLjGJk5&{BqBK19KzTbN^AcJnX7G)E(U$*_?c_Ma`gMD}W7U8{Wjp^;kF*PD zBIIBUfll?Yo68&YSwWfA$@upWRDQp-(*;JoZ9+<`W9JlmZsBIOoExtw0usEyX z7+#{bKK)O%?1g6M==}aPgMVtv340tELC{=`z@Hro(D{p@tK0x|d1JUOw9{lY8jn^W zbkjL|L0Im;oZl`OJ!^~511?)2G{D#pi;hpJ*KaD%!ZpN1Vzd5-u`6XDtNMO@50#xA zU6eqdqrB<6oK6fg4czex%$$2%bXPAfS8}=k{L}Z}aUx7bD><7)dz@&PeK~h_*OY%e zbIYLLIX)g9tQrYW_Qf9_>NsY#B|79?tm=(vr=_Y`a6>(H{4UZc&z#3xt7 z(sjntB98s;QU5Wqvm&~M#=u2#Nej@sWGQxZr=I^qSO*)Lg@qPfsEUPfGFJ7Lwa__4 zU45OJCpR}qvXe!vNVN(PzmC#w1*#q2*7Q138V%2)9OfhA8Iig`^Gs)vJ2UZt4R@DU zdu)4im2W+zG3^0=rV(|5;5tv<#5u0NE<8UXR~3_;h#JP6hQhj%cI3rqJh@$BXsNGi z1rG95=stH~zl)TXq$*k;v}{8a?W?;N?P_+U2=Pcu`52$F&n`Gg)9dVEkgTdR1lCR=^Zog z=#sqH2@y636`(uJ-v7nq4~UMtiVZ0nA=wfy;0U;cQv;1)fBls$!m0UCCB{b{LTB_U zb>B(l3!Bo_WaC;JLYm!)%(=DA*Q6a-)Xbhjf7S9q?aKUkAkQUI{IZfB>t=hxf*htC zpj-Z@U#$_Dy)f9x@^3egj1d;0d7rP4M+Tc+#QWfb1xY#MY4W$3?qgauJNh;jmC_sK z<=;Zo`ydnxo2_pKfqWZCfDqp zJ-?|Na5^Heh>#)SDrluHO1C~~*e_Mx@Lc3@@hb1yS3aLwA@y|U_*I2IJ;2#mcHo*R z$)a?4MizC$r8W6;Dqa*$%L4pnj-s`TH+%}TKXMft<|!p^gNR>1wBlAnB!X*JVgmry zGMgasNTih@^(9L%*2WNOqv&u6K`6|tB#|N}mXe1HS=0N>T4^1AT6x|GP(4IHSxU&y z8Kbb-gQx}9zS@{^3#fE}>**}`Vn+u+ETosO*JakK7}{Oe&af{&zlP0=Pm|*7|B1|K z2qe&2IzYHmxhLw%&|Ub>H2rOjZf>Uc2lQ$;kSl*HMsJlD$sRp-yfHJUD@g0WTM@NA z_?HyMeeGYHZ4Hb>e?;l{i_mVjP^xI7LaIKZaVAr;VbrAJEKv__dC+e45Sqq~I6(8; z!!)zMDS-%fuhkw}wwG!`UWZu>5n8qIuOZ{ckZTd4V0RrHSOq@@C~w`Trps@8D9a{o zHzL{o@YOgh)_|iiny#?evb)9!;<_eMj^5c!D&CJX;{hAB{mpZZq)7=^p8=)ZzQv=Y#lJ%|qnHv$pjxMuMdz{2|uG z5_?D`LL5zpL)Swa%BlgmC&!3Kg0`l6&fa+wL@aW+fOGgE8LN1Li{3X%FZCM=B16xi zl^-S|9Ed?OQt=w4x@F05d!$WvjAz=)isTiVhbRa%^n zjGcz&3t*HYUXE0|1FdjzU{ypA`3T+HaXF;OAct??qU`1SG$PU0WDA+7>JnyGWPd;P z-dq!1{xSBZ0Y~iQ?M(a|Gypoe($L_2Y?(WEj?}$)*1^Fx8zmB>c+m^a4NO($7x#h! zB|#NWxMq6L5(du5G<|C%((8k)CYsV%!`XcCc8Z|bynWD$jVgG*vJ@Ms#3GVb5Insx z%*0$MoL#6V7pJAYTX4iWaoJkGQ{Z?%4(mU`TUMh=R=XbEi;T`l0S7=UwL=G3D<^x? zb-M6cnb{g>OnP`#897~zg{$I z6*iPijY&kUq?pY0LD%Q=fKv{|1g&B~Rg4OCCgcdr=BpYEe`Hd!cK?P7*yb63joKDs zu>U%*403oFp(#pGf*3!{Pm^&*f3%xI&Rd`xaPPJ;63`s6Uf<`$CKu);i?5yMi+jLl z13vlb=$i&Tlo7blj^fXISMfwdttcNYI%om++*GFVr(PtWd}6?F%=`T{DERY2R@oZ1 zhxT@|ia*~X!J}-x8_Hkhcw?!tr|Gx{j9I|1=IGc_0Y6WTeoAuGx@5hlUi@81tiGzl zFx3XZx?#M9@uq?dlM*1-W{hHm=m1nG?blVS0pqxytm?yF8(N6fChP)>LsW9%DR8Oh zSb#_UMFJ?!1_>0LE~1~ouRk`T!7=|gjI2uC2WT+}#Ya{x8l@t+25JEFLHcI-*wMl1 z>B->p+SN-F!JxZ&-PdTcrb0f4j_gvoj2M!YIZAV(_LszkqYzO_&78JNA~L)-$N&lB zTW!FRM;LOSc1r zyj2AvjumRf`zbv#@^ZGJ{Vq$*8nbN&iij~LpMVldrkb~^a${Zqtu)#EJau8t(uKSa zvHyXJcTNM-rdx?oF}z}>u&M!suSR!)BOI~K;`Ns6kOvvg^y>B;#GT%ANn1>lNm8!h z`LKs3S|Syd`L3Ec;FG1j3Q$NS<)=)!ktkxMdxj#Unv>|0eTs!@S?Ktw6n5e3H>U1M^igjU%;NYIVoCZ>i2Y;|#URPr^&DpmK> zPd31YA>;epY@79K?VfeL(A2Eq+2}3>8^5@-v4YD(PzcdG{96}6&AiP=DLf^nrSwr$ zrG&9qM;mGj!!VJDT8=RJC2;_@}2Z+zFwEIegDjYpr*e;(6gdM9#N{ zFj^BtTnx!Y`bFN8mVY*KM?q=bOxCwZ-Wr3Zr+6%6vcg*KZWBtQh)eQxhq?HIm{D(j zdX)%QSrS#SI1aZ%`SHt1G96PcP?wXRkQx3|P%RWDNo2U#1P#~m7jz4p;BtO}pij#i zwDdQ!?!3U;Vg271hHkh74&063U&vNqckEMxru!XSUns5{XYn_M}CC9g@j8W zTW`3#WsW>@>kXq<$@E54QosJA_{=m&L($Blx)2Wh!|AEgyJ8H>_Y-70lHp`ioDG&Z14B`=a|RvU{Xht)*^%>>-eFF7pzavcqx7T~^5W4I zoJE-6muR&pM*L;Nv&PRCHFS!9pIYT1B8ZR>l6UDMFL4toLshs)3R&|K@%O^AErPG@ zjFwVSMiJW_m};anvOt97)LcwX7H{@L3w<9*bb~xMNGVm!P(DV&Owp0y3bjEPbMf_f zTpP`5O5jIRyKT>n8LReww3vaWWGEs+3rh%|KX;{iTaqpRMN!%n;!TbOY)`rD!SbMt zz^%wtLz#zdk~V;abi}o2#YZ+Q@C31<>M>p#hfBaTqluKTE>lBl+$7^`W>VUeN}H)> zPHQ@Z%Bsq_BUEl1i_OnzBJi>$(UTHx>%|k9S!5|@%~&A;Xbr6h73BH(-x00byrhK^ zDLafNa->odH<;XLLTH$&k-DvLW2*F9+4?&Z2SvVToN8-uNChW&)244tkuS^XAos+m zqRLwyiC5R0Bay-zmy%f-OGGFS2uBHO|IJe2Cu<&oa0A04YKr}g?8P*a=gTjHuhy~3 z!eh@2ov3_~<3O-&UbK3`8)lzY-paw?S8 z)bPv7{^#;HDlACsW_WLnFUFEc;@sDaFOTofuzq*U<={~}^$(%}+aEdwwtT|RZ_JZR z$-xa0TWNueX!Q9E6_D;teu&+?2bTmP=~E8tZ9RiV_V*Oot8Rk&aK5LG_g9vcg9%`f zK>wYnDhB*6iboouqRxASeyp0eD7uvso%XP3)Rjo3VA%-+`DUjC6s1q?13)vP)J6b8 zdj8C0cIF9Ah+74?R&ZHSu$)|2BpXZ5MTcRVLqeOm3aZ$n8b=SU?X^Jp1fqv5+$@_$ z!;sc)Am`y-Jk31XX?_k_OoV@h!XBe$dE-{XUP?ByQl1guX@PTyoCn3M*N$nnq z{I$t7YEFTEBhC9NsohPMx5yv5$K{*RWRZ+T#!~&X2V)ZxNBIpD?8wmAycp$7M~0v( znABn`8x?8{0S72m7M6{=M{4tWf*wi#mYh8?vNh$Rh1sbb^~iBNWwC^PQ|Gw)RR6u@VG4TMnl zx|q=dYX#Kas9__*3(br~h?U4v@9^@wmYsP$?B%7}95mwF#!Xgue21=wT2eTBxJI|D zIc*2i#5Y1MlhFcdFw&yUgRkU5sL$xNn!RONlT%e^@j{c8g8q{7T<#tiQCI~8b>Ny} z9bp9$T>DWj{Wscl6s?xxRKbt)nfh1kLc$Z^XuF+@N5}0YxNMexeLq7sCQ2-??!jY@IEoe(N`<3|AdW84PvPr&bOuYNU-ivv){E?!|ou z)$(VloR4{Bo|KcZEQPq6H)mz2DmYr$EnAZh;Vd^sdofj-z_^xIAyc>7ge@}S&P|#m z*BhMlyPwyYFCu$gp6=vbkO=A}VO)e)Cv-=RN~eJ)+eVlf&v z!yAl!DZ(s-(ru9L=sDbSEaV#PYn&xSxWvlzo;5||_mJ!U$NO)`FnVfBTl-o>54zxf zPaC%03Im)JB(ez0&NRIxd|_CqpuCn`P{dLS&)|(EC=((rLWV(ETvPhg*qhjn50aY`7!_U7C%ATg@AVN1!URMfU@h6t zy3xwpY{7vnI-q2!DL(+)5K}_mAcZ58ihVd6vJ;#~Jx)*H7%FE;kD5galfI*gHzsK( z)Y)9HoUP!>EUhi92!8d0&5l_RYN5vrtp1fG#C$EKK(H;=tOSFKGb`b9SPbOR&Y0PI zp@JH(P?eRlSOqzAkvv^^Pu^=(_Osa2j}_QPyyhQb(k5siyq|LOa0|B}au*aRRon1VC8 zn3w1r=dCZ70#OTosZ62H@69OAoH@U>H{+*YE@&2^+WG2cQwT; zT64DAt&q>=H;MLy=+@M~0G4j9X|PQgEml%0Z7t2?ovhVeh*f)7bLS zj-0k=ngr$kM7&H9B7GL@SIle_@-D>tfQ@D^hHqhph(CapdOPCqLTIap0rEYkC<}iR zRXMh%Er`w9v+m8c00FI99Ktb$3lZPt?HE<^dUVr-!1NBZ47M3h;&rFc92W&cUf~XY zw%WFjM8qd=?o!L@PcS&pFwI_YJXq(=vAL3Z+!;j)!4m}=8T%&E96z`?Lh9sTd+J1R zY|rN=5Sv!lh@AV^Nlj4V#qZ3blj(GFvz#oOcZ_#Ot{Q_We)oreeLnevRTk12naDbx z`W){fyUIDloP`Opge_tNUVYoTnze2Ow+H5%m?w^P}iidP6SN@I~6*I^*`d&@l*!HF-Bw7Ko90&s0SA-7^truYi|Y zew}2N3Sxi(L@Y8fPx>GNWVvjX;i&C4ryEh&Y?>}8$etUISan@W-tw2Q;;rJD6Va`{ zJAxB z(_3>LQhWMPmRPbB-uTV=eIVQ0i?qFZZ_Pfd_D?fP@Pgo%Lca-Rg!PU`&@iSWy+k?u z)zTyT+2xZ@=Xen0kZuh*mx%gjBPTAliN!i5WL^{f=UVeqpxkSYLh_{Z-14 z#>8IF_6my;Ew9$s24fH?9gmwnM`;RiLrm0DIWB`H3;3!Ll9GF&)mTvprv7~L{C#r% z+?X*WuGip&+gMnN>=cYJmc7H;sxpMwp_qG>^bSTvR`*L5SQ-yumS@dKHh$=r6>_7B z49B*JL*Z)Ixs{C_DL z@1*`X=}lo@x{aZQM8X{}17CNT3en9bR)RudO%d;uWN9PaYk}%Aize)ruKKhXAk*%A zAzh8UN&H;?o4O)_cw1q}KhrB@ViAzASjuZu4ofMZb|8oyyu7)@ zaSowW!iCZoLs|5sK3=br7G9(3v>M} z>({qInIdCWKwa&m;g}(+#ljJwZ(f7Hm?Rj&OEW`*v~lzkq%mtT2uQREkOq44^nQ1w zNnf*Pq)A^KB8^#A7H{m%+5O!!>bOzW{*%%9Efw7hg>ydWfVibWyUc_AQDQLOLNQ&7 zG#cKg4>(HazriCCB+#yFpmT3IyPff{Ljmwozl|4E_}7-i4D*W z^HHSE$b-J`Ka18e9$82gL(vsUNubm?C|2~fTr?AGXX#~ZsQe*tTsMO=j^<2eZv@xh ztmbbfV^2&~_`R85;_`m!(3D!7@fVh0*&pZgceF59M>G3`hiLW^jVQn`gD1OVnuA|f z={q;GHXb1lpv8AmH}p|AeFx_pK|#S?dzUdqZ-Bh^Nwp?~ggLe5ScWr3jIU&cBQfKF zc6_i5RK=W6SFj&87Nhrnk!yENceQ1_&9FpF5Ld@;cX9=-wo!b+$Zs}G0NB9zZi(@( zz{UzN$M{ZR>PQx1-K_nD87^^}QmacOy}%e^=3}{gJy#RoEn0`0(0F6r;@M)^pV5n- zUw>>IpB%NHo&m)bR!XbMXH6;Z5uwx61|-;Gc0FtIlJ#t;q&}WsGwKstnrmuiQt#kV z^P$o2_MSxWpq%-@Hik$Cgw2N5F)dn=Ab14Onfxbk{Rwu9H?@uHMyLHF3^u|wT)7I8 zjfi3;lJIYBpu|UI16)cP&e?3V-`YSeirR@skZgV2mpDI#UH#Su>b*485TmDGrC`f8 zP``D3vO)``W9Lk<;)TsBliZuou7g{eksQ?wi!_0i9n<5=$goyhhcpoF)3jCwQ6=fO zu1|=$eH6(4*7XSypk6uynm9xPEGZpC+B(9JCe`|%a@VK&U*;_6Sc8hiTKzQiGi#uB zpRAs#|3O4_7z?bB-tQlEo}Q2%iL6y4Z-me0??rejJ319UQEpcc5>`My(cL&>X=h@m zE&A6rwIam?P2Yy0g(f1@Ol}X!DV|Q|rJ)$*(Kg*RgKIpz9Xm`o8MM=2cz%AYV0hPZ zk!j#|D%yZiHjzfUoG&QLpsa65yxyqV(se2BQ|Pmk*~-+ zYsYLg{lY8t$CZ_uiHgmS>bzBM0|*!pTU{&*sYUYNZxMr|ZpCs)IS3i|_NI8@Af*l3 z{dv}YwA0{pX}A7FQ}IAvb<+$Vf2=J_Ifb+9cn1TJ$%Rh}j}$2q&?j42E;$sFVHOL|i;FrB~!kjo)cT8UXY+ zlOY7?gl=YUk&XhFQ?M*WGn7lBeMkdxo9)1$$t%ky;tLJP488Er)w1><_!;2$Ebzf3 zEpN!0g>ij`SRQyGIYWDwy&Ah_E8@f^ZJ*;rL&!S6es+k~-%--&fVt5rkcBqj`7j1I zHsJYWj*7p3;(F+>gt7se{(M+UG+r%?c zJQoO=%gFY+krljAvNKVRK%9;^X_4DBmz|_f68-YYYxyag$V^lQ&9;y<;W&P`#z2ra zE6kNv3&eZQrDbIM@nZD$?dTjCXZ6d`bcw>RdlT!+c`qB7ujb$1|6sbsQzXd{fpRfQ zJzd7?q^m(mDl`mFnz+by43UlmR0(RP8e9X1lUu(w3BNW`KSn8Y?`}@ur^$Q%O18f= zi8l~|$_Rg6yvahWkmJ_8h$m%YsZb#3U|Dk}81o5n7MbQoY{kYueOC73O}ip*@u#gk zhX02AF+yi}5~Tc`_<`jU^Q(ASNFzyJ-!RRR_+Q*F&sc>ieZDY8){6)hDh(=P=~On_ z3N(h!b}b@zFdpJS*n$N3i7VZOc)S+r)0 zk#o-YFPGL4*>@{Ys2Guo1m21Sc$E~vy6iqx7%nz4^idB zu*lQ&CnT{0O493oD-b?s8a&_dT=F#c2hr?{cLT6a&Vz8a+Tu14mY4b{4L6h2Dr4@B@{WA~WKH&2;4!jf+~{fn2pK6<; zIkCrqHbBM@!Mn-q9{epmgCX|*vidHaNcdAK=;Y}!FyiTeGPFY(ePF++dyd@RG9X6P zh;UvNv9=0iBZPU?oOLpkIiSL<^ec4QRFVR1CS~?mEC$|^>>3-N7V+Q6Thu&NgPoZG3&uw_BPy z%c!fClWgTBaUeyqK0ha7R))-1a1mT2Z;+Mz=63P|wH;Nkzdjgj0GIR8#k-&8=&X$j z9=A(W*Pgtc6@ZDVV0eaN0!*pWjSAz~O;n@AC!RZC1Ih|DeyYy6F$*jtEJ_K{v*2HAh1Ukm?lrO{>1UaY>ya z9`FzG!v<}+?2W?*q=eO0FT@#?b;H2qR&IFD2Awz?kk*4rO=+e+6de5M;bLJDrYqH9 zuOC58d5Z?fus_&Wp}3Tk?M8blB=1Abx84Z>TU_v6E}HY%R4Xqaa*Z2Y|4Ck4`;)D3 zML8cHyUKuw(2c070+>aPv!?#SQla@p!Y@DB;F92^vh3#=Ue->fcFVKc=31 z_%KEB6@+rR?EJ+$Q_YBamSnM+0-q`Cf{!VOB!_9mBzxZta@5GTuT}+SNCw^K2T7i_h*I- zEZQ#NJAcbtkrscjczZrNXgB1)7OaH9_x|BwqlS7s2X!{RL<9kNvRmcP?bod-JQYiZ zct8eA-oIh7()uBUfON!HlJBMF9`75Nn$DRA!IW=X7xC_u&ntoW$Z z8O7|5(3avDlL##XrA6tMfRvUFrDhFUH5%zS=&U`TvV0+BIV>=D3Z&bK7`63=!eAGN zI*eLJ?Q)6bu#4?sP+Pj4pZ6W;^7osPh6(vK(MF%O?BwpEz|KI}oKx9PZ1XezM2&I9 zf^6Ub{~8kv&Xaj(&O9r3i9k%Dm|@G29WOdhx-*dn{((^>qDtp>NYT=#>@f_sFyDC} ztL&n&2(M+Np1={%Ml|V75H7VTF>J$)8;B2W$PZSny*}tRo(XSfQyg}VQcPtTHczOC z$x(7md6$u_{{;qBpvsxT)%Q%^`h728(o@+KW$B2Z5NM3L%9tV;= z14)9>M%4qT_M--%Mm5?^qAW#*H&Uvw6zjR#i)dBKB`lQ|hE&KDBzVckqu9aoxklDa zYRqdl(_4fEOXQKUu)#vI?!ASAS^fL_+AG3NPvHi7W&vqI$V1eYAYoJSxfcw&)9z;l7`-MnA+E&i=~Lhq>o)pP=({l4-*&0G*!LNC35FWX=2$7rKNXL%{)U}>#>Te#Y^NW>Fhw)p_c9J#vc=cOBYt5xySY?*AYkXLps2zx~5 z@7XGBNJ3KT$qO+$?+sK#a%VSOM3X^ezSSV8j-8R`cKS;h8BZ@ zkfVDUZ-fHrZd>dOT3Wr+Uvh=@czmmx?L7^C*VtZ0=|+n$!~^%zHQKFs@vI{;@G7Uh zN)idNO=Zg=&;sQPZ>QrQkgoR?zCqFd^HkzD82Je!o;oDVmkVhG@^A@{#`w=tp#uVo z_l?>&V=RqDPW|BG|F(_-rUfpwT@XtN5y2O*=kha+hWy!j1;}*Zxxoksk^ezHiv@(6 zi#RP&ZtNs=SQC&b-PNF9N-~K`bk*6Dl! z8|WA9Hk3oV?%d-1m6tD6lf6)~`vQ3!HuFbQx(!ZCG71{)tVS1CQYe#lJsR@FE#uH4 zdfPREAw@@eU7#$+Y0w6mMXlYC|MN_f?$QaAh>USEve{(@$zn02j2gzsPvzq(F}i64 z{f4L1YB*wX1)2O)aydeaTIm|71y5j{Hz%^B#}HN!aZZV&f`^Dw@Zf`InHG-)Z36gD z)HhaTiz*no6fVfFIY!~pYN5%|*@(L+xlfu)(2XlkF#iaK+--OJpD49%1EDzxBnllT zfOtgMU7vwwJtJr~HyMwql}Cw7?HT%p&ljj`?qUK9hBN7DVtf+f5V>#TpmgM&qk0&p?FJA_rwG`Ql#0)mwDmIbRCHQzfs%4~q+7qK3ib5uh}a3UyHQK{nAaFzWy^ zmwnrYYj!NIGsJPQHx=BBp!K;#QFIq$+(7q zmxE?g`v$>GX^%FABjn3mzje_$lH94HS!;Rs);Ujl@O)TH6+RoOa?`nHT^5?glyzN% zL+zfC`@1vFH`J*joFJ_`7^rqibX*1l@`O!S81c#3U6c34{W`SjyB%}&lW zua>IaEpZX5kgr+8e}lK$HLb-0rFEve&|+s;lCShopb%yHv!u9uRsSzI#L(Kzp|S3j z^u4JI#ZQi9t*@~($O?lxtrG^8ZYY+bUw-+e{gm^L<^*kRC#Gzb&|C{KhfyAIpaCgl zbo@hU<$HNIYu10y`i9%)zfiLRSjEM+hwK zCoM&kU0AbzFI)GS_R1>$8laP*)hVf7f9U1tMLm?itWe$sP52UWu%iAvS^7`~+aDWb zQX}siPP5uAni;eFR21R817TJdWA3vKB@9a+W8$eU&Go$ z4EPqUrlZIIC95pdt`Pm|uNgt#wd?sJK_9`(Im}|JQ4Z_K-J<2;pPG3X7=VEw>Rg&E zDrx-3B^kQE!@ATR82+}$br^&N&v)J9WlKQ%hCG%bcTS#BP>V_0J9dSn2q7MI@%s;v z-iBmCU*cX*#;ZsoN~8V}WH*5TE1b@EIDEDVo;G)QzJmNU8YA5*l`p*(cnbQ>_UX4n zw6{a_hhrWE^D>|dRF%C8Z)Omdib&mxV7aA)Qp*heQ}3yQKvmpQSiueh z!SA1$od^Wf7VwXx$CU0W+Pt8@??2$Su3A+sIGRs4MmFVE^lRBt7}RKRvep=vvTWJ#;$_ z>&+wj{jG0ORtZO9&e0ma&(sC^M0Z=KR0NMus6vwigJ>A*;npTnAH}OBL0}HlAvXF* z&T8w3OPT?yE@s0XuJEZR5U5esCK()(PDEWiC_n$hK1*xT({|^$_vEtpZJo*{ksQ{j zbSLO7vVB6l$QvU1YB7?Sa_Na~@RD?sQMF^u+K`yv_u6~G8})?09w2j>Lg)Ij$Wi_V+(9WsrLEUBK`@8uY3X$!BBDZYB&5Lxg+N?`D|>4r|pZ&=a_F%r2@*p&9o7Qzf$Z=D@M5HJ7-3Wn zfG|F@91ut<9;_+oAFV-*DB9fOSr+KexXvxM|Jl$*e*wThmQg4Vi_=gTkcM{&!cT54 z-|+UKM%nznKAem{;L8M`E^jCJgx9eWnpSK2cKpG~4iY9bTsBONJj};@Qgb+CjpAJl z_{%GK`d|Vc#ejsOTlF{ZW-(jFUn@U(x1yO2t-SG@&5z#gu({vH?*+4%Nz%^;R1Tfp z%A^qyfVMJhkAa|e)JKayX>NKc_Z&&Fn$hjkgGfV9Oo5a`6$PmbbSK?Wq1z-N)(N8+DEOZ!QuBe>2cm zFZ?Lq5e7AX2l=$1-G$!zM{|#k4!9xlg*B4mi_&KRIBo3?1cOkNOXdbm@MY}?SKE^ku#}MaD>X25z$0U;Rz{VOF%pP_!`L%>eU&g;uB0U#1&FM}Xq{CLyOt8N!Z=H)aT?rvnmeCF_ocTi;u5Rf5Q zq?KgTl412>B`qreNvQy6uJIP9`s?%oIxZjbKaJquqLAK)*F`J_v>-vjtOU<6?qIUQ zvF8OS6OA#dPRcL(;Rn2|QCBKtX>W!WC1{h0Hz@&O&rIwZzGv`PwTqO(afLo5Gz*kV zm=0)shGDK+>2ETtJWdsl>ydX3954klvR*3~+WT>Xu`yBrITan4c|oF{jFpRiJ8sC{ z>eYe?gpY*h0>g06o7@PqM`lgDVqQpjQqLCl<~mutHIm7g>%seoilX;Aanmmxxk0Lw zsDb-X{&K2?vACfwT!zU2*&yispa1#)RDz$SuI62{f7$6?o}Qe%eh75xVFxASKDxm6 z{M|ua>LD#%A6ryMM2pMco=s{UesOtud1kUOIxY$0i_6|+|MKW#%k*H$zX+DK;VJTI zJ|gw4{`&9?#0?qO^B1F!jiaMZr=sO$|8V_X%km5u1(;TxT38-JSara*KNjCzBN&I5 z^NZUhw&X(-R0JXGLnh{=S$(C?YniL)eiqKL*7|R^$xB=0(H0{NE-sNJgIL|`w-3#E zGtFH_D5qpDpXD!=&eA}Mr}DuRcO3RgpOBzD+O9~`Sw^=}21W1vEmQz#KZy2x2{Y65 ze%PcF54oiG2+qE4{E=C>-;XX{$=7eNp2n->?L4Xd?c2tW=z6}IH~ySV?-FFAj~ZVu zCZlO%3CkF4X^Y9_W>eN#ELjDy1GGQ$RQlmjyIuM+W}^tZEn7Bs7)!stDFx2mWH~vX zpz7}ZgQhoraoQjC&QEkcpBPAV;9xq_-9?RZM9 zhwT)?wjiMg;WJCJx}}S=3aV;P7IE~O6#uoGO?I1-ce^rkqcod~-&s1k2vnEzIrgLm z@ohXAJ>;-v!xz*<_@@KM90HQg0G+3ko8b+rk3yBhJs-U7v#6d7lH=suu8}2>hkYjj zh|l2$rli@*-XY5`xW#U{xT@^#qO;e<0k^o;tVsga;hBsg6xTt(?J7lzK986^K$K-l zIAtEJQK$^Bcw__Eu$;&6i}TCz`DL>C5Lc7wn6{niJ-RY4uJ^x}(sVJH@TQxo2s=!( zY9rhE2pir##qP;_o4p=B#`l;)>K^x4pfwy_;&cTt(@i!=ZfGXlzGqQX1*CN1{1BgZ z7o@>XTZHnBP}4}GSsAp|TPKC)vAh`-EKe{yBjD;m?O-VzBDc8wC@{k=yvhSpQ?t#_ z&GWZe%HKN0V$KZF8MgZPR%;#enx1IrxBuC^gq~}5!(8^R>@we`E#_%NDV;5ym`d;t zRj5*#4}>m|`HtIdkL-tvLaRH#8q%NlGRfMgaGIhZk6m5#^-@>_Ma;6*Xl=m#AVRLL zpGPoUDXv&uU}@#o^OKA2#YGA%ALu+=nL)i9%`fBvf;#C>r2zM;KjCm~ zAn08fK*;NW{ zoJfcMc`qNdc7v1dwPT~|SN?>meM$iLuT}drj`FK-OaNoCG>??6eP-h)T1RI>=mpr6 zBZs%_!ErHKCNZ1Z>1n58Wv%_&)at1ep8AJs*7MyI`I?$Hf+dBY+|9KX)OK|i0R*N8 zQieWtAmDv094`8XCMI?3C<6}3M&nCireP6{n9NW{0Pdn{@01AFK;W^flu?v@9aenV zI9XmSCO0&}t$n3`G$Z0i#Ss+# zEMvBanRTo>B*q#eG)VuN0=rCf5M$N|^#>N;(nH)8wce>!)>5|qRn7pORy?xj$KkbT z7~8!{eV7uklIK^R{*YI7D0jdvvRcirhfT)dW)aQj0oZ(}7175+Rm}mznLku(V+&|y z3>gjJ3=wDN@1Q(U&4J0A{RK1Lh%-^JN`5c-$=cCi`j4P#u)ncQ@gP~7SlXv6f+1k{ zcS0CM=#e>kU-Uro2ZQx*rAJ;YHIrod;Aua!O@tBzq3$Ul+=b0v4}C6TGYt+`T(4UP zZGR0?^2}N1wdAfwhb#IS(dHWn&b6=KCrdaa!eGmgKqeAp8~hhUQ&vU=`WD*P9?MX$v#MX$Z6U_Xr>it{T+`wgEG9P$**?hz<)DlGzCKNt?w z$^7CVWMBa)R_Nt z?NT87+v4y|Q>W9DW&&qjKWnn3C}KR%3F+F`8=WRvdbILSpQyKq12yDOnN zLUI~HcM3(CxFBBGN=TaQ&LPE%_*qmUaV2->B+-r9~ zpJ57dwl+GbbDs2*)6odAi@8h6C5?chqO}NQdZn4hae!@DqgMVUzUn1dCnZmlMPS>f zbe9po({~!9SNW^0Nh)~F3By9fmfK|7RTBz71aVcs@~;yftAPPbq-hP7wS=Y}{|yLd zyGts+p}VYI+@4P^n&-)XPLjnzYtZO4T1O21^Is~{%FU9Va8d^GZ$X;c?-X%MU98&aB{tC^1^y3ZtT{J z;R5H9Zi#}9>-9nB*_G`+A5r!4lvo;`Lx^OAq$vM=3<%i;iJKs>pM^x+OkUMXx@hI+ zj3Fex32q3@+F3S{7x|#?laLi!KO@I3_E=41>NMO*?Ri6GTGo1-Hwn?D>hq`2G$_9q zHubn3h%BZIvRXVRY6UvnTmzuvzyZK!FuOS4?&-y-H@fr>bR=}rVqg(gHd`mEAxC!O zd@>KPqriF375c=X)9PkMRn#|X5mmpnM?o;noewktnA|KU%T34tCJ*(YAciuAdyI^= zu=R|MKBlC>3Zcdm_>|P<5~FquR?d?f^1_Z84W zCnkr1xb##7yvnzKW1=;6+UK$q(uL*5E6-BMZ*OnVJF3_jaR2Vo-`T7Qnyaj%-Xu~W z3=Ac>?>+rIxZ3}zk0Gf7^W{eOkrT(sSF}F;WIZ$_*xN)%VPHwbw-z1VJ&;kThiPh?m9!*v#K>A1{zZyoWXEHVv3lZ; z@fPscYV&An`eqYM&!hdrD|FF2XgAvb(qNc;qjhTSv)+Wi7$-?LIZDyrZj=d395DYJ z*Tfg?qS$Dvu~Z?NL!ql-kg!<{pV)XY>XEdvr|$MOG9B2_YzVg^ad z#vq1-SzR|a23+O)EIKFLPq#e%KPObK3ngxOe+T#v!j z6&Hh$u-nCh#SiWyZPCcQVwc+?e>{MR2BK!^FDySj8wcS7xm;gyP-QlRW+c>3LkrKv zJ61GFqU$QbuB=epLA|=%wf>>+@v=}ttU|=t*F8P$e>Q7SJ!n+um7|c-s}Ds~!Ha4V zui+U@nno!ZkV;#qH}5r+&ik$u;mi?+fUuMmf{hUsdpMOW6+De(9cpPVG$a~TRr;)i z4*LNc5u;wOf3igm#KNoBa@y;GP`QFFjX7$#NEw*J#_Ywz1Tw~Ag`VC}UeTu(!Vt zxpuD1m)iGSA@*#O?Xh;QOt0DZT>I_8$rkm6*3DHCUiLoMLA$#JiCjNd#8~Znu7l&_ zM*u5>sP;Y6Q-qj3!c>nKZnN*X_PT@PEj(iDFhCq_`<`om(0&AhB~GJ#PqlY))Y~FG zZ5>!e+1dA82S=wzkAMOqe(ig%rzicBErK3ro-lx5{mzjH1CRlZyQt~=bB)NNI`x_m!uB_CLP zaQU&(hk&sKWf^(PKkW^UJ9f6C_-r3OR+*;EmH&7XJU#7hBLMmPbJ}Yk9qaz|+t}^h z_ovs#VYEx$#|Fh#-tQg{2AkU<{FNW~dTq1vquxn-_l0-br^mg=UU;{2+Sx*e@E6`c zZXe+sqM+hI|M+P4mGAE;#&-JnsJ~6%wZCAvyFoH!!5x@gz}0u7L65s&5R7dqX8KeI za%O*EF>&uy=eXDDZlNOjJJK5r1_lF9Pr5jTPkBT7Ad5S$_vi#NsxJ5#GH`#_1uqXy z+9&&EgR!&MJsR|Oq_X`^|I`wjlcSS8to#&gxowgAvG_~xqZ%5h#_G$1NXYq+M(vwbS8?%|e-A6FH*zN8P+E1O6 zLhIepMnrkR@Rr`7{nSN=UI(JiZl;HR@ATBjh9KU^D}2%vtlNbhY_|m;L#H(hKH7`= zgYE|>NBbmTG4%&d%i|r@pA#4_4DkZZ-F2TKG{G%_`U3mo(RG^;k|W#@k+(6b`;@Gs z^qQQS*0$Z3k5(WMaQ3+T*tQ=XopyHIcJS-2!S`Ofw-1ip?G1oPkG%2$NcoOh?|G72v3}giT7!9 zF6K8PIz1WmWx;s|)+Fb^qTfm!SQF2IHL}|}e=)H>D)^b`c-d`g9oX3f3->%VdPa7g z87yhUl0DB%To=1d4cv)LY41}LC&O-28vr9jB;GgFXl~zaYQ5vWO6z-`ni$4+n;P7q zY6{u&)cUks?>M!iu8RD7o|@Q}cbnSD30T-890M@}?>4p5BSa`a!qmiKyW7%B9o z6Y6KXP3@?2+!r^mBtTh2)BjYPYE&B7sWb-ZxaNNxMxA?K%gKL2_yL z*==ShCh|Bk(XJgQPI~CY`6#>D+wpWlV}ZG7|9B=wjoo(h7%Jmq01algZLBwep4acH z4P(zVP3!}^K@Az~%+%RC;SdFWx2eHwf$+EePfa37%ctgA1t0bMyH4R_Lt-{74v6@< zowZwzh#<_0BO)?oXY~thnHl_8al|74WLLomMh-?DRvfY0>kW1#TSyOL*k8pF5$lgQ z-QAFvBO>mu>WHU^7u|V82GUg=a4-^M*N1J1$XTrz(zkeAI*Me%BEtda3G&U2un;N0b1fiX)yNx^edrq1;s7MWKX=6XA9waS{PgaYPt#JG-w@T<;Y}B-_}X;E21s z;)oEyI(r~E^fOl+5$8w)ba&8dAga>8TX94jM|F+vG@>|ID~{NO0Ji%+p3<>daYV$? z?X2~DY%r&-;(%z&v=83MB&~`gN}B!71VFN*Dvl`0@;i?xDH0V&l$7_KN0iiriX%$m z`pzRt5OviNnSQ?Wh!Q$laYQDO?>wM{dQ}`ya=~{VQG%>0jwl)3JC7)FO%+FEUiQud zN;px)0VTV7_YoP@QgKAdk=}X4KI|4CvG#hvVIj9o3+6T0Fjhr%U~F-*IsY7%Fd=@=lqdXDv%tKut9(YfReQ|pX>ZT zPj4^7g;ne?17VucQ zG>OTu^~skn7ndD>xyt9x#l>0ww0r5UpZ1^kPXniLLsxW$KdyzJIbp{Y{=^C0^+Zg= zRa^uP9z~nZ;5y8#d?=ig{d6fGKz~N1lOwt?wDxZg$n7qQye4JV4 zawol`;iIl>koy?&!|$TrS>nr1ai{1IP@eAIToj;y;&usS5+YaOOxT-_pH69Jh3EUu zNcb>!vwsD-ku8Xc#zMx*Ap9Jqz;2osj(rp^qa#h@M8K=2Hp?AzMhrDn)l3mPMcr!!AN9F_C z(NM~X-E{@zPDu)CU32LD=X_U)Wi_Sd&N?=J6;pp4N8?ZJTPZd!sCg2L7sioQb9dIn z6$NvJ8xF-a$DhY zp}XCvXkhr;uW$TuCO|>|9(@$!0>4Q?Yk&7peY^sxR`3&U035^pO6n9xY7@oj z5k!tp-1I&RlYA;NKPto`@2s&8hxRUp-GTU_sgECvWLC@v;`{GAn8qf2*u&wlsqclj z^gl((MB+1V?E}2N$O$cg5_3MvJ5r7g!aCw|ce`;P;_}W;XN|axh@sqRvyB_&Gd4H6 zCa89H$en11^DzJSP4Wv)KxDGHXW@z263lTbvvc+#pl2#R= zn1oK4S3*?vcI+3Kux`_pm9Ny7n=mP?BDJJ4pl8q?%tO|44rVyV#+iA-A?d)^X|^D} zOb5AyESos?l~kb6^1r-z|MDlBVOG!NOmCE5jXDhv(w{~gEVk# z7^3|y3|MK1K6`qCuLCXOtq4YpHh-d_t=l0QNI<4j>)q{Ql*W%A+c|&PeLxZ@3yU;6 z*F%j7)fC01BHe1W0(0syyR#BD*?h=WW6TXXrNJm3>F<>oKi9AY<$^BlYon*P5;EN- zg^Qt-(EcZJo1+u)k7|KDGbDms5yDlP^ZWYHG$0%&ALgWJfDwblu&y#M8>e_B@^Ka| zu}g;07s+Y?jE-V~Lnjx5JP~5U_|_3_-r1k-@*I&NC zyI`-#W(;qwmTM_%<%43Lr8jJNzX!ejgILY6|MoXyEo-rY&!l!8T1yRsbsfel;e$Ek zGsh;cFPV{63(w#2_u%K%VkEK-Hn;(jbdSzJ4$*Xj5fRk&c^JrH_`boPPLx>1xjHp$ zCrU)AF31(ct^q+H-0I8%xGgM(XDdEK`hAl{1)V3pZ#qG75nc|3TWpxVt$4VW_Uf9= zVd-bcC0&KVig`38t);{b?4ulX1ApX5HsnWYi2rdpvt62zv>JJb6Y0@h@o(F9r)eCg zkg2mP7Q`@lWsAsi-9qvvj*?GsnuxCgHlEsnx>(D(;X94fYna~dGA!nRF=9Ui0I22& zI}hw>m|N4(BS$#Qb9>{e;s4Ci)e=fzYw%1p>Y*78rR{msEP{SA#{UnD;dFQWwQAfN z&yDuxlJ;r(WuYL*?G6+L2Pk%d8#c{Tj+0>zX=By5aL3q0Zn)## z17Z>_*!&i}3L%ZFss8Y7%2*4I%!s6zCq??AbkrCd#LccCJI;_)q-Guot&wD$h@=d) z3qMN6@oEAxwga+X6#*eOClI_Dix=?ej+`ID^Uitj;pY3m`7l}c&Ne@G&W~oMtEU+> z^mDkM!s)@Hm)?@s>$v3pY?EGH#T!Nqx${{uPA39-5oE12jin{R`I8@_#mqv?Z#E;P z0Fw(guduCg3^6#g1rawQgVva7-Q(Z36~@KTenM0R3)abD8&(UsPi(7*3YzN)EZnv( zc{Z+fSYbgg2lMt8s+oMd zW-8+dqg>`Y=n}{j8R1n*2P&Ax?r0s=dSq*)7JfA8Kr>||BG%C)payr02!fdC}(>$u7-lHJT{NCrf3J-$dkDqQf`@JI_-D6SVCEi!?l! zBLwVc$3`R&I%00a8stE*Ns-E4g4sNRD*80w={Uw+&Ud7h!jWvh&-9Am^$bMulA2+i zafF-Hr46z*m!HMy2(5ZQ)%xneLjs_tB*vlt+|#rES+~{W1wl-}XOwx#T`7fr<)+t3 z&aGML1{?IWt$mP*pe)hVSd(+;*puiQL2^0aOOniijbOTWlTkb*gSC1A)>p}w^XC2# z#6<|312$^BLP};)c*iubxi`F^_J`v%_7)TG*qYE^Bb@pt)mf#A0Bcwj8H5t4QHUia zx`4$?7fP(~udW^2qNq*d-qZq5<;JcSPWON@VK%Xl!$B^`%NN#Sv~PQ3boNH+}6l^CTw z-ymTjTRoW!=db|h$J^z_A|1hG^QLJElz2HZ{33&ZWa)^%^M#j?9SyO23NvHpJnQ0p zYr2McrYIaB5mvwhZ3l|voK*@t3yK~3Y2tOQ=ll)U zZH!G#WAPcjL^Ml0&K!A*Xo7T_+D{xcOvD{k2Wh8x6clReN$nQX=_Jh1L#d=9)rNzH zcnL8MDN9WtKi?|6oT+ZFE^TiYx{gcLU^2#FJr1G3yo82m)Wm}c>sI{fCl zuFNGCLAemw2~oSwZTMiZ4MxnZ8u)8+m)hR&TlPa?*umawtZ2KE5aGgj*KS7p#obbtS#xu zYbuWtgwLBcgvt-C1(k6nK%T>u-w+^z>~FEdb!-`7 z+1>IH1PS0J-Dw^)=uU;1j|kL7Q{WTGfq7=wXkALDscf!1Ixb{r@fUG6rxww-Cp{Qx zz98t4rz}l8O~D$L zCuiTnOnY$+3Y-PiI8Ij+Z^cy$#I47l4^iaiIx*;$R5|%Ej{OSTlZ6ThD59hx%@8g{ zr4auj-rYXFy*tPksP&Pgy`XGPWh3)aZz}|3WvJm4l%*jdDl^stAkwGa=|<@{ zqZ?DGwOUdMM*r>KrL+lp_aYC;l1nmX+hPbD9bgl0mnKJu{|k|9nSPZo(Hs*nj{|nPl;_(XaZ>WMg-{i( zjaKlss9JR^5H~6k3x8nrBehV|N&HAUfr|_wDX9`jTHGxOUqb94sLYa35h%6P$#Xwo zRH6VOsfFcU5DU`2thL%fgl4I#z_!4XdREGlOGJ}pTmvMkjhAP~$G!H7-9IpTssfdj zV~-kroXIUYx^PU{Rj)-i8(51S?477%vj<%<_$e4%DYupx;aE>_UNDYYh3i60qEPB* zhGADzH;h!W50%*{nxRl)CSn-=*QE#4z|fM(RAdNAz-{fn#gB$qVgrEliBwoY`9#nZ zXWN2(*|sX#CYP=1N1*4}k#PMurXwsl1!z#8M67B@Ws#JOsG1c zJw3}l;BMqNOW@RgWrB~>pV&QvS&opkMksWvzCO^*78-H?r0}c z(`M&~$midv7!8=#g2L^Yu~r?}{|_>P7Y%JKk$(hP8K3KsUMu0_i4>qHEiD;T(`wX4jAq&8Mx@fhi1VPO7kg zG~kmr2$kX++r@j0d~*m2cLVSbhSA-LzYf7NJ5YKb3#Y%sgbk#4Cx&@a6UB9rz?j}C ziF^QDm$e}eM7|ZWb&1GVk+Yh_K=<(jpj*z`fbU(+4<~s?A91dB$CZH_iJCl>4D`s4 zF#{C{$<)IUcM3E^eC&dtnW*GzVcy7m(rX2kl0AJ+i)F7wTED~K`<9y zf>N8dc=idhU`kG_>JUV^NirM83>N;KVz)h0`Vaz`obh(lHzHmER1*{p!dK^Z^Q&;O zh?0Y3#J56!tLtYJ|Cqh=V1xL-sp)w(j8uIa&vi-W+eSimjBZ+bB;jH zSX`m5>}nMa;pj`le&OJb6Y0(!fR_<2VtJ1j32}8oy2|nLBWjUa&;&h4)+n|$aHA*}QD$*87x7Y`%r(Mo9C0+QC+@Hnxssu%-?tpXOV{BT`QSZHp6>Ht zi=y6dA!}!t>>4&Hd;FdSt!0YKc&O6vwmdV5-+57>r1hx0UByo`R2=Ai+rP+h*67yy zMcpyfAQGVnRB&5{qhW#9>O0DHNjk$kpufn_hp}+K+SC1kcf7kmuf%yeLRiMbigY$Z z&DG9tR9MDz2ax6ndLz`Z^Ct5H-WwWj4M0Un`5YeC?~Rm*HfB*lOUP zW{6|^J$!EN7~{)G;hK~&Wf+nP1w~T`8@g8pZv>%^>CQO}MY$D*+&e2Sg`#z2HM5dD zoktN;0$G7OO72?JTCTTD?Vx+t2w4@3*R)ZYH4PV0d>eQ`C<@2(+Z=&^_wN*deH)J7 z@z2-j=e`rOR9L_L%l$QlMW4DqiTGL|TOGPDAkAVo2a-^vl||D5&znd85kU{d7V@Vo zzz?2vyMroz6Lbf<#KeC(-~tgwHYLY`6}?nMFM4etG4W<|c~4x1^25{Pr|lX1ho`qE zzBZ{iQ$u&uS!I(9w%1Qso~NvPpW)RpS*9*C@Ieo8t4%pYIjdh40IX325IhG08@e&2 zuc>xtiVF*Za8;yjy|`4};0#G?e|bI*k3)4----m(nV(i?E`7lOdmktcWjPY`PTIG* zvw>hv+it@)huBank1pP5);SOy+__Wli8|q1LkAW^!D?s1g9Oy05!%6a&umoeBqfPRUm_fCuWJ)#KW#S`;g`gx+X41>Hnd(@?1r-z?qzXQW-*<#y9- zWVd@H6W7KYH;S_wmoEoczVr^%o`b!w0qLC^r#DcAkk3f^C>_KY3H&qKhGc7cdKer= zL5?Aff_WZ?xTvH1R~Oade|3G_DKN-k2MGieMg)jgT+!KG~e?DVQTr_7Sx5WY8!s zO`C%32J6L*$dS`9W7 zg?bECoBpd2k8x6)P*#@PswUx7O*C1B&Ec`RgOC`B#(&2r{vi$&p zdVrTV{v+=vG8%U~;R+gr8R#YdfImHqyz4NyD9bzv;(#5KSpO!Ysyk=`k*Y20FmkI2 zBsCla5cCrjlU2oH>$=c6a3Wb5Ru*Nsdj+dn*TNkVs!C0ka!CX!+Qfl2u1ytD$^tNi znsS7;9hAGq`)vlvS{85yE8H$vR1Ux?BpScZ;qlEDIV3>~rnN?**d945d1<*(baSwi zj|||ujZrr#tNl$nAg{0zs8%088#h&3&k7ZD{$+GF?vKZkS0w9z_70pxJDkcQ@Avwr z{pVY32MMxwE+f9%4iIS#!{uU+Snpa|-*;}@SZ8jz#g1utqlIvuA0Iz6;tFnrtHwD%(Q%XEIs(;Y2se{CgZvfLM=VfJPuE!uQ0DGL zzH%SkM7x~nuxyks*5_ZotP!m{Iqg0SbnMPPG<3(^lLKQiiGjS+v*Y74c`Ih`_^gNQ z4lZWjfGRTwIufq|;imS)!h6;VOLSEtF0FJTTnIj-08fT5oq@arKGGOWhA@eQgLqFD zx3{UG9jCm@9rv0^ZH_N8rTDA9u*Ai!V{{!-_0jPWIz?zC#|hA7@(0D8%srz1zxzz? z@#*myZuR)OE#h*l?(=8I-+HfnN$I`!3g`P&-E!LNpPZt4>gzTuiQrqEC;y88_ugX< zq>fXhe^P*s_G`C@^E~YxpMLA}WcE5}uaUu@{EsfsUh7Pm*O-!ck(n|#m@?BFi*PZk zVnsx7!3g&ThOfT=2EZ_--WwXe8UGtVLo{`7VE9hrZvYI-=NdEvm}dO1Yw*QTU%BkY zWdYf~2;YtJ)r+uXYF~ukF#qaBSTwXRf{pfA6MO|XZQ$rb{I@T{R?OG$gk;j5MPQ43 K?HU^>?*9iMmy~4y diff --git a/public/js/i18n.bundle.93a02e275ac1a708.js b/public/js/i18n.bundle.93a02e275ac1a708.js new file mode 100644 index 0000000000000000000000000000000000000000..6a134edcbe61e7297711e02e69357518cd953f04 GIT binary patch literal 26130 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 V$UMR#&`QmVkUR3wHP*1*{|~9gi>v?u literal 0 HcmV?d00001 diff --git a/public/js/landing.js b/public/js/landing.js index c9da7051de0f96448bb51dc1bc12745859352bf3..69ccb8f8b872e95fc49456935d274ca2ec81808e 100644 GIT binary patch delta 3859 zcmZu!U2Igx71r$D8{3Wfv6lcgHa5t_yJAc4{Mj^~zR|u~YZ>cOd3JcJ zC_)|tH{OpPT6v~2(_h&>Q=OZine4A5<(8h7TCrL>e|}T)WXqwX-g34$SKd53H&dHD zzPVH`&JAy#J^S?Gsfo?Q!?j|iG*TIQ!fqBT*`e0Uo-c&qBH1KHlFzN^_LT^tAltV> z<$V|`<--TFxKN}=p)!+DVXv2dLEQbi%ze?I&6BaG#_ z3DUa*tUo=}$=1MrMRI%XNlz)Gl1&SPbjNhnO4jac6&aB{yYA=ti0gE;lZ_7WIFia; zJPd?^SVInw9T}IlADNn*o$H@WZfq@NH`af`yojp+pWU5(*nQLYBczg+A{}aD?Iyza zLUL7{DoPdTs8l9F%AjPuZksI5mdcZ~%>y0SZOw(^+15>eY4dfg;~3r^Z@F-xtb!;? zD?LoQz-^St^=)6w{=Ib~=bI=Da5a3`Ud!%g*SD?9`!eEo=kbT&BsMbT1*VXZ-fUXD z&wQg~7-ZvWtkny7Ea(e^$(>!Z*~oC!&xexo^+GaHTbGSJ_T?=eSB67#AZx8oF&(j7~AkElVNA~IIKHn1o51U?3B_A!U&VIcRdOos=#E+|IvvcR$ z^LcHItyeO-Fq`#Wy1CZlk(9}pdLfxlpYa8-NPfSW&40hG#nW0u*gKzn`qC?{c_7Gr z;QB8S_G`s^?4wXi*a=h2YYf^-_0QJ<3^GKIhWnJb;I+xK*~Q( z`_hX8tnFeXkTHh^6>trauwX(NLY#yc##%s}ffz{~EtMxAN+Ci~Djyl1LWL(v<)gG6 zA`_+Zywx2om7j)AC>^8d(-1?g6^&-QO z#6=ip9e?<2+eIBHB&#wrM2QoW%_9}au?eW^N2^vg1~sfsKo(&@$*s;pmZ%79s`HRl zYzRBY$Pd({KS5_BE){$pKT3LGf&eo4kt&WLqY79UE&~*=HU~KrCd9@`8-x}Fo=G|o zMX^Gvrb*VABpBS&WFLoIL7t%?M4%$r&!2*b z0-jv?+q&#O@4YqXBXwbrUhQT(d<}HP>GLhj)c57t+bpMf(!Wql4{c)Wmu#lkEYyGM zvnv?Okx^pF={ioIVLpM_jVqdy>*FxcxFSG+eyO-7qwl5%+t|2I@!d1-dXRCbF)q@l z+Sr(9a33_VoAKoRfd^b%bilW5l8$$Ffi5&HIX2XR_dCvMow9xwrZ$WivkRCbYfLxwfG`OD?v{o}x)CzQB<}wI zA@9vA%vqZc1-Lg;N00J-mV4Y&ZEaVDie{A!*Psr|Ozo@N2p|N=ovlRA>t(j~S$4LB zQ{bEYD)iEFzqavR2wHKzm;@cAr`NE3OAAf&gB}oRbGFKeh&RFzI*{q50^7L()j)Q5 zvqE^q?^OtBiPOKXWgF~(a`A;2@DAkY`$m~Y;RR}pxhAt%?qwb8b`#8nE|n1tUM96z zc1w?j;3EF1;;Z5ya4ZCa1#E|MIND8fVhN#H_Stv4Shq)AKp<{(cxY6dcJ#3)FkcDt zb|_c|=d`hfwbfesX0;w+@}3hSagj53@U!NYy*aG|n$?2Dao%ukLFHV7RLO>G;D^dI zf|-rS#VH9WlmH64D-U``>iu~@qG3uL&3TOk-qOZ&BCh2e*b)=Z{9Yex_bl%^U1Fo@ zSRY#hzPI!?mUh`wc@)F>w}i`ivgt<9BY7iry66Nm6@w=X~~)Wj%{G) z(o!>P>QSpYB?ua7L0`|CP^Ww&xHVBEpLm+x*p;V;0(o=X$#FLKMcZx-KcH^Tk8ium zYAuDcVb~_II!EG2=@H}?Iav=?DF6^x}mw=RyQS#nSMOb67+{D4Gi(+l0+UY~nC(E))o~ZZ2Z{o0#ST zXCl>H#DS8F3myldZ0=Z!F;pE+e{(Rmz0S+*wO-#+jr3Osb35w2Utn)_;(arbTk`{~ zyWm^S;S}GUW|wp%Vhd#A&U+3RZvv4L=k{hhh} zogR@EcZve4e=?BU+35qq@MZ5cuX7)uvA|&5I^C1o(&<|oQ-^_9@GXt8oAlw0^+#uN zIozBGjXQGcTeMp;;FQwX4{7ff<$k*kmR9hBGNXGZPO}w=tCb8_507v+7Owy zbN=(+=KSCPpKmT-zW?Us`!9UA(Y0N5IDgg+yub}lb@GMljjddAPg?m(VJheOE*HPJ zt?uicDOP7^CUe!oiP_0x)zZ{tu4tV-JvP%{4f|L1H5W?QpF1}>J26ox6vCaWj)a$2 zJ#82B1C{DbX>xqP+Lm}2sGNE7=+wjjv`V?EHC!D!s1>k2pR2}ST>ZRZ`drwQ&2ZS> z)O#QI9amc6d#iWEr<(qoF#~QpbUGft(^|@y+_sVWCtJT1{-8Z?3S_L}U$>7l({G!LXG!S1`L`3fGf7dbNq|XmtWlN^9G(smV&Uzfw3GzTMpw{=Rcf_~Wkco7}ep zvfxo~Cu`~rV3*tq!eCd{+c3EoC@)nkhaVqW6aVeh2GevLPu1O)!?)&I{~!IJiqIWZ|CgHHUj{w8roL;W>hr>!@hsCYGDGrSR+RBLH0u6mBaj>Lf7Z z3*|v!21_obgXIL4!WOo&^2Z8OfWm~8KMt9rlyt59td_%~<(y zeLQXDPeA6m^S)jT@n!~fBV*_o4L6(SbMV_@CrPEU0@2~dtA#O zzSU!TO1ZAym9dk!E4^!(96NnEgy_hM?n1 zD$%hqtR19)DjY9j?fZ@pR=zlawP(97DpIUqEjSQ~^(@vL-~oD0kRM+K5|?z6Y`Kpb zqU^`9Mqm4|9Vb75?GQ3b6LjFX9>J+p#oBW?mHgY3guv_r3`t+v6n2`H4h9K8GfmpK z4+kmFPzsJCXgyoTI?U5||Gs^@j8tG#r%kl{F_S#D(yTD8!+vU-zUbaJ@*I;)V<$^af z@)kYXBATY%fepe&hopKvjNv6vF>KJD~l&Jx?r*X=$U5L-zscQ zy-MB#UpUT+-m_o@;oDIzDFVK}vBi@vPd|lvB|6o{_T2gT*Sc9p7F|J;dbkx+hXZaD zMfuXb$2TNBmA<)+Ey|GrpsqihLVRb<-68Z1>ob)y!vnw|17 z4w4Zqw6f6$aElxdZoS4J4$CzL_zq?<O{1yV$9zeANDtlp`D&vSomm>C^}Jgh-(QL<@jTPVjv#RwUrVIx^I?ag-}JEV zEc!{)fapjc>oRXjC-tBh-pG1DM}Xwi#vouv)4gmY z3zAkBcNoA_pyy4(7Nb}D*;lhbXMNmsC$y2|2eX*WG`k+fr~`e3#2|bvt7^We56lEZ zIz^&|(RKJWTopLWAC(Y9t)B5^w6BH9=WE|F6AA*_!6-UAj(!XrETZ{0$vg=7MMzDiX>9t68wyu7&Idi&zc7&LK+3p0)^8o zFtOLIp4bx!L_5LW)Xkhqi3e(F7Tt0shPw_M_Z7-8~p2K5xx zB4>gWsx+9SHqollPqwmMc#Z)1)RY)u>X2ZqrJ5KfLfwpsDm_=}6$0tYSE!#Qtqqcm z!LOt1=SK_Y-9SipC}$ORoIRhH!c)AyK|mqu8_%|7P~S)&bg;ewcMwY(3^B3fKnSUp z9SFSdbsNo3hKh(8?Y@LZx-rkLtTQ!JiAFE6-LE{x{?uj=S&2SblNqhG9A;hZre-X3 zzN1!}X3zH-#CT{wS_^)}jCO-qObz>WZKh^1iSFVr+1J`kO=S}0?Kj!4+D*-25)Jn~ zR$OChB13}RFFwXPe)hkAvNm)D?sDW38?J5tlwHLTAbwLrP8Zuu&2JKqe|#{rs||gL z4%O-H_c9-@%~IL*w)%KxQ=6&TP9nJd&CKuG&{DV(5>UX-=;ceSv(__{`3R~+fD+X& zKb!gC1IP_5C=qlmW*+Lp|0l4N2%i0IX1rbbAg+h=IbF!)pSQnS(7y-yVy-w?DO9a( T`p5Y^$a8UUZ_YY@*xL4gojow} diff --git a/public/js/manifest.js b/public/js/manifest.js index 7e718244da63b48e887e0c55f9477f66b0ded65a..f5e1a6459241cea5028e637e618fe7221f1b6174 100644 GIT binary patch literal 4010 zcmZ`++m72f68#mourNflTG3sy9d@uAWS(Xx3rya;-T_5YZ6&hgrRZcDw)5?KN*8x$ zX6MBgi>!OG>d^*~GhXX;{CxE+em2#qmA3BMg>W@H(+R`v zR_``-<$mpIwFvGwH_T{zGVQ|n`uAgJY(L0qhm-tVtF`u;PAIL`ccC{OS)Z@h&BWJ1 zqAx|$xa}+fR=j)*hQ%m>-vuulSK%Lc=V`v(dBXq5-NAUzl4~&Vt{qL5JbV*us?N9- z$NyXHH@bbY`%QI!&(1`LV_UfG*&Wy0YUe3ZQ{!;`7K+EiH4eLX;nH3%@{wD%Tz)J+ zYia#2`nh+Zp85K#HqWlB`6HI_$!)jwjxqMG_4{K}OYPPV4_?UDE);~GeJhtsS3(w} zcq(?*^SNp`H-xe0oy#`2VE14#yl2V7} zAuzNlV6@%)o90y!Zd?`!;K+YWki@IPV#Q&6=G6(*}RTs!;jpl3=J zrLS%5wY9L|rZ7umSU4-ZG37rxrVpK|^^GZyR}_gu|NWr*UOe5Dv4Ut$LA|C&}7rJ6l(^6x3gCTSZXhw;0X_7GVer>ab{UN{S>vY>r)T zZ--X-3+EJlZTcI>3i(?*vO9q3=E7=4lI32sPrB)Dsk-9w-aG!S4B|w|yc8-HLB5Kl zoQt6-imZ}>O7b{UMYsZesDWIHTvj3#Lg+lz(VY9JOjBKfLl>neRu$+&35cKyf=cG9 z3adDbA%Dz$ltpEg>$sGB#eq!26d{RqlE#UE-Z8(q$YhaZGLdC0E3$J;@d^Qs zbd;w;h9VCN(1$#yRTXAgmQ*XD%2fb=iy_;o%2%1rS5+LTAj=T9G0RmYbQ~6{Ork0b za>Rd3bXg^dRwcp~CZ$SaXG-+v-of=h)NTE~?|-5$;GUy6z+I7j|As~yZgbrsT`64^ zA~vE`xLU|moKanYBorhdw5v3l2*;xm2CburUXifOQ>r?!^hYtpmq#JL(2@4uva{mNrZif7xn9-f;?@KtFzs*lN$goYnfPKDLl+YU?V39$TRoug zm2c~V?oO76QGiIbzzWL8iC4C@T`MtHXp@2A$ITzi^T;?lVkFnd1p>^s-EIIFC+-6K zQ2dqOwL?4S^hJP+x2LA@WTS=oG@9d<03(Q)+}%3PTt{0f&T&R)a_;84`s?$d?3(3r{P%75D+;T9LSf$Ezyjnq|8d=Hbtxj2MijK)5c;NV36S15c;sh1} z2;1Sc&;bwQ9DuL|65PDf3~)Y_yM)EZdq6}8#q)rKd?4Th=NeafZVS&~-s~1YI(g!b zf&q5jLBZ%J!)+U1Ca?xu4pkcZJ8yle=64Q&Y)`XgJrbBF=^PeAald@gbw+_tu#7uD zLk{>B17KY^_vYSNR03dB`TuTR=ic5sYmnu=!>GZu=#AWYPw1972Y-gf10%S_0i*wW zLL9mDx^1|_t-Yf1fR@1U0O{!JL-~J1b`5_F-~o9##LN7cZ*=&r^w7a~y!Co|SXHj{ zdmvX}aUZF*~W4tW+iB5un#!NL`w*geZvFMn n<+9zOcIKvLUUjR0I@?gs{55$bP3IW9Vk8gUoR5V<>&^cF!Dk6a literal 4006 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 qLAY%B!7E`6wS5q_ksdq literal 0 HcmV?d00001 diff --git a/public/js/notifications.chunk.3b92cf46da469de1.js b/public/js/notifications.chunk.3b92cf46da469de1.js deleted file mode 100644 index 94f7da536d58ce8e6adadf531dbd7841c653ef78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50423 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 W;@p1_%yrAg@54~J?-(~w>;E63f6@s6 diff --git a/public/js/portfolio.js b/public/js/portfolio.js index f28a7af06b642500909073b5d56234c5e26246b1..0ed99db682799490471a0b5f2e21b86694b9a65e 100644 GIT binary patch delta 527 zcmY+B-%1-n6vk=T5yXnANTtz0Dz)7yYnVT~vvUaBkXvbMU%=KtqxgrU*(ALY?|cB| z+4nHHPgA%t9Boef?( zJZwm;C#(2vWva*}1dMvID+KtQR=(HcQZtI(&{rVR)0LYyPXIrS7Sh)GV$lH}0B$9P z^kV0Fns5eE8`6uk+rFA7flv%=iQV>o`tx}Biby6I!!P~9ni3i$^;!n4q|lmr9dp#6 z1yFA{ipx#6Lo2Z>eHjS6?C+5v40MPagSS{6%n&J9C`_fEE5QPvmcKT(>%D_6J@T@_ zdcyNuatJaJqii_8e|#X^_qD=j$$Q5#jC+w+JPeJjH3Poc>Hj%0Q80_}IZ#ZAY*2G4 z@jUtXFVAQg-fP?&K6PxoI6JE1ui*m8s5WEUngss0WjR^=rbRdiP>;_h{%u^u1})5e z&9PqbbARa}ZF5B?uI!j&BggirhKu9bf>eradag^OcZYGK+l!l}MwIj7 delta 536 zcmY+BTPs9S6vvC%O2|n(7!(sZM~BY7tbIx2@+y=M;9<&;OEVnCixlMpV841*%xN0T zvj>kpf(PtsAjTP%`ZAl+)k4<@ z!vIm7h73d!&7YRi@k%ibu@V5S+rs;F4&y{wck61Ua||)X!KTBl`<<6=1d<4!){dSB z5LM<`Ro9O8Tg|J+WKI#&UV6;WiUg>P;;NZW0I&&W&d*j_=I#&sgqxm$HoN}oj z9`_IihMC@|&jg2Qb$j=qTrhVkN{KD+(cbKbr@IiA)=b!q9 brBre=4#TLpaF&)&YH4M>Ty!6?{51LlwOpie diff --git a/public/js/post.chunk.5ff16664f9adb901.js b/public/js/post.chunk.5ff16664f9adb901.js new file mode 100644 index 0000000000000000000000000000000000000000..dce9cba7b66ee7dcc89be8cba6931fdf69211f56 GIT binary patch literal 221675 zcmeFa>vkJQvL^cfJOxzSBLiCic+(X$%vQ^CTeBov$CTS^FExj&2o%X`fdDoN5XGUi z)_I!q3iE#+Wu9cdFCsE4>jsjt+*a?_?%pk;Dl;oHGBPsmvGMP>THntWtw}mcrdiTT zr^mC!S-ebV)7E^F#CVh?NozjKmi^J`YWk*s@A!D@{{8!Rj~~V3qeq)t{l91Z=g*!z z{c-=Dbp0L+EeTPTMc-Z*n{rlGko&J24owi@Uet38D z;X(Iudwc8N)}X!YCf%$v{Pr?haXQOII$B1%S%1+^I^C>4ZYSN#adI55Cd)w^?T$Zm zcHhQ}Ryxeu_Z~ca(CNnf>(Qe}JDu(fe{Mf~@L;>so#M}(d-v|5!9tpDZKA5lmH(Y>vEoi1kHe)Q=6!>vwtg+I4;HXm(vx^w(_{{gg8f~oKBMBJG`8o&1chOxB0f(RgS2m} z>t^XVIf@s9S^h*nF!c{2Ok|n7TlUV9=_(p5@)u{xIE{OJ9jDV|5e+8!yGi;c$$DqA zaXg6zqx^Yx8ZYt}WB+22o}4Z@lh{9s7Rh|Jz;|_!RsMp=)5FL8ySaP!p*xP3@!)th z9TDT&oy$eCTrH-TY4&^;kCXA>)>aprz8JinrQ_CS7r+&uxt|!>y;>v_LEG#!oewTQ zbY;2jrTlm{9VhPwo89bmcK)0Te!;2o6KVEyGM`-VPpkrepg&vf%kq6QE`&RpJY6hi z3%15#8705Wv5Mr`7-N6<&^=1#fFnFsN*p(*&1$`+8T7DnK%O!9X=(5bT znE^Ry7>`C=kG|%9gkLa3w!Zj0mU$>(JY3EW)A62s`#8ap1t_)Or7Y+1H^KN(`fobOV%h6&sMBreY5Nm({Mhyc zNYNLJ(2m_@-*7;l>W?A<^!@H@6TkKPv%Iza!oDuu&%z!KbPoB+8Q$9b(CPF?JOxDv zW#=iMC4EpGkijhJE1Wx^r=b0JH+Kg1$6but45$wn-w;Jlw%>-%-Knw zse^(HJ;05&V9kKOK`R7L10s)G+=LfY!pFhCUA17$Cjso7^<|kCFmPc&mpLv z5ZK`8k?OTOYfpq>O12S>l||i4JrjdVAo%NpNDs7O8K!})syQkIW4=Oz=IhM^2SY(7 zoMFGs@}L0{<#YUYyK;PWcgJ@kd6Pu-bCOoa@!Q#IkuJf_*m30TImXQ7^&&Z%%uY`7 zw@QcH>!aE1%~`xa2iH4)(!FJ4_)lbqLU=*%b#VV;eEYT??Y@6zjfdb-K!fcDZ9Z#Z z8&F{RK>3V*=$_2}3??oFY(VacG%CW8BPcisn4a1r*G+b};S;DD4@u2&r}ot`KGzjMd!u=}hwNEU{?cka|t8GJrF zk0E@DuhP+*fN2&4T_ndD@KXeT4$|kK#vSp`K-#Unm6dcB^S25QN#HKnLc{^Y8GSgCI7b_J&C8&*|Oz&iVhQ$QcvqLJxltS z*D_r~2N(U>*3}ZtPSnMk(ijSyXdF*ZpeyUftL5wmXy3jA*}oXvOLnkTJiQ7Q$e2)l zH2~!rM;9U^`iq_^JVnC$CyOWe$Vrlq4rDMn8Glh6hR*Md`kP)l9W4_6<)kdaTI5l^ zh63I@czS1R_h^yCZ*~h~jgyZVtG5Lem;&V~G=7Ci+o2+@qBc;MOQO?W0+MIR;>1*G z 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 literal 0 HcmV?d00001 diff --git a/public/js/post.chunk.eb9804ff282909ae.js.LICENSE.txt b/public/js/post.chunk.5ff16664f9adb901.js.LICENSE.txt similarity index 100% rename from public/js/post.chunk.eb9804ff282909ae.js.LICENSE.txt rename to public/js/post.chunk.5ff16664f9adb901.js.LICENSE.txt diff --git a/public/js/post.chunk.eb9804ff282909ae.js b/public/js/post.chunk.eb9804ff282909ae.js deleted file mode 100644 index 130499b2cf1cba7a81d8aad263c97963cca4e1cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221711 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 diff --git a/public/js/profile-directory.js b/public/js/profile-directory.js index 11324401ebf0cf18f5b0a80555bdb9b602f16e36..59acb414deb2e94a307c1b4d04777703d87a12c3 100644 GIT binary patch delta 201 zcmdlZwMS}#BafwlrD3d2wS}RDvEk(XOlp%Qcz8C}TQjj58yZ>|Zr;!IiJjHN*wE5) z@-{AgF(V@jLsLz?l+?7u(wq{_S{<+eOy6W(ZXN+cGZQltO`Rg!R1Fg&19PLvp4`5y zh9)K!W|L=g2eCrLAQnlmLag1%V?VisM}!ru6=;~;S?!Y-VNH#r delta 200 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 literal 0 HcmV?d00001 diff --git a/public/js/profile.chunk.d52916cb68c9a146.js b/public/js/profile.chunk.d52916cb68c9a146.js deleted file mode 100644 index f6ea10fa02ad6ae6dc7a098ea02bf45119259fb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222906 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 diff --git a/public/js/profile.js b/public/js/profile.js index 0c28afe41fb385927f3b46dd472bdc5f5c6e06e0..22a867555906963af8aa75b4b34789c0e0631c0f 100644 GIT binary patch delta 2357 zcma)8OK4nG80OF1(^gG0fmUr=AE`o}3q!o;anHjznIs}ETC~=UAQ7TYA&^JSqZHb~ zV(P|4n>sj01TlSBD1x>f%7_NjEV>W`Ev~E4tq8iS;==!&bElKqMBL5&-;eKn|J*pf z`R4J>3qzeuaWzsrni`>X?aTzLojTS@l#qs9KDBoXVF?TJWV{;p&lFN^1dIC*k52ba zm1YXc7%k%B82)5J3mq3noz%HEhVD5niAuN@cgC$P9^2UvgMaTPN=usBKd!()cgmH> z0tN5+*Lv}Vb)vp~^JpkQJ zdIC;tA~fN?*F7%i84^)pxIMEBkNSdJ;c8J%D`T&|4^QTBnRcZPyKF}twjoLqrtEL- zJFUYGlJdkD_r24O*P(| zYNAuVBd4geE9Y857inMf&>}2A#HA6w^ZFw61|O$3&EsdG1jKMEJWkiv@br23JxV1{ zTO-?U^B17AN2iHm{z>#r7N#(-EB``r2(j8WW0cdWSIN3yGn?QUqzsnu<sG+hbx$xA$r2g^=HZsEkW6S)`aaOV8UIHaH-m= zh~ciiQ{*z(!6jU}_X%6|jN(J;RSpWdIFwkmSd z4e8yzX{V3;w44jF4qc_>WVm#&Q2_-wTDfIuiH)14#+Q`Tzg` delta 2270 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 literal 0 HcmV?d00001 diff --git a/public/js/profile~followers.bundle.5deed93248f20662.js b/public/js/profile~followers.bundle.5deed93248f20662.js deleted file mode 100644 index 171ddf72d22d9d134c4aab433171c92a15a28e79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27232 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 diff --git a/public/js/profile~following.bundle.7ca7cfa5aaae75e2.js b/public/js/profile~following.bundle.7ca7cfa5aaae75e2.js new file mode 100644 index 0000000000000000000000000000000000000000..258c66edee8a1c741ff43da7ce54ccae24fac78b GIT binary patch literal 27144 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 bgx%WiPB;X3bSLaic6Y)+{;{2`0ek-+!aCg5 literal 0 HcmV?d00001 diff --git a/public/js/profile~following.bundle.d2b3b1fc2e05dbd3.js b/public/js/profile~following.bundle.d2b3b1fc2e05dbd3.js deleted file mode 100644 index 9a35b3e2b864fcf7a10493f1e01bf80a47f0adb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27203 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 diff --git a/public/js/remote_auth.js b/public/js/remote_auth.js index bbc0e463fe19fc4fd1f707f804e165da14f51e42..62a934b9ff0aca4b5f2fa49a009b66a76675117c 100644 GIT binary patch delta 877 zcmaJK||+vj_;HOMgbT1s8VZ*7p&}D(+_PIrrTA?m6EMo8J$c@3v}+Gu4gux#W}< zsFpNXOqmR$oz5VTlq3B0!^&cnnlFQr@@Vl20LP4BPgb0BPj-Ff%i88LD1xcQ*NQrS zUEKOxFY`;o(?B?(+?LBazq|x7koU6cJ_AjFe*M} z=dDVyG&*%AmJ-D?@I}Pz3P$E^u|4_>3<=E4Ujl*)CEv93gLb|!Zs*6w&#M9xt`~-I z)q`a_fBQYsf`S~5n0 zxTmkDK`)F_b1*ku_xo@gV#*1c&$O!u$E-+1=JV_(YN}{ot{W*Nb%7Z1vjeV}(A^Za zTa-$M!p#SRbd;|??gS_r1jZ=I^^YfT)7;lw)wMSW#Uh?sEMtQ8-4w33govg3PrnU= z@0VB^4_riIMvdyUA5$^E-!5wHPQYbNPlj*@q#bXIE@8grBt6mQ%op8@P4S}s^AwKz zgy~@hTi_luLtMx_;?|rYE^i)jr_%YtZDS)ZXjz^qnrhZL7%i-iX b$~o#rK@fJ5{j8VoW$9|xae>w!bGYyiXzT2` delta 961 zcmaJ=KWGzC7$;4>1ZzlBA!tNw29>*Tg!k_Kx%VtdF%B*YRp&~IMv%6bm|O=fF^WSM zhsq6a5X9~-azjLL=qfmfyL%@)2tp_Cy*o??&3NzoeZTMb{l4FQKCFB?tn6JY3L@B= zvm+RDsoL+u$^s|Mvz_&3&mv4R^5M-=9whb| zgX`#pac)E+f0 z9SDCTZ2h z{K(OI^^~QgF-fkj!N`g2Q)M4&X~|qaZPTVqDPyg%rpy4^AoQlw3M5z6UFt~yrSn2a avF2Eo-E8d)y219quML{!|4{n@T=)Y@CF&*V4>cDPX3Ir{j>ec%2U-v5W3rr=aE&-|aeE(Xj56-+ej-KDD5aj)O6B=fA9=3L zFhqHfS{XG!C=w?g+ delta 351 zcmXw!%SyvQ6ov^j;-U-DB6K6!bq0r!$y_o+n0nc(rN->C2pT~s5t7tRDSd^?Jcdvx z-TMf>K|mit`U1vDyFKSW-}%4qmG!f-mRDWM1xe7%r;HH2j?eVhxc9#3+E@mRt(U*Y zwohe%cdNtA$I0i=<~;C-&d2DKVjm0WK4s78bW*`NVgfU7mv%asVY%6kEz71 z&!cW%5)w#=3YEDzNFjPr8ECIZ(a1L7eZDDl#C*oEi37s1-dHXAw_}!fND4^ hvT9o7PN|GPszBwIs)XTNS~%q#!mx7h0GuZ<`~%)HY83zg diff --git a/public/js/spa.js b/public/js/spa.js index baee816fde3938837fdd8bc3557024567fec61c9..56211dadcc4e720fd35ed0fcac0d1e1068fb5e3d 100644 GIT binary patch delta 4090 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 Rk4#T?kN2TopWH~d{0l*y*HQog delta 2726 zcmaJ@O>9&}9M8Nr4|dCz-O?6Y4A6qwU0utYkN4h;*pH<*MWGlH64S^=YoaMzXbT6i zLE_a2vI!GO&@>@I55|}^dBzJGqZdtV!oe6h5Dj=hrNkJTv=aX_^LAT8*W2#=zrTLJ z7k`}k{NvQw(gw;o8!MiSL?~5hle4{gxZLsHsm#+Ppe!g9UmKXt%P6F@P@IIHj0wsL z#R~i+k`ljAoYFsi!APMvVP2yk`0Tp_&$dpdU=jK|mgvV;zTzhSLuSnrA622Op zM}6wkTPV{vF!?KA*U|@h#-o(cpnB{gUFCL5?ZfhNFD5>v z4Yysn`&?=JzuxZN;E~8DzP7Ipp24F%?WS z0%WQ$UFyZm=aJ1O<4MWjPutvq+SmsZ4h|^^R+KFRo$fQ5@VqH*8AQ*w zVc`>MW~z?PqcISq;_Lg+&pr-C=KD}Sxj{%&Ddm_PjLt15zVaLT2)Tq1_4*CuV;D72^Ec2I^~DX;?gdOnO#f#d>|sQ$ zXva;|?L}e8xf;F+q99@s*P@4WlhZR#9vQD3-%*^-b6+BS!T@u1NE%J{40 z;}bK5*|WN=qLpOU?1^qF zv#!lHz#1D=$c2F~xP?C0K+05Q3GM0T0pX;93Ill`mI@XHYJSUeq;i%~w+lw*A34PGO z_i55BN}={Toz5@$G@=Jw>cU65Avn-+lNw%dVNESQ7#`oBb%xcu>zxkI zE}j7>;A5c2tTSXKcA1RC@bT9T8@nG`V%fG~)QQa&Xo(ACtG>!QCDp&d>2zVpy6W?{ zQN}WC!=}U2!;36N8Un2c>U3d3`b393p=s2bSa+Nmf#uM;K?l}dY=B0WCD9c#49!Xc z8B%T7xp2_c;c|~Uyva#hV%1h9tS?mi+pZ-oF0iY858&pY9`l^`2xGQ$U;sy?N*kR% z%dODo5v-5Iym^{dO^Zu#!f*lR)i5cfvd1$EZ8bHELM4!cXI9p}8Zu2X(6Cd_EDiLa zKN7%+3oE7fG;irt`^9=+R_Ow-hHA7ObOEret~q$4O96iBm9^;a31APM1$^Kom3bQR zlibY4o7^y9ds3R&MEht!+|*|9$W~!sS|>1qsGefdMGeTt!|#zvZYDXD3Rr8y;YTTqwc{HV$aGI@E7jMGkB_VB^4oAg3At75hpVo0%EJ>S$5wGdP8=LfKYEr6LPlZR}c1<*vWJ`E#7 zLlcY1O$APp7N#bK2AbB%`FX`93VF5~20FzWKzj`gCf_XxVuh%~>Nt>RbBpXJZz~jG zH3XRqv{N1G?c5>|-BkYO``7W+21}eyk)~)i&e(f_{OC zt2WV}=%=)e-n=na?JoD6d+s^sW1jh(XJ%(xTv6J_Q^Ex3=N5Z;$Q@D+O1gLy_K{D7 z;w~P9sE+^`V=guWmp)y*bqfwLgbBC{fdojk+VH zr2ZeZ^dHq2F&~~9HOG3g-1laFn}Q-~L_vUo{%W2da6x^Q1Y}B?A?Oa`(YPFk$TK^_ zxsuv%T{-DNk|imP>mWMOJJ*G5B5CyZ^>MVhY9@$IH?Wmm18F)7*f(n+%@RZxB_^fU W#t6A#bvo>gC&Reh3vIWr(cUk@HF?Sa delta 457 zcmZutO-lkn7)Dq-ZAg$(hd_dA28Ly3=WB;$w7Q3FYMwlZZ6L%7x^@xiSjS|3h?U16 z`WO9@uF>qSm!RwOKJWAOxhj3EO3!C56^!@MoKY;8A#H#8;2tSo1rnmE9swc+_aPc< zFai=YK_IfAlAOQ3469{MeJbqR@w!o4D~F39FyAdy6P#iOy_@)UFuP0P0!dD}pXr-} z$aM%O%6L&=-Y&Vr6eE8wYcKjKIf7$saJ#vuD3vNt<0W8_WwBALoOx_V4wb?;%PuvQ zj=TRKljrhJmYPM%ecn6U6-F>F8U}Fao z-K%o(u&GimhSR;K*}bkhnXK9Aoot5Dz0)m}%UTdPwh=W2mO13atbnxOg`G|XeFOm1 ljrk-_XOpBJ>ugbsQjmI69mDW`Fsa822*cEC0l4VH@E73ZdK3Tv diff --git a/public/js/timeline.js b/public/js/timeline.js index ed0fc12141bf522d7956e39d2ec2ad665cf561a4..18066913f7c2271831eb29cb295abb716046cf5b 100644 GIT binary patch delta 3404 zcmaJ@U1(g#6=vqnwPjmBtJtz7S(eudNGr2AckbNzYmmLN$xCa9oDv8XE{IeCgDahGRafAg-shm5=i2wP`#ACBm_eVmZQ?fP?C^D>Nzv_?y9b& zx7|DEocYc-=bZ0+eY5MEn_czk14;>gv3f0)Tr`&dI??}B%}^s z_KkIqYnvJ~S-mv%R7G2BvdQWd_$gDP^<;Gczaoa_gJXZFFqMj|@!VeFp1ni5OH$|z z%)O_#Ir*b2C8iT$Li9G6=B?SoV@w;xcX9sVGeZaBOdA_g@mU_-dWA6=58wi#_g~!G z8*8JB+&5i0&Jv|;K}8QU3h>^V9d5j^Vw!*c?PbcWD%e<^?VLAXSoxkZp?I6A6U+*o zN7{79nh(7E35lgnwP{SREj2c7j5q)C>f3vm%2I`2nj1IjB92#H}V6}~VUG*)})%y5%WZ{0Mb*hT{A8oz& zDH)-SH`e>y7oWjR-@i-d_Atv+1bofG>1gY_k4U9ER$B4@{rDJZ4gHNwbTOU?PZ8el zBc6{Vo>}+i5sEoI+71&u8pQqnHPY)Yen3jCKYmVLA`Hq{yXo8Hff&fKse5jX9Ch{E ziwlIr$x?7jh;Sc=42G!n5&6<~p%!|w4lG8AWAXoL0>kxCNR zw?;<#7cc#M{_>@T`LQZgw6U2A_Ft}%!!gvl(&7L39ddBM8q3w>sh`b1G57T4#mVc{ zR9o$Su};p!T4|`t`W^BwsP2>_rJ)5nC&q1jL7t@0SU^Q>%lFp$I_Y6d*-W_pWAwPI zuak0qUi*@4FvvpQbB$@ebC>*`M4AKOdwc+ead3R7HU1w`Cy{`9FQ0^pnpx@gZMpSs z`4u@$m`%Z^b|0;=A`jEL`!!jEZVcMe$#D2QE+CA>f$*;?(#^3G(yOJpAKg@_xAMRI0vJJS4f%xsWH zA|7Vl_F3WE!1O@2uvGm+)cWXfFX%%tt`#u!%mI3KNTiuHlMg!E{v0L{RJ$LY-#%ut9^b06=gJ@FPt9)lyalkPZ{plx(M z_R~Z_S$hP~jZ*H6AlXBQW8Y#O4J#Q93TfD-yj@~TxzK|69ZDF<1{ zK=*QcK0@J|*(90w=`%^8rSO!>-~E3s3XP0*wOF+#NBup?TH;$wB=E#k<~$Z2sD{*(F!Oo zP?h3zAi~0&zK@;;0TeJGd`PUaj#7jbtnfG#hk^p1Gq4cx;6hW-&bG9KwadeXDsXeK z2DH)TD;6Hc;j!^{hMSgK9?LOD7`L5m*s=-$0};N6cM`Z^wZBu4B~*;|RSX6_q*?@J z)<}*hD`E3;Cr0Ts!?^{oc-$q&=sv%H<=`xZ idKgq~Rw$KRtX3+M@agLO;?q|b%JWlxr~Kr8I`UrtGNpw8 delta 3316 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 XIkB%_k%y9aS1bCJOq((W@A>}&$3$nh delta 166785 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 z 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 S^omIhMbQoF Date: Mon, 19 Feb 2024 04:00:31 -0700 Subject: [PATCH 424/977] Add Curated Onboarding --- .../Admin/AdminSettingsController.php | 487 +++++++++--------- app/Http/Controllers/Api/ApiV1Controller.php | 4 +- app/Http/Controllers/Api/ApiV2Controller.php | 1 + .../Controllers/Auth/RegisterController.php | 8 +- .../Controllers/CuratedRegisterController.php | 398 ++++++++++++++ ...rdingNotifyAdminNewApplicationPipeline.php | 65 +++ app/Mail/CuratedRegisterAcceptUser.php | 55 ++ app/Mail/CuratedRegisterConfirmEmail.php | 55 ++ app/Mail/CuratedRegisterNotifyAdmin.php | 55 ++ ...CuratedRegisterNotifyAdminUserResponse.php | 55 ++ app/Mail/CuratedRegisterRejectUser.php | 55 ++ .../CuratedRegisterRequestDetailsFromUser.php | 58 +++ app/Mail/CuratedRegisterSendMessage.php | 55 ++ app/Models/CuratedRegister.php | 49 ++ app/Models/CuratedRegisterActivity.php | 38 ++ app/Services/ConfigCacheService.php | 2 + app/Services/LandingService.php | 4 +- config/instance.php | 31 ++ ..._073327_create_curated_registers_table.php | 44 ++ ...eate_curated_register_activities_table.php | 42 ++ .../admin/curated-register/index.blade.php | 86 ++++ .../partials/activity-log.blade.php | 463 +++++++++++++++++ .../curated-register/partials/nav.blade.php | 39 ++ .../partials/not-enabled.blade.php | 24 + .../admin/curated-register/show.blade.php | 102 ++++ .../views/admin/partial/sidenav.blade.php | 26 +- resources/views/admin/settings/home.blade.php | 23 +- .../auth/curated-register/concierge.blade.php | 124 +++++ .../curated-register/concierge_form.blade.php | 129 +++++ .../curated-register/confirm_email.blade.php | 91 ++++ .../email_confirmed.blade.php | 74 +++ .../auth/curated-register/index.blade.php | 88 ++++ .../partials/message-email-confirm.blade.php | 23 + .../partials/progress-bar.blade.php | 134 +++++ .../partials/server-rules.blade.php | 18 + .../partials/step-1.blade.php | 175 +++++++ .../partials/step-2.blade.php | 117 +++++ .../partials/step-3.blade.php | 30 ++ .../partials/step-4.blade.php | 2 + .../resend-confirmation.blade.php | 112 ++++ .../resent-confirmation.blade.php | 87 ++++ .../user_response_sent.blade.php | 79 +++ resources/views/auth/login.blade.php | 2 +- .../curated-register/admin_notify.blade.php | 34 ++ .../admin_notify_user_response.blade.php | 21 + .../curated-register/confirm_email.blade.php | 21 + .../message-from-admin.blade.php | 21 + .../request-accepted.blade.php | 37 ++ .../request-details-from-user.blade.php | 22 + .../request-rejected.blade.php | 19 + .../settings/privacy/domain-blocks.blade.php | 4 +- .../site/help/curated-onboarding.blade.php | 162 ++++++ .../views/site/help/partial/sidebar.blade.php | 7 + routes/web-admin.php | 20 +- routes/web.php | 24 +- 55 files changed, 3732 insertions(+), 269 deletions(-) create mode 100644 app/Http/Controllers/CuratedRegisterController.php create mode 100644 app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php create mode 100644 app/Mail/CuratedRegisterAcceptUser.php create mode 100644 app/Mail/CuratedRegisterConfirmEmail.php create mode 100644 app/Mail/CuratedRegisterNotifyAdmin.php create mode 100644 app/Mail/CuratedRegisterNotifyAdminUserResponse.php create mode 100644 app/Mail/CuratedRegisterRejectUser.php create mode 100644 app/Mail/CuratedRegisterRequestDetailsFromUser.php create mode 100644 app/Mail/CuratedRegisterSendMessage.php create mode 100644 app/Models/CuratedRegister.php create mode 100644 app/Models/CuratedRegisterActivity.php create mode 100644 database/migrations/2024_01_16_073327_create_curated_registers_table.php create mode 100644 database/migrations/2024_01_20_091352_create_curated_register_activities_table.php create mode 100644 resources/views/admin/curated-register/index.blade.php create mode 100644 resources/views/admin/curated-register/partials/activity-log.blade.php create mode 100644 resources/views/admin/curated-register/partials/nav.blade.php create mode 100644 resources/views/admin/curated-register/partials/not-enabled.blade.php create mode 100644 resources/views/admin/curated-register/show.blade.php create mode 100644 resources/views/auth/curated-register/concierge.blade.php create mode 100644 resources/views/auth/curated-register/concierge_form.blade.php create mode 100644 resources/views/auth/curated-register/confirm_email.blade.php create mode 100644 resources/views/auth/curated-register/email_confirmed.blade.php create mode 100644 resources/views/auth/curated-register/index.blade.php create mode 100644 resources/views/auth/curated-register/partials/message-email-confirm.blade.php create mode 100644 resources/views/auth/curated-register/partials/progress-bar.blade.php create mode 100644 resources/views/auth/curated-register/partials/server-rules.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-1.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-2.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-3.blade.php create mode 100644 resources/views/auth/curated-register/partials/step-4.blade.php create mode 100644 resources/views/auth/curated-register/resend-confirmation.blade.php create mode 100644 resources/views/auth/curated-register/resent-confirmation.blade.php create mode 100644 resources/views/auth/curated-register/user_response_sent.blade.php create mode 100644 resources/views/emails/curated-register/admin_notify.blade.php create mode 100644 resources/views/emails/curated-register/admin_notify_user_response.blade.php create mode 100644 resources/views/emails/curated-register/confirm_email.blade.php create mode 100644 resources/views/emails/curated-register/message-from-admin.blade.php create mode 100644 resources/views/emails/curated-register/request-accepted.blade.php create mode 100644 resources/views/emails/curated-register/request-details-from-user.blade.php create mode 100644 resources/views/emails/curated-register/request-rejected.blade.php create mode 100644 resources/views/site/help/curated-onboarding.blade.php diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php index 9d9c3dfb6..8b5a26796 100644 --- a/app/Http/Controllers/Admin/AdminSettingsController.php +++ b/app/Http/Controllers/Admin/AdminSettingsController.php @@ -17,269 +17,288 @@ use Illuminate\Support\Str; trait AdminSettingsController { - public function settings(Request $request) - { - $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); - $cloud_disk = config('filesystems.cloud'); - $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); - $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); - $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; - $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); - $png = in_array('image/png', $types); - $gif = in_array('image/gif', $types); - $mp4 = in_array('video/mp4', $types); - $webp = in_array('image/webp', $types); + public function settings(Request $request) + { + $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); + $cloud_disk = config('filesystems.cloud'); + $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); + $types = explode(',', ConfigCacheService::get('pixelfed.media_types')); + $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; + $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); + $png = in_array('image/png', $types); + $gif = in_array('image/gif', $types); + $mp4 = in_array('video/mp4', $types); + $webp = in_array('image/webp', $types); - $availableAdmins = User::whereIsAdmin(true)->get(); - $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $availableAdmins = User::whereIsAdmin(true)->get(); + $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; + $openReg = (bool) config_cache('pixelfed.open_registration'); + $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed'); - // $system = [ - // 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')), - // 'max_upload_size' => ini_get('post_max_size'), - // 'image_driver' => config('image.driver'), - // 'image_driver_loaded' => extension_loaded(config('image.driver')) - // ]; + return view('admin.settings.home', compact( + 'jpeg', + 'png', + 'gif', + 'mp4', + 'webp', + 'rules', + 'cloud_storage', + 'cloud_disk', + 'cloud_ready', + 'availableAdmins', + 'currentAdmin', + 'regState' + )); + } - return view('admin.settings.home', compact( - 'jpeg', - 'png', - 'gif', - 'mp4', - 'webp', - 'rules', - 'cloud_storage', - 'cloud_disk', - 'cloud_ready', - 'availableAdmins', - 'currentAdmin' - // 'system' - )); - } + public function settingsHomeStore(Request $request) + { + $this->validate($request, [ + 'name' => 'nullable|string', + 'short_description' => 'nullable', + 'long_description' => 'nullable', + 'max_photo_size' => 'nullable|integer|min:1', + 'max_album_length' => 'nullable|integer|min:1|max:100', + 'image_quality' => 'nullable|integer|min:1|max:100', + 'type_jpeg' => 'nullable', + 'type_png' => 'nullable', + 'type_gif' => 'nullable', + 'type_mp4' => 'nullable', + 'type_webp' => 'nullable', + 'admin_account_id' => 'nullable', + 'regs' => 'required|in:open,filtered,closed' + ]); - public function settingsHomeStore(Request $request) - { - $this->validate($request, [ - 'name' => 'nullable|string', - 'short_description' => 'nullable', - 'long_description' => 'nullable', - 'max_photo_size' => 'nullable|integer|min:1', - 'max_album_length' => 'nullable|integer|min:1|max:100', - 'image_quality' => 'nullable|integer|min:1|max:100', - 'type_jpeg' => 'nullable', - 'type_png' => 'nullable', - 'type_gif' => 'nullable', - 'type_mp4' => 'nullable', - 'type_webp' => 'nullable', - 'admin_account_id' => 'nullable', - ]); + $orb = false; + $cob = false; + switch($request->input('regs')) { + case 'open': + $orb = true; + $cob = false; + break; - if($request->filled('admin_account_id')) { - ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); - Cache::forget('api:v1:instance-data:contact'); - Cache::forget('api:v1:instance-data-response-v1'); - } - if($request->filled('rule_delete')) { - $index = (int) $request->input('rule_delete'); - $rules = ConfigCacheService::get('app.rules'); - $json = json_decode($rules, true); - if(!$rules || empty($json)) { - return; - } - unset($json[$index]); - $json = json_encode(array_values($json)); - ConfigCacheService::put('app.rules', $json); - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - return 200; - } + case 'filtered': + $orb = false; + $cob = true; + break; - $media_types = explode(',', config_cache('pixelfed.media_types')); - $media_types_original = $media_types; + case 'closed': + $orb = false; + $cob = false; + break; + } - $mimes = [ - 'type_jpeg' => 'image/jpeg', - 'type_png' => 'image/png', - 'type_gif' => 'image/gif', - 'type_mp4' => 'video/mp4', - 'type_webp' => 'image/webp', - ]; + ConfigCacheService::put('pixelfed.open_registration', (bool) $orb); + ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob); - foreach ($mimes as $key => $value) { - if($request->input($key) == 'on') { - if(!in_array($value, $media_types)) { - array_push($media_types, $value); - } - } else { - $media_types = array_diff($media_types, [$value]); - } - } + if($request->filled('admin_account_id')) { + ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); + Cache::forget('api:v1:instance-data:contact'); + Cache::forget('api:v1:instance-data-response-v1'); + } + if($request->filled('rule_delete')) { + $index = (int) $request->input('rule_delete'); + $rules = ConfigCacheService::get('app.rules'); + $json = json_decode($rules, true); + if(!$rules || empty($json)) { + return; + } + unset($json[$index]); + $json = json_encode(array_values($json)); + ConfigCacheService::put('app.rules', $json); + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + return 200; + } - if($media_types !== $media_types_original) { - ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); - } + $media_types = explode(',', config_cache('pixelfed.media_types')); + $media_types_original = $media_types; - $keys = [ - 'name' => 'app.name', - 'short_description' => 'app.short_description', - 'long_description' => 'app.description', - 'max_photo_size' => 'pixelfed.max_photo_size', - 'max_album_length' => 'pixelfed.max_album_length', - 'image_quality' => 'pixelfed.image_quality', - 'account_limit' => 'pixelfed.max_account_size', - 'custom_css' => 'uikit.custom.css', - 'custom_js' => 'uikit.custom.js', - 'about_title' => 'about.title' - ]; + $mimes = [ + 'type_jpeg' => 'image/jpeg', + 'type_png' => 'image/png', + 'type_gif' => 'image/gif', + 'type_mp4' => 'video/mp4', + 'type_webp' => 'image/webp', + ]; - foreach ($keys as $key => $value) { - $cc = ConfigCache::whereK($value)->first(); - $val = $request->input($key); - if($cc && $cc->v != $val) { - ConfigCacheService::put($value, $val); - } else if(!empty($val)) { - ConfigCacheService::put($value, $val); - } - } + foreach ($mimes as $key => $value) { + if($request->input($key) == 'on') { + if(!in_array($value, $media_types)) { + array_push($media_types, $value); + } + } else { + $media_types = array_diff($media_types, [$value]); + } + } - $bools = [ - 'activitypub' => 'federation.activitypub.enabled', - 'open_registration' => 'pixelfed.open_registration', - 'mobile_apis' => 'pixelfed.oauth_enabled', - 'stories' => 'instance.stories.enabled', - 'ig_import' => 'pixelfed.import.instagram.enabled', - 'spam_detection' => 'pixelfed.bouncer.enabled', - 'require_email_verification' => 'pixelfed.enforce_email_verification', - 'enforce_account_limit' => 'pixelfed.enforce_account_limit', - 'show_custom_css' => 'uikit.show_custom.css', - 'show_custom_js' => 'uikit.show_custom.js', - 'cloud_storage' => 'pixelfed.cloud_storage', - 'account_autofollow' => 'account.autofollow', - 'show_directory' => 'instance.landing.show_directory', - 'show_explore_feed' => 'instance.landing.show_explore', - ]; + if($media_types !== $media_types_original) { + ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); + } - foreach ($bools as $key => $value) { - $active = $request->input($key) == 'on'; + $keys = [ + 'name' => 'app.name', + 'short_description' => 'app.short_description', + 'long_description' => 'app.description', + 'max_photo_size' => 'pixelfed.max_photo_size', + 'max_album_length' => 'pixelfed.max_album_length', + 'image_quality' => 'pixelfed.image_quality', + 'account_limit' => 'pixelfed.max_account_size', + 'custom_css' => 'uikit.custom.css', + 'custom_js' => 'uikit.custom.js', + 'about_title' => 'about.title' + ]; - if($key == 'activitypub' && $active && !InstanceActor::exists()) { - Artisan::call('instance:actor'); - } + foreach ($keys as $key => $value) { + $cc = ConfigCache::whereK($value)->first(); + $val = $request->input($key); + if($cc && $cc->v != $val) { + ConfigCacheService::put($value, $val); + } else if(!empty($val)) { + ConfigCacheService::put($value, $val); + } + } - if( $key == 'mobile_apis' && - $active && - !file_exists(storage_path('oauth-public.key')) && - !file_exists(storage_path('oauth-private.key')) - ) { - Artisan::call('passport:keys'); - Artisan::call('route:cache'); - } + $bools = [ + 'activitypub' => 'federation.activitypub.enabled', + // 'open_registration' => 'pixelfed.open_registration', + 'mobile_apis' => 'pixelfed.oauth_enabled', + 'stories' => 'instance.stories.enabled', + 'ig_import' => 'pixelfed.import.instagram.enabled', + 'spam_detection' => 'pixelfed.bouncer.enabled', + 'require_email_verification' => 'pixelfed.enforce_email_verification', + 'enforce_account_limit' => 'pixelfed.enforce_account_limit', + 'show_custom_css' => 'uikit.show_custom.css', + 'show_custom_js' => 'uikit.show_custom.js', + 'cloud_storage' => 'pixelfed.cloud_storage', + 'account_autofollow' => 'account.autofollow', + 'show_directory' => 'instance.landing.show_directory', + 'show_explore_feed' => 'instance.landing.show_explore', + ]; - if(config_cache($value) !== $active) { - ConfigCacheService::put($value, (bool) $active); - } - } + foreach ($bools as $key => $value) { + $active = $request->input($key) == 'on'; - if($request->filled('new_rule')) { - $rules = ConfigCacheService::get('app.rules'); - $val = $request->input('new_rule'); - if(!$rules) { - ConfigCacheService::put('app.rules', json_encode([$val])); - } else { - $json = json_decode($rules, true); - $json[] = $val; - ConfigCacheService::put('app.rules', json_encode(array_values($json))); - } - Cache::forget('api:v1:instance-data:rules'); - Cache::forget('api:v1:instance-data-response-v1'); - } + if($key == 'activitypub' && $active && !InstanceActor::exists()) { + Artisan::call('instance:actor'); + } - if($request->filled('account_autofollow_usernames')) { - $usernames = explode(',', $request->input('account_autofollow_usernames')); - $names = []; + if( $key == 'mobile_apis' && + $active && + !file_exists(storage_path('oauth-public.key')) && + !file_exists(storage_path('oauth-private.key')) + ) { + Artisan::call('passport:keys'); + Artisan::call('route:cache'); + } - foreach($usernames as $n) { - $p = Profile::whereUsername($n)->first(); - if(!$p) { - continue; - } - array_push($names, $p->username); - } + if(config_cache($value) !== $active) { + ConfigCacheService::put($value, (bool) $active); + } + } - ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); - } + if($request->filled('new_rule')) { + $rules = ConfigCacheService::get('app.rules'); + $val = $request->input('new_rule'); + if(!$rules) { + ConfigCacheService::put('app.rules', json_encode([$val])); + } else { + $json = json_decode($rules, true); + $json[] = $val; + ConfigCacheService::put('app.rules', json_encode(array_values($json))); + } + Cache::forget('api:v1:instance-data:rules'); + Cache::forget('api:v1:instance-data-response-v1'); + } - Cache::forget(Config::CACHE_KEY); + if($request->filled('account_autofollow_usernames')) { + $usernames = explode(',', $request->input('account_autofollow_usernames')); + $names = []; - return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); - } + foreach($usernames as $n) { + $p = Profile::whereUsername($n)->first(); + if(!$p) { + continue; + } + array_push($names, $p->username); + } - public function settingsBackups(Request $request) - { - $path = storage_path('app/'.config('app.name')); - $files = is_dir($path) ? new \DirectoryIterator($path) : []; - return view('admin.settings.backups', compact('files')); - } + ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); + } - public function settingsMaintenance(Request $request) - { - return view('admin.settings.maintenance'); - } + Cache::forget(Config::CACHE_KEY); - public function settingsStorage(Request $request) - { - $storage = []; - return view('admin.settings.storage', compact('storage')); - } + return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); + } - public function settingsFeatures(Request $request) - { - return view('admin.settings.features'); - } + public function settingsBackups(Request $request) + { + $path = storage_path('app/'.config('app.name')); + $files = is_dir($path) ? new \DirectoryIterator($path) : []; + return view('admin.settings.backups', compact('files')); + } - public function settingsPages(Request $request) - { - $pages = Page::orderByDesc('updated_at')->paginate(10); - return view('admin.pages.home', compact('pages')); - } + public function settingsMaintenance(Request $request) + { + return view('admin.settings.maintenance'); + } - public function settingsPageEdit(Request $request) - { - return view('admin.pages.edit'); - } + public function settingsStorage(Request $request) + { + $storage = []; + return view('admin.settings.storage', compact('storage')); + } - public function settingsSystem(Request $request) - { - $sys = [ - 'pixelfed' => config('pixelfed.version'), - 'php' => phpversion(), - 'laravel' => app()->version(), - ]; - switch (config('database.default')) { - case 'pgsql': - $exp = DB::raw('select version();'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'Postgres', - 'version' => explode(' ', DB::select($expQuery)[0]->version)[1] - ]; - break; + public function settingsFeatures(Request $request) + { + return view('admin.settings.features'); + } - case 'mysql': - $exp = DB::raw('select version()'); - $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); - $sys['database'] = [ - 'name' => 'MySQL', - 'version' => DB::select($expQuery)[0]->{'version()'} - ]; - break; + public function settingsPages(Request $request) + { + $pages = Page::orderByDesc('updated_at')->paginate(10); + return view('admin.pages.home', compact('pages')); + } - default: - $sys['database'] = [ - 'name' => 'Unknown', - 'version' => '?' - ]; - break; - } - return view('admin.settings.system', compact('sys')); - } + public function settingsPageEdit(Request $request) + { + return view('admin.pages.edit'); + } + + public function settingsSystem(Request $request) + { + $sys = [ + 'pixelfed' => config('pixelfed.version'), + 'php' => phpversion(), + 'laravel' => app()->version(), + ]; + switch (config('database.default')) { + case 'pgsql': + $exp = DB::raw('select version();'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'Postgres', + 'version' => explode(' ', DB::select($expQuery)[0]->version)[1] + ]; + break; + + case 'mysql': + $exp = DB::raw('select version()'); + $expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); + $sys['database'] = [ + 'name' => 'MySQL', + 'version' => DB::select($expQuery)[0]->{'version()'} + ]; + break; + + default: + $sys['database'] = [ + 'name' => 'Unknown', + 'version' => '?' + ]; + break; + } + return view('admin.settings.system', compact('sys')); + } } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 55ecb25e2..0205648e8 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1652,13 +1652,13 @@ class ApiV1Controller extends Controller 'email' => config('instance.email'), 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')', 'urls' => [ - 'streaming_api' => 'wss://' . config('pixelfed.domain.app') + 'streaming_api' => null, ], 'stats' => $stats, 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'languages' => [config('app.locale')], 'registrations' => (bool) config_cache('pixelfed.open_registration'), - 'approval_required' => false, // (bool) config_cache('instance.curated_registration.enabled'), + 'approval_required' => (bool) config_cache('instance.curated_registration.enabled'), 'contact_account' => $contact, 'rules' => $rules, 'configuration' => [ diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index 23a122397..c585b3b04 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -141,6 +141,7 @@ class ApiV2Controller extends Controller }); $res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration'); + $res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled'); return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8c10e5d0c..8bdd57bf8 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -174,7 +174,7 @@ class RegisterController extends Controller */ public function showRegistrationForm() { - if(config_cache('pixelfed.open_registration')) { + if((bool) config_cache('pixelfed.open_registration')) { if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp(request()->ip()), 404); } @@ -191,7 +191,11 @@ class RegisterController extends Controller return view('auth.register'); } } else { - abort(404); + if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) { + return redirect('/auth/sign_up'); + } else { + abort(404); + } } } diff --git a/app/Http/Controllers/CuratedRegisterController.php b/app/Http/Controllers/CuratedRegisterController.php new file mode 100644 index 000000000..73bd17bff --- /dev/null +++ b/app/Http/Controllers/CuratedRegisterController.php @@ -0,0 +1,398 @@ +user(), 404); + return view('auth.curated-register.index', ['step' => 1]); + } + + public function concierge(Request $request) + { + abort_if($request->user(), 404); + $emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') && + $request->has('next') && + $request->session()->has('cur-reg-con.cr-id'); + return view('auth.curated-register.concierge', compact('emailConfirmed')); + } + + public function conciergeResponseSent(Request $request) + { + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeFormShow(Request $request) + { + abort_if($request->user(), 404); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $arid = $request->session()->get('cur-reg-con.ac-id'); + $showCaptcha = config('instance.curated_registration.captcha_enabled'); + if($attempts = $request->session()->get('cur-reg-con-attempt')) { + $showCaptcha = $attempts && $attempts >= 2; + } else { + $showCaptcha = false; + } + $activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid); + return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha')); + } + + public function conciergeFormStore(Request $request) + { + abort_if($request->user(), 404); + $request->session()->increment('cur-reg-con-attempt'); + abort_unless( + $request->session()->has('cur-reg-con.email-confirmed') && + $request->session()->has('cur-reg-con.cr-id') && + $request->session()->has('cur-reg-con.ac-id'), 404); + $attempts = $request->session()->get('cur-reg-con-attempt'); + $messages = []; + $rules = [ + 'response' => 'required|string|min:5|max:1000', + 'crid' => 'required|integer|min:1', + 'acid' => 'required|integer|min:1' + ]; + if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + $crid = $request->session()->get('cur-reg-con.cr-id'); + $acid = $request->session()->get('cur-reg-con.ac-id'); + abort_if((string) $crid !== $request->input('crid'), 404); + abort_if((string) $acid !== $request->input('acid'), 404); + + if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) { + return redirect()->back()->withErrors(['code' => 'You already replied to this request.']); + } + + $act = CuratedRegisterActivity::create([ + 'register_id' => $crid, + 'reply_to_id' => $acid, + 'type' => 'user_response', + 'message' => $request->input('response'), + 'from_user' => true, + 'action_required' => true, + ]); + + $request->session()->pull('cur-reg-con'); + $request->session()->pull('cur-reg-con-attempt'); + + return view('auth.curated-register.user_response_sent'); + } + + public function conciergeStore(Request $request) + { + abort_if($request->user(), 404); + $rules = [ + 'sid' => 'required_if:action,email|integer|min:1|max:20000000', + 'id' => 'required_if:action,email|integer|min:1|max:20000000', + 'code' => 'required_if:action,email', + 'action' => 'required|string|in:email,message', + 'email' => 'required_if:action,email|email', + 'response' => 'required_if:action,message|string|min:20|max:1000', + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $action = $request->input('action'); + $sid = $request->input('sid'); + $id = $request->input('id'); + $code = $request->input('code'); + $email = $request->input('email'); + + $cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid); + $ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id); + + if(!hash_equals($ac->secret_code, $code)) { + return redirect()->back()->withErrors(['code' => 'Invalid code']); + } + + if(!hash_equals($cr->email, $email)) { + return redirect()->back()->withErrors(['email' => 'Invalid email']); + } + + $request->session()->put('cur-reg-con.email-confirmed', true); + $request->session()->put('cur-reg-con.cr-id', $cr->id); + $request->session()->put('cur-reg-con.ac-id', $ac->id); + $emailConfirmed = true; + return redirect('/auth/sign_up/concierge/form'); + } + + public function confirmEmail(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.confirm_email'); + } + + public function emailConfirmed(Request $request) + { + if($request->user()) { + return redirect(route('help.email-confirmation-issues')); + } + return view('auth.curated-register.email_confirmed'); + } + + public function resendConfirmation(Request $request) + { + return view('auth.curated-register.resend-confirmation'); + } + + public function resendConfirmationProcess(Request $request) + { + $rules = [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'exists:curated_registers', + ] + ]; + + $messages = []; + + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + + $this->validate($request, $rules, $messages); + + $cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first(); + if(!$cur) { + return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']); + } + + $totalCount = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->count(); + + if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) { + return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please contact the admin team.']); + } + + $count = CuratedRegisterActivity::whereRegisterId($cur->id) + ->whereType('user_resend_email_confirmation') + ->where('created_at', '>', now()->subHours(12)) + ->count(); + + if($count) { + return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']); + } + + CuratedRegisterActivity::create([ + 'register_id' => $cur->id, + 'type' => 'user_resend_email_confirmation', + 'admin_only_view' => true, + 'from_admin' => false, + 'from_user' => false, + 'action_required' => false, + ]); + + Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur)); + return view('auth.curated-register.resent-confirmation'); + return $request->all(); + } + + public function confirmEmailHandle(Request $request) + { + $rules = [ + 'sid' => 'required', + 'code' => 'required' + ]; + $messages = []; + if(config('instance.curated_registration.captcha_enabled')) { + $rules['h-captcha-response'] = 'required|captcha'; + $messages['h-captcha-response.required'] = 'The captcha must be filled'; + } + $this->validate($request, $rules, $messages); + + $cr = CuratedRegister::whereNull('email_verified_at') + ->where('created_at', '>', now()->subHours(24)) + ->find($request->input('sid')); + if(!$cr) { + return redirect(route('help.email-confirmation-issues')); + } + if(!hash_equals($cr->verify_code, $request->input('code'))) { + return redirect(route('help.email-confirmation-issues')); + } + $cr->email_verified_at = now(); + $cr->save(); + + if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr); + } + return view('auth.curated-register.email_confirmed'); + } + + public function proceed(Request $request) + { + $this->validate($request, [ + 'step' => 'required|integer|in:1,2,3,4' + ]); + $step = $request->input('step'); + + switch($step) { + case 1: + $step = 2; + $request->session()->put('cur-step', 1); + return view('auth.curated-register.index', compact('step')); + break; + + case 2: + $this->stepTwo($request); + $step = 3; + $request->session()->put('cur-step', 2); + return view('auth.curated-register.index', compact('step')); + break; + + case 3: + $this->stepThree($request); + $step = 3; + $request->session()->put('cur-step', 3); + $verifiedEmail = true; + $request->session()->pull('cur-reg'); + return view('auth.curated-register.index', compact('step', 'verifiedEmail')); + break; + } + } + + protected function stepTwo($request) + { + if($request->filled('reason')) { + $request->session()->put('cur-reg.form-reason', $request->input('reason')); + } + if($request->filled('username')) { + $request->session()->put('cur-reg.form-username', $request->input('username')); + } + if($request->filled('email')) { + $request->session()->put('cur-reg.form-email', $request->input('email')); + } + $this->validate($request, [ + 'username' => [ + 'required', + 'min:2', + 'max:15', + 'unique:curated_registers', + 'unique:users', + function ($attribute, $value, $fail) { + $dash = substr_count($value, '-'); + $underscore = substr_count($value, '_'); + $period = substr_count($value, '.'); + + if(ends_with($value, ['.php', '.js', '.css'])) { + return $fail('Username is invalid.'); + } + + if(($dash + $underscore + $period) > 1) { + return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); + } + + if (!ctype_alnum($value[0])) { + return $fail('Username is invalid. Must start with a letter or number.'); + } + + if (!ctype_alnum($value[strlen($value) - 1])) { + return $fail('Username is invalid. Must end with a letter or number.'); + } + + $val = str_replace(['_', '.', '-'], '', $value); + if(!ctype_alnum($val)) { + return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); + } + + $restricted = RestrictedNames::get(); + if (in_array(strtolower($value), array_map('strtolower', $restricted))) { + return $fail('Username cannot be used.'); + } + }, + ], + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ], + 'password' => 'required|min:8', + 'password_confirmation' => 'required|same:password', + 'reason' => 'required|min:20|max:1000', + 'agree' => 'required|accepted' + ]); + $request->session()->put('cur-reg.form-email', $request->input('email')); + $request->session()->put('cur-reg.form-password', $request->input('password')); + } + + protected function stepThree($request) + { + $this->validate($request, [ + 'email' => [ + 'required', + 'string', + app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email', + 'max:255', + 'unique:users', + 'unique:curated_registers', + function ($attribute, $value, $fail) { + $banned = EmailService::isBanned($value); + if($banned) { + return $fail('Email is invalid.'); + } + }, + ] + ]); + $cr = new CuratedRegister; + $cr->email = $request->email; + $cr->username = $request->session()->get('cur-reg.form-username'); + $cr->password = bcrypt($request->session()->get('cur-reg.form-password')); + $cr->ip_address = $request->ip(); + $cr->reason_to_join = $request->session()->get('cur-reg.form-reason'); + $cr->verify_code = Str::random(40); + $cr->save(); + + Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr)); + } +} diff --git a/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php new file mode 100644 index 000000000..a1b5f279a --- /dev/null +++ b/app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php @@ -0,0 +1,65 @@ +cr = $cr; + } + + /** + * Execute the job. + */ + public function handle(): void + { + if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) { + return; + } + + config('instance.curated_registration.notify.admin.on_verify_email.bundle') ? + $this->handleBundled() : + $this->handleUnbundled(); + } + + protected function handleBundled() + { + $cr = $this->cr; + Storage::append('conanap.json', json_encode([ + 'id' => $cr->id, + 'email' => $cr->email, + 'created_at' => $cr->created_at, + 'updated_at' => $cr->updated_at, + ])); + } + + protected function handleUnbundled() + { + $cr = $this->cr; + if($aid = config_cache('instance.admin.pid')) { + $admin = User::whereProfileId($aid)->first(); + if($admin && $admin->email) { + Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr)); + } + } + } +} diff --git a/app/Mail/CuratedRegisterAcceptUser.php b/app/Mail/CuratedRegisterAcceptUser.php new file mode 100644 index 000000000..a87278ace --- /dev/null +++ b/app/Mail/CuratedRegisterAcceptUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-accepted', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterConfirmEmail.php b/app/Mail/CuratedRegisterConfirmEmail.php new file mode 100644 index 000000000..bf06c4311 --- /dev/null +++ b/app/Mail/CuratedRegisterConfirmEmail.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Welcome to Pixelfed! Please Confirm Your Email', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.confirm_email', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdmin.php b/app/Mail/CuratedRegisterNotifyAdmin.php new file mode 100644 index 000000000..28bdad971 --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdmin.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Requires Action]: New Curated Onboarding Application', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterNotifyAdminUserResponse.php b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php new file mode 100644 index 000000000..bc54d5c3e --- /dev/null +++ b/app/Mail/CuratedRegisterNotifyAdminUserResponse.php @@ -0,0 +1,55 @@ +activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Curated Register Notify Admin User Response', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.admin_notify_user_response', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRejectUser.php b/app/Mail/CuratedRegisterRejectUser.php new file mode 100644 index 000000000..448ea8462 --- /dev/null +++ b/app/Mail/CuratedRegisterRejectUser.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-rejected', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterRequestDetailsFromUser.php b/app/Mail/CuratedRegisterRequestDetailsFromUser.php new file mode 100644 index 000000000..b0ff1bb4d --- /dev/null +++ b/app/Mail/CuratedRegisterRequestDetailsFromUser.php @@ -0,0 +1,58 @@ +verify = $verify; + $this->activity = $activity; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: '[Action Needed]: Additional information requested', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.request-details-from-user', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/CuratedRegisterSendMessage.php b/app/Mail/CuratedRegisterSendMessage.php new file mode 100644 index 000000000..20ffc2749 --- /dev/null +++ b/app/Mail/CuratedRegisterSendMessage.php @@ -0,0 +1,55 @@ +verify = $verify; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.curated-register.message-from-admin', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/CuratedRegister.php b/app/Models/CuratedRegister.php new file mode 100644 index 000000000..edb4e1b22 --- /dev/null +++ b/app/Models/CuratedRegister.php @@ -0,0 +1,49 @@ + 'array', + 'admin_notes' => 'array', + 'email_verified_at' => 'datetime', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + ]; + + public function adminStatusLabel() + { + if(!$this->email_verified_at) { + return 'Unverified email'; + } + if($this->is_accepted) { return 'Approved'; } + if($this->is_rejected) { return 'Rejected'; } + if($this->is_awaiting_more_info ) { + return 'Awaiting Details'; + } + if($this->is_closed ) { return 'Closed'; } + + return 'Open'; + } + + public function emailConfirmUrl() + { + return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8)); + } + + public function adminReviewUrl() + { + return url('/i/admin/curated-onboarding/show/' . $this->id); + } +} diff --git a/app/Models/CuratedRegisterActivity.php b/app/Models/CuratedRegisterActivity.php new file mode 100644 index 000000000..5b5071e01 --- /dev/null +++ b/app/Models/CuratedRegisterActivity.php @@ -0,0 +1,38 @@ + 'array', + 'admin_notified_at' => 'datetime', + 'action_taken_at' => 'datetime', + ]; + + public function application() + { + return $this->belongsTo(CuratedRegister::class, 'register_id'); + } + + public function emailReplyUrl() + { + return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code); + } + + public function adminReviewUrl() + { + $url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id; + if($this->reply_to_id) { + $url .= '&rtid=' . $this->reply_to_id; + } + return url($url); + } +} diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index 9da5c8adc..65d9a8337 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -72,6 +72,8 @@ class ConfigCacheService 'instance.banner.blurhash', 'autospam.nlp.enabled', + + 'instance.curated_registration.enabled', // 'system.user_mode' ]; diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php index ba16af5b6..6b1aa62d4 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -48,13 +48,15 @@ class LandingService ->toArray() : []; }); + $openReg = (bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg'); + $res = [ 'name' => config_cache('app.name'), 'url' => config_cache('app.url'), 'domain' => config('pixelfed.domain.app'), 'show_directory' => config_cache('instance.landing.show_directory'), 'show_explore_feed' => config_cache('instance.landing.show_explore'), - 'open_registration' => config_cache('pixelfed.open_registration') == 1, + 'open_registration' => (bool) $openReg, 'version' => config('pixelfed.version'), 'about' => [ 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), diff --git a/config/instance.php b/config/instance.php index d1566da4a..cfc0468ab 100644 --- a/config/instance.php +++ b/config/instance.php @@ -145,4 +145,35 @@ return [ 'software-update' => [ 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false) ], + + 'notifications' => [ + 'gc' => [ + 'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false), + 'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365) + ] + ], + + 'curated_registration' => [ + 'enabled' => env('INSTANCE_CUR_REG', false), + + 'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5), + + 'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)), + + 'state' => [ + 'fallback_on_closed_reg' => true, + 'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true), + ], + + 'notify' => [ + 'admin' => [ + 'on_verify_email' => [ + 'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false), + 'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false), + 'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10), + ], + 'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false), + ] + ], + ], ]; diff --git a/database/migrations/2024_01_16_073327_create_curated_registers_table.php b/database/migrations/2024_01_16_073327_create_curated_registers_table.php new file mode 100644 index 000000000..8f665cdc3 --- /dev/null +++ b/database/migrations/2024_01_16_073327_create_curated_registers_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('email')->unique()->nullable()->index(); + $table->string('username')->unique()->nullable()->index(); + $table->string('password')->nullable(); + $table->string('ip_address')->nullable(); + $table->string('verify_code')->nullable(); + $table->text('reason_to_join')->nullable(); + $table->unsignedBigInteger('invited_by')->nullable()->index(); + $table->boolean('is_approved')->default(0)->index(); + $table->boolean('is_rejected')->default(0)->index(); + $table->boolean('is_awaiting_more_info')->default(0)->index(); + $table->boolean('is_closed')->default(0)->index(); + $table->json('autofollow_account_ids')->nullable(); + $table->json('admin_notes')->nullable(); + $table->unsignedInteger('approved_by_admin_id')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_registers'); + } +}; diff --git a/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php new file mode 100644 index 000000000..410804750 --- /dev/null +++ b/database/migrations/2024_01_20_091352_create_curated_register_activities_table.php @@ -0,0 +1,42 @@ +id(); + $table->unsignedInteger('register_id')->nullable()->index(); + $table->unsignedInteger('admin_id')->nullable(); + $table->unsignedInteger('reply_to_id')->nullable()->index(); + $table->string('secret_code')->nullable(); + $table->string('type')->nullable()->index(); + $table->string('title')->nullable(); + $table->string('link')->nullable(); + $table->text('message')->nullable(); + $table->json('metadata')->nullable(); + $table->boolean('from_admin')->default(false)->index(); + $table->boolean('from_user')->default(false)->index(); + $table->boolean('admin_only_view')->default(true); + $table->boolean('action_required')->default(false); + $table->timestamp('admin_notified_at')->nullable(); + $table->timestamp('action_taken_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_activities'); + } +}; diff --git a/resources/views/admin/curated-register/index.blade.php b/resources/views/admin/curated-register/index.blade.php new file mode 100644 index 000000000..1094e9113 --- /dev/null +++ b/resources/views/admin/curated-register/index.blade.php @@ -0,0 +1,86 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + + @if($records && $records->count()) +
+ + + + + + @if(in_array($filter, ['all', 'open'])) + + @endif + + + + + + + @foreach($records as $record) + + + + @if(in_array($filter, ['all', 'open'])) + + @endif + + + + + @endforeach + +
IDUsernameStatusReason for JoiningEmailCreated
+ + #{{ $record->id }} + + +

+ @{{ $record->username }} +

+
+ {!! $record->adminStatusLabel() !!} + + {{ str_limit($record->reason_to_join, 100) }} + +

+ {{ str_limit(\Illuminate\Support\Str::mask($record->email, '*', 5, 10), 10) }} +

+
{{ $record->created_at->diffForHumans() }}
+ +
+ {{ $records->links() }} +
+
+ @else +
+
+

+

No {{ request()->filled('filter') ? request()->filter : 'open' }} applications found!

+
+
+ @endif +
+
+@else +@include('admin.curated-register.partials.not-enabled') +@endif +@endsection diff --git a/resources/views/admin/curated-register/partials/activity-log.blade.php b/resources/views/admin/curated-register/partials/activity-log.blade.php new file mode 100644 index 000000000..1d7701f44 --- /dev/null +++ b/resources/views/admin/curated-register/partials/activity-log.blade.php @@ -0,0 +1,463 @@ + + + +@push('scripts') + +@endpush + +@push('styles') + +@endpush diff --git a/resources/views/admin/curated-register/partials/nav.blade.php b/resources/views/admin/curated-register/partials/nav.blade.php new file mode 100644 index 000000000..16241997e --- /dev/null +++ b/resources/views/admin/curated-register/partials/nav.blade.php @@ -0,0 +1,39 @@ +@if(request()->filled('a')) +@if(request()->input('a') === 'rj') +
+

Successfully rejected application!

+
+@endif +@if(request()->input('a') === 'aj') +
+

Successfully accepted application!

+
+@endif +@endif + + + + +@push('scripts') + +@endpush diff --git a/resources/views/admin/curated-register/partials/not-enabled.blade.php b/resources/views/admin/curated-register/partials/not-enabled.blade.php new file mode 100644 index 000000000..9248c3ea2 --- /dev/null +++ b/resources/views/admin/curated-register/partials/not-enabled.blade.php @@ -0,0 +1,24 @@ +
+
+
+
+
+
+

Feature not enabled

+ +

To enable this feature: + +

    +
  1. Go to the Settings page
  2. +
  3. + Under Registration Status select: +
    Filtered - Anyone can apply (Curated Onboarding)
    +
  4. +
  5. Save the changes
  6. +
+
+
+
+
+
+
diff --git a/resources/views/admin/curated-register/show.blade.php b/resources/views/admin/curated-register/show.blade.php new file mode 100644 index 000000000..8da14df0e --- /dev/null +++ b/resources/views/admin/curated-register/show.blade.php @@ -0,0 +1,102 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

+ + Back to Curated Onboarding + +

+

Application #{{ $record->id }}

+
+ @if($record->is_closed) + @else + + + Open / Awaiting Admin Action + + @endif +
+
+
+
+
+
+ +
+
+
+
+
+
Details
+
+
+
Username
+
{{ $record->username }}
+
+
+
Email
+
{{ $record->email }}
+
+
+
Created
+
{{ $record->created_at->diffForHumans() }}
+
+ @if($record->email_verified_at) +
+
Email Verified
+
{{ $record->email_verified_at->diffForHumans() }}
+
+ @else +
+
Email Verified
+
Not yet
+
+ @endif +
+
+ +
+
Reason for Joining
+
+
{{ $record->reason_to_join }}
+
+
+
+ +
+ @include('admin.curated-register.partials.activity-log', [ + 'id' => $record->id, + 'is_closed' => $record->is_closed, + 'is_approved' => $record->is_approved, + 'is_rejected' => $record->is_rejected, + 'action_taken_at' => $record->action_taken_at, + 'email_verified_at' => $record->email_verified_at + ]) +
+
+
+
+@endsection + +@push('styles') + +@endpush diff --git a/resources/views/admin/partial/sidenav.blade.php b/resources/views/admin/partial/sidenav.blade.php index bfd93e77e..4226c3488 100644 --- a/resources/views/admin/partial/sidenav.blade.php +++ b/resources/views/admin/partial/sidenav.blade.php @@ -18,7 +18,7 @@ @@ -56,6 +56,15 @@ Settings + + @if((bool) config_cache('instance.curated_registration.enabled')) + + @endif
@@ -64,7 +73,7 @@ @@ -96,10 +105,17 @@ + {{-- --}} + @@ -118,14 +134,14 @@ diff --git a/resources/views/admin/settings/home.blade.php b/resources/views/admin/settings/home.blade.php index 7635ac61f..6201872c8 100644 --- a/resources/views/admin/settings/home.blade.php +++ b/resources/views/admin/settings/home.blade.php @@ -77,6 +77,18 @@
+ +
+ +
+ +
+
+ @if($cloud_ready)
@@ -91,11 +103,18 @@

ActivityPub federation, compatible with Pixelfed, Mastodon and other projects.

-
+ {{--
-

Allow new user registrations.

+

Allow new user registrations.

--}} + + + {{--
+ + +
+

Manually review new account registration applications.

--}}
diff --git a/resources/views/auth/curated-register/concierge.blade.php b/resources/views/auth/curated-register/concierge.blade.php new file mode 100644 index 000000000..3d8f300b7 --- /dev/null +++ b/resources/views/auth/curated-register/concierge.blade.php @@ -0,0 +1,124 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + + @if($emailConfirmed) +

Information Requested

+

Our admin team requests the following information from you:

+
+

testing

+
+
+ + + @csrf + + +
+ + +
+ 0/1000 +
+
+ + + +
+

For additional information, please see our Curated Onboarding Help Center page.

+ @else + @include('auth.curated-register.partials.message-email-confirm', ['step' => 4]) + @endif +
+
+
+@endsection + +@push('scripts') + +@endpush + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/concierge_form.blade.php b/resources/views/auth/curated-register/concierge_form.blade.php new file mode 100644 index 000000000..b8a1f922c --- /dev/null +++ b/resources/views/auth/curated-register/concierge_form.blade.php @@ -0,0 +1,129 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + +

Information Requested

+

Before we can process your application to join, our admin team have requested additional information from you. Please respond at your earliest convenience!

+
+

From our Admins:

+
+

{{ $activity->message }}

+
+

If you don't understand this request, or need additional context you should request clarification from the admin team.

+ {{--
--}} + +
+ @csrf + + +
+ + +
+ 0/1000 +
+
+ @if($showCaptcha) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('scripts') + +@endpush + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/confirm_email.blade.php b/resources/views/auth/curated-register/confirm_email.blade.php new file mode 100644 index 000000000..786023a92 --- /dev/null +++ b/resources/views/auth/curated-register/confirm_email.blade.php @@ -0,0 +1,91 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + +

Confirm Email

+

Please confirm your email address so we can continue processing your registration application.

+ +
+ @csrf + input('sid')}}> + input('code')}}> + @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/email_confirmed.blade.php b/resources/views/auth/curated-register/email_confirmed.blade.php new file mode 100644 index 000000000..8d4f8dc13 --- /dev/null +++ b/resources/views/auth/curated-register/email_confirmed.blade.php @@ -0,0 +1,74 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + +

+

Email Confirmed!

+

Our admin team will review your application.

+
+

Most applications are processed within 24-48 hours. We will send you an email once your account is ready!

+

If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.

+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/index.blade.php b/resources/views/auth/curated-register/index.blade.php new file mode 100644 index 000000000..934c15ec7 --- /dev/null +++ b/resources/views/auth/curated-register/index.blade.php @@ -0,0 +1,88 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => $step ?? 1]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{{ $error }}

+
+ @endforeach +
+ @endif + @if($step === 1) + @include('auth.curated-register.partials.step-1') + @elseif ($step === 2) + @include('auth.curated-register.partials.step-2') + @elseif ($step === 3) + @include('auth.curated-register.partials.step-3') + @elseif ($step === 4) + @include('auth.curated-register.partials.step-4') + @endif +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/partials/message-email-confirm.blade.php b/resources/views/auth/curated-register/partials/message-email-confirm.blade.php new file mode 100644 index 000000000..3f0711b63 --- /dev/null +++ b/resources/views/auth/curated-register/partials/message-email-confirm.blade.php @@ -0,0 +1,23 @@ +

Please verify your email address

+
+ @csrf + +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+
+

For additional information, please see our Curated Onboarding Help Center page.

diff --git a/resources/views/auth/curated-register/partials/progress-bar.blade.php b/resources/views/auth/curated-register/partials/progress-bar.blade.php new file mode 100644 index 000000000..778ab1a9c --- /dev/null +++ b/resources/views/auth/curated-register/partials/progress-bar.blade.php @@ -0,0 +1,134 @@ +
+
    +
  • + 1 + Review Rules +
  • +
  • + 2 + Your Details +
  • +
  • + 3 + Confirm Email +
  • +
  • + 4 + Await Review +
  • +
+
+ +@push('styles') + +@endpush diff --git a/resources/views/auth/curated-register/partials/server-rules.blade.php b/resources/views/auth/curated-register/partials/server-rules.blade.php new file mode 100644 index 000000000..8264697f6 --- /dev/null +++ b/resources/views/auth/curated-register/partials/server-rules.blade.php @@ -0,0 +1,18 @@ +@php +$rules = json_decode(config_cache('app.rules'), true) +@endphp + +
+ @foreach($rules as $id => $rule) +
+
+
+ {{ $id + 1 }} +
+
+
+

{{ $rule }}

+
+
+ @endforeach +
diff --git a/resources/views/auth/curated-register/partials/step-1.blade.php b/resources/views/auth/curated-register/partials/step-1.blade.php new file mode 100644 index 000000000..c51ca3fef --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-1.blade.php @@ -0,0 +1,175 @@ +@php +$id = str_random(14); +@endphp +

Before you continue.

+@if(config_cache('app.rules') && strlen(config_cache('app.rules')) > 5) +

Let's go over a few basic guidelines established by the server's administrators.

+ +@include('auth.curated-register.partials.server-rules') +@else +

The admins have not specified any community rules, however we suggest youreview the Terms of Use and Privacy Policy.

+@endif + +
+
+ @csrf + + +
+ + Go back +
+ + + +@push('scripts') + +@endpush diff --git a/resources/views/auth/curated-register/partials/step-2.blade.php b/resources/views/auth/curated-register/partials/step-2.blade.php new file mode 100644 index 000000000..1d508a4c3 --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-2.blade.php @@ -0,0 +1,117 @@ +

Let's begin setting up your account on {{ config('pixelfed.domain.app') }}

+
+ @csrf + +
+
+ +
+ +
+ @{{ config('pixelfed.domain.app') }} +
+
+

You can use letters, numbers, and underscores with a max length of 15 chars.

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+

+ Our moderators manually review sign-ups. To assist in the processing of your registration, please provide some information about yourself and explain why you wish to create an account on {{ config('pixelfed.domain.app') }}. +

+
+
+ + +
+ 0/1000 +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+ +@push('scripts') + +@endpush + +@push('styles') + +@endpush diff --git a/resources/views/auth/curated-register/partials/step-3.blade.php b/resources/views/auth/curated-register/partials/step-3.blade.php new file mode 100644 index 000000000..5cb0541ac --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-3.blade.php @@ -0,0 +1,30 @@ +

Confirm Your Email

+@if(isset($verifiedEmail)) +
+

+

Please check your email inbox, we sent an email confirmation with a link that you need to visit.

+
+@else +

Please confirm your email address is correct, we will send a verification e-mail with a special verification link that you need to visit before proceeding.

+
+ @csrf + +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+@endif diff --git a/resources/views/auth/curated-register/partials/step-4.blade.php b/resources/views/auth/curated-register/partials/step-4.blade.php new file mode 100644 index 000000000..d2a1eafd1 --- /dev/null +++ b/resources/views/auth/curated-register/partials/step-4.blade.php @@ -0,0 +1,2 @@ +

Processing your membership request

+

We will send you an email once your account is ready!

diff --git a/resources/views/auth/curated-register/resend-confirmation.blade.php b/resources/views/auth/curated-register/resend-confirmation.blade.php new file mode 100644 index 000000000..822146b1a --- /dev/null +++ b/resources/views/auth/curated-register/resend-confirmation.blade.php @@ -0,0 +1,112 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + + @if ($errors->any()) + @foreach ($errors->all() as $error) +
+

{!! $error !!}

+
+ @endforeach +
+ @endif + +

Resend Confirmation

+

Please confirm your email address so we verify your registration application to re-send your email verification email.

+ +
+ @csrf +
+ +
+ @if(config('instance.curated_registration.captcha_enabled')) +
+ {!! Captcha::display() !!} +
+ @endif +
+ +
+
+ +
+
+ +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/resent-confirmation.blade.php b/resources/views/auth/curated-register/resent-confirmation.blade.php new file mode 100644 index 000000000..2ad27bb2d --- /dev/null +++ b/resources/views/auth/curated-register/resent-confirmation.blade.php @@ -0,0 +1,87 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 3]) + +
+

+

Please check your email inbox

+
+

We sent a confirmation link to your email that you need to verify before we can process your registration application.

+

The verification link expires after 24 hours.

+
+
+
+ +
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/curated-register/user_response_sent.blade.php b/resources/views/auth/curated-register/user_response_sent.blade.php new file mode 100644 index 000000000..17e57eaf1 --- /dev/null +++ b/resources/views/auth/curated-register/user_response_sent.blade.php @@ -0,0 +1,79 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+ + + @include('auth.curated-register.partials.progress-bar', ['step' => 4]) + +

+

Succesfully Sent Response!

+

Our admin team will review your application.

+
+

Most applications are processed within 24-48 hours. We will send you an email once your account is ready!

+

If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.

+
+

For additional information, please see our Curated Onboarding Help Center page.

+
+
+
+@endsection + +@push('styles') + + +@endpush diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index dadd08a4f..9df9ea8c9 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -111,7 +111,7 @@ @endif - @if(config_cache('pixelfed.open_registration')) + @if((bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled'))

diff --git a/resources/views/emails/curated-register/admin_notify.blade.php b/resources/views/emails/curated-register/admin_notify.blade.php new file mode 100644 index 000000000..72e7e082b --- /dev/null +++ b/resources/views/emails/curated-register/admin_notify.blade.php @@ -0,0 +1,34 @@ +@component('mail::message') +# [#{{$verify->id}}] New Curated Onboarding Application + +Hello admin, + +**Please review this new onboarding application.** + + +

+ +Username: {{ $verify->username }} + +
+ +Email: {{ $verify->email }} + +

+ +
+ +*The user provided the following reason to join:* +

{!!str_limit(nl2br($verify->reason_to_join), 300)!!}

+ + + +Review Onboarding Application + + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/admin_notify_user_response.blade.php b/resources/views/emails/curated-register/admin_notify_user_response.blade.php new file mode 100644 index 000000000..504795f83 --- /dev/null +++ b/resources/views/emails/curated-register/admin_notify_user_response.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# New Curated Onboarding Response ({{ '#' . $activity->id}}) + +Hello, + +You have a new response from a curated onboarding application from **{{$activity->application->email}}**. + + +

{!! $activity->message !!}

+
+ + +Review Onboarding Response + + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/confirm_email.blade.php b/resources/views/emails/curated-register/confirm_email.blade.php new file mode 100644 index 000000000..bcadc3832 --- /dev/null +++ b/resources/views/emails/curated-register/confirm_email.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# Action Needed: Confirm Your Email to Activate Your Pixelfed Account + +Hello **{{'@'.$verify->username}}**, + +Please confirm your email address so we can process your new registration application. + + +Confirm Email Address + + + +

If you did not create this account, please disregard this email. This link expires after 24 hours.

+
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/message-from-admin.blade.php b/resources/views/emails/curated-register/message-from-admin.blade.php new file mode 100644 index 000000000..55f4ceff9 --- /dev/null +++ b/resources/views/emails/curated-register/message-from-admin.blade.php @@ -0,0 +1,21 @@ +@component('mail::message') +# New Message from {{config('pixelfed.domain.app')}} + +Hello, + +You recently applied to join our Pixelfed community using the @**{{ $verify->username }}** username. + +The admins have a message for you: + + +

{{ $verify->message }}

+
+ +Please do not respond to this email, any replies will not be seen by our admin team. + +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message on behalf of our admin team, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/request-accepted.blade.php b/resources/views/emails/curated-register/request-accepted.blade.php new file mode 100644 index 000000000..79badefe5 --- /dev/null +++ b/resources/views/emails/curated-register/request-accepted.blade.php @@ -0,0 +1,37 @@ +@component('mail::message') +Hello **{{'@'.$verify->username}}**, + + +We are excited to inform you that your account has been successfully activated! + +Your journey into the world of visual storytelling begins now, and we can’t wait to see the incredible content you’ll create and share. + + +Sign-in to your account + + +Here’s what you can do next: + + +**Personalize Your Profile**: Customize your profile to reflect your personality or brand. + +**Start Sharing**: Post your first photo or album and share your unique perspective with the world. + +**Engage with the Community**: Follow other users, like and comment on posts, and become an active member of our vibrant community. + +**Explore**: Discover amazing content from a diverse range of users and hashtags. + + +Need help getting started? Visit our [Help Center]({{url('site/help')}}) for tips, tutorials, and FAQs. Remember, our community thrives on respect and creativity, so please familiarize yourself with our [Community Guidelines]({{url('site/kb/community-guidelines')}}). + +If you have any questions or need assistance, feel free to reach out to [our support team]({{url('/site/contact')}}). + +Happy posting, and once again, welcome to Pixelfed! + +Warm regards,
+{{ config('pixelfed.domain.app') }} + +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/emails/curated-register/request-details-from-user.blade.php b/resources/views/emails/curated-register/request-details-from-user.blade.php new file mode 100644 index 000000000..59c26c7cd --- /dev/null +++ b/resources/views/emails/curated-register/request-details-from-user.blade.php @@ -0,0 +1,22 @@ +@component('mail::message') +# Action Needed: Additional information requested + +Hello **{{'@'.$verify->username}}** + +To help us process your registration application, we require more information. + +Our onboarding team have requested the following details: + +@component('mail::panel') +

{!! $activity->message !!}

+@endcomponent + +Reply with your response + + +

Please respond promptly, your application will be automatically removed 7 days after your last interaction.

+
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +@endcomponent diff --git a/resources/views/emails/curated-register/request-rejected.blade.php b/resources/views/emails/curated-register/request-rejected.blade.php new file mode 100644 index 000000000..6d7a475dc --- /dev/null +++ b/resources/views/emails/curated-register/request-rejected.blade.php @@ -0,0 +1,19 @@ +@component('mail::message') +Hello **{{'@'.$verify->username}}**, + +We appreciate the time you took to apply for an account on {{ config('pixelfed.domain.app') }}. + +Unfortunately, after reviewing your [application]({{route('help.curated-onboarding')}}), we have decided not to proceed with the activation of your account. + +This decision is made to ensure the best experience for all members of our community. We encourage you to review our [guidelines]({{route('help.community-guidelines')}}) and consider applying again in the future. + +We appreciate your understanding. If you believe this decision was made in error, or if you have any questions, please don’t hesitate to [contact us]({{route('site.contact')}}). + +
+ +Thanks,
+{{ config('pixelfed.domain.app') }} +
+
+

This is an automated message, please be aware that replies to this email cannot be monitored or responded to.

+@endcomponent diff --git a/resources/views/settings/privacy/domain-blocks.blade.php b/resources/views/settings/privacy/domain-blocks.blade.php index d93e0b58e..1602527ec 100644 --- a/resources/views/settings/privacy/domain-blocks.blade.php +++ b/resources/views/settings/privacy/domain-blocks.blade.php @@ -208,7 +208,9 @@ swal.stopLoading() swal.close() this.index = 0 - this.blocks.unshift(parsedUrl.hostname) + if(this.blocks.indexOf(parsedUrl.hostname) === -1) { + this.blocks.unshift(parsedUrl.hostname) + } this.buildList() }) .catch(err => { diff --git a/resources/views/site/help/curated-onboarding.blade.php b/resources/views/site/help/curated-onboarding.blade.php new file mode 100644 index 000000000..6a4d980f3 --- /dev/null +++ b/resources/views/site/help/curated-onboarding.blade.php @@ -0,0 +1,162 @@ +@extends('site.help.partial.template', ['breadcrumb'=>'Curated Onboarding']) + +@section('section') +
+

Curated Onboarding

+
+
+@if((bool) config_cache('instance.curated_registration.enabled') == false) +
+
+ @if((bool) config_cache('pixelfed.open_registration')) +

Curated Onboarding is not available on this server, however anyone can join.

+
+

Create New Account

+ @else +

Curated Onboarding is not available on this server.

+ @endif +
+
+@endif +

Curated Onboarding is our innovative approach to ensure each new member is a perfect fit for our community.

+

This process goes beyond the usual sign-up routine. It's a thoughtful method to understand each applicant's intentions and aspirations within our platform.

+

If you're excited to be a part of a platform that values individuality, creativity, and community, we invite you to apply to join our community. Share with us your story, and let's embark on this visual journey together!

+@if((bool) config_cache('instance.curated_registration.enabled') && !request()->user()) +

+ Apply to Join +

+@endif +
+
How does Curated Onboarding work?
+
    +
  1. +

    Application Submission

    +

    Start your journey by providing your username and email, along with a personal note about why you're excited to join Pixelfed. This insight into your interests and aspirations helps us get to know you better.

    +
  2. +
    +
  3. +

    Admin Review and Interaction

    +

    Our team carefully reviews each application, assessing your fit within our community. If we're intrigued but need more information, we'll reach out directly. You'll receive an email with a link to a special form where you can view our request and respond in detail. This two-way communication ensures a thorough and fair evaluation process.

    +
  4. +
    +
  5. +

    Decision – Acceptance or Rejection

    +

    Each application is thoughtfully considered. If you're a match for our community, you'll be greeted with a warm welcome email and instructions to activate your account. If your application is not accepted, we will inform you respectfully, leaving the possibility open for future applications.

    +
  6. +
+
+
Why Curated Onboarding?
+
    +
  • +

    Fostering Quality Connections

    +

    At Pixelfed, we believe in the power of meaningful connections. It's not just about how many people are in the community, but how they enrich and enliven our platform. Our curated onboarding process is designed to welcome members who share our enthusiasm for creativity and engagement, ensuring every interaction on Pixelfed is enjoyable and rewarding.

    +
  • +
    +
  • +

    Ensuring Safety and Respect

    +

    A careful onboarding process is critical for maintaining a safe and respectful environment. It allows Pixelfed to align every new member with the platform's values of kindness and inclusivity.

    +
  • +
    +
  • +

    Encouraging Community Engagement

    +

    By engaging with applicants from the start, Pixelfed fosters a community that's not just active but passionate. This approach welcomes users who are genuinely interested in making a positive contribution to Pixelfed's vibrant community.

    +
  • +
+
+
FAQs & Troubleshooting
+

+ +

+
+ This indicates that you've attempted to verify your email address too many times. This most likely is the result of an issue delivering the verification emails to your email provider. If you are experiencing this issue, we suggest that you contact the admin onboarding team and mention that you're having issues verifying your email address. +
+
+

+

+ +

+
+ This indicates the desired username is already in-use or was previously used by a now deleted account. You need to pick a different username. +
+
+

+

+ +

+
+ This indicates the desired email is not supported. While it may be a valid email, admins may have blocked specific domains from being associated with account email addresses. +
+
+

+

+ +

+
+ This indicates the desired username is already in-use or was previously used by a now deleted account. You need to pick a different username. +
+
+

+

+ +

+
+ This indicates the desired username is not a valid format, usernames may only contain one dash (-), period (.) or underscore (_) and must start with a letter or number. +
+
+

+

+ +

+
+ This indicates the reason you provided for joining is less than the minimum accepted characters. The reason should be atleast 20 characters long, up to 1000 characters. If you desire to share a longer reason than 1000 characters, consider using a pastebin and posting the link. We can't guarantee that admins will visit any links you provided, so ideally you can keep the length within 1000 chars. +
+
+

+

+ +

+
+

We understand that receiving a notification of rejection can be disappointing. Here's what you can consider if your application to join Pixelfed hasn't been successful:

+ +
    +
  • + Review Your Application: Reflect on the information you provided. Our decision may have been influenced by a variety of factors, including the clarity of your intentions or how well they align with our community values. Consider if there was any additional context or passion for Pixelfed that you could have included. +
  • +
  • + Reapply with Updated Information: We encourage you to reapply if you feel your initial application didn’t fully capture your enthusiasm or alignment with our community values. However, we recommend exercising caution and thoughtfulness. Please take time to refine and enhance your application before resubmitting, as repetitive or frequent submissions can overwhelm our admin team. We value careful consideration and meaningful updates in reapplications, as this helps us maintain a fair and manageable review process for everyone. Your patience and understanding in this regard are greatly appreciated and can positively influence the outcome of future applications. +
  • +
  • + Seek Feedback: If you are seeking clarity on why your application wasn't successful, you're welcome to contact us for feedback. However, please be mindful that our admins handle a high volume of queries and applications. While we strive to provide helpful responses, our ability to offer detailed individual feedback may be limited. We ask for your patience and understanding in this matter. When reaching out, ensure your query is concise and considerate of the admins' time. This approach will help us assist you more effectively and maintain a positive interaction, even if your initial application didn't meet our criteria. +
  • +
  • + Stay Engaged: Even if you're not a member yet, you can stay connected with Pixelfed through our public forums, blog, or social media channels. This will keep you updated on any changes or new features that might make our platform a better fit for you in the future. +
  • +
+ +

Remember, a rejection is not necessarily a reflection of your qualities or potential as a member of our community. It's often about finding the right fit at the right time. We appreciate your interest in Pixelfed and hope you won't be discouraged from exploring other ways to engage with our platform.

+

+
+
+

+@endsection diff --git a/resources/views/site/help/partial/sidebar.blade.php b/resources/views/site/help/partial/sidebar.blade.php index e0a82c9d3..1e8c3d68f 100644 --- a/resources/views/site/help/partial/sidebar.blade.php +++ b/resources/views/site/help/partial/sidebar.blade.php @@ -33,6 +33,13 @@ + @if((bool) config_cache('instance.curated_registration.enabled')) + + @endif diff --git a/routes/web-admin.php b/routes/web-admin.php index 72572b5c0..9f01d5c7c 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -104,11 +104,11 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('asf/create', 'AdminShadowFilterController@store'); Route::get('asf/home', 'AdminShadowFilterController@home'); - // Route::redirect('curated-onboarding/', 'curated-onboarding/home'); - // Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); - // Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); - // Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); - // Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); + Route::redirect('curated-onboarding/', 'curated-onboarding/home'); + Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); + Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); + Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); + Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); Route::prefix('api')->group(function() { Route::get('stats', 'AdminController@getStats'); @@ -157,10 +157,10 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('autospam/config/enable', 'AdminController@enableAutospamApi'); Route::post('autospam/config/disable', 'AdminController@disableAutospamApi'); // Route::get('instances/{id}/accounts', 'AdminController@getInstanceAccounts'); - // Route::get('curated-onboarding/show/{id}/activity-log', 'AdminCuratedRegisterController@apiActivityLog'); - // Route::post('curated-onboarding/show/{id}/message/preview', 'AdminCuratedRegisterController@apiMessagePreviewStore'); - // Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); - // Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); - // Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); + Route::get('curated-onboarding/show/{id}/activity-log', 'AdminCuratedRegisterController@apiActivityLog'); + Route::post('curated-onboarding/show/{id}/message/preview', 'AdminCuratedRegisterController@apiMessagePreviewStore'); + Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); + Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); + Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); }); }); diff --git a/routes/web.php b/routes/web.php index 3947bf5da..6a5b878ee 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,16 +29,18 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); - // Route::get('auth/sign_up', 'CuratedRegisterController@index'); - // Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); - // Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); - // Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); - // Route::post('auth/sign_up/concierge', 'CuratedRegisterController@conciergeStore'); - // Route::get('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormShow'); - // Route::post('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormStore'); - // Route::get('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmail'); - // Route::post('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmailHandle'); - // Route::get('auth/sign_up/confirmed', 'CuratedRegisterController@emailConfirmed'); + Route::get('auth/sign_up', 'CuratedRegisterController@index')->name('auth.curated-onboarding'); + Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); + Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); + Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); + Route::post('auth/sign_up/concierge', 'CuratedRegisterController@conciergeStore'); + Route::get('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormShow'); + Route::post('auth/sign_up/concierge/form', 'CuratedRegisterController@conciergeFormStore'); + Route::get('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmail'); + Route::post('auth/sign_up/confirm', 'CuratedRegisterController@confirmEmailHandle'); + Route::get('auth/sign_up/confirmed', 'CuratedRegisterController@emailConfirmed'); + Route::get('auth/sign_up/resend-confirmation', 'CuratedRegisterController@resendConfirmation'); + Route::post('auth/sign_up/resend-confirmation', 'CuratedRegisterController@resendConfirmationProcess'); Route::get('auth/forgot/email', 'UserEmailForgotController@index')->name('email.forgot'); Route::post('auth/forgot/email', 'UserEmailForgotController@store')->middleware('throttle:10,900,forgotEmail'); @@ -306,7 +308,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::view('import', 'site.help.import')->name('help.import'); Route::view('parental-controls', 'site.help.parental-controls'); // Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues'); - // Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); + Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); }); Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); Route::get('newsroom/archive', 'NewsroomController@archive'); From 8355d5d00cabb99a30e18c1a1a9cc739868c23da Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 04:03:39 -0700 Subject: [PATCH 425/977] Add AdminCuratedRegisterController --- .../AdminCuratedRegisterController.php | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 app/Http/Controllers/AdminCuratedRegisterController.php diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php new file mode 100644 index 000000000..bea6e58cf --- /dev/null +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -0,0 +1,226 @@ +middleware(['auth','admin']); + } + + public function index(Request $request) + { + $this->validate($request, [ + 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected' + ]); + $filter = $request->input('filter', 'open'); + $records = CuratedRegister::when($filter, function($q, $filter) { + if($filter === 'open') { + return $q->where('is_rejected', false) + ->whereNotNull('email_verified_at') + ->whereIsClosed(false); + } else if($filter === 'all') { + return $q; + } elseif ($filter === 'awaiting') { + return $q->whereIsClosed(false) + ->whereNull('is_rejected') + ->whereNull('is_approved'); + } elseif ($filter === 'approved') { + return $q->whereIsClosed(true)->whereIsApproved(true); + } elseif ($filter === 'rejected') { + return $q->whereIsClosed(true)->whereIsRejected(true); + } + }) + ->latest() + ->paginate(10); + return view('admin.curated-register.index', compact('records', 'filter')); + } + + public function show(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + return view('admin.curated-register.show', compact('record')); + } + + public function apiActivityLog(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + + $res = collect([ + [ + 'id' => 1, + 'action' => 'created', + 'title' => 'Onboarding application created', + 'message' => null, + 'link' => null, + 'timestamp' => $record->created_at, + ] + ]); + + if($record->email_verified_at) { + $res->push([ + 'id' => 3, + 'action' => 'email_verified_at', + 'title' => 'Applicant successfully verified email address', + 'message' => null, + 'link' => null, + 'timestamp' => $record->email_verified_at, + ]); + } + + $activities = CuratedRegisterActivity::whereRegisterId($record->id)->get(); + + $idx = 4; + $userResponses = collect([]); + + foreach($activities as $activity) { + $idx++; + if($activity->from_user) { + $userResponses->push($activity); + continue; + } + $res->push([ + 'id' => $idx, + 'aid' => $activity->id, + 'action' => $activity->type, + 'title' => $activity->from_admin ? 'Admin requested info' : 'User responded', + 'message' => $activity->message, + 'link' => $activity->adminReviewUrl(), + 'timestamp' => $activity->created_at, + ]); + } + + foreach($userResponses as $ur) { + $res = $res->map(function($r) use($ur) { + if(!isset($r['aid'])) { + return $r; + } + if($ur->reply_to_id === $r['aid']) { + $r['user_response'] = $ur; + return $r; + } + return $r; + }); + } + + if($record->is_approved) { + $idx++; + $res->push([ + 'id' => $idx, + 'action' => 'approved', + 'title' => 'Application Approved', + 'message' => null, + 'link' => null, + 'timestamp' => $record->action_taken_at, + ]); + } else if ($record->is_rejected) { + $idx++; + $res->push([ + 'id' => $idx, + 'action' => 'rejected', + 'title' => 'Application Rejected', + 'message' => null, + 'link' => null, + 'timestamp' => $record->action_taken_at, + ]); + } + + return $res->reverse()->values(); + } + + public function apiMessagePreviewStore(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + return $request->all(); + } + + public function apiMessageSendStore(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:5|max:1000' + ]); + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $activity = new CuratedRegisterActivity; + $activity->register_id = $record->id; + $activity->admin_id = $request->user()->id; + $activity->secret_code = Str::random(32); + $activity->type = 'request_details'; + $activity->from_admin = true; + $activity->message = $request->input('message'); + $activity->save(); + $record->is_awaiting_more_info = true; + $record->save(); + Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); + return $request->all(); + } + + public function previewDetailsMessageShow(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $activity = new CuratedRegisterActivity; + $activity->message = $request->input('message'); + return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity); + } + + + public function previewMessageShow(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); + $record->message = $request->input('message'); + return new \App\Mail\CuratedRegisterSendMessage($record); + } + + public function apiHandleReject(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:reject-email,reject-silent' + ]); + $action = $request->input('action'); + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email'); + $record->is_rejected = true; + $record->is_closed = true; + $record->action_taken_at = now(); + $record->save(); + if($action === 'reject-email') { + Mail::to($record->email)->send(new CuratedRegisterRejectUser($record)); + } + return [200]; + } + + public function apiHandleApprove(Request $request, $id) + { + $record = CuratedRegister::findOrFail($id); + abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email'); + $record->is_approved = true; + $record->is_closed = true; + $record->action_taken_at = now(); + $record->save(); + $user = User::create([ + 'name' => $record->username, + 'username' => $record->username, + 'email' => $record->email, + 'password' => $record->password, + 'app_register_ip' => $record->ip_address, + 'email_verified_at' => now(), + 'register_source' => 'cur_onboarding' + ]); + + Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record)); + return [200]; + } +} From cae26c666da35d138050b417d0cc380d808a3203 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 04:33:29 -0700 Subject: [PATCH 426/977] Update landing nav, fix curated onboarding state --- .../components/landing/sections/nav.vue | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/resources/assets/components/landing/sections/nav.vue b/resources/assets/components/landing/sections/nav.vue index 38fde6ef2..16dbfef1e 100644 --- a/resources/assets/components/landing/sections/nav.vue +++ b/resources/assets/components/landing/sections/nav.vue @@ -1,33 +1,46 @@ From 06655c3a8ba852a85b1cd66817056c7de73aca89 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 04:34:57 -0700 Subject: [PATCH 427/977] Update LandingService, add curated onboarding parameter --- app/Services/LandingService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Services/LandingService.php b/app/Services/LandingService.php index 6b1aa62d4..199768b86 100644 --- a/app/Services/LandingService.php +++ b/app/Services/LandingService.php @@ -48,7 +48,7 @@ class LandingService ->toArray() : []; }); - $openReg = (bool) config_cache('pixelfed.open_registration') || (bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg'); + $openReg = (bool) config_cache('pixelfed.open_registration'); $res = [ 'name' => config_cache('app.name'), @@ -57,6 +57,7 @@ class LandingService 'show_directory' => config_cache('instance.landing.show_directory'), 'show_explore_feed' => config_cache('instance.landing.show_explore'), 'open_registration' => (bool) $openReg, + 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), 'version' => config('pixelfed.version'), 'about' => [ 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), From 0ad3654da37dbf3bdaeb2402f6baa896aeffcc1b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 19 Feb 2024 04:35:11 -0700 Subject: [PATCH 428/977] Update compiled assets --- public/js/landing.js | Bin 184610 -> 184777 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/js/landing.js b/public/js/landing.js index 69ccb8f8b872e95fc49456935d274ca2ec81808e..8c059cc7da399ba32dd8703150fd2a55f7a4e2c6 100644 GIT binary patch delta 273 zcmZ3qi2LMX?hR|j>yz_y3rb5;Q>?0sQqz4h^Rlg~(^E^V(n|A^OEUBGG&F(YC8b4q z3MCnt#d^v4d1;yHdie#ZdGSE?nZ+eVi6CY6O8O9PYLSu^vdZLApn}wt`24)2{KTS^ z%)E4<(!|n|4E^HF^t|{|t^y^ivi!^x1%ukj_Y}o89}t&JVX?5ZFr7Y8lTm*1f$W6M zF*PYnvNbiSFv~HGoP40JkTX>e>VeHR^=6Y;jf^b}47QgGGIoftfY{R$`55J=Z%}3A YWCa;-I(?xUqhoua8sqjtHKvWL01+u;cK`qY delta 104 zcmX@Pn0wJ8?hR|jCl~1pZ}yRpOkuS&wy-psTqvftc|y$|CPw?oid#e`*VV^xE9n=d zre_wHq!uY{eq3)hiPgl|(A0FhoDgG&2!uVIQH4<*V!(7kRYu2l33bNp66#DFR{;Pw CxgY`n diff --git a/public/mix-manifest.json b/public/mix-manifest.json index ee5fc63b44194da3296f511442225fcdb5bc2850..75fb83c2b355fa0d3bb7417331db8a3ec17d46e1 100644 GIT binary patch delta 45 zcmeyZ@mpg Date: Mon, 19 Feb 2024 05:44:34 -0700 Subject: [PATCH 429/977] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a2cb6c6..79d7b9fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev) +### Features + +- Curated Onboarding ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf)) + ### Updates - Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad)) From abee7d4d620f11e4c70d13a2ec9de0d20782d9a0 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 21 Feb 2024 21:52:59 +0000 Subject: [PATCH 430/977] add missing profiles --- .env.docker | 22 ++++++++++++---------- docker-compose.yml | 4 +++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.env.docker b/.env.docker index a38c6b61c..2368e85d6 100644 --- a/.env.docker +++ b/.env.docker @@ -1080,6 +1080,9 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/ca # docker redis ################################################################################ +# Set this to a non-empty value (e.g. "disabled") to disable the [redis] service +#DOCKER_REDIS_PROFILE= + # Redis version to use as Docker tag # # @see https://hub.docker.com/_/redis @@ -1105,7 +1108,6 @@ DOCKER_REDIS_HOST_PORT="${REDIS_PORT:?error}" # @default "" # @dottie/validate required #DOCKER_REDIS_CONFIG_FILE="/etc/redis/redis.conf" - # How often Docker health check should run for [redis] service # # @default "10s" @@ -1116,6 +1118,9 @@ DOCKER_REDIS_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?er # docker db ################################################################################ +# Set this to a non-empty value (e.g. "disabled") to disable the [db] service +#DOCKER_DB_PROFILE= + # Docker image for the DB service # @dottie/validate required DOCKER_DB_IMAGE="mariadb:${DB_VERSION}" @@ -1124,9 +1129,6 @@ DOCKER_DB_IMAGE="mariadb:${DB_VERSION}" # @dottie/validate required DOCKER_DB_COMMAND="--default-authentication-plugin=mysql_native_password" -# Set this to a non-empty value (e.g. "disabled") to disable the [db] service -#DOCKER_DB_PROFILE="" - # Path (on host system) where the [db] container will store its data # # Path is relative (./some/other/path) to the docker-compose.yml or absolute (/some/other/path) @@ -1190,18 +1192,18 @@ DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?e # docker proxy ################################################################################ -# The version of nginx-proxy to use -# -# @see https://hub.docker.com/r/nginxproxy/nginx-proxy -# @dottie/validate required -DOCKER_PROXY_VERSION="1.4" - # Set this to a non-empty value (e.g. "disabled") to disable the [proxy] and [proxy-acme] service #DOCKER_PROXY_PROFILE= # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service #DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE?error}" +# The version of nginx-proxy to use +# +# @see https://hub.docker.com/r/nginxproxy/nginx-proxy +# @dottie/validate required +DOCKER_PROXY_VERSION="1.4" + # How often Docker health check should run for [proxy] service # @dottie/validate required DOCKER_PROXY_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error}" diff --git a/docker-compose.yml b/docker-compose.yml index 8b537db14..77e184c2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,9 +112,9 @@ services: container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped - stop_signal: SIGTERM profiles: - ${DOCKER_WORKER_PROFILE:-} + stop_signal: SIGTERM build: target: ${DOCKER_APP_RUNTIME}-runtime args: @@ -182,6 +182,8 @@ services: image: redis:${DOCKER_REDIS_VERSION} container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped + profiles: + - ${DOCKER_REDIS_PROFILE:-} command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" environment: TZ: "${TZ:?error}" From adf1af3703cc51fe38bbda85ea74baba41f0d2f9 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 21 Feb 2024 22:15:41 +0000 Subject: [PATCH 431/977] add small bash/artisan helper commands --- docker/artisan | 6 ++++++ docker/bash | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100755 docker/artisan create mode 100755 docker/bash diff --git a/docker/artisan b/docker/artisan new file mode 100755 index 000000000..d50d65244 --- /dev/null +++ b/docker/artisan @@ -0,0 +1,6 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +exec docker compose exec --user "${user}" "${service}" php artisan "${@}" diff --git a/docker/bash b/docker/bash new file mode 100755 index 000000000..fc889b964 --- /dev/null +++ b/docker/bash @@ -0,0 +1,12 @@ +#!/bin/bash + +declare service="${PF_SERVICE:=worker}" +declare user="${PF_USER:=www-data}" + +declare -a command=("bash") + +if [[ $# -ge 1 ]]; then + command=("$@") +fi + +exec docker compose exec --user "${user}" "${service}" "${command[@]}" From f486bfb73e507e368396abbabf4f27c3d809849a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Wed, 21 Feb 2024 22:29:07 +0000 Subject: [PATCH 432/977] add small dottie wrapper --- docker/dottie | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100755 docker/dottie diff --git a/docker/dottie b/docker/dottie new file mode 100755 index 000000000..bd268a070 --- /dev/null +++ b/docker/dottie @@ -0,0 +1,16 @@ +#!/bin/bash + +declare root="${PWD}" + +if command -v git &>/dev/null; then + root=$(git rev-parse --show-toplevel) +fi + +exec docker run \ + --rm \ + --interactive \ + --tty \ + --volume "${root}:/var/www" \ + --workdir /var/www \ + ghcr.io/jippi/dottie \ + "$@" From c4dde641194af07eceb242912e1b7da271878455 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Feb 2024 01:54:18 -0700 Subject: [PATCH 433/977] Update AdminCuratedRegisterController, show oldest applications first --- app/Http/Controllers/AdminCuratedRegisterController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php index bea6e58cf..b28fa438f 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -42,7 +42,6 @@ class AdminCuratedRegisterController extends Controller return $q->whereIsClosed(true)->whereIsRejected(true); } }) - ->latest() ->paginate(10); return view('admin.curated-register.index', compact('records', 'filter')); } From 4c5e8288b062acda12a44fcb1d1c29ce452969a6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Feb 2024 01:55:08 -0700 Subject: [PATCH 434/977] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d7b9fd3..f186d633d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Curated Onboarding ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf)) +- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf)) ### Updates @@ -14,6 +14,7 @@ - Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9)) - Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e)) - Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d)) +- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 59c70239f87c630d36d51017b02dfa71b9009b39 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Feb 2024 03:39:13 -0700 Subject: [PATCH 435/977] Update Directory logic, add curated onboarding support --- .../Admin/AdminDirectoryController.php | 11 +++++--- .../PixelfedDirectoryController.php | 9 ++++--- .../components/admin/AdminDirectory.vue | 25 ++++++++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php index 1e4db7d2d..8d3e4b7fc 100644 --- a/app/Http/Controllers/Admin/AdminDirectoryController.php +++ b/app/Http/Controllers/Admin/AdminDirectoryController.php @@ -75,6 +75,7 @@ trait AdminDirectoryController } $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; + $res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled'); $res['open_registration'] = (bool) config_cache('pixelfed.open_registration'); $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); @@ -124,7 +125,7 @@ trait AdminDirectoryController $res['requirements_validator'] = $validator->errors(); - $res['is_eligible'] = $res['open_registration'] && + $res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) && $res['oauth_enabled'] && $res['activitypub_enabled'] && count($res['requirements_validator']) === 0 && @@ -227,7 +228,7 @@ trait AdminDirectoryController ->each(function($name) { Storage::delete($name); }); - $path = $request->file('banner_image')->store('public/headers'); + $path = $request->file('banner_image')->storePublicly('public/headers'); $res['banner_image'] = $path; ConfigCacheService::put('app.banner_image', url(Storage::url($path))); @@ -249,7 +250,8 @@ trait AdminDirectoryController { $reqs = []; $reqs['feature_config'] = [ - 'open_registration' => config_cache('pixelfed.open_registration'), + 'open_registration' => (bool) config_cache('pixelfed.open_registration'), + 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), 'activitypub_enabled' => config_cache('federation.activitypub.enabled'), 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'), 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), @@ -265,7 +267,8 @@ trait AdminDirectoryController ]; $validator = Validator::make($reqs['feature_config'], [ - 'open_registration' => 'required|accepted', + 'open_registration' => 'required_unless:curated_onboarding,true', + 'curated_onboarding' => 'required_unless:open_registration,true', 'activitypub_enabled' => 'required|accepted', 'oauth_enabled' => 'required|accepted', 'media_types' => [ diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php index 6290cd398..cfe3f690a 100644 --- a/app/Http/Controllers/PixelfedDirectoryController.php +++ b/app/Http/Controllers/PixelfedDirectoryController.php @@ -78,10 +78,11 @@ class PixelfedDirectoryController extends Controller $res['community_guidelines'] = json_decode($guidelines->v, true); } - $openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first(); - if($openRegistration) { - $res['open_registration'] = (bool) $openRegistration; - } + $openRegistration = (bool) config_cache('pixelfed.open_registration'); + $res['open_registration'] = $openRegistration; + + $curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled'); + $res['curated_onboarding'] = $curatedOnboarding; $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); if($oauthEnabled) { diff --git a/resources/assets/components/admin/AdminDirectory.vue b/resources/assets/components/admin/AdminDirectory.vue index 0676b4057..53eade198 100644 --- a/resources/assets/components/admin/AdminDirectory.vue +++ b/resources/assets/components/admin/AdminDirectory.vue @@ -109,12 +109,20 @@
- - - {{ requirements.open_registration ? 'Open' : 'Closed' }} account registration - + +
@@ -895,6 +903,7 @@ activitypub_enabled: undefined, open_registration: undefined, oauth_enabled: undefined, + curated_onboarding: undefined, }, feature_config: [], requirements_validator: [], @@ -951,6 +960,10 @@ this.requirements.open_registration = res.data.open_registration; } + if(res.data.curated_onboarding) { + this.requirements.curated_onboarding = res.data.curated_onboarding; + } + if(res.data.oauth_enabled) { this.requirements.oauth_enabled = res.data.oauth_enabled; } From b0ecdc8162244e7754183ab97f71532e4b096a9e Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 22 Feb 2024 03:50:56 -0700 Subject: [PATCH 436/977] Update compiled assets --- public/js/admin.js | Bin 216055 -> 216502 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index e21b77485fd1f936510d2fd39008db70940fdf4f..3e112c876b02e0f4981f59051021a447902387ac 100644 GIT binary patch delta 614 zcmex9op;-8-VGO6_?#SE$lwXoxQdy8{ zTRHgxpVVakQW55SjmpW4{9=<|@rejJ>!oBC7vv;X`X%P3+Li)k90bHCcbCR8I#1rn zUnN+nX{Ax37atEcCqCY`auL!;@19?Tlk zc@>zfr(f`5l-h3K&&b8Au4rIuYfI2y_R|&97$v3$d}I`#{$U*>>vZ2ZMvm!*NsP6V zCp=+ccZLMwbYN(EPyf)##K~v{(x5zjUos=l^o$tB%OFwK=?;91Y|{^o+HU>Y-V9#xxL(pnT4GN#GcH!Nq)M37c(a-(7%=z(*4-VGO6_$xIut83Fr^O8$4^Yav(C*NnOV|3me$C}E?Td7x2lwXoxQdy8{ z>pb}apVVY=ei7z;4d=;>{9=<|@rejl>ZN2B7vv;X`X%P3+Li)k90bHC|0|7Utem`& zze>Y+{%y*`bRZ*rWy&~&MI#<X_qt0&Jel%1TvhHbl*8Z##gW94)gE#?5m%I#CNnCD3H8ycCI wnriA~+9qlkT3T98H*{s5zx}=ob2J+(SZex4Z)SCbVX|eac~Vk}L7I_~p_%dK#jH2@08lXw A4gdfE delta 45 zcmeyZ@mpg Date: Thu, 22 Feb 2024 03:51:10 -0700 Subject: [PATCH 437/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f186d633d..b38fb1b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e)) - Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d)) - Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641)) +- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 26d6f8f9febd0ee9966acd3cc25eb104fec571e4 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 13:21:58 +0000 Subject: [PATCH 438/977] push build cache to registry as well --- .github/workflows/docker.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9a451937c..595d4a2e5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -189,8 +189,11 @@ jobs: build-args: | PHP_VERSION=${{ matrix.php_version }} PHP_BASE_TYPE=${{ matrix.php_base }} - cache-from: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} - cache-to: type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + cache-from: | + type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + cache-to: | + type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + type=registry,ref=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}-cache # goss validate the image # From 14f8478e6ae14cb6d224c87a68748fa85d294059 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 13:25:56 +0000 Subject: [PATCH 439/977] push build cache to registry as well --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 595d4a2e5..ee49d1351 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -193,7 +193,7 @@ jobs: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} cache-to: | type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} - type=registry,ref=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}-cache + type=registry,ref=ghcr.io/${{ github.repository }}-cache:${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} # goss validate the image # From ae358e47cbef2570583f2a56591a7b0ab562f273 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 13:38:27 +0000 Subject: [PATCH 440/977] push build cache to registry as well --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ee49d1351..e5a3384d5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -193,7 +193,7 @@ jobs: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} cache-to: | type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} - type=registry,ref=ghcr.io/${{ github.repository }}-cache:${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} + type=registry,ref=ghcr.io/${{ github.repository }}-cache:${{ steps.meta.outputs.tags }} # goss validate the image # From 9c26bf26dd668a0aa12694b8e28801b58b4a865b Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 13:40:32 +0000 Subject: [PATCH 441/977] push build cache to registry as well --- .github/workflows/docker.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e5a3384d5..50c2cbc32 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -173,6 +173,27 @@ jobs: env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + - name: Docker meta (Cache)) + uses: docker/metadata-action@v5 + id: cache + with: + images: | + name=ghcr.io/${{ github.repository }}-cache,enable=true + name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }} + flavor: | + latest=auto + suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }} + tags: | + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} + type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }} + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + type=ref,event=branch,prefix=branch- + type=ref,event=pr,prefix=pr- + type=ref,event=tag + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + - name: Build and push Docker image uses: docker/build-push-action@v5 with: @@ -193,7 +214,7 @@ jobs: type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} cache-to: | type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }} - type=registry,ref=ghcr.io/${{ github.repository }}-cache:${{ steps.meta.outputs.tags }} + ${{ steps.cache.outputs.tags }} # goss validate the image # From 0ecebbb8bf0f13f3f8c0b199d18e9bcf33a471f8 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:07:51 +0000 Subject: [PATCH 442/977] push build cache to registry as well --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 50c2cbc32..d14c88dfc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -173,7 +173,7 @@ jobs: env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index - - name: Docker meta (Cache)) + - name: Docker meta (Cache) uses: docker/metadata-action@v5 id: cache with: From 3bfd043792158dc8c929493ab1bfda68da6801a1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:32:50 +0000 Subject: [PATCH 443/977] update ignore files --- .dockerignore | 22 +++++++++++++++++++--- .gitignore | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index c8ae49a4e..7c06534dc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,20 @@ -.env -.git -.gitignore +.DS_Store +/.composer/ +/.env +/.git +/.gitconfig +/.gitignore +/bootstrap/cache /docker-compose-state/ +/node_modules +/npm-debug.log +/public/storage +/public/vendor/horizon +/storage/*.key +/storage/docker +/vendor +/yarn-error.log + +# exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore diff --git a/.gitignore b/.gitignore index f83ec13c4..b8e9b18de 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /public/hot /public/storage /storage/*.key +/storage/docker /vendor Homestead.json Homestead.yaml From 28b83b575f13ad8d27aa714085cdbb8cf1c3fd94 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:48:31 +0000 Subject: [PATCH 444/977] Bump dottie --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e82de9d45..c6b297105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.8.0" +ARG DOTTIE_VERSION="v0.9.3" ### # PHP base configuration @@ -142,6 +142,7 @@ ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA} # Install and configure base layer COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh + RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/base.sh From 0addfe5605c06571e897aad33022407642a69551 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:49:18 +0000 Subject: [PATCH 445/977] allow .env control of a couple of PHP settings --- .env.docker | 51 +++++++++++++++++-- .../templates/usr/local/etc/php/php.ini | 14 ++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/.env.docker b/.env.docker index 2368e85d6..9a6af8bbd 100644 --- a/.env.docker +++ b/.env.docker @@ -1050,6 +1050,16 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/ca # @dottie/validate required,boolean #DOCKER_APP_ENTRYPOINT_DEBUG="0" +# Show the "diff" when applying templating to files +# +# @default "1" +# @dottie/validate required,boolean +#DOCKER_APP_ENTRYPOINT_SHOW_TEMPLATE_DIFF="1" + +# Docker entrypoints that should be skipped on startup +# @default "" +#ENTRYPOINT_SKIP_SCRIPTS="" + # List of extra APT packages (separated by space) to install when building # locally using [docker compose build]. # @@ -1076,6 +1086,43 @@ DOCKER_APP_HOST_CACHE_PATH="${DOCKER_ALL_HOST_DATA_ROOT_PATH:?error}/pixelfed/ca # @dottie/validate required #DOCKER_APP_PHP_MEMORY_LIMIT="128M" +# @default "E_ALL & ~E_DEPRECATED & ~E_STRICT" +# @see http://php.net/error-reporting +# @dottie/validate required +#DOCKER_APP_PHP_ERROR_REPORTING="E_ALL & ~E_DEPRECATED & ~E_STRICT" + +# @default "off" +# @see http://php.net/display-errors +# @dottie/validate required,oneof=on off +#DOCKER_APP_PHP_DISPLAY_ERRORS="off" + +# Enables the opcode cache. +# +# When disabled, code is not optimised or cached. +# +# @default "1" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enable +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_ENABLE="1" + +# If enabled, OPcache will check for updated scripts every [opcache.revalidate_freq] seconds. +# +# When this directive is disabled, you must reset OPcache manually via opcache_reset(), +# opcache_invalidate() or by restarting the Web server for changes to the filesystem to take effect. +# +# @default "0" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.validate-timestamps +# @dottie/validate required,oneof=0 1 +#DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" + +# How often to check script timestamps for updates, in seconds. +# 0 will result in OPcache checking for updates on every request. +# +# @default "2" +# @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq +# @dottie/validate required,oneof=0 1 2 +#DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ="2" + ################################################################################ # docker redis ################################################################################ @@ -1165,7 +1212,6 @@ DOCKER_DB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?error ################################################################################ # Set this to a non-empty value (e.g. "disabled") to disable the [web] service -# @dottie/validate required #DOCKER_WEB_PROFILE="" # Port to expose [web] container will listen on *outside* the container (e.g. the host machine) for *HTTP* traffic only @@ -1181,7 +1227,6 @@ DOCKER_WEB_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?erro ################################################################################ # Set this to a non-empty value (e.g. "disabled") to disable the [worker] service -# @dottie/validate required #DOCKER_WORKER_PROFILE="" # How often Docker health check should run for [worker] service @@ -1196,7 +1241,7 @@ DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?e #DOCKER_PROXY_PROFILE= # Set this to a non-empty value (e.g. "disabled") to disable the [proxy-acme] service -#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE?error}" +#DOCKER_PROXY_ACME_PROFILE="${DOCKER_PROXY_PROFILE:-}" # The version of nginx-proxy to use # diff --git a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini index 0ca96819b..130166e80 100644 --- a/docker/shared/root/docker/templates/usr/local/etc/php/php.ini +++ b/docker/shared/root/docker/templates/usr/local/etc/php/php.ini @@ -462,7 +462,7 @@ memory_limit = {{ getenv "DOCKER_APP_PHP_MEMORY_LIMIT" "128M" }} ; Development Value: E_ALL ; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT ; http://php.net/error-reporting -error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +error_reporting = {{ getenv "DOCKER_APP_PHP_ERROR_REPORTING" "E_ALL & ~E_DEPRECATED & ~E_STRICT" }} ; This directive controls whether or not and where PHP will output errors, ; notices and warnings too. Error output is very useful during development, but @@ -479,7 +479,7 @@ error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT ; Development Value: On ; Production Value: Off ; http://php.net/display-errors -display_errors = Off +display_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; The display of errors which occur during PHP's startup sequence are handled ; separately from display_errors. We strongly recommend you set this to 'off' @@ -488,7 +488,7 @@ display_errors = Off ; Development Value: On ; Production Value: Off ; http://php.net/display-startup-errors -display_startup_errors = Off +display_startup_errors = {{ getenv "DOCKER_APP_PHP_DISPLAY_ERRORS" "off" }} ; Besides displaying errors, PHP can also log errors to locations such as a ; server-specific log, STDERR, or a location specified by the error_log @@ -680,7 +680,7 @@ auto_globals_jit = On ; Its value may be 0 to disable the limit. It is ignored if POST data reading ; is disabled through enable_post_data_reading. ; http://php.net/post-max-size -post_max_size = {{ getenv "POST_MAX_SIZE" }} +post_max_size = {{ getenv "POST_MAX_SIZE" "61M" }} ; Automatically add files before PHP document. ; http://php.net/auto-prepend-file @@ -1736,7 +1736,7 @@ ldap.max_links = -1 [opcache] ; Determines if Zend OPCache is enabled -;opcache.enable=1 +opcache.enable={{ getenv "DOCKER_APP_PHP_OPCACHE_ENABLE" "1" }} ; Determines if Zend OPCache is enabled for the CLI version of PHP ;opcache.enable_cli=0 @@ -1762,12 +1762,12 @@ ldap.max_links = -1 ; When disabled, you must reset the OPcache manually or restart the ; webserver for changes to the filesystem to take effect. -;opcache.validate_timestamps=1 +opcache.validate_timestamps={{ getenv "DOCKER_APP_PHP_OPCACHE_VALIDATE_TIMESTAMPS" "0" }} ; How often (in seconds) to check file timestamps for changes to the shared ; memory storage allocation. ("1" means validate once per second, but only ; once per request. "0" means always validate) -;opcache.revalidate_freq=2 +opcache.revalidate_freq={{ getenv "DOCKER_APP_PHP_OPCACHE_REVALIDATE_FREQ" "2" }} ; Enables or disables file search in include_path optimization ;opcache.revalidate_path=0 From 8fd27c6f0c978cbeebe2aa9d690b2df8e610f6d5 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:50:15 +0000 Subject: [PATCH 446/977] use remote build cache for faster local dev --- docker-compose.yml | 4 ++++ .../docker/shared/proxy/conf.d/docker-pixelfed.conf | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf diff --git a/docker-compose.yml b/docker-compose.yml index 77e184c2c..c8eb9458f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,6 +74,8 @@ services: - ${DOCKER_WEB_PROFILE:-} build: target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" args: APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" @@ -117,6 +119,8 @@ services: stop_signal: SIGTERM build: target: ${DOCKER_APP_RUNTIME}-runtime + cache_from: + - "type=registry,ref=${DOCKER_APP_IMAGE}-cache:${DOCKER_APP_TAG}" args: APT_PACKAGES_EXTRA: "${DOCKER_APP_APT_PACKAGES_EXTRA:-}" PHP_BASE_TYPE: "${DOCKER_APP_BASE_TYPE}" diff --git a/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf b/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf new file mode 100644 index 000000000..1b5a9a80f --- /dev/null +++ b/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf @@ -0,0 +1,7 @@ +########################################################### +# DO NOT CHANGE +########################################################### +# This file is generated by the Pixelfed Docker setup, and +# will be rewritten on every container start + +client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; From d9d2a475d8379939516ae682fd4ac739452ef098 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:53:39 +0000 Subject: [PATCH 447/977] sort keys in compose --- docker-compose.yml | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c8eb9458f..ca4ece22b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,8 @@ services: restart: unless-stopped profiles: - ${DOCKER_PROXY_PROFILE:-} + environment: + DOCKER_SERVICE_NAME: "proxy" volumes: - "${DOCKER_PROXY_HOST_DOCKER_SOCKET_PATH}:/tmp/docker.sock:ro" - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/etc/nginx/conf.d" @@ -83,17 +85,23 @@ services: PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" - volumes: - - "./.env:/var/www/.env" - - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" - - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "web" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} + # Used by [proxy] service LETSENCRYPT_HOST: "${DOCKER_PROXY_LETSENCRYPT_HOST:?error}" LETSENCRYPT_EMAIL: "${DOCKER_PROXY_LETSENCRYPT_EMAIL:?error}" LETSENCRYPT_TEST: "${DOCKER_PROXY_LETSENCRYPT_TEST:-}" VIRTUAL_HOST: "${APP_DOMAIN}" VIRTUAL_PORT: "80" + volumes: + - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/docker/shared/proxy/conf.d" + - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" + - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" labels: com.github.nginx-proxy.nginx-proxy.keepalive: 30 com.github.nginx-proxy.nginx-proxy.http2.enable: true @@ -114,9 +122,9 @@ services: container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-worker" command: gosu www-data php artisan horizon restart: unless-stopped + stop_signal: SIGTERM profiles: - ${DOCKER_WORKER_PROFILE:-} - stop_signal: SIGTERM build: target: ${DOCKER_APP_RUNTIME}-runtime cache_from: @@ -128,11 +136,17 @@ services: PHP_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_EXTENSIONS_EXTRA:-}" PHP_PECL_EXTENSIONS_EXTRA: "${DOCKER_APP_PHP_PECL_EXTENSIONS_EXTRA:-}" PHP_VERSION: "${DOCKER_APP_PHP_VERSION:?error}" + environment: + # Used by Pixelfed Docker init script + DOCKER_SERVICE_NAME: "worker" + DOCKER_APP_ENTRYPOINT_DEBUG: ${DOCKER_APP_ENTRYPOINT_DEBUG:-0} + ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} volumes: - "./.env:/var/www/.env" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/docker/shared/proxy/conf.d" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" + - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" depends_on: - db - redis @@ -186,9 +200,9 @@ services: image: redis:${DOCKER_REDIS_VERSION} container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-redis" restart: unless-stopped + command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" profiles: - ${DOCKER_REDIS_PROFILE:-} - command: "${DOCKER_REDIS_CONFIG_FILE:-} --requirepass '${REDIS_PASSWORD:-}'" environment: TZ: "${TZ:?error}" REDISCLI_AUTH: ${REDIS_PASSWORD:-} From f264dd1cbbe70188151a4fcc5e66cdbdb573233a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:53:59 +0000 Subject: [PATCH 448/977] space redirects in shell scripts --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index a56c5a37a..b368b11ec 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,7 +20,7 @@ indent_size = 4 shell_variant = bash # like -ln=bash binary_next_line = true # like -bn switch_case_indent = true # like -ci -space_redirects = false # like -sr +space_redirects = true # like -sr keep_padding = false # like -kp function_next_line = true # like -fn never_split = true # like -ns From 193d536ca1e6f6fca0368f2da6db993484550583 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:54:20 +0000 Subject: [PATCH 449/977] update dottie --- docker/dottie | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/dottie b/docker/dottie index bd268a070..6e409def2 100755 --- a/docker/dottie +++ b/docker/dottie @@ -1,16 +1,17 @@ #!/bin/bash -declare root="${PWD}" +declare project_root="${PWD}" if command -v git &>/dev/null; then - root=$(git rev-parse --show-toplevel) + project_root=$(git rev-parse --show-toplevel) fi exec docker run \ --rm \ --interactive \ --tty \ - --volume "${root}:/var/www" \ + --volume "${project_root}:/var/www" \ + --volume "/tmp:/tmp" \ --workdir /var/www \ ghcr.io/jippi/dottie \ "$@" From af47d91e7d4ab4b6f560142070e66262cb3790ec Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:56:08 +0000 Subject: [PATCH 450/977] give nginx config default max upload size --- .../nginx/root/docker/templates/etc/nginx/conf.d/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf index 671332e78..15bf17beb 100644 --- a/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf +++ b/docker/nginx/root/docker/templates/etc/nginx/conf.d/default.conf @@ -14,7 +14,7 @@ server { index index.html index.htm index.php; charset utf-8; - client_max_body_size {{ getenv "POST_MAX_SIZE" }}; + client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; location / { try_files $uri $uri/ /index.php?$query_string; From e2821adccaae9dcf55c0fac719029cd4f18d9411 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:56:33 +0000 Subject: [PATCH 451/977] fix spacing --- .editorconfig | 4 ++-- docker/shared/root/docker/entrypoint.d/01-permissions.sh | 2 +- docker/shared/root/docker/entrypoint.d/05-templating.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index b368b11ec..eff249956 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,8 +20,8 @@ indent_size = 4 shell_variant = bash # like -ln=bash binary_next_line = true # like -bn switch_case_indent = true # like -ci -space_redirects = true # like -sr -keep_padding = false # like -kp +space_redirects = false # like -sr +keep_padding = false # like -kp function_next_line = true # like -fn never_split = true # like -ns simplify = true diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index f5624721b..11766a742 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -16,7 +16,7 @@ run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" : "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" declare -a ensure_ownership_paths=() -IFS=' ' read -ar ensure_ownership_paths <<< "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" +IFS=' ' read -ar ensure_ownership_paths <<<"${DOCKER_APP_ENSURE_OWNERSHIP_PATHS}" if [[ ${#ensure_ownership_paths[@]} == 0 ]]; then log-info "No paths has been configured for ownership fixes via [\$DOCKER_APP_ENSURE_OWNERSHIP_PATHS]." diff --git a/docker/shared/root/docker/entrypoint.d/05-templating.sh b/docker/shared/root/docker/entrypoint.d/05-templating.sh index 23d01487a..4d229b11c 100755 --- a/docker/shared/root/docker/entrypoint.d/05-templating.sh +++ b/docker/shared/root/docker/entrypoint.d/05-templating.sh @@ -51,7 +51,7 @@ find "${ENTRYPOINT_TEMPLATE_DIR}" -follow -type f -print | while read -r templat # Render the template log-info "Running [gomplate] on [${template_file}] --> [${output_file_path}]" - gomplate < "${template_file}" > "${output_file_path}" + gomplate <"${template_file}" >"${output_file_path}" # Show the diff from the envsubst command if is-true "${ENTRYPOINT_SHOW_TEMPLATE_DIFF}"; then From 027f858d853da63c70687fae5338dcf1c25783ac Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:56:54 +0000 Subject: [PATCH 452/977] ensure correct ownership of ./storage/docker --- docker/shared/root/docker/entrypoint.d/01-permissions.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 11766a742..930f69bca 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -11,6 +11,7 @@ entrypoint-set-script-name "$0" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" +run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker" # Optionally fix ownership of configured paths : "${DOCKER_APP_ENSURE_OWNERSHIP_PATHS:=""}" From 5a43d7a65d0ad30583a4e2448ad320a3bea793b7 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:57:10 +0000 Subject: [PATCH 453/977] improve error handling for [run-command-as] helper --- docker/shared/root/docker/helpers.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker/shared/root/docker/helpers.sh b/docker/shared/root/docker/helpers.sh index 190c04bc4..3a21ee89e 100644 --- a/docker/shared/root/docker/helpers.sh +++ b/docker/shared/root/docker/helpers.sh @@ -91,20 +91,29 @@ function run-command-as() log-info-stderr "${notice_message_color}👷 Running [${*}] as [${target_user}]${color_clear}" + # disable error on exit behavior temporarily while we run the command + set +e + if [[ ${target_user} != "root" ]]; then stream-prefix-command-output su --preserve-environment "${target_user}" --shell /bin/bash --command "${*}" else stream-prefix-command-output "${@}" fi + # capture exit code exit_code=$? + # re-enable exit code handling + set -e + if [[ $exit_code != 0 ]]; then log-error "${error_message_color}❌ Error!${color_clear}" + return "$exit_code" fi log-info-stderr "${success_message_color}✅ OK!${color_clear}" + return "$exit_code" } From 7ffbd5d44a44bdb73faa22fdc8224c9263b69047 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 14:58:03 +0000 Subject: [PATCH 454/977] rename docker/bash to docker/shell --- docker/{bash => shell} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker/{bash => shell} (100%) diff --git a/docker/bash b/docker/shell similarity index 100% rename from docker/bash rename to docker/shell From f0e30c8ab6cc56667bb1cba813bdefd2e1b69999 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 15:02:53 +0000 Subject: [PATCH 455/977] expand docs for proxy nginx config --- .../docker/shared/proxy/conf.d/docker-pixelfed.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf b/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf index 1b5a9a80f..8bb8f1c95 100644 --- a/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf +++ b/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf @@ -3,5 +3,13 @@ ########################################################### # This file is generated by the Pixelfed Docker setup, and # will be rewritten on every container start +# +# You can put any .conf file in this directory and it will be loaded +# by nginx on startup. +# +# Run [docker compose exec proxy bash -c 'nginx -t && nginx -s reload'] +# to test your config and reload the proxy +# +# See: https://github.com/nginx-proxy/nginx-proxy/blob/main/docs/README.md#custom-nginx-configuration client_max_body_size {{ getenv "POST_MAX_SIZE" "61M" }}; From 515198b28c460ab83e6d90678e9f2b881d90babc Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 15:12:22 +0000 Subject: [PATCH 456/977] sync ignore files --- .dockerignore | 14 ++++++++++++-- .gitignore | 32 ++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7c06534dc..757a67a51 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,23 @@ .DS_Store -/.composer/ +/.bash_history +/.bash_profile +/.bashrc +/.composer /.env +/.env.dottie-backup /.git +/.git-credentials /.gitconfig /.gitignore +/.idea +/.vagrant /bootstrap/cache /docker-compose-state/ +/Homestead.json +/Homestead.yaml /node_modules /npm-debug.log +/public/hot /public/storage /public/vendor/horizon /storage/*.key @@ -15,6 +25,6 @@ /vendor /yarn-error.log -# exceptions - these *MUST* be last +# Exceptions - these *MUST* be last !/bootstrap/cache/.gitignore !/public/vendor/horizon/.gitignore diff --git a/.gitignore b/.gitignore index b8e9b18de..ba2c7d07f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,30 @@ -.bash_history -.bash_profile -.bashrc .DS_Store -.env -.env.dottie-backup -.git-credentials -.gitconfig -/.composer/ +/.bash_history +/.bash_profile +/.bashrc +/.composer +/.env +/.env.dottie-backup +#/.git +/.git-credentials +/.gitconfig +#/.gitignore /.idea /.vagrant +/bootstrap/cache /docker-compose-state/ +/Homestead.json +/Homestead.yaml /node_modules +/npm-debug.log /public/hot /public/storage +/public/vendor/horizon /storage/*.key /storage/docker /vendor -Homestead.json -Homestead.yaml -npm-debug.log -yarn-error.log +/yarn-error.log + +# Exceptions - these *MUST* be last +!/bootstrap/cache/.gitignore +!/public/vendor/horizon/.gitignore From 2d8e81c83f24db100324c972a53a91a7a99aa547 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 15:14:32 +0000 Subject: [PATCH 457/977] ensure ownership of shared proxy conf --- docker/shared/root/docker/entrypoint.d/01-permissions.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 930f69bca..ad0dac724 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -11,6 +11,7 @@ entrypoint-set-script-name "$0" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" +run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "/docker/shared/proxy/conf.d" run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker" # Optionally fix ownership of configured paths From c8c2e1c2eb03234cc9cbcb058f1b7cf9ce7984a1 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 15:24:51 +0000 Subject: [PATCH 458/977] update docs + paths --- docker-compose.yml | 4 ++-- docker/shared/root/docker/entrypoint.d/01-permissions.sh | 1 - .../{docker => }/shared/proxy/conf.d/docker-pixelfed.conf | 5 +++-- docker/shared/root/shared/proxy/conf.d/.gitignore | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename docker/shared/root/docker/templates/{docker => }/shared/proxy/conf.d/docker-pixelfed.conf (78%) create mode 100644 docker/shared/root/shared/proxy/conf.d/.gitignore diff --git a/docker-compose.yml b/docker-compose.yml index ca4ece22b..5df433c83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -98,7 +98,7 @@ services: VIRTUAL_PORT: "80" volumes: - "./.env:/var/www/.env" - - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/docker/shared/proxy/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" @@ -143,7 +143,7 @@ services: ENTRYPOINT_SKIP_SCRIPTS: ${ENTRYPOINT_SKIP_SCRIPTS:-} volumes: - "./.env:/var/www/.env" - - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/docker/shared/proxy/conf.d" + - "${DOCKER_ALL_HOST_CONFIG_ROOT_PATH}/proxy/conf.d:/shared/proxy/conf.d" - "${DOCKER_APP_HOST_CACHE_PATH}:/var/www/bootstrap/cache" - "${DOCKER_APP_HOST_OVERRIDES_PATH}:/docker/overrides:ro" - "${DOCKER_APP_HOST_STORAGE_PATH}:/var/www/storage" diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index ad0dac724..930f69bca 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -11,7 +11,6 @@ entrypoint-set-script-name "$0" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./storage" -run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "/docker/shared/proxy/conf.d" run-as-current-user chown --verbose --recursive "${RUNTIME_UID}:${RUNTIME_GID}" "./storage/docker" # Optionally fix ownership of configured paths diff --git a/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf similarity index 78% rename from docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf rename to docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf index 8bb8f1c95..0b221e604 100644 --- a/docker/shared/root/docker/templates/docker/shared/proxy/conf.d/docker-pixelfed.conf +++ b/docker/shared/root/docker/templates/shared/proxy/conf.d/docker-pixelfed.conf @@ -4,8 +4,9 @@ # This file is generated by the Pixelfed Docker setup, and # will be rewritten on every container start # -# You can put any .conf file in this directory and it will be loaded -# by nginx on startup. +# You can put any [.conf] file in this directory +# (docker-compose-state/config/proxy/conf.d) and it will +# be loaded by nginx on startup. # # Run [docker compose exec proxy bash -c 'nginx -t && nginx -s reload'] # to test your config and reload the proxy diff --git a/docker/shared/root/shared/proxy/conf.d/.gitignore b/docker/shared/root/shared/proxy/conf.d/.gitignore new file mode 100644 index 000000000..e69de29bb From df1f62e73453654af0d45d9e2ace7950a7651ad7 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 22 Feb 2024 15:30:34 +0000 Subject: [PATCH 459/977] update docs --- docker/shared/root/docker/entrypoint.d/01-permissions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/shared/root/docker/entrypoint.d/01-permissions.sh b/docker/shared/root/docker/entrypoint.d/01-permissions.sh index 930f69bca..efff58110 100755 --- a/docker/shared/root/docker/entrypoint.d/01-permissions.sh +++ b/docker/shared/root/docker/entrypoint.d/01-permissions.sh @@ -6,7 +6,7 @@ source "${ENTRYPOINT_ROOT}/helpers.sh" entrypoint-set-script-name "$0" -# Ensure the two Docker volumes and dot-env files are owned by the runtime user as other scripts +# Ensure the Docker volumes and required files are owned by the runtime user as other scripts # will be writing to these run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./.env" run-as-current-user chown --verbose "${RUNTIME_UID}:${RUNTIME_GID}" "./bootstrap/cache" From 089ba3c4712db049c8d31b606dc4176b16111606 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 23 Feb 2024 19:37:02 -0700 Subject: [PATCH 460/977] Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id --- app/Observers/StatusObserver.php | 8 ++++++++ app/Util/ActivityPub/Inbox.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Observers/StatusObserver.php b/app/Observers/StatusObserver.php index d78585175..6c2c4c36d 100644 --- a/app/Observers/StatusObserver.php +++ b/app/Observers/StatusObserver.php @@ -38,6 +38,10 @@ class StatusObserver */ public function updated(Status $status) { + if(!in_array($status->scope, ['public', 'unlisted', 'private'])) { + return; + } + if(config('instance.timeline.home.cached')) { Cache::forget('pf:timelines:home:' . $status->profile_id); } @@ -55,6 +59,10 @@ class StatusObserver */ public function deleted(Status $status) { + if(!in_array($status->scope, ['public', 'unlisted', 'private'])) { + return; + } + if(config('instance.timeline.home.cached')) { Cache::forget('pf:timelines:home:' . $status->profile_id); } diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 62ab3be75..0ef7d6a7c 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -423,7 +423,7 @@ class Inbox $status->uri = $activity['id']; $status->object_url = $activity['id']; $status->in_reply_to_profile_id = $profile->id; - $status->saveQuietly(); + $status->save(); $dm = new DirectMessage; $dm->to_id = $profile->id; From 84aeec3b4e17223b55c7e7c471451d255dbe97a3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Fri, 23 Feb 2024 19:37:55 -0700 Subject: [PATCH 461/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b38fb1b58..367afbc21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d)) - Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641)) - Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239)) +- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 2b5d72358226cdba8d6f1bb2e9cdcd86eb44626d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 24 Feb 2024 03:45:09 -0700 Subject: [PATCH 462/977] Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state --- .../AdminCuratedRegisterController.php | 25 ++++++++++--- .../Controllers/CuratedRegisterController.php | 1 + app/Models/CuratedRegister.php | 28 ++++++++++++--- ...s_responded_to_curated_registers_table.php | 36 +++++++++++++++++++ .../admin/curated-register/index.blade.php | 4 +-- .../curated-register/partials/nav.blade.php | 11 +++--- 6 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php index b28fa438f..1bed66b7c 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -22,27 +22,43 @@ class AdminCuratedRegisterController extends Controller public function index(Request $request) { $this->validate($request, [ - 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected' + 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses', + 'sort' => 'sometimes|in:asc,desc' ]); $filter = $request->input('filter', 'open'); + $sort = $request->input('sort', 'asc'); $records = CuratedRegister::when($filter, function($q, $filter) { if($filter === 'open') { return $q->where('is_rejected', false) + ->where(function($query) { + return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false); + }) ->whereNotNull('email_verified_at') ->whereIsClosed(false); } else if($filter === 'all') { return $q; + } else if($filter === 'responses') { + return $q->whereIsClosed(false) + ->whereNotNull('email_verified_at') + ->where('user_has_responded', true) + ->where('is_awaiting_more_info', true); } elseif ($filter === 'awaiting') { return $q->whereIsClosed(false) - ->whereNull('is_rejected') - ->whereNull('is_approved'); + ->where('is_rejected', false) + ->where('is_approved', false) + ->where('user_has_responded', false) + ->where('is_awaiting_more_info', true); } elseif ($filter === 'approved') { return $q->whereIsClosed(true)->whereIsApproved(true); } elseif ($filter === 'rejected') { return $q->whereIsClosed(true)->whereIsRejected(true); } }) - ->paginate(10); + ->when($sort, function($query, $sort) { + return $query->orderBy('id', $sort); + }) + ->paginate(10) + ->withQueryString(); return view('admin.curated-register.index', compact('records', 'filter')); } @@ -160,6 +176,7 @@ class AdminCuratedRegisterController extends Controller $activity->message = $request->input('message'); $activity->save(); $record->is_awaiting_more_info = true; + $record->user_has_responded = false; $record->save(); Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); return $request->all(); diff --git a/app/Http/Controllers/CuratedRegisterController.php b/app/Http/Controllers/CuratedRegisterController.php index 73bd17bff..58bddb498 100644 --- a/app/Http/Controllers/CuratedRegisterController.php +++ b/app/Http/Controllers/CuratedRegisterController.php @@ -105,6 +105,7 @@ class CuratedRegisterController extends Controller 'action_required' => true, ]); + CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]); $request->session()->pull('cur-reg-con'); $request->session()->pull('cur-reg-con-attempt'); diff --git a/app/Models/CuratedRegister.php b/app/Models/CuratedRegister.php index edb4e1b22..eeeb82784 100644 --- a/app/Models/CuratedRegister.php +++ b/app/Models/CuratedRegister.php @@ -9,25 +9,43 @@ class CuratedRegister extends Model { use HasFactory; + protected $fillable = [ + 'user_has_responded' + ]; + protected $casts = [ 'autofollow_account_ids' => 'array', 'admin_notes' => 'array', 'email_verified_at' => 'datetime', 'admin_notified_at' => 'datetime', 'action_taken_at' => 'datetime', + 'user_has_responded' => 'boolean', + 'is_awaiting_more_info' => 'boolean', + 'is_accepted' => 'boolean', + 'is_rejected' => 'boolean', + 'is_closed' => 'boolean', ]; public function adminStatusLabel() { + if($this->user_has_responded) { + return 'Awaiting Admin Response'; + } if(!$this->email_verified_at) { return 'Unverified email'; } - if($this->is_accepted) { return 'Approved'; } - if($this->is_rejected) { return 'Rejected'; } - if($this->is_awaiting_more_info ) { - return 'Awaiting Details'; + if($this->is_approved) { + return 'Approved'; + } + if($this->is_rejected) { + return 'Rejected'; + } + if($this->is_awaiting_more_info ) { + return 'Awaiting User Response'; + } + if($this->is_closed ) { + return 'Closed'; } - if($this->is_closed ) { return 'Closed'; } return 'Open'; } diff --git a/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php b/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php new file mode 100644 index 000000000..9453a73d7 --- /dev/null +++ b/database/migrations/2024_02_24_093824_add_has_responded_to_curated_registers_table.php @@ -0,0 +1,36 @@ +boolean('user_has_responded')->default(false)->index()->after('is_awaiting_more_info'); + }); + + CuratedRegisterActivity::whereFromUser(true)->get()->each(function($cra) { + $cr = CuratedRegister::find($cra->register_id); + $cr->user_has_responded = true; + $cr->save(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('curated_registers', function (Blueprint $table) { + $table->dropColumn('user_has_responded'); + }); + } +}; diff --git a/resources/views/admin/curated-register/index.blade.php b/resources/views/admin/curated-register/index.blade.php index 1094e9113..5ff8c4dd5 100644 --- a/resources/views/admin/curated-register/index.blade.php +++ b/resources/views/admin/curated-register/index.blade.php @@ -26,7 +26,7 @@ ID Username - @if(in_array($filter, ['all', 'open'])) + @if(in_array($filter, ['all', 'open', 'awaiting', 'responses'])) Status @endif Reason for Joining @@ -47,7 +47,7 @@ @{{ $record->username }}

- @if(in_array($filter, ['all', 'open'])) + @if(in_array($filter, ['all', 'open', 'awaiting', 'responses'])) {!! $record->adminStatusLabel() !!} diff --git a/resources/views/admin/curated-register/partials/nav.blade.php b/resources/views/admin/curated-register/partials/nav.blade.php index 16241997e..b23cc8dcc 100644 --- a/resources/views/admin/curated-register/partials/nav.blade.php +++ b/resources/views/admin/curated-register/partials/nav.blade.php @@ -18,16 +18,19 @@ Open Applications +
From 795e91e3bca1f619e64f53d5812ffa4ae79b2da2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 24 Feb 2024 03:50:26 -0700 Subject: [PATCH 463/977] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 367afbc21..0eb452440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641)) - Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239)) - Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4)) +- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 1976af6dd1a30ecf5b1c599ecf798d18e08440e4 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 24 Feb 2024 22:50:48 +0000 Subject: [PATCH 464/977] ensure color in dottie output by passing through env --- docker/artisan | 7 ++++++- docker/dottie | 45 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docker/artisan b/docker/artisan index d50d65244..3bbf58aea 100755 --- a/docker/artisan +++ b/docker/artisan @@ -3,4 +3,9 @@ declare service="${PF_SERVICE:=worker}" declare user="${PF_USER:=www-data}" -exec docker compose exec --user "${user}" "${service}" php artisan "${@}" +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + php artisan "${@}" diff --git a/docker/dottie b/docker/dottie index 6e409def2..a6ad7bc78 100755 --- a/docker/dottie +++ b/docker/dottie @@ -1,17 +1,44 @@ #!/bin/bash +set -e -o errexit -o nounset -o pipefail + declare project_root="${PWD}" if command -v git &>/dev/null; then project_root=$(git rev-parse --show-toplevel) fi -exec docker run \ - --rm \ - --interactive \ - --tty \ - --volume "${project_root}:/var/www" \ - --volume "/tmp:/tmp" \ - --workdir /var/www \ - ghcr.io/jippi/dottie \ - "$@" +declare -r release="${DOTTIE_VERSION:-latest}" + +declare -r update_check_file="/tmp/.dottie-update-check" # file to check age of since last update +declare -i update_check_max_age=$((8 * 60 * 60)) # 8 hours between checking for dottie version +declare -i update_check_cur_age=$((update_check_max_age + 1)) # by default the "update" event should happen + +# default [docker run] flags +declare -a flags=( + --rm + --interactive + --tty + --env TERM + --env COLORTERM + --volume "/tmp:/tmp" + --volume "${project_root}:/var/www" + --workdir /var/www +) + +# if update file exists, find its age since last modification +if [[ -f "${update_check_file}" ]]; then + now=$(date +%s) + changed=$(date -r "${update_check_file}" +%s) + update_check_cur_age=$((now - changed)) +fi + +# if update file is older than max allowed poll for new version of dottie +if [[ $update_check_cur_age -gt $update_check_max_age ]]; then + flags+=(--pull always) + + touch "${update_check_file}" +fi + +# run dottie +exec docker run "${flags[@]}" "ghcr.io/jippi/dottie:${release}" "$@" From b08bb3669d1904f7d56578adadaaf32c0b491d7a Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 24 Feb 2024 23:00:38 +0000 Subject: [PATCH 465/977] bump dottie version --- Dockerfile | 2 +- docker/shell | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c6b297105..42238329c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ ARG FOREGO_VERSION="0.17.2" ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie -ARG DOTTIE_VERSION="v0.9.3" +ARG DOTTIE_VERSION="v0.9.5" ### # PHP base configuration diff --git a/docker/shell b/docker/shell index fc889b964..7b725e1b0 100755 --- a/docker/shell +++ b/docker/shell @@ -9,4 +9,9 @@ if [[ $# -ge 1 ]]; then command=("$@") fi -exec docker compose exec --user "${user}" "${service}" "${command[@]}" +exec docker compose exec \ + --user "${user}" \ + --env TERM \ + --env COLORTERM \ + "${service}" \ + "${command[@]}" From c1c361ef9bd866c11e09d28b2f26b6fd43d66dad Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sat, 24 Feb 2024 23:29:45 +0000 Subject: [PATCH 466/977] tune for new dottie image --- docker/dottie | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/dottie b/docker/dottie index a6ad7bc78..8bd304a03 100755 --- a/docker/dottie +++ b/docker/dottie @@ -3,6 +3,7 @@ set -e -o errexit -o nounset -o pipefail declare project_root="${PWD}" +declare user="${PF_USER:=www-data}" if command -v git &>/dev/null; then project_root=$(git rev-parse --show-toplevel) @@ -19,9 +20,9 @@ declare -a flags=( --rm --interactive --tty + --user "${user}" --env TERM --env COLORTERM - --volume "/tmp:/tmp" --volume "${project_root}:/var/www" --workdir /var/www ) From 02369cce66c3d59b0caff833d23d1698a5628099 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 25 Feb 2024 10:53:29 +0000 Subject: [PATCH 467/977] fix validation issues in the .env.docker file --- .env.docker | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.env.docker b/.env.docker index 9a6af8bbd..0d5836745 100644 --- a/.env.docker +++ b/.env.docker @@ -202,7 +202,7 @@ APP_TIMEZONE="UTC" # # @default "false" # @see https://docs.pixelfed.org/technical-documentation/config/#account_delete_after -# @dottie/validate required,boolean +# @dottie/validate required,boolean|number #ACCOUNT_DELETE_AFTER="false" # @default "Pixelfed - Photo sharing for everyone" @@ -722,14 +722,13 @@ LOG_CHANNEL="stderr" # # @default "debug" # @see https://docs.pixelfed.org/technical-documentation/config/#log_level -# @dottie/validate required,boolean +# @dottie/validate required,oneof=debug info notice warning error critical alert emergency #LOG_LEVEL="debug" # Used by stderr. # # @default "" # @see https://docs.pixelfed.org/technical-documentation/config/#log_stderr_formatter -# @dottie/validate required #LOG_STDERR_FORMATTER="" # Used by slack. @@ -984,7 +983,7 @@ DOCKER_APP_RUNTIME="apache" # The Debian release variant to use of the [php] Docker image # # Examlpe: [bookworm] or [bullseye] -# @dottie/validate required,oneof=bookwork bullseye +# @dottie/validate required,oneof=bookworm bullseye DOCKER_APP_DEBIAN_RELEASE="bullseye" # The [php] Docker image base type From acb699bf133e218671769698abd86eae37f0fb9f Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Sun, 25 Feb 2024 11:00:26 +0000 Subject: [PATCH 468/977] use the correct buildkit env for downloading binaries --- Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 42238329c..a6ad884b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,8 +98,8 @@ FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image # It's in its own layer so it can be fetched in parallel with other build steps FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image -ARG BUILDARCH -ARG BUILDOS +ARG TARGETARCH +ARG TARGETOS ARG GOMPLATE_VERSION RUN set -ex \ @@ -108,7 +108,7 @@ RUN set -ex \ --show-error \ --location \ --output /usr/local/bin/gomplate \ - https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${BUILDOS}-${BUILDARCH} \ + https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \ && chmod +x /usr/local/bin/gomplate \ && /usr/local/bin/gomplate --version @@ -220,9 +220,6 @@ COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/ FROM php-extensions AS shared-runtime -ARG BUILDARCH -ARG BUILDOS -ARG GOMPLATE_VERSION ARG RUNTIME_GID ARG RUNTIME_UID From 071163b47b4b86994c0a74b9fafa8009a350eb84 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 26 Feb 2024 20:41:27 -0700 Subject: [PATCH 469/977] Add Curated Onboarding Templates --- .../AdminCuratedRegisterController.php | 183 +++++++++++++----- app/Models/CuratedRegisterTemplate.php | 19 ++ ...reate_curated_register_templates_table.php | 32 +++ .../partials/activity-log.blade.php | 58 +++++- .../curated-register/partials/nav.blade.php | 5 +- .../template-create.blade.php | 98 ++++++++++ .../curated-register/template-edit.blade.php | 148 ++++++++++++++ .../curated-register/templates.blade.php | 91 +++++++++ routes/web-admin.php | 7 + 9 files changed, 590 insertions(+), 51 deletions(-) create mode 100644 app/Models/CuratedRegisterTemplate.php create mode 100644 database/migrations/2024_02_24_105641_create_curated_register_templates_table.php create mode 100644 resources/views/admin/curated-register/template-create.blade.php create mode 100644 resources/views/admin/curated-register/template-edit.blade.php create mode 100644 resources/views/admin/curated-register/templates.blade.php diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php index 1bed66b7c..91400afb4 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -2,69 +2,72 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Models\CuratedRegister; -use App\Models\CuratedRegisterActivity; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Mail; -use App\Mail\CuratedRegisterRequestDetailsFromUser; use App\Mail\CuratedRegisterAcceptUser; use App\Mail\CuratedRegisterRejectUser; +use App\Mail\CuratedRegisterRequestDetailsFromUser; +use App\Models\CuratedRegister; +use App\Models\CuratedRegisterActivity; +use App\Models\CuratedRegisterTemplate; use App\User; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Str; class AdminCuratedRegisterController extends Controller { public function __construct() { - $this->middleware(['auth','admin']); + $this->middleware(['auth', 'admin']); } public function index(Request $request) { $this->validate($request, [ 'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses', - 'sort' => 'sometimes|in:asc,desc' + 'sort' => 'sometimes|in:asc,desc', ]); $filter = $request->input('filter', 'open'); $sort = $request->input('sort', 'asc'); - $records = CuratedRegister::when($filter, function($q, $filter) { - if($filter === 'open') { - return $q->where('is_rejected', false) - ->where(function($query) { + $records = CuratedRegister::when($filter, function ($q, $filter) { + if ($filter === 'open') { + return $q->where('is_rejected', false) + ->where(function ($query) { return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false); }) ->whereNotNull('email_verified_at') ->whereIsClosed(false); - } else if($filter === 'all') { - return $q; - } else if($filter === 'responses') { - return $q->whereIsClosed(false) - ->whereNotNull('email_verified_at') - ->where('user_has_responded', true) - ->where('is_awaiting_more_info', true); - } elseif ($filter === 'awaiting') { - return $q->whereIsClosed(false) - ->where('is_rejected', false) - ->where('is_approved', false) - ->where('user_has_responded', false) - ->where('is_awaiting_more_info', true); - } elseif ($filter === 'approved') { - return $q->whereIsClosed(true)->whereIsApproved(true); - } elseif ($filter === 'rejected') { - return $q->whereIsClosed(true)->whereIsRejected(true); - } - }) - ->when($sort, function($query, $sort) { + } elseif ($filter === 'all') { + return $q; + } elseif ($filter === 'responses') { + return $q->whereIsClosed(false) + ->whereNotNull('email_verified_at') + ->where('user_has_responded', true) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'awaiting') { + return $q->whereIsClosed(false) + ->where('is_rejected', false) + ->where('is_approved', false) + ->where('user_has_responded', false) + ->where('is_awaiting_more_info', true); + } elseif ($filter === 'approved') { + return $q->whereIsClosed(true)->whereIsApproved(true); + } elseif ($filter === 'rejected') { + return $q->whereIsClosed(true)->whereIsRejected(true); + } + }) + ->when($sort, function ($query, $sort) { return $query->orderBy('id', $sort); }) ->paginate(10) ->withQueryString(); + return view('admin.curated-register.index', compact('records', 'filter')); } public function show(Request $request, $id) { $record = CuratedRegister::findOrFail($id); + return view('admin.curated-register.show', compact('record')); } @@ -80,10 +83,10 @@ class AdminCuratedRegisterController extends Controller 'message' => null, 'link' => null, 'timestamp' => $record->created_at, - ] + ], ]); - if($record->email_verified_at) { + if ($record->email_verified_at) { $res->push([ 'id' => 3, 'action' => 'email_verified_at', @@ -99,10 +102,11 @@ class AdminCuratedRegisterController extends Controller $idx = 4; $userResponses = collect([]); - foreach($activities as $activity) { + foreach ($activities as $activity) { $idx++; - if($activity->from_user) { + if ($activity->from_user) { $userResponses->push($activity); + continue; } $res->push([ @@ -116,20 +120,22 @@ class AdminCuratedRegisterController extends Controller ]); } - foreach($userResponses as $ur) { - $res = $res->map(function($r) use($ur) { - if(!isset($r['aid'])) { + foreach ($userResponses as $ur) { + $res = $res->map(function ($r) use ($ur) { + if (! isset($r['aid'])) { return $r; } - if($ur->reply_to_id === $r['aid']) { + if ($ur->reply_to_id === $r['aid']) { $r['user_response'] = $ur; + return $r; } + return $r; }); } - if($record->is_approved) { + if ($record->is_approved) { $idx++; $res->push([ 'id' => $idx, @@ -139,7 +145,7 @@ class AdminCuratedRegisterController extends Controller 'link' => null, 'timestamp' => $record->action_taken_at, ]); - } else if ($record->is_rejected) { + } elseif ($record->is_rejected) { $idx++; $res->push([ 'id' => $idx, @@ -157,13 +163,14 @@ class AdminCuratedRegisterController extends Controller public function apiMessagePreviewStore(Request $request, $id) { $record = CuratedRegister::findOrFail($id); + return $request->all(); } public function apiMessageSendStore(Request $request, $id) { $this->validate($request, [ - 'message' => 'required|string|min:5|max:1000' + 'message' => 'required|string|min:5|max:1000', ]); $record = CuratedRegister::findOrFail($id); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); @@ -179,6 +186,7 @@ class AdminCuratedRegisterController extends Controller $record->user_has_responded = false; $record->save(); Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity)); + return $request->all(); } @@ -188,22 +196,23 @@ class AdminCuratedRegisterController extends Controller abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); $activity = new CuratedRegisterActivity; $activity->message = $request->input('message'); + return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity); } - public function previewMessageShow(Request $request, $id) { $record = CuratedRegister::findOrFail($id); abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email'); $record->message = $request->input('message'); + return new \App\Mail\CuratedRegisterSendMessage($record); } public function apiHandleReject(Request $request, $id) { $this->validate($request, [ - 'action' => 'required|in:reject-email,reject-silent' + 'action' => 'required|in:reject-email,reject-silent', ]); $action = $request->input('action'); $record = CuratedRegister::findOrFail($id); @@ -212,9 +221,10 @@ class AdminCuratedRegisterController extends Controller $record->is_closed = true; $record->action_taken_at = now(); $record->save(); - if($action === 'reject-email') { + if ($action === 'reject-email') { Mail::to($record->email)->send(new CuratedRegisterRejectUser($record)); } + return [200]; } @@ -233,10 +243,89 @@ class AdminCuratedRegisterController extends Controller 'password' => $record->password, 'app_register_ip' => $record->ip_address, 'email_verified_at' => now(), - 'register_source' => 'cur_onboarding' + 'register_source' => 'cur_onboarding', ]); Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record)); + return [200]; } + + public function templates(Request $request) + { + $templates = CuratedRegisterTemplate::paginate(10); + + return view('admin.curated-register.templates', compact('templates')); + } + + public function templateCreate(Request $request) + { + return view('admin.curated-register.template-create'); + } + + public function templateEdit(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + + return view('admin.curated-register.template-edit', compact('template')); + } + + public function templateEditStore(Request $request, $id) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + $template = CuratedRegisterTemplate::findOrFail($id); + $template->name = $request->input('name'); + $template->content = $request->input('content'); + $template->description = $request->input('description'); + $template->is_active = $request->boolean('active'); + $template->save(); + + return redirect()->back()->with('status', 'Successfully updated template!'); + } + + public function templateDelete(Request $request, $id) + { + $template = CuratedRegisterTemplate::findOrFail($id); + $template->delete(); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!'); + } + + public function templateStore(Request $request) + { + $this->validate($request, [ + 'name' => 'required|string|max:30', + 'content' => 'required|string|min:5|max:3000', + 'description' => 'nullable|sometimes|string|max:1000', + 'active' => 'sometimes', + ]); + CuratedRegisterTemplate::create([ + 'name' => $request->input('name'), + 'content' => $request->input('content'), + 'description' => $request->input('description'), + 'is_active' => $request->boolean('active'), + ]); + + return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!'); + } + + public function getActiveTemplates(Request $request) + { + $templates = CuratedRegisterTemplate::whereIsActive(true) + ->orderBy('order') + ->get() + ->map(function ($tmp) { + return [ + 'name' => $tmp->name, + 'content' => $tmp->content, + ]; + }); + + return response()->json($templates); + } } diff --git a/app/Models/CuratedRegisterTemplate.php b/app/Models/CuratedRegisterTemplate.php new file mode 100644 index 000000000..a5def0cef --- /dev/null +++ b/app/Models/CuratedRegisterTemplate.php @@ -0,0 +1,19 @@ + 'boolean', + ]; +} diff --git a/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php new file mode 100644 index 000000000..4829fefb2 --- /dev/null +++ b/database/migrations/2024_02_24_105641_create_curated_register_templates_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->nullable(); + $table->text('description')->nullable(); + $table->text('content')->nullable(); + $table->boolean('is_active')->default(false)->index(); + $table->tinyInteger('order')->default(10)->unsigned()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('curated_register_templates'); + } +}; diff --git a/resources/views/admin/curated-register/partials/activity-log.blade.php b/resources/views/admin/curated-register/partials/activity-log.blade.php index 1d7701f44..d141a4424 100644 --- a/resources/views/admin/curated-register/partials/activity-log.blade.php +++ b/resources/views/admin/curated-register/partials/activity-log.blade.php @@ -46,6 +46,21 @@

Request Additional Details

Use this form to request additional details. Once you press Send, we'll send the potential user an email with a special link they can visit with a form that they can provide additional details with. You can also Preview the email before it's sent.

+
+

Template Responses

+ +
+ +
+
+
@@ -60,7 +75,7 @@

@{{ composeMessage && composeMessage.length ? composeMessage.length : 0 }} / - 500 + 2000

@@ -76,6 +91,12 @@ target="_blank"> Preview + + Clear +
@@ -90,6 +111,21 @@

Send Message

Use this form to send a message to the applicant. Once you press Send, we'll send the potential user an email with your message. You can also Preview the email before it's sent.

+
+

Template Responses

+ +
+ +
+
+
@@ -187,11 +223,13 @@ messageFormOpen: false, composeMessage: null, messageBody: null, + responseTemplates: [], } }, mounted() { setTimeout(() => { + this.fetchResponseTemplates(); this.fetchActivities(); }, 1000) }, @@ -233,10 +271,16 @@ return str; }, + fetchResponseTemplates() { + axios.get('/i/admin/api/curated-onboarding/templates/get') + .then(res => { + this.responseTemplates = res.data; + }) + }, + fetchActivities() { axios.get('/i/admin/api/curated-onboarding/show/{{$id}}/activity-log') .then(res => { - console.log(res.data); this.activities = res.data; }) .finally(() => { @@ -379,7 +423,15 @@ openUserResponse(activity) { swal('User Response', activity.user_response.message) - } + }, + + useTemplate(tmpl) { + this.composeMessage = tmpl.content; + }, + + useTemplateMessage(tmpl) { + this.messageBody = tmpl.content; + }, } }); diff --git a/resources/views/admin/curated-register/partials/nav.blade.php b/resources/views/admin/curated-register/partials/nav.blade.php index b23cc8dcc..8634813ad 100644 --- a/resources/views/admin/curated-register/partials/nav.blade.php +++ b/resources/views/admin/curated-register/partials/nav.blade.php @@ -15,7 +15,7 @@
diff --git a/resources/views/admin/curated-register/template-create.blade.php b/resources/views/admin/curated-register/template-create.blade.php new file mode 100644 index 000000000..5c37abfca --- /dev/null +++ b/resources/views/admin/curated-register/template-create.blade.php @@ -0,0 +1,98 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+
+
+

Create Template

+

Create re-usable templates of messages and application requests.

+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +
+ @csrf + +
+ + +
+ +
+ + +
+ +

+ +

+ +
+
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+@endif + +@endsection diff --git a/resources/views/admin/curated-register/template-edit.blade.php b/resources/views/admin/curated-register/template-edit.blade.php new file mode 100644 index 000000000..236b979d3 --- /dev/null +++ b/resources/views/admin/curated-register/template-edit.blade.php @@ -0,0 +1,148 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+ @if (session('status')) +
+ {{ session('status') }} +
+ + @endif +
+
+

Edit Template

+
+
+
+
+
+
+
+
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +
+ @csrf + +
+ + +
+ +
+ + +
+ + @if($template->description == null) +

+ +

+ @endif + +
+
+ + +
+
+ +
+ is_active ? 'checked' : ''}}> + +
+ +
+
+ + +
+
+
+ @method('DELETE') + @csrf +
+
+
+
+
+
+
+@endif + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/curated-register/templates.blade.php b/resources/views/admin/curated-register/templates.blade.php new file mode 100644 index 000000000..17b10da80 --- /dev/null +++ b/resources/views/admin/curated-register/templates.blade.php @@ -0,0 +1,91 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+
+

Curated Onboarding

+

The ideal solution for communities seeking a balance between open registration and invite-only membership

+
+
+
+
+
+ +@if((bool) config_cache('instance.curated_registration.enabled')) +
+
+ @include('admin.curated-register.partials.nav') + +
+
+ @if (session('status')) +
+ {{ session('status') }} +
+ + @endif +
+
+

Create and manage re-usable templates of messages and application requests.

+ Create new Template +
+
+
+ +
+ +
+ + + + + + + + + + + + @foreach($templates as $template) + + + + + + + + @endforeach + +
IDShortcut/NameContentActiveCreated
+ + {{ $template->id }} + + + {{ $template->name }} + + {{ str_limit($template->content, 80) }} + + {{ $template->is_active ? '✅' : '❌' }} + + {{ $template->created_at->format('M d Y') }} +
+ +
+ {{ $templates->links() }} +
+
+
+
+
+
+@endif + +@endsection diff --git a/routes/web-admin.php b/routes/web-admin.php index 9f01d5c7c..64a3ecffd 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -106,6 +106,12 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('asf/home', 'AdminShadowFilterController@home'); Route::redirect('curated-onboarding/', 'curated-onboarding/home'); Route::get('curated-onboarding/home', 'AdminCuratedRegisterController@index')->name('admin.curated-onboarding'); + Route::get('curated-onboarding/templates', 'AdminCuratedRegisterController@templates')->name('admin.curated-onboarding.templates'); + Route::get('curated-onboarding/templates/create', 'AdminCuratedRegisterController@templateCreate')->name('admin.curated-onboarding.create-template'); + Route::post('curated-onboarding/templates/create', 'AdminCuratedRegisterController@templateStore'); + Route::get('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateEdit'); + Route::post('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateEditStore'); + Route::delete('curated-onboarding/templates/edit/{id}', 'AdminCuratedRegisterController@templateDelete'); Route::get('curated-onboarding/show/{id}/preview-details-message', 'AdminCuratedRegisterController@previewDetailsMessageShow'); Route::get('curated-onboarding/show/{id}/preview-message', 'AdminCuratedRegisterController@previewMessageShow'); Route::get('curated-onboarding/show/{id}', 'AdminCuratedRegisterController@show'); @@ -162,5 +168,6 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('curated-onboarding/show/{id}/message/send', 'AdminCuratedRegisterController@apiMessageSendStore'); Route::post('curated-onboarding/show/{id}/reject', 'AdminCuratedRegisterController@apiHandleReject'); Route::post('curated-onboarding/show/{id}/approve', 'AdminCuratedRegisterController@apiHandleApprove'); + Route::get('curated-onboarding/templates/get', 'AdminCuratedRegisterController@getActiveTemplates'); }); }); From 767522a85c1d9c29dd05d2b3b6c502379dc073d8 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 26 Feb 2024 21:33:10 -0700 Subject: [PATCH 470/977] Update AdminReports, add story reports and fix cs --- .../assets/components/admin/AdminReports.vue | 1403 +++++++++-------- 1 file changed, 732 insertions(+), 671 deletions(-) diff --git a/resources/assets/components/admin/AdminReports.vue b/resources/assets/components/admin/AdminReports.vue index fdd11b012..7bfcd10a9 100644 --- a/resources/assets/components/admin/AdminReports.vue +++ b/resources/assets/components/admin/AdminReports.vue @@ -9,15 +9,15 @@
-
+
Active Reports
- {{ prettyCount(stats.open) }} + class="text-white h2 font-weight-bold mb-0 human-size" + data-toggle="tooltip" + data-placement="bottom" + :title="stats.open + ' open reports'"> + {{ prettyCount(stats.open) }}
@@ -26,11 +26,11 @@
Active Spam Detections
{{ prettyCount(stats.autospam_open) }} + class="text-white h2 font-weight-bold mb-0 human-size" + data-toggle="tooltip" + data-placement="bottom" + :title="stats.autospam_open + ' open spam detections'" + >{{ prettyCount(stats.autospam_open) }}
@@ -38,11 +38,11 @@
Total Reports
{{ prettyCount(stats.total) }} + class="text-white h2 font-weight-bold mb-0 human-size" + data-toggle="tooltip" + data-placement="bottom" + :title="stats.total + ' total reports'" + >{{ prettyCount(stats.total) }}
@@ -51,11 +51,11 @@
Total Spam Detections
- {{ prettyCount(stats.autospam) }} + class="text-white h2 font-weight-bold mb-0 human-size" + data-toggle="tooltip" + data-placement="bottom" + :title="stats.autospam + ' total spam detections'"> + {{ prettyCount(stats.autospam) }}
@@ -75,73 +75,73 @@
@@ -151,10 +151,10 @@ - - - - + + + + @@ -167,49 +167,49 @@ - + @@ -218,17 +218,17 @@
IDReportReported AccountReported ByIDReportReported AccountReported By Created View Report
-

-
- -
- - -
-

@{{report.reported.username}}

-
- {{report.reported.followers_count}} Followers - · - Joined {{ timeAgo(report.reported.created_at) }} -
-
-
-
+

- -
- + +
+ -
-

@{{report.reporter.username}}

-
- {{report.reporter.followers_count}} Followers - · - Joined {{ timeAgo(report.reporter.created_at) }} -
-
-
-
+
+

@{{report.reported.username}}

+
+ {{report.reported.followers_count}} Followers + · + Joined {{ timeAgo(report.reported.created_at) }} +
+
+
+ +
+ +
+ + +
+

@{{report.reporter.username}}

+
+ {{report.reporter.followers_count}} Followers + · + Joined {{ timeAgo(report.reporter.created_at) }} +
+
+
+
{{ timeAgo(report.created_at) }} View
-
-
-

-

{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}

-
-
+
+
+

+

{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}

+
+
-
- -
- -
+
+ +
-
-
- -
+
+ +
-
-
- -
+
+ +
-
From a16309ac188eee92faecf21afca41d40cda93a3f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 26 Feb 2024 21:39:09 -0700 Subject: [PATCH 471/977] Update AdminReportController, add story report support --- .../Admin/AdminReportController.php | 1921 +++++++++-------- 1 file changed, 1014 insertions(+), 907 deletions(-) diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index ac238f28c..2ad2bab8a 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -2,461 +2,468 @@ namespace App\Http\Controllers\Admin; +use App\AccountInterstitial; +use App\Http\Resources\AdminReport; +use App\Http\Resources\AdminSpamReport; +use App\Jobs\DeletePipeline\DeleteAccountPipeline; +use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; +use App\Jobs\StatusPipeline\RemoteStatusDelete; +use App\Jobs\StatusPipeline\StatusDelete; +use App\Jobs\StoryPipeline\StoryDelete; +use App\Notification; +use App\Profile; +use App\Report; +use App\Services\AccountService; +use App\Services\ModLogService; +use App\Services\NetworkTimelineService; +use App\Services\NotificationService; +use App\Services\PublicTimelineService; +use App\Services\StatusService; +use App\Status; +use App\Story; +use App\User; use Cache; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; -use App\Services\AccountService; -use App\Services\StatusService; -use App\{ - AccountInterstitial, - Contact, - Hashtag, - Newsroom, - Notification, - OauthClient, - Profile, - Report, - Status, - Story, - User -}; -use Illuminate\Validation\Rule; -use App\Services\StoryService; -use App\Services\ModLogService; -use App\Jobs\DeletePipeline\DeleteAccountPipeline; -use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; -use App\Jobs\StatusPipeline\StatusDelete; -use App\Jobs\StatusPipeline\RemoteStatusDelete; -use App\Http\Resources\AdminReport; -use App\Http\Resources\AdminSpamReport; -use App\Services\NotificationService; -use App\Services\PublicTimelineService; -use App\Services\NetworkTimelineService; trait AdminReportController { - public function reports(Request $request) - { - $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; - $page = $request->input('page') ?? 1; + public function reports(Request $request) + { + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + $page = $request->input('page') ?? 1; - $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() { - return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(); - }); + $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function () { + return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(); + }); - $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() { - return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count(); - }); + $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function () { + return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count(); + }); - $mailVerifications = Redis::scard('email:manual'); + $mailVerifications = Redis::scard('email:manual'); - if($filter == 'open' && $page == 1) { - $reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) { - return Report::whereHas('status') - ->whereHas('reportedUser') - ->whereHas('reporter') - ->orderBy('created_at','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->paginate(6); - }); - } else { - $reports = Report::whereHas('status') - ->whereHas('reportedUser') - ->whereHas('reporter') - ->orderBy('created_at','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->paginate(6); - } + if ($filter == 'open' && $page == 1) { + $reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) { + return Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + }); + } else { + $reports = Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + } - return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications')); - } + return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications')); + } - public function showReport(Request $request, $id) - { - $report = Report::with('status')->findOrFail($id); - if($request->has('ref') && $request->input('ref') == 'email') { - return redirect('/i/admin/reports?tab=report&id=' . $report->id); - } - return view('admin.reports.show', compact('report')); - } + public function showReport(Request $request, $id) + { + $report = Report::with('status')->findOrFail($id); + if ($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=report&id='.$report->id); + } - public function appeals(Request $request) - { - $appeals = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->latest() - ->paginate(6); - return view('admin.reports.appeals', compact('appeals')); - } + return view('admin.reports.show', compact('report')); + } - public function showAppeal(Request $request, $id) - { - $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->findOrFail($id); - $meta = json_decode($appeal->meta); - return view('admin.reports.show_appeal', compact('appeal', 'meta')); - } + public function appeals(Request $request) + { + $appeals = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->latest() + ->paginate(6); - public function spam(Request $request) - { - $this->validate($request, [ - 'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions' - ]); + return view('admin.reports.appeals', compact('appeals')); + } - $tab = $request->input('tab', 'home'); + public function showAppeal(Request $request, $id) + { + $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->findOrFail($id); + $meta = json_decode($appeal->meta); - $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function() { - return AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->count(); - }); + return view('admin.reports.show_appeal', compact('appeal', 'meta')); + } - $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function() { - return AccountInterstitial::whereType('post.autospam') - ->where('created_at', '>', now()->subMonth()) - ->count(); - }); + public function spam(Request $request) + { + $this->validate($request, [ + 'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions', + ]); - $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function() { - return AccountInterstitial::whereType('post.autospam')->count(); - }); + $tab = $request->input('tab', 'home'); - $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function() { - return AccountInterstitial::whereType('post.autospam') - ->whereIsSpam(null) - ->whereNotNull('appeal_handled_at') - ->exists(); - }); + $openCount = Cache::remember('admin-dash:reports:spam-count', 3600, function () { + return AccountInterstitial::whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->count(); + }); - $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function() { - if(config('database.default') != 'mysql') { - return 0; - } - return AccountInterstitial::selectRaw('*, count(id) as counter') - ->whereType('post.autospam') - ->groupBy('user_id') - ->get() - ->avg('counter'); - }); + $monthlyCount = Cache::remember('admin-dash:reports:spam-count:30d', 43200, function () { + return AccountInterstitial::whereType('post.autospam') + ->where('created_at', '>', now()->subMonth()) + ->count(); + }); - $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() { - if(config('database.default') != 'mysql') { - return "0"; - } - $seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get(); - if(!$seconds) { - return "0"; - } - $mins = floor($seconds->avg('timediff') / 60); + $totalCount = Cache::remember('admin-dash:reports:spam-count:total', 43200, function () { + return AccountInterstitial::whereType('post.autospam')->count(); + }); - if($mins < 60) { - return $mins . ' min(s)'; - } + $uncategorized = Cache::remember('admin-dash:reports:spam-sync', 3600, function () { + return AccountInterstitial::whereType('post.autospam') + ->whereIsSpam(null) + ->whereNotNull('appeal_handled_at') + ->exists(); + }); - if($mins < 2880) { - return floor($mins / 60) . ' hour(s)'; - } + $avg = Cache::remember('admin-dash:reports:spam-count:avg', 43200, function () { + if (config('database.default') != 'mysql') { + return 0; + } - return floor($mins / 60 / 24) . ' day(s)'; - }); - $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0"; + return AccountInterstitial::selectRaw('*, count(id) as counter') + ->whereType('post.autospam') + ->groupBy('user_id') + ->get() + ->avg('counter'); + }); - if(in_array($tab, ['home', 'spam', 'not-spam'])) { - $appeals = AccountInterstitial::whereType('post.autospam') - ->when($tab, function($q, $tab) { - switch($tab) { - case 'home': - return $q->whereNull('appeal_handled_at'); - break; - case 'spam': - return $q->whereIsSpam(true); - break; - case 'not-spam': - return $q->whereIsSpam(false); - break; - } - }) - ->latest() - ->paginate(6); + $avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () { + if (config('database.default') != 'mysql') { + return '0'; + } + $seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get(); + if (! $seconds) { + return '0'; + } + $mins = floor($seconds->avg('timediff') / 60); - if($tab !== 'home') { - $appeals = $appeals->appends(['tab' => $tab]); - } - } else { - $appeals = new class { - public function count() { - return 0; - } + if ($mins < 60) { + return $mins.' min(s)'; + } - public function render() { - return; - } - }; - } + if ($mins < 2880) { + return floor($mins / 60).' hour(s)'; + } + return floor($mins / 60 / 24).' day(s)'; + }); + $avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0'; - return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized')); - } + if (in_array($tab, ['home', 'spam', 'not-spam'])) { + $appeals = AccountInterstitial::whereType('post.autospam') + ->when($tab, function ($q, $tab) { + switch ($tab) { + case 'home': + return $q->whereNull('appeal_handled_at'); + break; + case 'spam': + return $q->whereIsSpam(true); + break; + case 'not-spam': + return $q->whereIsSpam(false); + break; + } + }) + ->latest() + ->paginate(6); - public function showSpam(Request $request, $id) - { - $appeal = AccountInterstitial::whereType('post.autospam') - ->findOrFail($id); - if($request->has('ref') && $request->input('ref') == 'email') { - return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id); - } - $meta = json_decode($appeal->meta); - return view('admin.reports.show_spam', compact('appeal', 'meta')); - } + if ($tab !== 'home') { + $appeals = $appeals->appends(['tab' => $tab]); + } + } else { + $appeals = new class + { + public function count() + { + return 0; + } - public function fixUncategorizedSpam(Request $request) - { - if(Cache::get('admin-dash:reports:spam-sync-active')) { - return redirect('/i/admin/reports/autospam'); - } + public function render() + { - Cache::put('admin-dash:reports:spam-sync-active', 1, 900); + } + }; + } - AccountInterstitial::chunk(500, function($reports) { - foreach($reports as $report) { - if($report->item_type != 'App\Status') { - continue; - } + return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized')); + } - if($report->type != 'post.autospam') { - continue; - } + public function showSpam(Request $request, $id) + { + $appeal = AccountInterstitial::whereType('post.autospam') + ->findOrFail($id); + if ($request->has('ref') && $request->input('ref') == 'email') { + return redirect('/i/admin/reports?tab=autospam&id='.$appeal->id); + } + $meta = json_decode($appeal->meta); - if($report->is_spam != null) { - continue; - } + return view('admin.reports.show_spam', compact('appeal', 'meta')); + } - $status = StatusService::get($report->item_id, false); - if(!$status) { - return; - } - $scope = $status['visibility']; - $report->is_spam = $scope == 'unlisted'; - $report->in_violation = $report->is_spam; - $report->severity_index = 1; - $report->save(); - } - }); + public function fixUncategorizedSpam(Request $request) + { + if (Cache::get('admin-dash:reports:spam-sync-active')) { + return redirect('/i/admin/reports/autospam'); + } - Cache::forget('admin-dash:reports:spam-sync'); - return redirect('/i/admin/reports/autospam'); - } + Cache::put('admin-dash:reports:spam-sync-active', 1, 900); - public function updateSpam(Request $request, $id) - { - $this->validate($request, [ - 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer' - ]); + AccountInterstitial::chunk(500, function ($reports) { + foreach ($reports as $report) { + if ($report->item_type != 'App\Status') { + continue; + } - $action = $request->input('action'); - $appeal = AccountInterstitial::whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->findOrFail($id); + if ($report->type != 'post.autospam') { + continue; + } - $meta = json_decode($appeal->meta); - $res = ['status' => 'success']; - $now = now(); - Cache::forget('admin-dash:reports:spam-count:total'); - Cache::forget('admin-dash:reports:spam-count:30d'); + if ($report->is_spam != null) { + continue; + } - if($action == 'delete-account') { - if(config('pixelfed.account_deletion') == false) { - abort(404); - } + $status = StatusService::get($report->item_id, false); + if (! $status) { + return; + } + $scope = $status['visibility']; + $report->is_spam = $scope == 'unlisted'; + $report->in_violation = $report->is_spam; + $report->severity_index = 1; + $report->save(); + } + }); - $user = User::findOrFail($appeal->user_id); - $profile = $user->profile; + Cache::forget('admin-dash:reports:spam-sync'); - if($user->is_admin == true) { - $mid = $request->user()->id; - abort_if($user->id < $mid, 403); - } + return redirect('/i/admin/reports/autospam'); + } - $ts = now()->addMonth(); - $user->status = 'delete'; - $profile->status = 'delete'; - $user->delete_after = $ts; - $profile->delete_after = $ts; - $user->save(); - $profile->save(); + public function updateSpam(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer', + ]); - ModLogService::boot() - ->objectUid($user->id) - ->objectId($user->id) - ->objectType('App\User::class') - ->user($request->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + $action = $request->input('action'); + $appeal = AccountInterstitial::whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->findOrFail($id); - Cache::forget('profiles:private'); - DeleteAccountPipeline::dispatch($user); - return; - } + $meta = json_decode($appeal->meta); + $res = ['status' => 'success']; + $now = now(); + Cache::forget('admin-dash:reports:spam-count:total'); + Cache::forget('admin-dash:reports:spam-count:30d'); - if($action == 'dismiss') { - $appeal->is_spam = true; - $appeal->appeal_handled_at = $now; - $appeal->save(); + if ($action == 'delete-account') { + if (config('pixelfed.account_deletion') == false) { + abort(404); + } - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + $user = User::findOrFail($appeal->user_id); + $profile = $user->profile; - if($action == 'dismiss-all') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update(['appeal_handled_at' => $now, 'is_spam' => true]); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + if ($user->is_admin == true) { + $mid = $request->user()->id; + abort_if($user->id < $mid, 403); + } - if($action == 'approve-all') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->get() - ->each(function($report) use($meta) { - $report->is_spam = false; - $report->appeal_handled_at = now(); - $report->save(); - $status = Status::find($report->item_id); - if($status) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id, true); - } - }); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + $ts = now()->addMonth(); + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); - if($action == 'mark-spammer') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update(['appeal_handled_at' => $now, 'is_spam' => true]); + ModLogService::boot() + ->objectUid($user->id) + ->objectId($user->id) + ->objectType('App\User::class') + ->user($request->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); - $pro = Profile::whereUserId($appeal->user_id)->firstOrFail(); + Cache::forget('profiles:private'); + DeleteAccountPipeline::dispatch($user); - $pro->update([ - 'unlisted' => true, - 'cw' => true, - 'no_autolink' => true - ]); + return; + } - Status::whereProfileId($pro->id) - ->get() - ->each(function($report) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id, true); - }); + if ($action == 'dismiss') { + $appeal->is_spam = true; + $appeal->appeal_handled_at = $now; + $appeal->save(); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - $status = $appeal->status; - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); + return $res; + } - $appeal->is_spam = false; - $appeal->appeal_handled_at = now(); - $appeal->save(); + if ($action == 'dismiss-all') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update(['appeal_handled_at' => $now, 'is_spam' => true]); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - StatusService::del($status->id); + return $res; + } - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - Cache::forget('admin-dash:reports:spam-count'); + if ($action == 'approve-all') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->get() + ->each(function ($report) use ($meta) { + $report->is_spam = false; + $report->appeal_handled_at = now(); + $report->save(); + $status = Status::find($report->item_id); + if ($status) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id, true); + } + }); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - return $res; - } + return $res; + } - public function updateAppeal(Request $request, $id) - { - $this->validate($request, [ - 'action' => 'required|in:dismiss,approve' - ]); + if ($action == 'mark-spammer') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update(['appeal_handled_at' => $now, 'is_spam' => true]); - $action = $request->input('action'); - $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') - ->whereNull('appeal_handled_at') - ->findOrFail($id); + $pro = Profile::whereUserId($appeal->user_id)->firstOrFail(); - if($action == 'dismiss') { - $appeal->appeal_handled_at = now(); - $appeal->save(); - Cache::forget('admin-dash:reports:ai-count'); - return redirect('/i/admin/reports/appeals'); - } + $pro->update([ + 'unlisted' => true, + 'cw' => true, + 'no_autolink' => true, + ]); - switch ($appeal->type) { - case 'post.cw': - $status = $appeal->status; - $status->is_nsfw = false; - $status->save(); - break; + Status::whereProfileId($pro->id) + ->get() + ->each(function ($report) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id, true); + }); - case 'post.unlist': - $status = $appeal->status; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - break; + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); - default: - # code... - break; - } + return $res; + } - $appeal->appeal_handled_at = now(); - $appeal->save(); - StatusService::del($status->id, true); - Cache::forget('admin-dash:reports:ai-count'); + $status = $appeal->status; + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); - return redirect('/i/admin/reports/appeals'); - } + $appeal->is_spam = false; + $appeal->appeal_handled_at = now(); + $appeal->save(); + + StatusService::del($status->id); + + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); + + return $res; + } + + public function updateAppeal(Request $request, $id) + { + $this->validate($request, [ + 'action' => 'required|in:dismiss,approve', + ]); + + $action = $request->input('action'); + $appeal = AccountInterstitial::whereNotNull('appeal_requested_at') + ->whereNull('appeal_handled_at') + ->findOrFail($id); + + if ($action == 'dismiss') { + $appeal->appeal_handled_at = now(); + $appeal->save(); + Cache::forget('admin-dash:reports:ai-count'); + + return redirect('/i/admin/reports/appeals'); + } + + switch ($appeal->type) { + case 'post.cw': + $status = $appeal->status; + $status->is_nsfw = false; + $status->save(); + break; + + case 'post.unlist': + $status = $appeal->status; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + break; + + default: + // code... + break; + } + + $appeal->appeal_handled_at = now(); + $appeal->save(); + StatusService::del($status->id, true); + Cache::forget('admin-dash:reports:ai-count'); + + return redirect('/i/admin/reports/appeals'); + } public function updateReport(Request $request, $id) { $this->validate($request, [ - 'action' => 'required|string', + 'action' => 'required|string', ]); $action = $request->input('action'); @@ -470,7 +477,7 @@ trait AdminReportController 'ban', ]; - if (!in_array($action, $actions)) { + if (! in_array($action, $actions)) { return abort(403); } @@ -479,7 +486,7 @@ trait AdminReportController $this->handleReportAction($report, $action); Cache::forget('admin-dash:reports:list-cache'); - return response()->json(['msg'=> 'Success']); + return response()->json(['msg' => 'Success']); } public function handleReportAction(Report $report, $action) @@ -541,7 +548,7 @@ trait AdminReportController '3' => 'unlist', '4' => 'delete', '5' => 'shadowban', - '6' => 'ban' + '6' => 'ban', ]; } @@ -549,675 +556,775 @@ trait AdminReportController { $this->validate($request, [ 'action' => 'required|integer|min:1|max:10', - 'ids' => 'required|array' + 'ids' => 'required|array', ]); $action = $this->actionMap()[$request->input('action')]; $ids = $request->input('ids'); $reports = Report::whereIn('id', $ids)->whereNull('admin_seen')->get(); - foreach($reports as $report) { + foreach ($reports as $report) { $this->handleReportAction($report, $action); } $res = [ 'message' => 'Success', - 'code' => 200 + 'code' => 200, ]; + return response()->json($res); } public function reportMailVerifications(Request $request) { - $ids = Redis::smembers('email:manual'); - $ignored = Redis::smembers('email:manual-ignored'); - $reports = []; - if($ids) { - $reports = collect($ids) - ->filter(function($id) use($ignored) { - return !in_array($id, $ignored); - }) - ->map(function($id) { - $user = User::whereProfileId($id)->first(); - if(!$user || $user->email_verified_at) { - return []; - } - $account = AccountService::get($id, true); - if(!$account) { - return []; - } - $account['email'] = $user->email; - return $account; - }) - ->filter(function($res) { - return $res && isset($res['id']); - }) - ->values(); - } - return view('admin.reports.mail_verification', compact('reports', 'ignored')); + $ids = Redis::smembers('email:manual'); + $ignored = Redis::smembers('email:manual-ignored'); + $reports = []; + if ($ids) { + $reports = collect($ids) + ->filter(function ($id) use ($ignored) { + return ! in_array($id, $ignored); + }) + ->map(function ($id) { + $user = User::whereProfileId($id)->first(); + if (! $user || $user->email_verified_at) { + return []; + } + $account = AccountService::get($id, true); + if (! $account) { + return []; + } + $account['email'] = $user->email; + + return $account; + }) + ->filter(function ($res) { + return $res && isset($res['id']); + }) + ->values(); + } + + return view('admin.reports.mail_verification', compact('reports', 'ignored')); } public function reportMailVerifyIgnore(Request $request) { - $id = $request->input('id'); - Redis::sadd('email:manual-ignored', $id); - return redirect('/i/admin/reports'); + $id = $request->input('id'); + Redis::sadd('email:manual-ignored', $id); + + return redirect('/i/admin/reports'); } public function reportMailVerifyApprove(Request $request) { - $id = $request->input('id'); - $user = User::whereProfileId($id)->firstOrFail(); - Redis::srem('email:manual', $id); - Redis::srem('email:manual-ignored', $id); - $user->email_verified_at = now(); - $user->save(); - return redirect('/i/admin/reports'); + $id = $request->input('id'); + $user = User::whereProfileId($id)->firstOrFail(); + Redis::srem('email:manual', $id); + Redis::srem('email:manual-ignored', $id); + $user->email_verified_at = now(); + $user->save(); + + return redirect('/i/admin/reports'); } public function reportMailVerifyClearIgnored(Request $request) { - Redis::del('email:manual-ignored'); - return [200]; + Redis::del('email:manual-ignored'); + + return [200]; } public function reportsStats(Request $request) { - $stats = [ - 'total' => Report::count(), - 'open' => Report::whereNull('admin_seen')->count(), - 'closed' => Report::whereNotNull('admin_seen')->count(), - 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), - 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), - 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), - 'email_verification_requests' => Redis::scard('email:manual') - ]; - return $stats; + $stats = [ + 'total' => Report::count(), + 'open' => Report::whereNull('admin_seen')->count(), + 'closed' => Report::whereNotNull('admin_seen')->count(), + 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), + 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), + 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), + 'email_verification_requests' => Redis::scard('email:manual'), + ]; + + return $stats; } public function reportsApiAll(Request $request) { - $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; - $reports = AdminReport::collection( - Report::orderBy('id','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->groupBy(['id', 'object_id', 'object_type', 'profile_id']) - ->cursorPaginate(6) - ->withQueryString() - ); + $reports = AdminReport::collection( + Report::orderBy('id', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->groupBy(['id', 'object_id', 'object_type', 'profile_id']) + ->cursorPaginate(6) + ->withQueryString() + ); - return $reports; + return $reports; } public function reportsApiGet(Request $request, $id) { - $report = Report::findOrFail($id); - return new AdminReport($report); + $report = Report::findOrFail($id); + + return new AdminReport($report); } public function reportsApiHandle(Request $request) { - $this->validate($request, [ - 'object_id' => 'required', - 'object_type' => 'required', - 'id' => 'required', - 'action' => 'required|in:ignore,nsfw,unlist,private,delete', - 'action_type' => 'required|in:post,profile' - ]); + $this->validate($request, [ + 'object_id' => 'required', + 'object_type' => 'required', + 'id' => 'required', + 'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all', + 'action_type' => 'required|in:post,profile,story', + ]); - $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id')); + $report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id')); - if($request->input('action_type') === 'profile') { - return $this->reportsHandleProfileAction($report, $request->input('action')); - } else if($request->input('action_type') === 'post') { - return $this->reportsHandleStatusAction($report, $request->input('action')); - } + if ($request->input('action_type') === 'profile') { + return $this->reportsHandleProfileAction($report, $request->input('action')); + } elseif ($request->input('action_type') === 'post') { + return $this->reportsHandleStatusAction($report, $request->input('action')); + } elseif ($request->input('action_type') === 'story') { + return $this->reportsHandleStoryAction($report, $request->input('action')); + } - return $report; + return $report; + } + + protected function reportsHandleStoryAction($report, $action) + { + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + return [200]; + break; + + case 'delete': + $profile = Profile::find($report->reported_profile_id); + $story = Story::whereProfileId($profile->id)->find($report->object_id); + + abort_if(! $story, 400, 'Invalid or missing story'); + + $story->active = false; + $story->save(); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($report->object_id) + ->objectType('App\Story::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'delete', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + StoryDelete::dispatch($story)->onQueue('story'); + + return [200]; + break; + + case 'delete-all': + $profile = Profile::find($report->reported_profile_id); + $stories = Story::whereProfileId($profile->id)->whereActive(true)->get(); + + abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories'); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($report->object_id) + ->objectType('App\Story::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'delete-all', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); + + Report::where('reported_profile_id', $profile->id) + ->whereObjectType('App\Story') + ->whereNull('admin_seen') + ->update([ + 'admin_seen' => now(), + ]); + $stories->each(function ($story) { + StoryDelete::dispatch($story)->onQueue('story'); + }); + + return [200]; + break; + } } protected function reportsHandleProfileAction($report, $action) { - switch($action) { - case 'ignore': - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - case 'nsfw': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + return [200]; + break; - if(!$profile) { - return; - } + case 'nsfw': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + if (! $profile) { + return; + } - $profile->cw = true; - $profile->save(); + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - foreach(Status::whereProfileId($profile->id)->cursor() as $status) { - $status->is_nsfw = true; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + $profile->cw = true; + $profile->save(); - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + foreach (Status::whereProfileId($profile->id)->cursor() as $status) { + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'nsfw' => true, - 'admin_seen' => now() - ]); - return [200]; - break; + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - case 'unlist': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now(), + ]); - if(!$profile) { - return; - } + return [200]; + break; - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + case 'unlist': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - $profile->unlisted = true; - $profile->save(); + if (! $profile) { + return; + } - foreach(Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) { - $status->scope = 'unlisted'; - $status->visibility = 'unlisted'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'unlisted', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + $profile->unlisted = true; + $profile->save(); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - case 'private': - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'unlisted', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if(!$profile) { - return; - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); + return [200]; + break; - $profile->unlisted = true; - $profile->save(); + case 'private': + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - foreach(Status::whereProfileId($profile->id)->cursor() as $status) { - $status->scope = 'private'; - $status->visibility = 'private'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + if (! $profile) { + return; + } - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.moderate') - ->metadata([ - 'action' => 'private', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot moderate an admin account.'); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + $profile->unlisted = true; + $profile->save(); - case 'delete': - if(config('pixelfed.account_deletion') == false) { - abort(404); - } + foreach (Status::whereProfileId($profile->id)->cursor() as $status) { + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - if($report->object_type === 'App\Profile') { - $profile = Profile::find($report->object_id); - } else if($report->object_type === 'App\Status') { - $status = Status::find($report->object_id); - if(!$status) { - return [200]; - } - $profile = Profile::find($status->profile_id); - } + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if(!$profile) { - return; - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.'); + return [200]; + break; - $ts = now()->addMonth(); + case 'delete': + if (config('pixelfed.account_deletion') == false) { + abort(404); + } - if($profile->user_id) { - $user = $profile->user; - abort_if($user->is_admin, 403, 'You cannot delete admin accounts.'); - $user->status = 'delete'; - $user->delete_after = $ts; - $user->save(); - } + if ($report->object_type === 'App\Profile') { + $profile = Profile::find($report->object_id); + } elseif ($report->object_type === 'App\Status') { + $status = Status::find($report->object_id); + if (! $status) { + return [200]; + } + $profile = Profile::find($status->profile_id); + } - $profile->status = 'delete'; - $profile->delete_after = $ts; - $profile->save(); + if (! $profile) { + return; + } - ModLogService::boot() - ->objectUid($profile->id) - ->objectId($profile->id) - ->objectType('App\Profile::class') - ->user(request()->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account.'); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); + $ts = now()->addMonth(); - if($profile->user_id) { - DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); - DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); - $user->email = $user->id; - $user->password = ''; - $user->status = 'delete'; - $user->save(); - $profile->status = 'delete'; - $profile->delete_after = now()->addMonth(); - $profile->save(); - AccountService::del($profile->id); - DeleteAccountPipeline::dispatch($user)->onQueue('high'); - } else { - $profile->status = 'delete'; - $profile->delete_after = now()->addMonth(); - $profile->save(); - AccountService::del($profile->id); - DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); - } - return [200]; - break; - } + if ($profile->user_id) { + $user = $profile->user; + abort_if($user->is_admin, 403, 'You cannot delete admin accounts.'); + $user->status = 'delete'; + $user->delete_after = $ts; + $user->save(); + } + + $profile->status = 'delete'; + $profile->delete_after = $ts; + $profile->save(); + + ModLogService::boot() + ->objectUid($profile->id) + ->objectId($profile->id) + ->objectType('App\Profile::class') + ->user(request()->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + if ($profile->user_id) { + DB::table('oauth_access_tokens')->whereUserId($user->id)->delete(); + DB::table('oauth_auth_codes')->whereUserId($user->id)->delete(); + $user->email = $user->id; + $user->password = ''; + $user->status = 'delete'; + $user->save(); + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteAccountPipeline::dispatch($user)->onQueue('high'); + } else { + $profile->status = 'delete'; + $profile->delete_after = now()->addMonth(); + $profile->save(); + AccountService::del($profile->id); + DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high'); + } + + return [200]; + break; + } } protected function reportsHandleStatusAction($report, $action) { - switch($action) { - case 'ignore': - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + switch ($action) { + case 'ignore': + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - case 'nsfw': - $status = Status::find($report->object_id); + return [200]; + break; - if(!$status) { - return [200]; - } + case 'nsfw': + $status = Status::find($report->object_id); - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - $status->is_nsfw = true; - $status->save(); - StatusService::del($status->id); + if (! $status) { + return [200]; + } - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'cw', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + $status->is_nsfw = true; + $status->save(); + StatusService::del($status->id); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'nsfw' => true, - 'admin_seen' => now() - ]); - return [200]; - break; + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'cw', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - case 'private': - $status = Status::find($report->object_id); + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'nsfw' => true, + 'admin_seen' => now(), + ]); - if(!$status) { - return [200]; - } + return [200]; + break; - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + case 'private': + $status = Status::find($report->object_id); - $status->scope = 'private'; - $status->visibility = 'private'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); + if (! $status) { + return [200]; + } - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'private', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + $status->scope = 'private'; + $status->visibility = 'private'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); - case 'unlist': - $status = Status::find($report->object_id); + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'private', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - if(!$status) { - return [200]; - } + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); + return [200]; + break; - if($status->scope === 'public') { - $status->scope = 'unlisted'; - $status->visibility = 'unlisted'; - $status->save(); - StatusService::del($status->id); - PublicTimelineService::rem($status->id); - } + case 'unlist': + $status = Status::find($report->object_id); - ModLogService::boot() - ->objectUid($status->profile_id) - ->objectId($status->profile_id) - ->objectType('App\Status::class') - ->user(request()->user()) - ->action('admin.status.moderate') - ->metadata([ - 'action' => 'unlist', - 'message' => 'Success!' - ]) - ->accessLevel('admin') - ->save(); + if (! $status) { + return [200]; + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); - return [200]; - break; + abort_if($status->profile->user && $status->profile->user->is_admin, 400, 'Cannot moderate an admin account post.'); - case 'delete': - $status = Status::find($report->object_id); + if ($status->scope === 'public') { + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->save(); + StatusService::del($status->id); + PublicTimelineService::rem($status->id); + } - if(!$status) { - return [200]; - } + ModLogService::boot() + ->objectUid($status->profile_id) + ->objectId($status->profile_id) + ->objectType('App\Status::class') + ->user(request()->user()) + ->action('admin.status.moderate') + ->metadata([ + 'action' => 'unlist', + 'message' => 'Success!', + ]) + ->accessLevel('admin') + ->save(); - $profile = $status->profile; + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); - abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.'); + return [200]; + break; - StatusService::del($status->id); + case 'delete': + $status = Status::find($report->object_id); - if($profile->user_id != null && $profile->domain == null) { - PublicTimelineService::del($status->id); - StatusDelete::dispatch($status)->onQueue('high'); - } else { - NetworkTimelineService::del($status->id); - RemoteStatusDelete::dispatch($status)->onQueue('high'); - } + if (! $status) { + return [200]; + } - Report::whereObjectId($report->object_id) - ->whereObjectType($report->object_type) - ->update([ - 'admin_seen' => now() - ]); + $profile = $status->profile; - return [200]; - break; - } + abort_if($profile->user && $profile->user->is_admin, 400, 'Cannot delete an admin account post.'); + + StatusService::del($status->id); + + if ($profile->user_id != null && $profile->domain == null) { + PublicTimelineService::del($status->id); + StatusDelete::dispatch($status)->onQueue('high'); + } else { + NetworkTimelineService::del($status->id); + RemoteStatusDelete::dispatch($status)->onQueue('high'); + } + + Report::whereObjectId($report->object_id) + ->whereObjectType($report->object_type) + ->update([ + 'admin_seen' => now(), + ]); + + return [200]; + break; + } } public function reportsApiSpamAll(Request $request) { - $tab = $request->input('tab', 'home'); + $tab = $request->input('tab', 'home'); - $appeals = AdminSpamReport::collection( - AccountInterstitial::orderBy('id', 'desc') - ->whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->cursorPaginate(6) - ->withQueryString() - ); + $appeals = AdminSpamReport::collection( + AccountInterstitial::orderBy('id', 'desc') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->cursorPaginate(6) + ->withQueryString() + ); - return $appeals; + return $appeals; } public function reportsApiSpamHandle(Request $request) { - $this->validate($request, [ - 'id' => 'required', - 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile', - ]); + $this->validate($request, [ + 'id' => 'required', + 'action' => 'required|in:mark-read,mark-not-spam,mark-all-read,mark-all-not-spam,delete-profile', + ]); - $action = $request->input('action'); + $action = $request->input('action'); - abort_if( - $action === 'delete-profile' && - !config('pixelfed.account_deletion'), - 404, - "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config." - ); + abort_if( + $action === 'delete-profile' && + ! config('pixelfed.account_deletion'), + 404, + "Cannot delete profile, account_deletion is disabled.\n\n Set `ACCOUNT_DELETION=true` in .env and re-cache config." + ); - $report = AccountInterstitial::with('user') - ->whereType('post.autospam') - ->whereNull('appeal_handled_at') - ->findOrFail($request->input('id')); + $report = AccountInterstitial::with('user') + ->whereType('post.autospam') + ->whereNull('appeal_handled_at') + ->findOrFail($request->input('id')); - $this->reportsHandleSpamAction($report, $action); - Cache::forget('admin-dash:reports:spam-count'); - Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id); - Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id); - return [$action, $report]; + $this->reportsHandleSpamAction($report, $action); + Cache::forget('admin-dash:reports:spam-count'); + Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$report->user->profile_id); + Cache::forget('pf:bouncer_v0:recent_by_pid:'.$report->user->profile_id); + + return [$action, $report]; } public function reportsHandleSpamAction($appeal, $action) { - $meta = json_decode($appeal->meta); + $meta = json_decode($appeal->meta); - if($action == 'mark-read') { - $appeal->is_spam = true; - $appeal->appeal_handled_at = now(); - $appeal->save(); - PublicTimelineService::del($appeal->item_id); - } + if ($action == 'mark-read') { + $appeal->is_spam = true; + $appeal->appeal_handled_at = now(); + $appeal->save(); + PublicTimelineService::del($appeal->item_id); + } - if($action == 'mark-not-spam') { - $status = $appeal->status; - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); + if ($action == 'mark-not-spam') { + $status = $appeal->status; + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); - $appeal->is_spam = false; - $appeal->appeal_handled_at = now(); - $appeal->save(); + $appeal->is_spam = false; + $appeal->appeal_handled_at = now(); + $appeal->save(); - Notification::whereAction('autospam.warning') - ->whereProfileId($appeal->user->profile_id) - ->get() - ->each(function($n) use($appeal) { - NotificationService::del($appeal->user->profile_id, $n->id); - $n->forceDelete(); - }); + Notification::whereAction('autospam.warning') + ->whereProfileId($appeal->user->profile_id) + ->get() + ->each(function ($n) use ($appeal) { + NotificationService::del($appeal->user->profile_id, $n->id); + $n->forceDelete(); + }); - StatusService::del($status->id); - StatusService::get($status->id); - if($status->in_reply_to_id == null && $status->reblog_of_id == null) { - PublicTimelineService::add($status->id); - } - } + StatusService::del($status->id); + StatusService::get($status->id); + if ($status->in_reply_to_id == null && $status->reblog_of_id == null) { + PublicTimelineService::add($status->id); + } + } - if($action == 'mark-all-read') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereNull('appeal_handled_at') - ->whereUserId($appeal->user_id) - ->update([ - 'appeal_handled_at' => now(), - 'is_spam' => true - ]); - } + if ($action == 'mark-all-read') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereNull('appeal_handled_at') + ->whereUserId($appeal->user_id) + ->update([ + 'appeal_handled_at' => now(), + 'is_spam' => true, + ]); + } - if($action == 'mark-all-not-spam') { - AccountInterstitial::whereType('post.autospam') - ->whereItemType('App\Status') - ->whereUserId($appeal->user_id) - ->get() - ->each(function($report) use($meta) { - $report->is_spam = false; - $report->appeal_handled_at = now(); - $report->save(); - $status = Status::find($report->item_id); - if($status) { - $status->is_nsfw = $meta->is_nsfw; - $status->scope = 'public'; - $status->visibility = 'public'; - $status->save(); - StatusService::del($status->id); - } - Notification::whereAction('autospam.warning') - ->whereProfileId($report->user->profile_id) - ->get() - ->each(function($n) use($report) { - NotificationService::del($report->user->profile_id, $n->id); - $n->forceDelete(); - }); - }); - } + if ($action == 'mark-all-not-spam') { + AccountInterstitial::whereType('post.autospam') + ->whereItemType('App\Status') + ->whereUserId($appeal->user_id) + ->get() + ->each(function ($report) use ($meta) { + $report->is_spam = false; + $report->appeal_handled_at = now(); + $report->save(); + $status = Status::find($report->item_id); + if ($status) { + $status->is_nsfw = $meta->is_nsfw; + $status->scope = 'public'; + $status->visibility = 'public'; + $status->save(); + StatusService::del($status->id); + } + Notification::whereAction('autospam.warning') + ->whereProfileId($report->user->profile_id) + ->get() + ->each(function ($n) use ($report) { + NotificationService::del($report->user->profile_id, $n->id); + $n->forceDelete(); + }); + }); + } - if($action == 'delete-profile') { - $user = User::findOrFail($appeal->user_id); - $profile = $user->profile; + if ($action == 'delete-profile') { + $user = User::findOrFail($appeal->user_id); + $profile = $user->profile; - if($user->is_admin == true) { - $mid = request()->user()->id; - abort_if($user->id < $mid, 403, 'You cannot delete an admin account.'); - } + if ($user->is_admin == true) { + $mid = request()->user()->id; + abort_if($user->id < $mid, 403, 'You cannot delete an admin account.'); + } - $ts = now()->addMonth(); - $user->status = 'delete'; - $profile->status = 'delete'; - $user->delete_after = $ts; - $profile->delete_after = $ts; - $user->save(); - $profile->save(); + $ts = now()->addMonth(); + $user->status = 'delete'; + $profile->status = 'delete'; + $user->delete_after = $ts; + $profile->delete_after = $ts; + $user->save(); + $profile->save(); - $appeal->appeal_handled_at = now(); - $appeal->save(); + $appeal->appeal_handled_at = now(); + $appeal->save(); - ModLogService::boot() - ->objectUid($user->id) - ->objectId($user->id) - ->objectType('App\User::class') - ->user(request()->user()) - ->action('admin.user.delete') - ->accessLevel('admin') - ->save(); + ModLogService::boot() + ->objectUid($user->id) + ->objectId($user->id) + ->objectType('App\User::class') + ->user(request()->user()) + ->action('admin.user.delete') + ->accessLevel('admin') + ->save(); - Cache::forget('profiles:private'); - DeleteAccountPipeline::dispatch($user); - } + Cache::forget('profiles:private'); + DeleteAccountPipeline::dispatch($user); + } } public function reportsApiSpamGet(Request $request, $id) { - $report = AccountInterstitial::findOrFail($id); - return new AdminSpamReport($report); + $report = AccountInterstitial::findOrFail($id); + + return new AdminSpamReport($report); } } From 2f48df8ca847fd6d6a5c4673fd759c825bc9e62a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 28 Feb 2024 21:06:21 -0700 Subject: [PATCH 472/977] Update kb, add email confirmation issues page --- .../help/email-confirmation-issues.blade.php | 16 ++++++++++++++++ routes/web.php | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 resources/views/site/help/email-confirmation-issues.blade.php diff --git a/resources/views/site/help/email-confirmation-issues.blade.php b/resources/views/site/help/email-confirmation-issues.blade.php new file mode 100644 index 000000000..6c1c52cab --- /dev/null +++ b/resources/views/site/help/email-confirmation-issues.blade.php @@ -0,0 +1,16 @@ +@extends('site.help.partial.template', ['breadcrumb'=>'Email Confirmation Issues']) + +@section('section') +
+

Email Confirmation Issues

+
+
+

If you have been redirected to this page, it may be due to one of the following reasons:

+ +
    +
  • The email confirmation link has already been used.
  • +
  • The email confirmation link may have expired, they are only valid for 24 hours.
  • +
  • You cannot confirm an email for another account while logged in to a different account. Try logging out, or use a different browser to open the email confirmation link.
  • +
  • The account the associated email belongs to may have been deleted, or the account may have changed the email address.
  • +
+@endsection diff --git a/routes/web.php b/routes/web.php index 6a5b878ee..059651d2a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -307,7 +307,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit'); Route::view('import', 'site.help.import')->name('help.import'); Route::view('parental-controls', 'site.help.parental-controls'); - // Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues'); + Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues'); Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding'); }); Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show'); From ab9ecb6efda40d235877529e57b1005d021ebcca Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 02:04:43 -0700 Subject: [PATCH 473/977] Update AdminCuratedRegisterController, filter confirmation activities from activitylog --- app/Http/Controllers/AdminCuratedRegisterController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/AdminCuratedRegisterController.php b/app/Http/Controllers/AdminCuratedRegisterController.php index 91400afb4..7b25ac369 100644 --- a/app/Http/Controllers/AdminCuratedRegisterController.php +++ b/app/Http/Controllers/AdminCuratedRegisterController.php @@ -104,6 +104,10 @@ class AdminCuratedRegisterController extends Controller foreach ($activities as $activity) { $idx++; + + if ($activity->type === 'user_resend_email_confirmation') { + continue; + } if ($activity->from_user) { $userResponses->push($activity); From ef0ff78e4a1db6a66eab333785da46f72ffefa01 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 03:24:33 -0700 Subject: [PATCH 474/977] Add Remote Reports to Admin Dashboard Reports page --- .../Admin/AdminReportController.php | 191 ++++++++++++++++++ app/Http/Resources/AdminRemoteReport.php | 49 +++++ app/Util/ActivityPub/Inbox.php | 28 ++- .../assets/components/admin/AdminReports.vue | 163 ++++++++++++++- .../admin/partial/AdminModalPost.vue | 139 +++++++++++++ routes/web-admin.php | 2 + 6 files changed, 567 insertions(+), 5 deletions(-) create mode 100644 app/Http/Resources/AdminRemoteReport.php create mode 100644 resources/assets/components/admin/partial/AdminModalPost.vue diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index 2ad2bab8a..8695a5003 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin; use App\AccountInterstitial; use App\Http\Resources\AdminReport; +use App\Http\Resources\AdminRemoteReport; use App\Http\Resources\AdminSpamReport; use App\Jobs\DeletePipeline\DeleteAccountPipeline; use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline; @@ -13,6 +14,7 @@ use App\Jobs\StoryPipeline\StoryDelete; use App\Notification; use App\Profile; use App\Report; +use App\Models\RemoteReport; use App\Services\AccountService; use App\Services\ModLogService; use App\Services\NetworkTimelineService; @@ -23,6 +25,7 @@ use App\Status; use App\Story; use App\User; use Cache; +use Storage; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -640,6 +643,7 @@ trait AdminReportController 'autospam' => AccountInterstitial::whereType('post.autospam')->count(), 'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(), 'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(), + 'remote_open' => RemoteReport::whereNull('action_taken_at')->count(), 'email_verification_requests' => Redis::scard('email:manual'), ]; @@ -665,6 +669,24 @@ trait AdminReportController return $reports; } + public function reportsApiRemote(Request $request) + { + $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; + + $reports = AdminRemoteReport::collection( + RemoteReport::orderBy('id', 'desc') + ->when($filter, function ($q, $filter) { + return $filter == 'open' ? + $q->whereNull('action_taken_at') : + $q->whereNotNull('action_taken_at'); + }) + ->cursorPaginate(6) + ->withQueryString() + ); + + return $reports; + } + public function reportsApiGet(Request $request, $id) { $report = Report::findOrFail($id); @@ -1327,4 +1349,173 @@ trait AdminReportController return new AdminSpamReport($report); } + + public function reportsApiRemoteHandle(Request $request) + { + $this->validate($request, [ + 'id' => 'required|exists:remote_reports,id', + 'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts' + ]); + + $report = RemoteReport::findOrFail($request->input('id')); + $user = User::whereProfileId($report->account_id)->first(); + $ogPublicStatuses = []; + $ogUnlistedStatuses = []; + $ogNonCwStatuses = []; + + switch ($request->input('action')) { + case 'mark-read': + $report->action_taken_at = now(); + $report->save(); + break; + case 'mark-all-read-by-domain': + RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]); + break; + case 'cw-posts': + $statuses = Status::find($report->status_ids); + foreach($statuses as $status) { + if($report->account_id != $status->profile_id) { + continue; + } + if(!$status->is_nsfw) { + $ogNonCwStatuses[] = $status->id; + } + $status->is_nsfw = true; + $status->saveQuietly(); + StatusService::del($status->id); + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'cw-all-posts': + foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if($status->is_nsfw || $status->reblog_of_id) { + continue; + } + if(!$status->is_nsfw) { + $ogNonCwStatuses[] = $status->id; + } + $status->is_nsfw = true; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'unlist-posts': + $statuses = Status::find($report->status_ids); + foreach($statuses as $status) { + if($report->account_id != $status->profile_id) { + continue; + } + if($status->scope === 'public') { + $ogPublicStatuses[] = $status->id; + $status->scope = 'unlisted'; + $status->visibility = 'unlisted'; + $status->saveQuietly(); + StatusService::del($status->id); + } + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'unlist-all-posts': + foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if($status->visibility !== 'public' || $status->reblog_of_id) { + continue; + } + $ogPublicStatuses[] = $status->id; + $status->visibility = 'unlisted'; + $status->scope = 'unlisted'; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'private-posts': + $statuses = Status::find($report->status_ids); + foreach($statuses as $status) { + if($report->account_id != $status->profile_id) { + continue; + } + if(in_array($status->scope, ['public', 'unlisted', 'private'])) { + if($status->scope === 'public') { + $ogPublicStatuses[] = $status->id; + } + $status->scope = 'private'; + $status->visibility = 'private'; + $status->saveQuietly(); + StatusService::del($status->id); + } + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'private-all-posts': + foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) { + if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) { + continue; + } + if($status->visibility === 'public') { + $ogPublicStatuses[] = $status->id; + } else if($status->visibility === 'unlisted') { + $ogUnlistedStatuses[] = $status->id; + } + $status->visibility = 'private'; + $status->scope = 'private'; + $status->saveQuietly(); + StatusService::del($status->id); + } + break; + case 'delete-posts': + $statuses = Status::find($report->status_ids); + foreach($statuses as $status) { + if($report->account_id != $status->profile_id) { + continue; + } + StatusDelete::dispatch($status); + } + $report->action_taken_at = now(); + $report->save(); + break; + case 'mark-all-read-by-username': + RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]); + break; + + default: + abort(404); + break; + } + + if($ogPublicStatuses && count($ogPublicStatuses)) { + Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + if($ogNonCwStatuses && count($ogNonCwStatuses)) { + Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + if($ogUnlistedStatuses && count($ogUnlistedStatuses)) { + Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + ModLogService::boot() + ->user(request()->user()) + ->objectUid($user ? $user->id : null) + ->objectId($report->id) + ->objectType('App\Report::class') + ->action('admin.report.moderate') + ->metadata([ + 'action' => $request->input('action'), + 'duration_active' => now()->parse($report->created_at)->diffForHumans() + ]) + ->accessLevel('admin') + ->save(); + + if($report->status_ids) { + foreach($report->status_ids as $sid) { + RemoteReport::whereNull('action_taken_at') + ->whereJsonContains('status_ids', [$sid]) + ->update(['action_taken_at' => now()]); + } + } + return [200]; + } } diff --git a/app/Http/Resources/AdminRemoteReport.php b/app/Http/Resources/AdminRemoteReport.php new file mode 100644 index 000000000..a726e25cc --- /dev/null +++ b/app/Http/Resources/AdminRemoteReport.php @@ -0,0 +1,49 @@ + + */ + public function toArray(Request $request): array + { + $instance = parse_url($this->uri, PHP_URL_HOST); + $statuses = []; + if($this->status_ids && count($this->status_ids)) { + foreach($this->status_ids as $sid) { + $s = StatusService::get($sid, false); + if($s && $s['in_reply_to_id'] != null) { + $parent = StatusService::get($s['in_reply_to_id'], false); + if($parent) { + $s['parent'] = $parent; + } + } + if($s) { + $statuses[] = $s; + } + } + } + $res = [ + 'id' => $this->id, + 'instance' => $instance, + 'reported' => AccountService::get($this->account_id, true), + 'status_ids' => $this->status_ids, + 'statuses' => $statuses, + 'message' => $this->comment, + 'report_meta' => $this->report_meta, + 'created_at' => optional($this->created_at)->format('c'), + 'action_taken_at' => optional($this->action_taken_at)->format('c'), + ]; + return $res; + } +} diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 0ef7d6a7c..ed6b964e8 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -1243,7 +1243,14 @@ class Inbox return; } - $content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null; + $content = null; + if(isset($this->payload['content'])) { + if(strlen($this->payload['content']) > 5000) { + $content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)'); + } else { + $content = Purify::clean($this->payload['content']); + } + } $object = $this->payload['object']; if(empty($object) || (!is_array($object) && !is_string($object))) { @@ -1259,7 +1266,7 @@ class Inbox foreach($object as $objectUrl) { if(!Helpers::validateLocalUrl($objectUrl)) { - continue; + return; } if(str_contains($objectUrl, '/users/')) { @@ -1280,6 +1287,23 @@ class Inbox return; } + if($objects->count()) { + $obc = $objects->count(); + if($obc > 25) { + if($obc > 30) { + return; + } else { + $objLimit = $objects->take(20); + $objects = collect($objLimit->all()); + $obc = $objects->count(); + } + } + $count = Status::whereProfileId($accountId)->find($objects)->count(); + if($obc !== $count) { + return; + } + } + $instanceHost = parse_url($id, PHP_URL_HOST); $instance = Instance::updateOrCreate([ diff --git a/resources/assets/components/admin/AdminReports.vue b/resources/assets/components/admin/AdminReports.vue index 7bfcd10a9..e43f8af62 100644 --- a/resources/assets/components/admin/AdminReports.vue +++ b/resources/assets/components/admin/AdminReports.vue @@ -103,6 +103,21 @@ +
@@ -372,7 +470,7 @@
Reporter Account
-
+
+ +
diff --git a/routes/web-admin.php b/routes/web-admin.php index 64a3ecffd..90df95abf 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -146,6 +146,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::post('instances/import-data', 'AdminController@importBackup'); Route::get('reports/stats', 'AdminController@reportsStats'); Route::get('reports/all', 'AdminController@reportsApiAll'); + Route::get('reports/remote', 'AdminController@reportsApiRemote'); + Route::post('reports/remote/handle', 'AdminController@reportsApiRemoteHandle'); Route::get('reports/get/{id}', 'AdminController@reportsApiGet'); Route::post('reports/handle', 'AdminController@reportsApiHandle'); Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll'); From 372a116a2c27fb48abf9ab984ff9504d5ca1934b Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 03:27:03 -0700 Subject: [PATCH 475/977] Add remote report components --- .../admin/partial/AdminReadMore.vue | 106 ++++++ .../admin/partial/AdminRemoteReportModal.vue | 304 ++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 resources/assets/components/admin/partial/AdminReadMore.vue create mode 100644 resources/assets/components/admin/partial/AdminRemoteReportModal.vue diff --git a/resources/assets/components/admin/partial/AdminReadMore.vue b/resources/assets/components/admin/partial/AdminReadMore.vue new file mode 100644 index 000000000..1a1b3d6ee --- /dev/null +++ b/resources/assets/components/admin/partial/AdminReadMore.vue @@ -0,0 +1,106 @@ + + + diff --git a/resources/assets/components/admin/partial/AdminRemoteReportModal.vue b/resources/assets/components/admin/partial/AdminRemoteReportModal.vue new file mode 100644 index 000000000..63beff06d --- /dev/null +++ b/resources/assets/components/admin/partial/AdminRemoteReportModal.vue @@ -0,0 +1,304 @@ + + + From 0bb7d379c5caf5073ed16a1256fd9ecd216a40d2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 03:27:28 -0700 Subject: [PATCH 476/977] Update compiled assets --- public/js/admin.js | Bin 216502 -> 238520 bytes public/mix-manifest.json | Bin 5243 -> 5243 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 3e112c876b02e0f4981f59051021a447902387ac..4c442370fafe3b10494a6a030e4fb862129eeb18 100644 GIT binary patch delta 18886 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 delta 5096 zcmc&&eRPxM6+h2?`%T*BqYX`)&@^d7LPKb2#Zt#>#P#TiZye*C#!zx&kI=@M5ZugjL zJpq5KD7n8iU}ZH+!;vzWg&iBA(kQxuqFW5Q`~i=z&|>EQ%&qe3pam;!V1MXLGG3+v zJ)Y@+S<-#O`MA9kvSjPne+T49E56lDFv>(6egHD?jtyYMP&%0L^9LXm91f{-*g3Xc zRp%Go-pR3%nJJx+$1N0KO(zsi@UGhk*|EL%Zv-bUutEMP_*r+s6=+hil%4gTFnn_p zl*JXzn?uS;4U4g zasSX5!cP!!+i>Bb%T+t~gd2w8a;D z?|%TYCik9&&Mv5wj-S?}sfDQ{iBD_t`h%Y34o9J--d8W8riB@CyA9&7@fD`$Nl?(> zzMag3)tjMaN>u9csm)L@&gX}l!8#i8BtG$vTObd9h_ks<-GP9|?G0Kc)ExUC1VeUI zs>3x7FCQzj0)f}soWyHAR_*7yz#*r7>oJyrMirZapFII4Ov{4l(nnvbOrk5?XExjD zrOWH_tq@mMc+kHU3h}A}u*S=)aK42Smop6(;8|cAESOIHWgcX6q>*!R2?{Z>=VMli z0UKP8+s_lFwh82TGfBN?TzKVE)W}B+!Zzl{&i&xS{Z_Kt^&zORawnu53cK}e5nT>6P>u#`c5Yf-nPm{Q1tC$)q;#rC0Ja?iXT1{I4gcbn)Z zomzdZ-@V+kynMME0PYJzeegkG!?g4Ay2CKt^kV) zgbWw8z1g$2*|$ROM06(#SvaC+ zDR}BI`!QBF(m6O#NEtYKBh$+baTG}XRC{3xK2sfT%W(TCZ8_ec$eVP+se{HoQj?}c zL61W>z*UOIkvUhx7soM&P>{P&CGfea+0 z&qRywOAre2#0!wCoe%)k5dZlimH$_JSRVEspcZ~3N3m{2^=E|Vw1)bN%%ux_?V6)t zz-3xii|dXOI2iw)?@5qx}|S$g&Cbj&ddi?N^U4;fdZ zw;S@P=j)Zb6WwHu+BBUbpE3z(J!?gcg9IoBY?@IT2Ot}bH?b7DrV#aUt>Iw#K2py* zB}u!GphZH+#N2~$ONTd6^PgUGAnPQF`gDb9_u`-7kYR z5>gs&ewpA&a@Z{bvSCBqwEqj~EO zSyT?XO2eKnXj|n;h{p^AyMFZ98vMe*l1FdDODx%}OnLU;NvOv|*~~h@8%K~g%CtG0 z25;~kvdbJCGO_?BzYcjRq^2G_k-l8S9TW*J9E8N^aMrv|xMbQ^<_bwV(!S^(6lE5T zE{QN(_IE2=gbTl-PJHZyrrSat6FAFMWST5ypz1LBqUen&ftB<%!6M7K{O0Dvkf7vX z-Rsm+uO226$~lm4<+pP9^6jQKpo)i|j<=gAO`KkaixB$Syk6OUq?e~Ai8D*(^MuE& zS5>$AZ)x^=6cAL%9MqVFYw(3Z(2=xdW99qQm&r#V)hZuAmY=Th`CB97{=_*|Zoskb z>UtaEc+f|Q*)<1A-M@H;jD3~IH1w4BYRsil>vQ@ZMc zI!_se>cyYCTU*R-uh(4d4~ju^)uO8x^D&T)ko3hg&*Y|&m=F9Ft{w~K?{7gWH{pnw zxX$No4vJ$&l>KJzLAq6Or(^Kc{+bw@l>qXS>W+{M2|U~ z@$p`GCddt2=|T|my`Fik04d8;Eb@& zAH=OSl!X`_!FfthG=pHggeFc@hTb7MQ%?^5pNPn(4fR=3%l~xF2i31S6NETtz4pO$*Ym0q*7FWFo496!cHXmln(aMPmrbVX|eac~Vk}L7I_~p_%dK#jH2@08lXw A4gdfE From c4190eec085defd14722cdecadea55de52bf27ac Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 03:27:46 -0700 Subject: [PATCH 477/977] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb452440..7b0b7fc21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf)) +- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4)) +- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e)) ### Updates @@ -18,6 +20,10 @@ - Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239)) - Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4)) - Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235)) +- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8)) +- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac)) +- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c)) +- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From 402a4607c93b1f4fd7998db520f848633ece8f26 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 04:51:56 -0700 Subject: [PATCH 478/977] Update Inbox, fix flag validation condition, allow profile reports --- app/Util/ActivityPub/Inbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index ed6b964e8..4ab87f40b 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -1283,7 +1283,7 @@ class Inbox } } - if(!$accountId || !$objects->count()) { + if(!$accountId && !$objects->count()) { return; } From 542d110673fb2e5fe0506de931f1ff16c86c24ac Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 04:59:13 -0700 Subject: [PATCH 479/977] Update AccountTransformer, fix follower/following count visibility bug --- app/Http/Controllers/Api/ApiV1Controller.php | 2 ++ .../Controllers/Settings/PrivacySettings.php | 17 ++++++----- app/Transformer/Api/AccountTransformer.php | 30 +++++++++++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 0205648e8..c8728bcf4 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -409,6 +409,7 @@ class ApiV1Controller extends Controller if($settings->show_profile_follower_count != $show_profile_follower_count) { $settings->show_profile_follower_count = $show_profile_follower_count; $changes = true; + Cache::forget('pf:acct-trans:hideFollowers:' . $profile->id); } } @@ -417,6 +418,7 @@ class ApiV1Controller extends Controller if($settings->show_profile_following_count != $show_profile_following_count) { $settings->show_profile_following_count = $show_profile_following_count; $changes = true; + Cache::forget('pf:acct-trans:hideFollowing:' . $profile->id); } } diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php index bd2222d48..6697bf3c8 100644 --- a/app/Http/Controllers/Settings/PrivacySettings.php +++ b/app/Http/Controllers/Settings/PrivacySettings.php @@ -84,14 +84,17 @@ trait PrivacySettings } $settings->save(); } - Cache::forget('profile:settings:' . $profile->id); + $pid = $profile->id; + Cache::forget('profile:settings:' . $pid); Cache::forget('user:account:id:' . $profile->user_id); - Cache::forget('profile:follower_count:' . $profile->id); - Cache::forget('profile:following_count:' . $profile->id); - Cache::forget('profile:atom:enabled:' . $profile->id); - Cache::forget('profile:embed:' . $profile->id); - Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id); - Cache::forget('pf:acct:settings:hidden-following:' . $profile->id); + Cache::forget('profile:follower_count:' . $pid); + Cache::forget('profile:following_count:' . $pid); + Cache::forget('profile:atom:enabled:' . $pid); + Cache::forget('profile:embed:' . $pid); + Cache::forget('pf:acct:settings:hidden-followers:' . $pid); + Cache::forget('pf:acct:settings:hidden-following:' . $pid); + Cache::forget('pf:acct-trans:hideFollowing:' . $pid); + Cache::forget('pf:acct-trans:hideFollowers:' . $pid); return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!'); } diff --git a/app/Transformer/Api/AccountTransformer.php b/app/Transformer/Api/AccountTransformer.php index 6c6fa17e4..52026eb8e 100644 --- a/app/Transformer/Api/AccountTransformer.php +++ b/app/Transformer/Api/AccountTransformer.php @@ -6,14 +6,15 @@ use Auth; use Cache; use App\Profile; use App\User; +use App\UserSetting; use League\Fractal; use App\Services\PronounService; class AccountTransformer extends Fractal\TransformerAbstract { - protected $defaultIncludes = [ - // 'relationship', - ]; + protected $defaultIncludes = [ + // 'relationship', + ]; public function transform(Profile $profile) { @@ -26,6 +27,25 @@ class AccountTransformer extends Fractal\TransformerAbstract }); $local = $profile->private_key != null; + $local = $profile->user_id && $profile->private_key != null; + $hideFollowing = false; + $hideFollowers = false; + if($local) { + $hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:' . $profile->id, 2592000, function() use($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if(!$settings) { + return false; + } + return $settings->show_profile_following_count == false; + }); + $hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:' . $profile->id, 2592000, function() use($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if(!$settings) { + return false; + } + return $settings->show_profile_follower_count == false; + }); + } $is_admin = !$local ? false : in_array($profile->id, $adminIds); $acct = $local ? $profile->username : substr($profile->username, 1); $username = $local ? $profile->username : explode('@', $acct)[0]; @@ -36,8 +56,8 @@ class AccountTransformer extends Fractal\TransformerAbstract 'display_name' => $profile->name, 'discoverable' => true, 'locked' => (bool) $profile->is_private, - 'followers_count' => (int) $profile->followers_count, - 'following_count' => (int) $profile->following_count, + 'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count, + 'following_count' => $hideFollowing ? 0 : (int) $profile->following_count, 'statuses_count' => (int) $profile->status_count, 'note' => $profile->bio ?? '', 'note_text' => $profile->bio ? strip_tags($profile->bio) : null, From d5a6d9cc8d25af908eb17364f57e7ab734131588 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 29 Feb 2024 05:00:44 -0700 Subject: [PATCH 480/977] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0b7fc21..f8b4fde43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ - Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac)) - Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c)) - Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e)) +- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607)) +- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) From f8145a78cf6748aeb92b8ab06389db0200ba11eb Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 2 Mar 2024 04:21:04 -0700 Subject: [PATCH 481/977] Add Profile Migrations --- .../Controllers/ProfileAliasController.php | 51 +++++-- .../ProfileMigrationController.php | 62 ++++++++ .../Requests/ProfileMigrationStoreRequest.php | 76 ++++++++++ .../ProfileMigrationMoveFollowersPipeline.php | 55 +++++++ app/Models/ProfileAlias.php | 2 + app/Models/ProfileMigration.php | 19 +++ app/Services/FetchCacheService.php | 79 ++++++++++ app/Services/WebfingerService.php | 132 +++++++++------- app/Transformer/Api/AccountTransformer.php | 141 ++++++++++-------- app/Util/Webfinger/WebfingerUrl.php | 14 +- ...094235_create_profile_migrations_table.php | 31 ++++ resources/assets/components/Profile.vue | 38 ++++- .../partials/profile/ProfileSidebar.vue | 23 +++ resources/views/settings/home.blade.php | 8 + .../views/settings/migration/index.blade.php | 99 ++++++++++++ routes/web.php | 5 + 16 files changed, 703 insertions(+), 132 deletions(-) create mode 100644 app/Http/Controllers/ProfileMigrationController.php create mode 100644 app/Http/Requests/ProfileMigrationStoreRequest.php create mode 100644 app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php create mode 100644 app/Models/ProfileMigration.php create mode 100644 app/Services/FetchCacheService.php create mode 100644 database/migrations/2024_03_02_094235_create_profile_migrations_table.php create mode 100644 resources/views/settings/migration/index.blade.php diff --git a/app/Http/Controllers/ProfileAliasController.php b/app/Http/Controllers/ProfileAliasController.php index 024005a8e..559dcb9a6 100644 --- a/app/Http/Controllers/ProfileAliasController.php +++ b/app/Http/Controllers/ProfileAliasController.php @@ -2,11 +2,13 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Util\Lexer\Nickname; -use App\Util\Webfinger\WebfingerUrl; use App\Models\ProfileAlias; +use App\Models\ProfileMigration; +use App\Services\AccountService; use App\Services\WebfingerService; +use App\Util\Lexer\Nickname; +use Cache; +use Illuminate\Http\Request; class ProfileAliasController extends Controller { @@ -18,31 +20,47 @@ class ProfileAliasController extends Controller public function index(Request $request) { $aliases = $request->user()->profile->aliases; + return view('settings.aliases.index', compact('aliases')); } public function store(Request $request) { $this->validate($request, [ - 'acct' => 'required' + 'acct' => 'required', ]); $acct = $request->input('acct'); - if($request->user()->profile->aliases->count() >= 3) { + $nn = Nickname::normalizeProfileUrl($acct); + if (! $nn) { + return back()->with('error', 'Invalid account alias.'); + } + + if ($nn['domain'] === config('pixelfed.domain.app')) { + if (strtolower($nn['username']) == ($request->user()->username)) { + return back()->with('error', 'You cannot add an alias to your own account.'); + } + } + + if ($request->user()->profile->aliases->count() >= 3) { return back()->with('error', 'You can only add 3 account aliases.'); } $webfingerService = WebfingerService::lookup($acct); - if(!$webfingerService || !isset($webfingerService['url'])) { + $webfingerUrl = WebfingerService::rawGet($acct); + + if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) { return back()->with('error', 'Invalid account, cannot add alias at this time.'); } $alias = new ProfileAlias; $alias->profile_id = $request->user()->profile_id; $alias->acct = $acct; - $alias->uri = $webfingerService['url']; + $alias->uri = $webfingerUrl; $alias->save(); + Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id); + return back()->with('status', 'Successfully added alias!'); } @@ -50,14 +68,25 @@ class ProfileAliasController extends Controller { $this->validate($request, [ 'acct' => 'required', - 'id' => 'required|exists:profile_aliases' + 'id' => 'required|exists:profile_aliases', ]); - - $alias = ProfileAlias::where('profile_id', $request->user()->profile_id) - ->where('acct', $request->input('acct')) + $pid = $request->user()->profile_id; + $acct = $request->input('acct'); + $alias = ProfileAlias::where('profile_id', $pid) + ->where('acct', $acct) ->findOrFail($request->input('id')); + $migration = ProfileMigration::whereProfileId($pid) + ->whereAcct($acct) + ->first(); + if ($migration) { + $request->user()->profile->update([ + 'moved_to_profile_id' => null, + ]); + } $alias->delete(); + Cache::forget('pf:activitypub:user-object:by-id:'.$pid); + AccountService::del($pid); return back()->with('status', 'Successfully deleted alias!'); } diff --git a/app/Http/Controllers/ProfileMigrationController.php b/app/Http/Controllers/ProfileMigrationController.php new file mode 100644 index 000000000..d9158b9a3 --- /dev/null +++ b/app/Http/Controllers/ProfileMigrationController.php @@ -0,0 +1,62 @@ +middleware('auth'); + } + + public function index(Request $request) + { + $hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id) + ->where('created_at', '>', now()->subDays(30)) + ->exists(); + + return view('settings.migration.index', compact('hasExistingMigration')); + } + + public function store(ProfileMigrationStoreRequest $request) + { + $acct = WebfingerService::rawGet($request->safe()->acct); + if (! $acct) { + return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']); + } + $newAccount = Helpers::profileFetch($acct); + if (! $newAccount) { + return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']); + } + $user = $request->user(); + ProfileAlias::updateOrCreate([ + 'profile_id' => $user->profile_id, + 'acct' => $request->safe()->acct, + 'uri' => $acct, + ]); + ProfileMigration::create([ + 'profile_id' => $request->user()->profile_id, + 'acct' => $request->safe()->acct, + 'followers_count' => $request->user()->profile->followers_count, + 'target_profile_id' => $newAccount['id'], + ]); + $user->profile->update([ + 'moved_to_profile_id' => $newAccount->id, + 'indexable' => false, + ]); + AccountService::del($user->profile_id); + + ProfileMigrationMoveFollowersPipeline::dispatch($user->profile_id, $newAccount->id); + + return redirect()->back()->with(['status' => 'Succesfully migrated account!']); + } +} diff --git a/app/Http/Requests/ProfileMigrationStoreRequest.php b/app/Http/Requests/ProfileMigrationStoreRequest.php new file mode 100644 index 000000000..de9e31b8a --- /dev/null +++ b/app/Http/Requests/ProfileMigrationStoreRequest.php @@ -0,0 +1,76 @@ +user() || $this->user()->status) { + return false; + } + + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'acct' => 'required|email', + 'password' => 'required|current_password', + ]; + } + + public function after(): array + { + return [ + function (Validator $validator) { + $err = $this->validateNewAccount(); + if ($err !== 'noerr') { + $validator->errors()->add( + 'acct', + $err + ); + } + }, + ]; + } + + protected function validateNewAccount() + { + if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) { + return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.'; + } + $acct = WebfingerService::rawGet($this->acct); + if (! $acct) { + return 'The new account you provided is not responding to our requests.'; + } + $pr = FetchCacheService::getJson($acct); + if (! $pr || ! isset($pr['alsoKnownAs'])) { + return 'Invalid account lookup response.'; + } + if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) { + return 'The new account does not contain an alias to your current account.'; + } + $curAcctUrl = $this->user()->profile->permalink(); + if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) { + return 'The new account does not contain an alias to your current account.'; + } + + return 'noerr'; + } +} diff --git a/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php b/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php new file mode 100644 index 000000000..f5c311888 --- /dev/null +++ b/app/Jobs/ProfilePipeline/ProfileMigrationMoveFollowersPipeline.php @@ -0,0 +1,55 @@ +oldPid = $oldPid; + $this->newPid = $newPid; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $og = Profile::find($this->oldPid); + $ne = Profile::find($this->newPid); + if(!$og || !$ne || $og == $ne) { + return; + } + $ne->followers_count = $og->followers_count; + $ne->save(); + $og->followers_count = 0; + $og->save(); + foreach (Follower::whereFollowingId($this->oldPid)->lazyById(200, 'id') as $follower) { + try { + $follower->following_id = $this->newPid; + $follower->save(); + } catch (Exception $e) { + $follower->delete(); + } + } + AccountService::del($this->oldPid); + AccountService::del($this->newPid); + } +} diff --git a/app/Models/ProfileAlias.php b/app/Models/ProfileAlias.php index b7a3bdc9c..aef91bebc 100644 --- a/app/Models/ProfileAlias.php +++ b/app/Models/ProfileAlias.php @@ -10,6 +10,8 @@ class ProfileAlias extends Model { use HasFactory; + protected $guarded = []; + public function profile() { return $this->belongsTo(Profile::class); diff --git a/app/Models/ProfileMigration.php b/app/Models/ProfileMigration.php new file mode 100644 index 000000000..a40d52c95 --- /dev/null +++ b/app/Models/ProfileMigration.php @@ -0,0 +1,19 @@ +belongsTo(Profile::class, 'profile_id'); + } +} diff --git a/app/Services/FetchCacheService.php b/app/Services/FetchCacheService.php new file mode 100644 index 000000000..2e23fb009 --- /dev/null +++ b/app/Services/FetchCacheService.php @@ -0,0 +1,79 @@ + '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')', + ]; + + if ($allowRedirects) { + $options = [ + 'allow_redirects' => [ + 'max' => 2, + 'strict' => true, + ], + ]; + } else { + $options = [ + 'allow_redirects' => false, + ]; + } + try { + $res = Http::withOptions($options) + ->retry(3, function (int $attempt, $exception) { + return $attempt * 500; + }) + ->acceptJson() + ->withHeaders($headers) + ->timeout(40) + ->get($url); + } catch (RequestException $e) { + Cache::put($key, 1, $ttl); + + return false; + } catch (ConnectionException $e) { + Cache::put($key, 1, $ttl); + + return false; + } catch (Exception $e) { + Cache::put($key, 1, $ttl); + + return false; + } + + if (! $res->ok()) { + Cache::put($key, 1, $ttl); + + return false; + } + + return $res->json(); + } +} diff --git a/app/Services/WebfingerService.php b/app/Services/WebfingerService.php index 385bff023..7340109f5 100644 --- a/app/Services/WebfingerService.php +++ b/app/Services/WebfingerService.php @@ -2,69 +2,95 @@ namespace App\Services; -use Cache; use App\Profile; +use App\Util\ActivityPub\Helpers; use App\Util\Webfinger\WebfingerUrl; use Illuminate\Support\Facades\Http; -use App\Util\ActivityPub\Helpers; -use App\Services\AccountService; class WebfingerService { - public static function lookup($query, $mastodonMode = false) - { - return (new self)->run($query, $mastodonMode); - } + public static function rawGet($url) + { + $n = WebfingerUrl::get($url); + if (! $n) { + return false; + } + $webfinger = FetchCacheService::getJson($n); + if (! $webfinger) { + return false; + } - protected function run($query, $mastodonMode) - { - if($profile = Profile::whereUsername($query)->first()) { - return $mastodonMode ? - AccountService::getMastodon($profile->id, true) : - AccountService::get($profile->id); - } - $url = WebfingerUrl::generateWebfingerUrl($query); - if(!Helpers::validateUrl($url)) { - return []; - } + if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) { + return false; + } + $link = collect($webfinger['links']) + ->filter(function ($link) { + return $link && + isset($link['rel'], $link['type'], $link['href']) && + $link['rel'] === 'self' && + in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); + }) + ->pluck('href') + ->first(); - try { - $res = Http::retry(3, 100) - ->acceptJson() - ->withHeaders([ - 'User-Agent' => '(Pixelfed/' . config('pixelfed.version') . '; +' . config('app.url') . ')' - ]) - ->timeout(20) - ->get($url); - } catch (\Illuminate\Http\Client\ConnectionException $e) { - return []; - } + return $link; + } - if(!$res->successful()) { - return []; - } + public static function lookup($query, $mastodonMode = false) + { + return (new self)->run($query, $mastodonMode); + } - $webfinger = $res->json(); - if(!isset($webfinger['links']) || !is_array($webfinger['links']) || empty($webfinger['links'])) { - return []; - } + protected function run($query, $mastodonMode) + { + if ($profile = Profile::whereUsername($query)->first()) { + return $mastodonMode ? + AccountService::getMastodon($profile->id, true) : + AccountService::get($profile->id); + } + $url = WebfingerUrl::generateWebfingerUrl($query); + if (! Helpers::validateUrl($url)) { + return []; + } - $link = collect($webfinger['links']) - ->filter(function($link) { - return $link && - isset($link['rel'], $link['type'], $link['href']) && - $link['rel'] === 'self' && - in_array($link['type'], ['application/activity+json','application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); - }) - ->pluck('href') - ->first(); + try { + $res = Http::retry(3, 100) + ->acceptJson() + ->withHeaders([ + 'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')', + ]) + ->timeout(20) + ->get($url); + } catch (\Illuminate\Http\Client\ConnectionException $e) { + return []; + } - $profile = Helpers::profileFetch($link); - if(!$profile) { - return; - } - return $mastodonMode ? - AccountService::getMastodon($profile->id, true) : - AccountService::get($profile->id); - } + if (! $res->successful()) { + return []; + } + + $webfinger = $res->json(); + if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) { + return []; + } + + $link = collect($webfinger['links']) + ->filter(function ($link) { + return $link && + isset($link['rel'], $link['type'], $link['href']) && + $link['rel'] === 'self' && + in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']); + }) + ->pluck('href') + ->first(); + + $profile = Helpers::profileFetch($link); + if (! $profile) { + return; + } + + return $mastodonMode ? + AccountService::getMastodon($profile->id, true) : + AccountService::get($profile->id); + } } diff --git a/app/Transformer/Api/AccountTransformer.php b/app/Transformer/Api/AccountTransformer.php index 52026eb8e..9411d5a18 100644 --- a/app/Transformer/Api/AccountTransformer.php +++ b/app/Transformer/Api/AccountTransformer.php @@ -2,80 +2,91 @@ namespace App\Transformer\Api; -use Auth; -use Cache; use App\Profile; +use App\Services\AccountService; +use App\Services\PronounService; use App\User; use App\UserSetting; +use Cache; use League\Fractal; -use App\Services\PronounService; class AccountTransformer extends Fractal\TransformerAbstract { - protected $defaultIncludes = [ - // 'relationship', - ]; + protected $defaultIncludes = [ + // 'relationship', + ]; - public function transform(Profile $profile) - { - if(!$profile) { - return []; - } + public function transform(Profile $profile) + { + if (! $profile) { + return []; + } - $adminIds = Cache::remember('pf:admin-ids', 604800, function() { - return User::whereIsAdmin(true)->pluck('profile_id')->toArray(); - }); + $adminIds = Cache::remember('pf:admin-ids', 604800, function () { + return User::whereIsAdmin(true)->pluck('profile_id')->toArray(); + }); - $local = $profile->private_key != null; - $local = $profile->user_id && $profile->private_key != null; - $hideFollowing = false; - $hideFollowers = false; - if($local) { - $hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:' . $profile->id, 2592000, function() use($profile) { - $settings = UserSetting::whereUserId($profile->user_id)->first(); - if(!$settings) { - return false; - } - return $settings->show_profile_following_count == false; - }); - $hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:' . $profile->id, 2592000, function() use($profile) { - $settings = UserSetting::whereUserId($profile->user_id)->first(); - if(!$settings) { - return false; - } - return $settings->show_profile_follower_count == false; - }); - } - $is_admin = !$local ? false : in_array($profile->id, $adminIds); - $acct = $local ? $profile->username : substr($profile->username, 1); - $username = $local ? $profile->username : explode('@', $acct)[0]; - return [ - 'id' => (string) $profile->id, - 'username' => $username, - 'acct' => $acct, - 'display_name' => $profile->name, - 'discoverable' => true, - 'locked' => (bool) $profile->is_private, - 'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count, - 'following_count' => $hideFollowing ? 0 : (int) $profile->following_count, - 'statuses_count' => (int) $profile->status_count, - 'note' => $profile->bio ?? '', - 'note_text' => $profile->bio ? strip_tags($profile->bio) : null, - 'url' => $profile->url(), - 'avatar' => $profile->avatarUrl(), - 'website' => $profile->website, - 'local' => (bool) $local, - 'is_admin' => (bool) $is_admin, - 'created_at' => $profile->created_at->toJSON(), - 'header_bg' => $profile->header_bg, - 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(), - 'pronouns' => PronounService::get($profile->id), - 'location' => $profile->location - ]; - } + $local = $profile->private_key != null; + $local = $profile->user_id && $profile->private_key != null; + $hideFollowing = false; + $hideFollowers = false; + if ($local) { + $hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:'.$profile->id, 2592000, function () use ($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if (! $settings) { + return false; + } - protected function includeRelationship(Profile $profile) - { - return $this->item($profile, new RelationshipTransformer()); - } + return $settings->show_profile_following_count == false; + }); + $hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:'.$profile->id, 2592000, function () use ($profile) { + $settings = UserSetting::whereUserId($profile->user_id)->first(); + if (! $settings) { + return false; + } + + return $settings->show_profile_follower_count == false; + }); + } + $is_admin = ! $local ? false : in_array($profile->id, $adminIds); + $acct = $local ? $profile->username : substr($profile->username, 1); + $username = $local ? $profile->username : explode('@', $acct)[0]; + $res = [ + 'id' => (string) $profile->id, + 'username' => $username, + 'acct' => $acct, + 'display_name' => $profile->name, + 'discoverable' => true, + 'locked' => (bool) $profile->is_private, + 'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count, + 'following_count' => $hideFollowing ? 0 : (int) $profile->following_count, + 'statuses_count' => (int) $profile->status_count, + 'note' => $profile->bio ?? '', + 'note_text' => $profile->bio ? strip_tags($profile->bio) : null, + 'url' => $profile->url(), + 'avatar' => $profile->avatarUrl(), + 'website' => $profile->website, + 'local' => (bool) $local, + 'is_admin' => (bool) $is_admin, + 'created_at' => $profile->created_at->toJSON(), + 'header_bg' => $profile->header_bg, + 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(), + 'pronouns' => PronounService::get($profile->id), + 'location' => $profile->location, + ]; + + if ($profile->moved_to_profile_id) { + $mt = AccountService::getMastodon($profile->moved_to_profile_id, true); + if ($mt) { + $res['moved'] = $mt; + } + } + + return $res; + } + + protected function includeRelationship(Profile $profile) + { + return $this->item($profile, new RelationshipTransformer()); + } } diff --git a/app/Util/Webfinger/WebfingerUrl.php b/app/Util/Webfinger/WebfingerUrl.php index 091d7ce14..b51d536d9 100644 --- a/app/Util/Webfinger/WebfingerUrl.php +++ b/app/Util/Webfinger/WebfingerUrl.php @@ -3,16 +3,28 @@ namespace App\Util\Webfinger; use App\Util\Lexer\Nickname; +use App\Services\InstanceService; class WebfingerUrl { + public static function get($url) + { + $n = Nickname::normalizeProfileUrl($url); + if(!$n || !isset($n['domain'], $n['username'])) { + return false; + } + if(in_array($n['domain'], InstanceService::getBannedDomains())) { + return false; + } + return 'https://' . $n['domain'] . '/.well-known/webfinger?resource=acct:' . $n['username'] . '@' . $n['domain']; + } + public static function generateWebfingerUrl($url) { $url = Nickname::normalizeProfileUrl($url); $domain = $url['domain']; $username = $url['username']; $path = "https://{$domain}/.well-known/webfinger?resource=acct:{$username}@{$domain}"; - return $path; } } diff --git a/database/migrations/2024_03_02_094235_create_profile_migrations_table.php b/database/migrations/2024_03_02_094235_create_profile_migrations_table.php new file mode 100644 index 000000000..2eafaa43b --- /dev/null +++ b/database/migrations/2024_03_02_094235_create_profile_migrations_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('profile_id'); + $table->string('acct')->nullable(); + $table->unsignedBigInteger('followers_count')->default(0); + $table->unsignedBigInteger('target_profile_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('profile_migrations'); + } +}; diff --git a/resources/assets/components/Profile.vue b/resources/assets/components/Profile.vue index 1dc90d0ba..f526109e4 100644 --- a/resources/assets/components/Profile.vue +++ b/resources/assets/components/Profile.vue @@ -1,7 +1,40 @@