Browse Source

Merge branch 'staging' into dev

daniel 6 months ago
parent
commit
3dd515006c
100 changed files with 1886 additions and 866 deletions
  1. 19 0
      CHANGELOG.md
  2. 1 0
      README.md
  3. 1 1
      app/Auth/BearerTokenResponse.php
  4. 5 3
      app/Console/Commands/CatchUnoptimizedMedia.php
  5. 1 1
      app/Http/Controllers/Admin/AdminSettingsController.php
  6. 9 1
      app/Http/Controllers/Api/ApiV1Controller.php
  7. 4 6
      app/Http/Controllers/Api/ApiV1Dot1Controller.php
  8. 1 1
      app/Http/Controllers/CollectionController.php
  9. 1 0
      app/Http/Controllers/CommentController.php
  10. 7 2
      app/Http/Controllers/ComposeController.php
  11. 99 0
      app/Http/Controllers/OAuth/OobAuthorizationController.php
  12. 3 1
      app/Http/Controllers/StatusController.php
  13. 2 1
      app/Http/Middleware/VerifyCsrfToken.php
  14. 106 71
      app/Jobs/StatusPipeline/StatusTagsPipeline.php
  15. 1 0
      app/Media.php
  16. 86 68
      app/Providers/AppServiceProvider.php
  17. 1 0
      app/Providers/AuthServiceProvider.php
  18. 1 1
      app/Services/ImportService.php
  19. 455 307
      app/Util/ActivityPub/Helpers.php
  20. 8 3
      app/Util/ActivityPub/HttpSignature.php
  21. 1 0
      composer.json
  22. 389 165
      composer.lock
  23. 19 1
      config/cache.php
  24. 18 0
      config/database.php
  25. 4 0
      config/media.php
  26. 236 0
      config/pulse.php
  27. 2 0
      config/services.php
  28. 1 1
      database/migrations/2018_09_30_051108_create_direct_messages_table.php
  29. 84 0
      database/migrations/2023_06_07_000001_create_pulse_tables.php
  30. 4 0
      database/migrations/2024_05_20_062706_update_group_posts_table.php
  31. 1 1
      docker-compose.yml
  32. 315 231
      package-lock.json
  33. 1 0
      package.json
  34. BIN
      public/_lang/af.json
  35. BIN
      public/_lang/ar.json
  36. BIN
      public/_lang/bn.json
  37. BIN
      public/_lang/bs.json
  38. BIN
      public/_lang/ca.json
  39. BIN
      public/_lang/cs.json
  40. BIN
      public/_lang/cy.json
  41. BIN
      public/_lang/da.json
  42. BIN
      public/_lang/de.json
  43. BIN
      public/_lang/el.json
  44. BIN
      public/_lang/es.json
  45. BIN
      public/_lang/eu.json
  46. BIN
      public/_lang/fa.json
  47. BIN
      public/_lang/fi.json
  48. BIN
      public/_lang/fr.json
  49. BIN
      public/_lang/gd.json
  50. BIN
      public/_lang/gl.json
  51. BIN
      public/_lang/he.json
  52. BIN
      public/_lang/hi.json
  53. BIN
      public/_lang/hr.json
  54. BIN
      public/_lang/hu.json
  55. BIN
      public/_lang/id.json
  56. BIN
      public/_lang/it.json
  57. BIN
      public/_lang/ja.json
  58. BIN
      public/_lang/ko.json
  59. BIN
      public/_lang/me.json
  60. BIN
      public/_lang/mk.json
  61. BIN
      public/_lang/nl.json
  62. BIN
      public/_lang/no.json
  63. BIN
      public/_lang/oc.json
  64. BIN
      public/_lang/pl.json
  65. BIN
      public/_lang/pt.json
  66. BIN
      public/_lang/ro.json
  67. BIN
      public/_lang/ru.json
  68. BIN
      public/_lang/sk.json
  69. BIN
      public/_lang/sr.json
  70. BIN
      public/_lang/sv.json
  71. BIN
      public/_lang/th.json
  72. BIN
      public/_lang/tr.json
  73. BIN
      public/_lang/vi.json
  74. BIN
      public/_lang/zh.json
  75. 0 0
      public/js/changelog.bundle.d40f01eba00c9885.js
  76. BIN
      public/js/compose.chunk.b06beb250e24db17.js
  77. 0 0
      public/js/compose.chunk.b06beb250e24db17.js.LICENSE.txt
  78. BIN
      public/js/compose.chunk.e1f297b242137d23.js
  79. BIN
      public/js/compose.js
  80. 0 0
      public/js/daci.chunk.61b540b1630f8445.js
  81. 0 0
      public/js/discover.chunk.00d9b5656d32080e.js
  82. 0 0
      public/js/discover~findfriends.chunk.6d494abb9e464081.js
  83. 0 0
      public/js/discover~hashtag.bundle.7455573dc9d2be1f.js
  84. 0 0
      public/js/discover~memories.chunk.9541b66de9d5d907.js
  85. 0 0
      public/js/discover~myhashtags.chunk.e2ca0db60346d0c2.js
  86. 0 0
      public/js/discover~serverfeed.chunk.138d9d53d1debac1.js
  87. 0 0
      public/js/discover~settings.chunk.b1b5642ccef06123.js
  88. 0 0
      public/js/dms.chunk.1a2a644df5c78346.js
  89. 0 0
      public/js/dms~message.chunk.4e68bb824f396d86.js
  90. 0 0
      public/js/error404.bundle.e2f43f5006962e80.js
  91. 0 0
      public/js/group.create.72c3a1e5c1dc00dc.js
  92. 0 0
      public/js/groups-page-about.76a616aa7e1a367b.js
  93. 0 0
      public/js/groups-page-media.056a7bbc46b79034.js
  94. 0 0
      public/js/groups-page-members.a8ea4f209fcbe238.js
  95. 0 0
      public/js/groups-page-topics.f69667c933f7d122.js
  96. 0 0
      public/js/groups-page.d484dab549a033ca.js
  97. 0 0
      public/js/groups-post.4c3d4860b029bbaf.js
  98. 0 0
      public/js/groups-profile.1bb8be935d1f108a.js
  99. BIN
      public/js/home.chunk.acf96f52790bffa6.js
  100. 0 0
      public/js/home.chunk.acf96f52790bffa6.js.LICENSE.txt

+ 19 - 0
CHANGELOG.md

@@ -2,6 +2,12 @@
 
 
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
 
 
+### Features
+- WebGL photo filters ([#5374](https://github.com/pixelfed/pixelfed/pull/5374))
+
+### OAuth
+- Fix oauth oob (urn:ietf:wg:oauth:2.0:oob) support. ([8afbdb03](https://github.com/pixelfed/pixelfed/commit/8afbdb03))
+
 ### Updates
 ### Updates
 - Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849))
 - Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849))
 - Update DirectMessage API, fix broken threading ([044d410c](https://github.com/pixelfed/pixelfed/commit/044d410c))
 - Update DirectMessage API, fix broken threading ([044d410c](https://github.com/pixelfed/pixelfed/commit/044d410c))
@@ -12,6 +18,19 @@
 - Update DirectMessageController, remove 72h limit for admins ([639df410](https://github.com/pixelfed/pixelfed/commit/639df410))
 - Update DirectMessageController, remove 72h limit for admins ([639df410](https://github.com/pixelfed/pixelfed/commit/639df410))
 - Update StatusService, fix newlines ([56c07b7a](https://github.com/pixelfed/pixelfed/commit/56c07b7a))
 - Update StatusService, fix newlines ([56c07b7a](https://github.com/pixelfed/pixelfed/commit/56c07b7a))
 - Update confirm email template, add plaintext link. Fixes #5375 ([45986707](https://github.com/pixelfed/pixelfed/commit/45986707))
 - Update confirm email template, add plaintext link. Fixes #5375 ([45986707](https://github.com/pixelfed/pixelfed/commit/45986707))
+- Update UserVerifyEmail command ([77da9ad8](https://github.com/pixelfed/pixelfed/commit/77da9ad8))
+- Update StatusStatelessTransformer, refactor the caption field to be compliant with the MastoAPI. Fixes #5364 ([79039ba5](https://github.com/pixelfed/pixelfed/commit/79039ba5))
+- Update mailgun config, add endpoint and scheme ([271d5114](https://github.com/pixelfed/pixelfed/commit/271d5114))
+- Update search and status logic to fix postgres bugs ([8c39ef4](https://github.com/pixelfed/pixelfed/commit/8c39ef4))
+- Update db, fix sqlite migrations ([#5379](https://github.com/pixelfed/pixelfed/pull/5379))
+- Update CatchUnoptimizedMedia command, make 1hr limit opt-in ([99b15b73](https://github.com/pixelfed/pixelfed/commit/99b15b73))
+- Update IG, fix Instagram import. Closes #5411 ([fd434aec](https://github.com/pixelfed/pixelfed/commit/fd434aec))
+- Update StatusTagsPipeline, fix hashtag bug and formatting ([d516b799](https://github.com/pixelfed/pixelfed/commit/d516b799))
+- Update CollectionController, fix showCollection signature ([4e1dd599](https://github.com/pixelfed/pixelfed/commit/4e1dd599))
+- Update ApiV1Dot1Controller, fix in-app registration ([56f17b99](https://github.com/pixelfed/pixelfed/commit/56f17b99))
+- Update VerifyCsrfToken middleware, add oauth token. Fixes #5426 ([79ebbc2d](https://github.com/pixelfed/pixelfed/commit/79ebbc2d))
+- Update AdminSettingsController, increase max photo size limit from 50MB to 1GB ([aa448354](https://github.com/pixelfed/pixelfed/commit/aa448354))
+- Update BearerTokenResponse, return scopes in /oauth/token endpoint. Fixes #5286 ([d8f5c302](https://github.com/pixelfed/pixelfed/commit/d8f5c302))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 
 ## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)
 ## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)

+ 1 - 0
README.md

@@ -4,6 +4,7 @@
 <a href="https://circleci.com/gh/pixelfed/pixelfed"><img src="https://circleci.com/gh/pixelfed/pixelfed.svg?style=svg" alt="Build Status"></a>
 <a href="https://circleci.com/gh/pixelfed/pixelfed"><img src="https://circleci.com/gh/pixelfed/pixelfed.svg?style=svg" alt="Build Status"></a>
 <a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/v/stable.svg" alt="Latest Stable Version"></a>
 <a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/v/stable.svg" alt="Latest Stable Version"></a>
 <a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/license.svg" alt="License"></a>
 <a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/license.svg" alt="License"></a>
+<a title="Crowdin" target="_blank" href="https://crowdin.com/project/pixelfed"><img src="https://badges.crowdin.net/pixelfed/localized.svg"></a>
 </p>
 </p>
 
 
 ## Introduction
 ## Introduction

+ 1 - 1
app/Auth/BearerTokenResponse.php

@@ -11,13 +11,13 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
      * AuthorizationServer::getResponseType() to pull in your version of
      * AuthorizationServer::getResponseType() to pull in your version of
      * this class rather than the default.
      * this class rather than the default.
      *
      *
-     * @param AccessTokenEntityInterface $accessToken
      *
      *
      * @return array
      * @return array
      */
      */
     protected function getExtraParams(AccessTokenEntityInterface $accessToken)
     protected function getExtraParams(AccessTokenEntityInterface $accessToken)
     {
     {
         return [
         return [
+            'scope' => array_map(fn ($scope) => $scope->getIdentifier(), $accessToken->getScopes()),
             'created_at' => time(),
             'created_at' => time(),
         ];
         ];
     }
     }

+ 5 - 3
app/Console/Commands/CatchUnoptimizedMedia.php

@@ -40,10 +40,11 @@ class CatchUnoptimizedMedia extends Command
      */
      */
     public function handle()
     public function handle()
     {
     {
+        $hasLimit = (bool) config('media.image_optimize.catch_unoptimized_media_hour_limit');
         Media::whereNull('processed_at')
         Media::whereNull('processed_at')
-            ->where('created_at', '>', now()->subHours(1))
-            ->where('skip_optimize', '!=', true)
-            ->whereNull('remote_url')
+            ->when($hasLimit, function($q, $hasLimit) {
+                $q->where('created_at', '>', now()->subHours(1));
+            })->whereNull('remote_url')
             ->whereNotNull('status_id')
             ->whereNotNull('status_id')
             ->whereNotNull('media_path')
             ->whereNotNull('media_path')
             ->whereIn('mime', [
             ->whereIn('mime', [
@@ -52,6 +53,7 @@ class CatchUnoptimizedMedia extends Command
             ])
             ])
             ->chunk(50, function($medias) {
             ->chunk(50, function($medias) {
                 foreach ($medias as $media) {
                 foreach ($medias as $media) {
+					if ($media->skip_optimize) continue;
                     ImageOptimize::dispatch($media);
                     ImageOptimize::dispatch($media);
                 }
                 }
             });
             });

+ 1 - 1
app/Http/Controllers/Admin/AdminSettingsController.php

@@ -600,7 +600,7 @@ trait AdminSettingsController
         $this->validate($request, [
         $this->validate($request, [
             'image_quality' => 'required|integer|min:1|max:100',
             'image_quality' => 'required|integer|min:1|max:100',
             'max_album_length' => 'required|integer|min:1|max:20',
             'max_album_length' => 'required|integer|min:1|max:20',
-            'max_photo_size' => 'required|integer|min:100|max:50000',
+            'max_photo_size' => 'required|integer|min:100|max:1000000',
             'media_types' => 'required',
             'media_types' => 'required',
             'optimize_image' => 'required',
             'optimize_image' => 'required',
             'optimize_video' => 'required',
             'optimize_video' => 'required',

+ 9 - 1
app/Http/Controllers/Api/ApiV1Controller.php

@@ -137,7 +137,10 @@ class ApiV1Controller extends Controller
             'redirect_uris' => 'required',
             'redirect_uris' => 'required',
         ]);
         ]);
 
 
-        $uris = implode(',', explode('\n', $request->redirect_uris));
+        $uris = collect(explode("\n", $request->redirect_uris))
+            ->map('urldecode')
+            ->filter()
+            ->join(',');
 
 
         $client = Passport::client()->forceFill([
         $client = Passport::client()->forceFill([
             'user_id' => null,
             'user_id' => null,
@@ -3509,6 +3512,7 @@ class ApiV1Controller extends Controller
 
 
             $status = new Status;
             $status = new Status;
             $status->caption = $content;
             $status->caption = $content;
+            $status->rendered = $defaultCaption;
             $status->scope = $visibility;
             $status->scope = $visibility;
             $status->visibility = $visibility;
             $status->visibility = $visibility;
             $status->profile_id = $user->profile_id;
             $status->profile_id = $user->profile_id;
@@ -3533,6 +3537,7 @@ class ApiV1Controller extends Controller
             if (! $in_reply_to_id) {
             if (! $in_reply_to_id) {
                 $status = new Status;
                 $status = new Status;
                 $status->caption = $content;
                 $status->caption = $content;
+                $status->rendered = $defaultCaption;
                 $status->profile_id = $user->profile_id;
                 $status->profile_id = $user->profile_id;
                 $status->is_nsfw = $cw;
                 $status->is_nsfw = $cw;
                 $status->cw_summary = $spoilerText;
                 $status->cw_summary = $spoilerText;
@@ -3685,7 +3690,10 @@ class ApiV1Controller extends Controller
             }
             }
         }
         }
 
 
+        $defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
         $share = Status::firstOrCreate([
         $share = Status::firstOrCreate([
+            'caption' => $defaultCaption,
+            'rendered' => $defaultCaption,
             'profile_id' => $user->profile_id,
             'profile_id' => $user->profile_id,
             'reblog_of_id' => $status->id,
             'reblog_of_id' => $status->id,
             'type' => 'share',
             'type' => 'share',

+ 4 - 6
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -629,9 +629,6 @@ class ApiV1Dot1Controller extends Controller
             abort_if(BouncerService::checkIp($request->ip()), 404);
             abort_if(BouncerService::checkIp($request->ip()), 404);
         }
         }
 
 
-        $rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
-        abort_if(! $rl, 429, 'Too many requests');
-
         $request->validate([
         $request->validate([
             'user_token' => 'required',
             'user_token' => 'required',
             'random_token' => 'required',
             'random_token' => 'required',
@@ -658,7 +655,7 @@ class ApiV1Dot1Controller extends Controller
         $user->last_active_at = now();
         $user->last_active_at = now();
         $user->save();
         $user->save();
 
 
-        $token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
+        $token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'push']);
 
 
         return response()->json([
         return response()->json([
             'access_token' => $token->accessToken,
             'access_token' => $token->accessToken,
@@ -1292,13 +1289,14 @@ class ApiV1Dot1Controller extends Controller
         if ($user->last_active_at == null) {
         if ($user->last_active_at == null) {
             return [];
             return [];
         }
         }
-
-        $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : null;
+        $defaultCaption = '';
+        $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption;
         $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
         $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
         $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
         $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
 
 
         $status = new Status;
         $status = new Status;
         $status->caption = $content;
         $status->caption = $content;
+        $status->rendered = $defaultCaption;
         $status->profile_id = $user->profile_id;
         $status->profile_id = $user->profile_id;
         $status->is_nsfw = $cw;
         $status->is_nsfw = $cw;
         $status->cw_summary = $spoilerText;
         $status->cw_summary = $spoilerText;

+ 1 - 1
app/Http/Controllers/CollectionController.php

@@ -29,7 +29,7 @@ class CollectionController extends Controller
         return view('collection.create', compact('collection'));
         return view('collection.create', compact('collection'));
     }
     }
 
 
-    public function show(Request $request, int $id)
+    public function show(Request $request, $id)
     {
     {
         $user = $request->user();
         $user = $request->user();
         $collection = CollectionService::getCollection($id);
         $collection = CollectionService::getCollection($id);

+ 1 - 0
app/Http/Controllers/CommentController.php

@@ -60,6 +60,7 @@ class CommentController extends Controller
             $reply->profile_id = $profile->id;
             $reply->profile_id = $profile->id;
             $reply->is_nsfw = $nsfw;
             $reply->is_nsfw = $nsfw;
             $reply->caption = Purify::clean($comment);
             $reply->caption = Purify::clean($comment);
+            $reply->rendered = "";
             $reply->in_reply_to_id = $status->id;
             $reply->in_reply_to_id = $status->id;
             $reply->in_reply_to_profile_id = $status->profile_id;
             $reply->in_reply_to_profile_id = $status->profile_id;
             $reply->scope = $scope;
             $reply->scope = $scope;

+ 7 - 2
app/Http/Controllers/ComposeController.php

@@ -30,6 +30,7 @@ use App\Util\Media\License;
 use Auth;
 use Auth;
 use Cache;
 use Cache;
 use DB;
 use DB;
+use Purify;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Support\Str;
 use Illuminate\Support\Str;
 use League\Fractal;
 use League\Fractal;
@@ -569,7 +570,9 @@ class ComposeController extends Controller
             $status->cw_summary = $request->input('spoiler_text');
             $status->cw_summary = $request->input('spoiler_text');
         }
         }
 
 
-        $status->caption = strip_tags($request->caption);
+        $defaultCaption = "";
+        $status->caption = strip_tags($request->input('caption')) ?? $defaultCaption;
+        $status->rendered = $defaultCaption;
         $status->scope = 'draft';
         $status->scope = 'draft';
         $status->visibility = 'draft';
         $status->visibility = 'draft';
         $status->profile_id = $profile->id;
         $status->profile_id = $profile->id;
@@ -673,6 +676,7 @@ class ComposeController extends Controller
         $place = $request->input('place');
         $place = $request->input('place');
         $cw = $request->input('cw');
         $cw = $request->input('cw');
         $tagged = $request->input('tagged');
         $tagged = $request->input('tagged');
+        $defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
 
 
         if ($place && is_array($place)) {
         if ($place && is_array($place)) {
             $status->place_id = $place['id'];
             $status->place_id = $place['id'];
@@ -682,7 +686,8 @@ class ComposeController extends Controller
             $status->comments_disabled = (bool) $request->input('comments_disabled');
             $status->comments_disabled = (bool) $request->input('comments_disabled');
         }
         }
 
 
-        $status->caption = strip_tags($request->caption);
+        $status->caption = $request->filled('caption') ? strip_tags($request->caption) : $defaultCaption;
+        $status->rendered = $defaultCaption;
         $status->profile_id = $profile->id;
         $status->profile_id = $profile->id;
         $entities = [];
         $entities = [];
         $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
         $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;

+ 99 - 0
app/Http/Controllers/OAuth/OobAuthorizationController.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace App\Http\Controllers\OAuth;
+
+use Laravel\Passport\Http\Controllers\ApproveAuthorizationController;
+use Illuminate\Http\Request;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use Nyholm\Psr7\Response as Psr7Response;
+
+class OobAuthorizationController extends ApproveAuthorizationController
+{
+    /**
+     * Approve the authorization request.
+     *
+     * @param  Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function approve(Request $request)
+    {
+        $this->assertValidAuthToken($request);
+
+        $authRequest = $this->getAuthRequestFromSession($request);
+        $authRequest->setAuthorizationApproved(true);
+
+        return $this->withErrorHandling(function () use ($authRequest) {
+            $response = $this->server->completeAuthorizationRequest($authRequest, new Psr7Response);
+            
+            if ($this->isOutOfBandRequest($authRequest)) {
+                $code = $this->extractAuthorizationCode($response);
+                return response()->json([
+                    'code' => $code,
+                    'state' => $authRequest->getState()
+                ]);
+            }
+
+            return $this->convertResponse($response);
+        });
+    }
+
+    /**
+     * Check if the request is an out-of-band OAuth request.
+     *
+     * @param  \League\OAuth2\Server\RequestTypes\AuthorizationRequest  $authRequest
+     * @return bool
+     */
+    protected function isOutOfBandRequest($authRequest)
+    {
+        return $authRequest->getRedirectUri() === 'urn:ietf:wg:oauth:2.0:oob';
+    }
+
+    /**
+     * Extract the authorization code from the PSR-7 response.
+     *
+     * @param  \Psr\Http\Message\ResponseInterface  $response
+     * @return string
+     * @throws \League\OAuth2\Server\Exception\OAuthServerException
+     */
+    protected function extractAuthorizationCode($response)
+    {
+        $location = $response->getHeader('Location')[0] ?? '';
+        
+        if (empty($location)) {
+            throw OAuthServerException::serverError('Missing authorization code in response');
+        }
+
+        parse_str(parse_url($location, PHP_URL_QUERY), $params);
+        
+        if (!isset($params['code'])) {
+            throw OAuthServerException::serverError('Invalid authorization code format');
+        }
+
+        return $params['code'];
+    }
+
+    /**
+     * Handle OAuth errors for both redirect and OOB flows.
+     *
+     * @param  \Closure  $callback
+     * @return \Illuminate\Http\Response
+     */
+    protected function withErrorHandling($callback)
+    {
+        try {
+            return $callback();
+        } catch (OAuthServerException $e) {
+            if ($this->isOutOfBandRequest($this->getAuthRequestFromSession(request()))) {
+                return response()->json([
+                    'error' => $e->getErrorType(),
+                    'message' => $e->getMessage(),
+                    'hint' => $e->getHint()
+                ], $e->getHttpStatusCode());
+            }
+
+            return $this->convertResponse(
+                $e->generateHttpResponse(new Psr7Response)
+            );
+        }
+    }
+}

+ 3 - 1
app/Http/Controllers/StatusController.php

@@ -309,7 +309,7 @@ class StatusController extends Controller
         abort_if(! $statusAccount || isset($statusAccount['moved'], $statusAccount['moved']['id']), 422, 'Account moved');
         abort_if(! $statusAccount || isset($statusAccount['moved'], $statusAccount['moved']['id']), 422, 'Account moved');
 
 
         $count = $status->reblogs_count;
         $count = $status->reblogs_count;
-
+        $defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
         $exists = Status::whereProfileId(Auth::user()->profile->id)
         $exists = Status::whereProfileId(Auth::user()->profile->id)
             ->whereReblogOfId($status->id)
             ->whereReblogOfId($status->id)
             ->exists();
             ->exists();
@@ -324,6 +324,8 @@ class StatusController extends Controller
             }
             }
         } else {
         } else {
             $share = new Status;
             $share = new Status;
+            $share->caption = $defaultCaption;
+            $share->rendered = $defaultCaption;
             $share->profile_id = $profile->id;
             $share->profile_id = $profile->id;
             $share->reblog_of_id = $status->id;
             $share->reblog_of_id = $status->id;
             $share->in_reply_to_profile_id = $status->profile_id;
             $share->in_reply_to_profile_id = $status->profile_id;

+ 2 - 1
app/Http/Middleware/VerifyCsrfToken.php

@@ -12,6 +12,7 @@ class VerifyCsrfToken extends Middleware
      * @var array
      * @var array
      */
      */
     protected $except = [
     protected $except = [
-        '/api/v1/*'
+        '/api/v1/*',
+        'oauth/token'
     ];
     ];
 }
 }

+ 106 - 71
app/Jobs/StatusPipeline/StatusTagsPipeline.php

@@ -2,27 +2,28 @@
 
 
 namespace App\Jobs\StatusPipeline;
 namespace App\Jobs\StatusPipeline;
 
 
-use Illuminate\Bus\Queueable;
-use Illuminate\Contracts\Queue\ShouldBeUnique;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\SerializesModels;
+use App\Hashtag;
+use App\Jobs\MentionPipeline\MentionPipeline;
+use App\Mention;
 use App\Services\AccountService;
 use App\Services\AccountService;
 use App\Services\CustomEmojiService;
 use App\Services\CustomEmojiService;
 use App\Services\StatusService;
 use App\Services\StatusService;
-use App\Jobs\MentionPipeline\MentionPipeline;
-use App\Mention;
-use App\Hashtag;
-use App\StatusHashtag;
 use App\Services\TrendingHashtagService;
 use App\Services\TrendingHashtagService;
+use App\StatusHashtag;
 use App\Util\ActivityPub\Helpers;
 use App\Util\ActivityPub\Helpers;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\DB;
 
 
 class StatusTagsPipeline implements ShouldQueue
 class StatusTagsPipeline implements ShouldQueue
 {
 {
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
 
     protected $activity;
     protected $activity;
+
     protected $status;
     protected $status;
 
 
     /**
     /**
@@ -46,92 +47,126 @@ class StatusTagsPipeline implements ShouldQueue
         $res = $this->activity;
         $res = $this->activity;
         $status = $this->status;
         $status = $this->status;
 
 
-        if(isset($res['tag']['type'], $res['tag']['name'])) {
+        if (isset($res['tag']['type'], $res['tag']['name'])) {
             $res['tag'] = [$res['tag']];
             $res['tag'] = [$res['tag']];
         }
         }
 
 
         $tags = collect($res['tag']);
         $tags = collect($res['tag']);
 
 
         // Emoji
         // Emoji
-        $tags->filter(function($tag) {
+        $tags->filter(function ($tag) {
             return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji';
             return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji';
         })
         })
-        ->map(function($tag) {
-            CustomEmojiService::import($tag['id'], $this->status->id);
-        });
+            ->map(function ($tag) {
+                CustomEmojiService::import($tag['id'], $this->status->id);
+            });
 
 
         // Hashtags
         // Hashtags
-        $tags->filter(function($tag) {
+        $tags->filter(function ($tag) {
             return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']);
             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'];
+            ->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(in_array(strtolower($name), array_map('strtolower', $banned))) {
-                    return;
+                if (count($banned)) {
+                    if (in_array(strtolower($name), array_map('strtolower', $banned))) {
+                        return;
+                    }
                 }
                 }
-            }
-
-            if(config('database.default') === 'pgsql') {
-                $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 (config('database.default') === 'pgsql') {
+                    $hashtag = DB::transaction(function () use ($name) {
+                        $baseSlug = str_slug($name, '-', false);
+                        $slug = $baseSlug;
+                        $counter = 1;
+
+                        $existing = Hashtag::where('name', $name)
+                            ->lockForUpdate()
+                            ->first();
+
+                        if ($existing) {
+                            if ($existing->slug !== $slug) {
+                                while (Hashtag::where('slug', $slug)
+                                    ->where('name', '!=', $name)
+                                    ->exists()) {
+                                    $slug = $baseSlug.'-'.$counter++;
+                                }
+                                $existing->slug = $slug;
+                                $existing->save();
+                            }
+
+                            return $existing;
+                        }
+
+                        while (Hashtag::where('slug', $slug)->exists()) {
+                            $slug = $baseSlug.'-'.$counter++;
+                        }
+
+                        return Hashtag::create([
+                            'name' => $name,
+                            'slug' => $slug,
+                        ]);
+                    });
+                } else {
+                    $hashtag = DB::transaction(function () use ($name) {
+                        $baseSlug = str_slug($name, '-', false);
+                        $slug = $baseSlug;
+                        $counter = 1;
+
+                        while (Hashtag::where('slug', $slug)
+                            ->where('name', '!=', $name)
+                            ->exists()) {
+                            $slug = $baseSlug.'-'.$counter++;
+                        }
+
+                        return Hashtag::updateOrCreate(
+                            ['name' => $name],
+                            ['slug' => $slug]
+                        );
+                    });
                 }
                 }
-            } else {
-                $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
         // Mentions
-        $tags->filter(function($tag) {
+        $tags->filter(function ($tag) {
             return $tag &&
             return $tag &&
                 $tag['type'] == 'Mention' &&
                 $tag['type'] == 'Mention' &&
                 isset($tag['href']) &&
                 isset($tag['href']) &&
                 substr($tag['href'], 0, 8) === 'https://';
                 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;
+            ->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;
                 }
                 }
-                $pid = $acct->id;
-            }
-            $mention = new Mention;
-            $mention->status_id = $status->id;
-            $mention->profile_id = $pid;
-            $mention->save();
-            MentionPipeline::dispatch($status, $mention);
-        });
+                $mention = new Mention;
+                $mention->status_id = $status->id;
+                $mention->profile_id = $pid;
+                $mention->save();
+                MentionPipeline::dispatch($status, $mention);
+            });
 
 
         StatusService::refresh($status->id);
         StatusService::refresh($status->id);
     }
     }

+ 1 - 0
app/Media.php

@@ -22,6 +22,7 @@ class Media extends Model
     protected $casts = [
     protected $casts = [
         'srcset' => 'array',
         'srcset' => 'array',
         'deleted_at' => 'datetime',
         'deleted_at' => 'datetime',
+        'skip_optimize' => 'boolean'
     ];
     ];
 
 
     public function status()
     public function status()

+ 86 - 68
app/Providers/AppServiceProvider.php

@@ -2,81 +2,99 @@
 
 
 namespace App\Providers;
 namespace App\Providers;
 
 
-use App\Observers\{
-	AvatarObserver,
-	FollowerObserver,
-	HashtagFollowObserver,
-	LikeObserver,
-	NotificationObserver,
-	ModLogObserver,
-	ProfileObserver,
-    StatusHashtagObserver,
-    StatusObserver,
-	UserObserver,
-	UserFilterObserver,
-};
-use App\{
-	Avatar,
-	Follower,
-	HashtagFollow,
-	Like,
-	Notification,
-	ModLog,
-	Profile,
-	StatusHashtag,
-    Status,
-	User,
-	UserFilter
-};
-use Auth, Horizon, URL;
-use Illuminate\Support\Facades\Blade;
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Support\ServiceProvider;
+use App\Avatar;
+use App\Follower;
+use App\HashtagFollow;
+use App\Like;
+use App\ModLog;
+use App\Notification;
+use App\Observers\AvatarObserver;
+use App\Observers\FollowerObserver;
+use App\Observers\HashtagFollowObserver;
+use App\Observers\LikeObserver;
+use App\Observers\ModLogObserver;
+use App\Observers\NotificationObserver;
+use App\Observers\ProfileObserver;
+use App\Observers\StatusHashtagObserver;
+use App\Observers\StatusObserver;
+use App\Observers\UserFilterObserver;
+use App\Observers\UserObserver;
+use App\Profile;
+use App\Services\AccountService;
+use App\Status;
+use App\StatusHashtag;
+use App\User;
+use App\UserFilter;
+use Auth;
+use Horizon;
+use Illuminate\Database\Eloquent\Model;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Pagination\Paginator;
+use Illuminate\Support\Facades\Gate;
+use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\Facades\Validator;
-use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\ServiceProvider;
+use Laravel\Pulse\Facades\Pulse;
+use URL;
 
 
 class AppServiceProvider extends ServiceProvider
 class AppServiceProvider extends ServiceProvider
 {
 {
-	/**
-	 * Bootstrap any application services.
-	 *
-	 * @return void
-	 */
-	public function boot()
-	{
-		if(config('instance.force_https_urls', true)) {
-			URL::forceScheme('https');
-		}
+    /**
+     * Bootstrap any application services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        if (config('instance.force_https_urls', true)) {
+            URL::forceScheme('https');
+        }
 
 
-		Schema::defaultStringLength(191);
-		Paginator::useBootstrap();
-		Avatar::observe(AvatarObserver::class);
-		Follower::observe(FollowerObserver::class);
-		HashtagFollow::observe(HashtagFollowObserver::class);
-		Like::observe(LikeObserver::class);
-		Notification::observe(NotificationObserver::class);
-		ModLog::observe(ModLogObserver::class);
-		Profile::observe(ProfileObserver::class);
-		StatusHashtag::observe(StatusHashtagObserver::class);
-		User::observe(UserObserver::class);
+        Schema::defaultStringLength(191);
+        Paginator::useBootstrap();
+        Avatar::observe(AvatarObserver::class);
+        Follower::observe(FollowerObserver::class);
+        HashtagFollow::observe(HashtagFollowObserver::class);
+        Like::observe(LikeObserver::class);
+        Notification::observe(NotificationObserver::class);
+        ModLog::observe(ModLogObserver::class);
+        Profile::observe(ProfileObserver::class);
+        StatusHashtag::observe(StatusHashtagObserver::class);
+        User::observe(UserObserver::class);
         Status::observe(StatusObserver::class);
         Status::observe(StatusObserver::class);
-		UserFilter::observe(UserFilterObserver::class);
-		Horizon::auth(function ($request) {
-			return Auth::check() && $request->user()->is_admin;
-		});
-		Validator::includeUnvalidatedArrayKeys();
+        UserFilter::observe(UserFilterObserver::class);
+        Horizon::auth(function ($request) {
+            return Auth::check() && $request->user()->is_admin;
+        });
+        Validator::includeUnvalidatedArrayKeys();
+
+        Gate::define('viewPulse', function (User $user) {
+            return $user->is_admin === 1;
+        });
+
+        Pulse::user(function ($user) {
+            $acct = AccountService::get($user->profile_id, true);
+
+            return $acct ? [
+                'name' => $acct['username'],
+                'extra' => $user->email,
+                'avatar' => $acct['avatar'],
+            ] : [
+                'name' => $user->username,
+                'extra' => 'DELETED',
+                'avatar' => '/storage/avatars/default.jpg',
+            ];
+        });
 
 
-		// Model::preventLazyLoading(true);
-	}
+        // Model::preventLazyLoading(true);
+    }
 
 
-	/**
-	 * Register any application services.
-	 *
-	 * @return void
-	 */
-	public function register()
-	{
-		//
-	}
+    /**
+     * Register any application services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        //
+    }
 }
 }

+ 1 - 0
app/Providers/AuthServiceProvider.php

@@ -25,6 +25,7 @@ class AuthServiceProvider extends ServiceProvider
     public function boot()
     public function boot()
     {
     {
         if(config('pixelfed.oauth_enabled') == true) {
         if(config('pixelfed.oauth_enabled') == true) {
+            Passport::ignoreRoutes();
             Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
             Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
             Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
             Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
             Passport::enableImplicitGrant();
             Passport::enableImplicitGrant();

+ 1 - 1
app/Services/ImportService.php

@@ -14,7 +14,7 @@ class ImportService
         if($userId > 999999) {
         if($userId > 999999) {
             return;
             return;
         }
         }
-        if($year < 9 || $year > 23) {
+        if($year < 9 || $year > (int) now()->addYear()->format('y')) {
             return;
             return;
         }
         }
         if($month < 1 || $month > 12) {
         if($month < 1 || $month > 12) {

File diff suppressed because it is too large
+ 455 - 307
app/Util/ActivityPub/Helpers.php


+ 8 - 3
app/Util/ActivityPub/HttpSignature.php

@@ -71,9 +71,14 @@ class HttpSignature
     public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
     public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
     {
     {
         $keyId = config('app.url').'/i/actor#main-key';
         $keyId = config('app.url').'/i/actor#main-key';
-        $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
-            return InstanceActor::first()->private_key;
-        });
+        if(config_cache('database.default') === 'mysql') {
+            $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
+                return InstanceActor::first()->private_key;
+            });
+        } else {
+            $privateKey = InstanceActor::first()?->private_key;
+        }
+        abort_if(!$privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor');
         if ($body) {
         if ($body) {
             $digest = self::_digest($body);
             $digest = self::_digest($body);
         }
         }

+ 1 - 0
composer.json

@@ -25,6 +25,7 @@
 		"laravel/helpers": "^1.1",
 		"laravel/helpers": "^1.1",
 		"laravel/horizon": "^5.0",
 		"laravel/horizon": "^5.0",
 		"laravel/passport": "^12.0",
 		"laravel/passport": "^12.0",
+		"laravel/pulse": "^1.3",
 		"laravel/tinker": "^2.9",
 		"laravel/tinker": "^2.9",
 		"laravel/ui": "^4.2",
 		"laravel/ui": "^4.2",
 		"league/flysystem-aws-s3-v3": "^3.0",
 		"league/flysystem-aws-s3-v3": "^3.0",

File diff suppressed because it is too large
+ 389 - 165
composer.lock


+ 19 - 1
config/cache.php

@@ -85,11 +85,29 @@ return [
                 'database' => env('REDIS_DATABASE', 0),
                 'database' => env('REDIS_DATABASE', 0),
             ],
             ],
 
 
+            'session' => [
+                'scheme'   => env('REDIS_SCHEME', 'tcp'),
+                'path'     => env('REDIS_PATH'),
+                'host'     => env('REDIS_HOST', '127.0.0.1'),
+                'password' => env('REDIS_PASSWORD', null),
+                'port'     => env('REDIS_PORT', 6379),
+                'database' => env('REDIS_DATABASE_SESSION', 1),
+            ],
+
+            'pulse' => [
+                'scheme'   => env('REDIS_SCHEME', 'tcp'),
+                'path'     => env('REDIS_PATH'),
+                'host'     => env('REDIS_HOST', '127.0.0.1'),
+                'password' => env('REDIS_PASSWORD', null),
+                'port'     => env('REDIS_PORT', 6379),
+                'database' => env('REDIS_DATABASE_PULSE', 2),
+            ],
+
         ],
         ],
 
 
         'redis:session' => [
         'redis:session' => [
             'driver' => 'redis',
             'driver' => 'redis',
-            'connection' => 'default',
+            'connection' => 'session',
             'prefix' => 'pf_session',
             'prefix' => 'pf_session',
         ],
         ],
 
 

+ 18 - 0
config/database.php

@@ -143,6 +143,24 @@ return [
             'database' => env('REDIS_DATABASE', 0),
             'database' => env('REDIS_DATABASE', 0),
         ],
         ],
 
 
+        'session' => [
+            'scheme'   => env('REDIS_SCHEME', 'tcp'),
+            'path'     => env('REDIS_PATH'),
+            'host'     => env('REDIS_HOST', '127.0.0.1'),
+            'password' => env('REDIS_PASSWORD', null),
+            'port'     => env('REDIS_PORT', 6379),
+            'database' => env('REDIS_DATABASE_SESSION', 1),
+        ],
+
+        'pulse' => [
+            'scheme'   => env('REDIS_SCHEME', 'tcp'),
+            'path'     => env('REDIS_PATH'),
+            'host'     => env('REDIS_HOST', '127.0.0.1'),
+            'password' => env('REDIS_PASSWORD', null),
+            'port'     => env('REDIS_PORT', 6379),
+            'database' => env('REDIS_DATABASE_PULSE', 2),
+        ],
+
     ],
     ],
 
 
 	'dbal' => [
 	'dbal' => [

+ 4 - 0
config/media.php

@@ -24,6 +24,10 @@ return [
         ],
         ],
     ],
     ],
 
 
+    'image_optimize' => [
+        'catch_unoptimized_media_hour_limit' => env('PF_CATCHUNOPTIMIZEDMEDIA', false),
+    ],
+
     'hls' => [
     'hls' => [
         /*
         /*
         |--------------------------------------------------------------------------
         |--------------------------------------------------------------------------

+ 236 - 0
config/pulse.php

@@ -0,0 +1,236 @@
+<?php
+
+use Laravel\Pulse\Http\Middleware\Authorize;
+use Laravel\Pulse\Pulse;
+use Laravel\Pulse\Recorders;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Domain
+    |--------------------------------------------------------------------------
+    |
+    | This is the subdomain which the Pulse dashboard will be accessible from.
+    | When set to null, the dashboard will reside under the same domain as
+    | the application. Remember to configure your DNS entries correctly.
+    |
+    */
+
+    'domain' => env('PULSE_DOMAIN'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Path
+    |--------------------------------------------------------------------------
+    |
+    | This is the path which the Pulse dashboard will be accessible from. Feel
+    | free to change this path to anything you'd like. Note that this won't
+    | affect the path of the internal API that is never exposed to users.
+    |
+    */
+
+    'path' => env('PULSE_PATH', 'pulse'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Master Switch
+    |--------------------------------------------------------------------------
+    |
+    | This configuration option may be used to completely disable all Pulse
+    | data recorders regardless of their individual configurations. This
+    | provides a single option to quickly disable all Pulse recording.
+    |
+    */
+
+    'enabled' => env('PULSE_ENABLED', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Storage Driver
+    |--------------------------------------------------------------------------
+    |
+    | This configuration option determines which storage driver will be used
+    | while storing entries from Pulse's recorders. In addition, you also
+    | may provide any options to configure the selected storage driver.
+    |
+    */
+
+    'storage' => [
+        'driver' => env('PULSE_STORAGE_DRIVER', 'database'),
+
+        'trim' => [
+            'keep' => env('PULSE_STORAGE_KEEP', '7 days'),
+        ],
+
+        'database' => [
+            'connection' => env('PULSE_DB_CONNECTION'),
+            'chunk' => 1000,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Ingest Driver
+    |--------------------------------------------------------------------------
+    |
+    | This configuration options determines the ingest driver that will be used
+    | to capture entries from Pulse's recorders. Ingest drivers are great to
+    | free up your request workers quickly by offloading the data storage.
+    |
+    */
+
+    'ingest' => [
+        'driver' => env('PULSE_INGEST_DRIVER', 'storage'),
+
+        'buffer' => env('PULSE_INGEST_BUFFER', 5_000),
+
+        'trim' => [
+            'lottery' => [1, 1_000],
+            'keep' => env('PULSE_INGEST_KEEP', '7 days'),
+        ],
+
+        'redis' => [
+            'connection' => env('PULSE_REDIS_CONNECTION'),
+            'chunk' => 1000,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Cache Driver
+    |--------------------------------------------------------------------------
+    |
+    | This configuration option determines the cache driver that will be used
+    | for various tasks, including caching dashboard results, establishing
+    | locks for events that should only occur on one server and signals.
+    |
+    */
+
+    'cache' => env('PULSE_CACHE_DRIVER'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Route Middleware
+    |--------------------------------------------------------------------------
+    |
+    | These middleware will be assigned to every Pulse route, giving you the
+    | chance to add your own middleware to this list or change any of the
+    | existing middleware. Of course, reasonable defaults are provided.
+    |
+    */
+
+    'middleware' => [
+        'web',
+        Authorize::class,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Pulse Recorders
+    |--------------------------------------------------------------------------
+    |
+    | The following array lists the "recorders" that will be registered with
+    | Pulse, along with their configuration. Recorders gather application
+    | event data from requests and tasks to pass to your ingest driver.
+    |
+    */
+
+    'recorders' => [
+        Recorders\CacheInteractions::class => [
+            'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true),
+            'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1),
+            'ignore' => [
+                ...Pulse::defaultVendorCacheKeys(),
+            ],
+            'groups' => [
+                '/^job-exceptions:.*/' => 'job-exceptions:*',
+                // '/:\d+/' => ':*',
+            ],
+        ],
+
+        Recorders\Exceptions::class => [
+            'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true),
+            'sample_rate' => env('PULSE_EXCEPTIONS_SAMPLE_RATE', 1),
+            'location' => env('PULSE_EXCEPTIONS_LOCATION', true),
+            'ignore' => [
+                // '/^Package\\\\Exceptions\\\\/',
+            ],
+        ],
+
+        Recorders\Queues::class => [
+            'enabled' => env('PULSE_QUEUES_ENABLED', true),
+            'sample_rate' => env('PULSE_QUEUES_SAMPLE_RATE', 1),
+            'ignore' => [
+                // '/^Package\\\\Jobs\\\\/',
+            ],
+        ],
+
+        Recorders\Servers::class => [
+            'server_name' => env('PULSE_SERVER_NAME', gethostname()),
+            'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')),
+        ],
+
+        Recorders\SlowJobs::class => [
+            'enabled' => env('PULSE_SLOW_JOBS_ENABLED', true),
+            'sample_rate' => env('PULSE_SLOW_JOBS_SAMPLE_RATE', 1),
+            'threshold' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000),
+            'ignore' => [
+                // '/^Package\\\\Jobs\\\\/',
+            ],
+        ],
+
+        Recorders\SlowOutgoingRequests::class => [
+            'enabled' => env('PULSE_SLOW_OUTGOING_REQUESTS_ENABLED', true),
+            'sample_rate' => env('PULSE_SLOW_OUTGOING_REQUESTS_SAMPLE_RATE', 1),
+            'threshold' => env('PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD', 1000),
+            'ignore' => [
+                // '#^http://127\.0\.0\.1:13714#', // Inertia SSR...
+            ],
+            'groups' => [
+                // '#^https://api\.github\.com/repos/.*$#' => 'api.github.com/repos/*',
+                // '#^https?://([^/]*).*$#' => '\1',
+                // '#/\d+#' => '/*',
+            ],
+        ],
+
+        Recorders\SlowQueries::class => [
+            'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
+            'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1),
+            'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000),
+            'location' => env('PULSE_SLOW_QUERIES_LOCATION', true),
+            'max_query_length' => env('PULSE_SLOW_QUERIES_MAX_QUERY_LENGTH'),
+            'ignore' => [
+                '/(["`])pulse_[\w]+?\1/', // Pulse tables...
+                '/(["`])telescope_[\w]+?\1/', // Telescope tables...
+            ],
+        ],
+
+        Recorders\SlowRequests::class => [
+            'enabled' => env('PULSE_SLOW_REQUESTS_ENABLED', true),
+            'sample_rate' => env('PULSE_SLOW_REQUESTS_SAMPLE_RATE', 1),
+            'threshold' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000),
+            'ignore' => [
+                '#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
+                '#^/telescope#', // Telescope dashboard...
+            ],
+        ],
+
+        Recorders\UserJobs::class => [
+            'enabled' => env('PULSE_USER_JOBS_ENABLED', true),
+            'sample_rate' => env('PULSE_USER_JOBS_SAMPLE_RATE', 1),
+            'ignore' => [
+                // '/^Package\\\\Jobs\\\\/',
+            ],
+        ],
+
+        Recorders\UserRequests::class => [
+            'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true),
+            'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1),
+            'ignore' => [
+                '#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
+                '#^/telescope#', // Telescope dashboard...
+            ],
+        ],
+    ],
+];

+ 2 - 0
config/services.php

@@ -17,6 +17,8 @@ return [
     'mailgun' => [
     'mailgun' => [
         'domain' => env('MAILGUN_DOMAIN'),
         'domain' => env('MAILGUN_DOMAIN'),
         'secret' => env('MAILGUN_SECRET'),
         'secret' => env('MAILGUN_SECRET'),
+        'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
+        'scheme' => 'https',
     ],
     ],
 
 
     'ses' => [
     'ses' => [

+ 1 - 1
database/migrations/2018_09_30_051108_create_direct_messages_table.php

@@ -19,7 +19,7 @@ class CreateDirectMessagesTable extends Migration
             $table->bigInteger('from_id')->unsigned()->index();
             $table->bigInteger('from_id')->unsigned()->index();
             $table->string('from_profile_ids')->nullable();
             $table->string('from_profile_ids')->nullable();
             $table->boolean('group_message')->default(false);
             $table->boolean('group_message')->default(false);
-            $table->bigInteger('status_id')->unsigned()->integer();
+            $table->bigInteger('status_id')->unsigned();
             $table->unique(['to_id', 'from_id', 'status_id']);
             $table->unique(['to_id', 'from_id', 'status_id']);
             $table->timestamp('read_at')->nullable();
             $table->timestamp('read_at')->nullable();
             $table->timestamps();
             $table->timestamps();

+ 84 - 0
database/migrations/2023_06_07_000001_create_pulse_tables.php

@@ -0,0 +1,84 @@
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Laravel\Pulse\Support\PulseMigration;
+
+return new class extends PulseMigration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        if (! $this->shouldRun()) {
+            return;
+        }
+
+        Schema::create('pulse_values', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('timestamp');
+            $table->string('type');
+            $table->mediumText('key');
+            match ($this->driver()) {
+                'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
+                'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
+                'sqlite' => $table->string('key_hash'),
+            };
+            $table->mediumText('value');
+
+            $table->index('timestamp'); // For trimming...
+            $table->index('type'); // For fast lookups and purging...
+            $table->unique(['type', 'key_hash']); // For data integrity and upserts...
+        });
+
+        Schema::create('pulse_entries', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('timestamp');
+            $table->string('type');
+            $table->mediumText('key');
+            match ($this->driver()) {
+                'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
+                'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
+                'sqlite' => $table->string('key_hash'),
+            };
+            $table->bigInteger('value')->nullable();
+
+            $table->index('timestamp'); // For trimming...
+            $table->index('type'); // For purging...
+            $table->index('key_hash'); // For mapping...
+            $table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries...
+        });
+
+        Schema::create('pulse_aggregates', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('bucket');
+            $table->unsignedMediumInteger('period');
+            $table->string('type');
+            $table->mediumText('key');
+            match ($this->driver()) {
+                'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
+                'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
+                'sqlite' => $table->string('key_hash'),
+            };
+            $table->string('aggregate');
+            $table->decimal('value', 20, 2);
+            $table->unsignedInteger('count')->nullable();
+
+            $table->unique(['bucket', 'period', 'type', 'aggregate', 'key_hash']); // Force "on duplicate update"...
+            $table->index(['period', 'bucket']); // For trimming...
+            $table->index('type'); // For purging...
+            $table->index(['period', 'type', 'aggregate', 'bucket']); // For aggregate queries...
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('pulse_values');
+        Schema::dropIfExists('pulse_entries');
+        Schema::dropIfExists('pulse_aggregates');
+    }
+};

+ 4 - 0
database/migrations/2024_05_20_062706_update_group_posts_table.php

@@ -2,6 +2,7 @@
 
 
 use Illuminate\Database\Migrations\Migration;
 use Illuminate\Database\Migrations\Migration;
 use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\Schema;
 
 
 return new class extends Migration
 return new class extends Migration
@@ -12,6 +13,9 @@ return new class extends Migration
     public function up(): void
     public function up(): void
     {
     {
         Schema::table('group_posts', function (Blueprint $table) {
         Schema::table('group_posts', function (Blueprint $table) {
+            if (DB::getDriverName() === 'sqlite') {
+                $table->dropUnique(['status_id']);
+            }
             $table->dropColumn('status_id');
             $table->dropColumn('status_id');
             $table->dropColumn('reply_child_id');
             $table->dropColumn('reply_child_id');
             $table->dropColumn('in_reply_to_id');
             $table->dropColumn('in_reply_to_id');

+ 1 - 1
docker-compose.yml

@@ -17,7 +17,7 @@ services:
   #
   #
   # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
   # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
   proxy:
   proxy:
-    image: nginxproxy/nginx-proxy:1.4
+    image: nginxproxy/nginx-proxy:1.6.2
     container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
     container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
     restart: unless-stopped
     restart: unless-stopped
     profiles:
     profiles:

File diff suppressed because it is too large
+ 315 - 231
package-lock.json


+ 1 - 0
package.json

@@ -71,6 +71,7 @@
 		"vue-loading-overlay": "^3.3.3",
 		"vue-loading-overlay": "^3.3.3",
 		"vue-timeago": "^5.1.2",
 		"vue-timeago": "^5.1.2",
 		"vue-tribute": "^1.0.7",
 		"vue-tribute": "^1.0.7",
+		"webgl-media-editor": "^0.0.1",
 		"zuck.js": "^1.6.0"
 		"zuck.js": "^1.6.0"
 	},
 	},
 	"collective": {
 	"collective": {

BIN
public/_lang/af.json


BIN
public/_lang/ar.json


BIN
public/_lang/bn.json


BIN
public/_lang/bs.json


BIN
public/_lang/ca.json


BIN
public/_lang/cs.json


BIN
public/_lang/cy.json


BIN
public/_lang/da.json


BIN
public/_lang/de.json


BIN
public/_lang/el.json


BIN
public/_lang/es.json


BIN
public/_lang/eu.json


BIN
public/_lang/fa.json


BIN
public/_lang/fi.json


BIN
public/_lang/fr.json


BIN
public/_lang/gd.json


BIN
public/_lang/gl.json


BIN
public/_lang/he.json


BIN
public/_lang/hi.json


BIN
public/_lang/hr.json


BIN
public/_lang/hu.json


BIN
public/_lang/id.json


BIN
public/_lang/it.json


BIN
public/_lang/ja.json


BIN
public/_lang/ko.json


BIN
public/_lang/me.json


BIN
public/_lang/mk.json


BIN
public/_lang/nl.json


BIN
public/_lang/no.json


BIN
public/_lang/oc.json


BIN
public/_lang/pl.json


BIN
public/_lang/pt.json


BIN
public/_lang/ro.json


BIN
public/_lang/ru.json


BIN
public/_lang/sk.json


BIN
public/_lang/sr.json


BIN
public/_lang/sv.json


BIN
public/_lang/th.json


BIN
public/_lang/tr.json


BIN
public/_lang/vi.json


BIN
public/_lang/zh.json


+ 0 - 0
public/js/changelog.bundle.7fc2ee6c4475458c.js → public/js/changelog.bundle.d40f01eba00c9885.js


BIN
public/js/compose.chunk.b06beb250e24db17.js


+ 0 - 0
public/js/compose.chunk.e1f297b242137d23.js.LICENSE.txt → public/js/compose.chunk.b06beb250e24db17.js.LICENSE.txt


BIN
public/js/compose.chunk.e1f297b242137d23.js


BIN
public/js/compose.js


+ 0 - 0
public/js/daci.chunk.3ed914c15dec4ff4.js → public/js/daci.chunk.61b540b1630f8445.js


+ 0 - 0
public/js/discover.chunk.2986d7e977f5188a.js → public/js/discover.chunk.00d9b5656d32080e.js


+ 0 - 0
public/js/discover~findfriends.chunk.84758c764668a02c.js → public/js/discover~findfriends.chunk.6d494abb9e464081.js


+ 0 - 0
public/js/discover~hashtag.bundle.db1d86f9e9dcb79a.js → public/js/discover~hashtag.bundle.7455573dc9d2be1f.js


+ 0 - 0
public/js/discover~memories.chunk.3b45432a80b08e9b.js → public/js/discover~memories.chunk.9541b66de9d5d907.js


+ 0 - 0
public/js/discover~myhashtags.chunk.67fd16950ee21ad8.js → public/js/discover~myhashtags.chunk.e2ca0db60346d0c2.js


+ 0 - 0
public/js/discover~serverfeed.chunk.93bc564867eaa7c3.js → public/js/discover~serverfeed.chunk.138d9d53d1debac1.js


+ 0 - 0
public/js/discover~settings.chunk.950c11c918a541b0.js → public/js/discover~settings.chunk.b1b5642ccef06123.js


+ 0 - 0
public/js/dms.chunk.b7e970fb49da0199.js → public/js/dms.chunk.1a2a644df5c78346.js


+ 0 - 0
public/js/dms~message.chunk.011f31232754f650.js → public/js/dms~message.chunk.4e68bb824f396d86.js


+ 0 - 0
public/js/error404.bundle.ad885ef6f9b2c101.js → public/js/error404.bundle.e2f43f5006962e80.js


+ 0 - 0
public/js/group.create.0d645a1de271e28d.js → public/js/group.create.72c3a1e5c1dc00dc.js


+ 0 - 0
public/js/groups-page-about.06576420562628e3.js → public/js/groups-page-about.76a616aa7e1a367b.js


+ 0 - 0
public/js/groups-page-media.f611a51e684c48ef.js → public/js/groups-page-media.056a7bbc46b79034.js


+ 0 - 0
public/js/groups-page-members.bfdefdd66058e838.js → public/js/groups-page-members.a8ea4f209fcbe238.js


+ 0 - 0
public/js/groups-page-topics.431ebaf843ca9b16.js → public/js/groups-page-topics.f69667c933f7d122.js


+ 0 - 0
public/js/groups-page.53eccead9512c61f.js → public/js/groups-page.d484dab549a033ca.js


+ 0 - 0
public/js/groups-post.639cb121bdc6f4a7.js → public/js/groups-post.4c3d4860b029bbaf.js


+ 0 - 0
public/js/groups-profile.3b11ffa46ae76520.js → public/js/groups-profile.1bb8be935d1f108a.js


BIN
public/js/home.chunk.c362371940daf318.js → public/js/home.chunk.acf96f52790bffa6.js


+ 0 - 0
public/js/home.chunk.c362371940daf318.js.LICENSE.txt → public/js/home.chunk.acf96f52790bffa6.js.LICENSE.txt


Some files were not shown because too many files changed in this diff