Explorar o código

Merge pull request #5992 from pixelfed/staging

Staging
daniel hai 2 meses
pai
achega
510ba56b22

+ 0 - 58
.circleci/config.yml

@@ -1,58 +0,0 @@
-# PHP CircleCI 2.0 configuration file
-#
-# Check https://circleci.com/docs/2.0/language-php/ for more details
-#
-version: 2
-jobs:
-  build:
-    docker:
-      # Specify the version you desire here
-      - image: cimg/php:8.3.8
-
-      # Specify service dependencies here if necessary
-      # CircleCI maintains a library of pre-built images
-      # documented at https://circleci.com/docs/2.0/circleci-images/
-      # Using the RAM variation mitigates I/O contention
-      # for database intensive operations.
-      # - image: circleci/mysql:5.7-ram
-      #
-      # - image: redis:2.8.19
-
-    steps:
-      - checkout
-
-      - run:
-          name: "Create Environment file and generate app key"
-          command: |
-            mv .env.testing .env
-
-      - run: sudo apt install zlib1g-dev libsqlite3-dev
-
-      # Download and cache dependencies
-
-      # composer cache
-      - restore_cache:
-          keys:
-          # "composer.lock" can be used if it is committed to the repo
-          - v2-dependencies-{{ checksum "composer.json" }}
-          # fallback to using the latest cache if no exact match is found
-          - v2-dependencies-
-
-      - run: composer install -n --prefer-dist
-
-      - save_cache:
-          key: v2-dependencies-{{ checksum "composer.json" }}
-          paths:
-            - vendor
-
-      - run: php artisan config:cache
-      - run: php artisan route:clear
-      - run: php artisan storage:link
-      - run: php artisan key:generate
-
-      # run tests with phpunit or codecept
-      - run: php artisan test
-      - store_test_results:
-          path: tests/_output
-      - store_artifacts:
-          path: tests/_output

+ 55 - 0
.github/workflows/laravel.yml

@@ -0,0 +1,55 @@
+name: Laravel Test Suite
+
+on:
+  push:
+    branches: [ staging ]
+  pull_request:
+    branches: [ staging ]
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        php: [ '8.3' ]
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: mbstring, sqlite, xml, ctype, json, openssl
+          ini-values: post_max_size=256M, memory_limit=512M
+
+      - name: Cache Composer dependencies
+        uses: actions/cache@v3
+        with:
+          path: vendor
+          key: composer-${{ hashFiles('**/composer.lock') }}
+
+      - name: Install Composer dependencies
+        run: composer install --no-progress --no-suggest --prefer-dist -n
+
+      - name: Copy .env and generate key
+        run: |
+          cp .env.example .env
+          php artisan key:generate
+
+      - name: Prepare SQLite database
+        run: |
+          touch database/database.sqlite
+          php artisan migrate --env=testing --force --database=sqlite
+
+      - name: Run tests
+        run: |
+          php artisan test --env=testing --log-junit=tests/_output/junit.xml
+        continue-on-error: false
+
+      - name: Upload JUnit test results
+        if: always()
+        uses: actions/upload-artifact@v3
+        with:
+          name: junit-results
+          path: tests/_output/junit.xml

+ 5 - 0
CHANGELOG.md

@@ -27,6 +27,11 @@
 - Update sidebar with gap padding for footer links ([dbd8289fe](https://github.com/pixelfed/pixelfed/commit/dbd8289fe))
 - Update translations for Stories ([0a4dc7724](https://github.com/pixelfed/pixelfed/commit/0a4dc7724))
 - Update translations for Auth ([756102696](https://github.com/pixelfed/pixelfed/commit/756102696))
+- Update HttpSignatures, auto generate instance actor if missing ([bb16c95b1](https://github.com/pixelfed/pixelfed/commit/bb16c95b1))
+- Update CreateNote to use cached MediaService attachments ([6a7307104](https://github.com/pixelfed/pixelfed/commit/6a7307104))
+- Update ComposeController, fix cache invalidation order ([ae47ba73d](https://github.com/pixelfed/pixelfed/commit/ae47ba73d))
+- Update ApiV1Controller, fix cache invalidation order ([4747266b0](https://github.com/pixelfed/pixelfed/commit/4747266b0))
+- Update CreateNote, improve media attachement handling by leveraging the MediaService cache ([7ae61a74a](https://github.com/pixelfed/pixelfed/commit/7ae61a74a))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.12.5 (2025-03-23)](https://github.com/pixelfed/pixelfed/compare/v0.12.5...dev)

+ 6 - 4
README.md

@@ -1,15 +1,13 @@
 <p align="center"><img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/logos/pixelfed-full-color.svg" width="300px"></p>
 
 <p align="center">
-<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/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 align="center">
-<a href="http://kck.st/4g34fFb"><img src="https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fwww.kickstarter.com%2Fprojects%2Fpixelfed%2Fpixelfed-foundation-2024-real-ethical-social-networks%2Fwidget%2Fcard.html&query=%2F%2Fli%5B%40class%3D'js-amount-pledged'%5D%2F%2Fspan%5B%40class%3D'money'%5D&logo=kickstarter&label=Kickstarter&color=purple" alt="Kickstarter Campaign" /></a>
-<a href="https://fedidb.org/software/pixelfed"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.fedidb.org%2Fv1%2Fsoftware%2Fpixelfed&query=%24.monthly_actives&logo=pixelfed&logoColor=white&label=Monthly%20Active%20Users" alt="Monthly active users from FediDB" /></a>
+<a href="https://fedidb.org/software/pixelfed"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.fedidb.org%2Fv1%2Fsoftware%2Fpixelfed&query=%24.user_count&logo=pixelfed&logoColor=white&label=Total%20Users" alt="Total Pixelfed users from FediDB" /></a>
 </p>
 
 ## Introduction
@@ -17,7 +15,11 @@
 A free and ethical photo sharing platform, powered by ActivityPub federation.
 
 <p align="center">
-<img src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-screenshot.jpg">
+<picture>
+  <source media="(prefers-color-scheme: dark)" srcset="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-readme-dark.jpg">
+  <source media="(prefers-color-scheme: light)" srcset="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-readme-light.jpg">
+  <img alt="Pixelfed web user interface in light mode" src="https://pixelfed.nyc3.cdn.digitaloceanspaces.com/media/pixelfed-readme-light.jpg">
+</picture>
 </p>
 
 ## Official Documentation

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

@@ -3775,10 +3775,8 @@ class ApiV1Controller extends Controller
             abort(500, 'An error occured.');
         }
 
-        NewStatusPipeline::dispatch($status);
-        if ($status->in_reply_to_id) {
-            CommentPipeline::dispatch($parent, $status);
-        }
+        Cache::forget('pf:status:ap:v1:sid:'.$status->id);
+        Cache::forget('status:transformer:media:attachments:'.$status->id);
         Cache::forget('user:account:id:'.$user->id);
         Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
         Cache::forget('profile:status_count:'.$user->profile_id);
@@ -3786,6 +3784,11 @@ class ApiV1Controller extends Controller
         Cache::forget('profile:embed:'.$status->profile_id);
         Cache::forget($limitKey);
 
+        NewStatusPipeline::dispatch($status);
+        if ($status->in_reply_to_id) {
+            CommentPipeline::dispatch($parent, $status);
+        }
+
         if ($request->has('collection_ids') && $ids) {
             $collections = Collection::whereProfileId($user->profile_id)
                 ->find($request->input('collection_ids'))
@@ -4605,8 +4608,10 @@ class ApiV1Controller extends Controller
         AccountService::del($id);
 
         $res = RelationshipService::get($id, $pid);
+
         return $this->json($res);
     }
+
     /**
      *  GET /api/v1/statuses/{id}/pin
      */

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

@@ -640,13 +640,13 @@ class ComposeController extends Controller
                 });
         }
 
-        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('profile:embed:'.$status->profile_id);
         Cache::forget($limitKey);
+        NewStatusPipeline::dispatch($status);
 
         return $status->url();
     }

+ 2 - 2
app/Media.php

@@ -89,14 +89,14 @@ class Media extends Model
 
     public function activityVerb()
     {
-        $verb = 'Image';
+        $verb = 'Document';
         switch ($this->mimeType()) {
             case 'audio':
                 $verb = 'Audio';
                 break;
 
             case 'image':
-                $verb = 'Image';
+                $verb = 'Document';
                 break;
 
             case 'video':

+ 77 - 71
app/Services/MediaService.php

@@ -2,88 +2,94 @@
 
 namespace App\Services;
 
-use Cache;
-use Illuminate\Support\Facades\Redis;
 use App\Media;
-use App\Status;
+use App\Transformer\Api\MediaTransformer;
+use Cache;
+use Illuminate\Support\Arr;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
-use League\Fractal\Pagination\IlluminatePaginatorAdapter;
-use App\Transformer\Api\MediaTransformer;
-use App\Util\Media\License;
 
 class MediaService
 {
-	const CACHE_KEY = 'status:transformer:media:attachments:';
+    const CACHE_KEY = 'status:transformer:media:attachments:';
+
+    public static function get($statusId)
+    {
+        return Cache::remember(self::CACHE_KEY.$statusId, 21600, function () use ($statusId) {
+            $media = Media::whereStatusId($statusId)->orderBy('order')->get();
+            if (! $media) {
+                return [];
+            }
+            $fractal = new Fractal\Manager;
+            $fractal->setSerializer(new ArraySerializer);
+            $resource = new Fractal\Resource\Collection($media, new MediaTransformer);
+
+            return $fractal->createData($resource)->toArray();
+        });
+    }
+
+    public static function getMastodon($id)
+    {
+        $media = self::get($id);
+        if (! $media) {
+            return [];
+        }
+        $medias = collect($media)
+            ->map(function ($media) {
+                $mime = $media['mime'] ? explode('/', $media['mime']) : false;
+                unset(
+                    $media['optimized_url'],
+                    $media['license'],
+                    $media['is_nsfw'],
+                    $media['orientation'],
+                    $media['filter_name'],
+                    $media['filter_class'],
+                    $media['mime'],
+                    $media['hls_manifest']
+                );
+
+                $media['type'] = $mime ? strtolower($mime[0]) : 'unknown';
 
-	public static function get($statusId)
-	{
-		return Cache::remember(self::CACHE_KEY.$statusId, 21600, function() use($statusId) {
-			$media = Media::whereStatusId($statusId)->orderBy('order')->get();
-			if(!$media) {
-				return [];
-			}
-			$fractal = new Fractal\Manager();
-			$fractal->setSerializer(new ArraySerializer());
-			$resource = new Fractal\Resource\Collection($media, new MediaTransformer());
-			return $fractal->createData($resource)->toArray();
-		});
-	}
+                return $media;
+            })
+            ->filter(function ($m) {
+                return $m && isset($m['url']);
+            })
+            ->values();
 
-	public static function getMastodon($id)
-	{
-		$media = self::get($id);
-		if(!$media) {
-			return [];
-		}
-		$medias = collect($media)
-		->map(function($media) {
-			$mime = $media['mime'] ? explode('/', $media['mime']) : false;
-			unset(
-				$media['optimized_url'],
-				$media['license'],
-				$media['is_nsfw'],
-				$media['orientation'],
-				$media['filter_name'],
-				$media['filter_class'],
-				$media['mime'],
-				$media['hls_manifest']
-			);
+        return $medias->toArray();
+    }
 
-			$media['type'] = $mime ? strtolower($mime[0]) : 'unknown';
-			return $media;
-		})
-		->filter(function($m) {
-			return $m && isset($m['url']);
-		})
-		->values();
+    public static function del($statusId)
+    {
+        return Cache::forget(self::CACHE_KEY.$statusId);
+    }
 
-		return $medias->toArray();
-	}
+    public static function activitypub($statusId, $fresh = false)
+    {
+        if ($fresh) {
+            self::del($statusId);
+        }
 
-	public static function del($statusId)
-	{
-		return Cache::forget(self::CACHE_KEY . $statusId);
-	}
+        $status = self::get($statusId);
+        if (! $status) {
+            return [];
+        }
 
-	public static function activitypub($statusId)
-	{
-		$status = self::get($statusId);
-		if(!$status) {
-			return [];
-		}
+        return collect($status)->map(function ($s) {
+            $original = Arr::get($s, 'meta.original', []);
+            $mime = $s['mime'] === 'image/jpg' ? 'image/jpeg' : $s['mime'];
 
-		return collect($status)->map(function($s) {
-			$license = isset($s['license']) && $s['license']['title'] ? $s['license']['title'] : null;
-			return [
-				'type'      => 'Document',
-				'mediaType' => $s['mime'],
-				'url'       => $s['url'],
-				'name'      => $s['description'],
-				'summary'   => $s['description'],
-				'blurhash'  => $s['blurhash'],
-				'license'   => $license
-			];
-		});
-	}
+            return [
+                'type' => 'Document',
+                'mediaType' => $mime,
+                'url' => $s['url'],
+                'name' => $s['description'],
+                'blurhash' => $s['blurhash'],
+                'focalPoint' => [0, 0],
+                'width' => $original['width'] ?? null,
+                'height' => $original['height'] ?? null,
+            ];
+        });
+    }
 }

+ 1 - 0
app/Services/MediaStorageService.php

@@ -117,6 +117,7 @@ class MediaStorageService
             }
         }
         if ($media->status_id) {
+            Cache::forget('pf:status:ap:v1:sid:'.$media->status_id);
             Cache::forget('status:transformer:media:attachments:'.$media->status_id);
             MediaService::del($media->status_id);
             StatusService::del($media->status_id, false);

+ 58 - 53
app/Status.php

@@ -2,16 +2,17 @@
 
 namespace App;
 
-use Auth, Cache, Hashids, Storage;
-use Illuminate\Database\Eloquent\Model;
 use App\HasSnowflakePrimary;
 use App\Http\Controllers\StatusController;
-use Illuminate\Database\Eloquent\SoftDeletes;
 use App\Models\Poll;
+use App\Models\StatusEdit;
 use App\Services\AccountService;
 use App\Services\StatusService;
-use App\Models\StatusEdit;
+use Auth;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Support\Str;
+use Storage;
 
 class Status extends Model
 {
@@ -31,7 +32,7 @@ class Status extends Model
      */
     protected $casts = [
         'deleted_at' => 'datetime',
-        'edited_at'  => 'datetime'
+        'edited_at' => 'datetime',
     ];
 
     protected $guarded = [];
@@ -49,7 +50,7 @@ class Status extends Model
         'story:reply',
         'story:reaction',
         'story:live',
-        'loop'
+        'loop',
     ];
 
     const MAX_MENTIONS = 20;
@@ -75,22 +76,24 @@ class Status extends Model
 
     public function viewType()
     {
-        if($this->type) {
+        if ($this->type) {
             return $this->type;
         }
+
         return $this->setType();
     }
 
     public function setType()
     {
-        if(in_array($this->type, self::STATUS_TYPES)) {
+        if (in_array($this->type, self::STATUS_TYPES)) {
             return $this->type;
         }
         $mimes = $this->media->pluck('mime')->toArray();
         $type = StatusController::mimeTypeCheck($mimes);
-        if($type) {
+        if ($type) {
             $this->type = $type;
             $this->save();
+
             return $type;
         }
     }
@@ -99,22 +102,22 @@ class Status extends Model
     {
         $entity = StatusService::get($this->id, false);
 
-        if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
+        if (! $entity || ! isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
             return url(Storage::url('public/no-preview.png'));
         }
 
-        if((!isset($entity['sensitive']) || $entity['sensitive']) && !$showNsfw) {
+        if ((! isset($entity['sensitive']) || $entity['sensitive']) && ! $showNsfw) {
             return url(Storage::url('public/no-preview.png'));
         }
 
-        if(!isset($entity['visibility']) || !in_array($entity['visibility'], ['public', 'unlisted'])) {
+        if (! isset($entity['visibility']) || ! in_array($entity['visibility'], ['public', 'unlisted'])) {
             return url(Storage::url('public/no-preview.png'));
         }
 
         return collect($entity['media_attachments'])
-            ->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png', 'image/jpg']))
-            ->map(function($media) {
-                if(!Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) {
+            ->filter(fn ($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png', 'image/jpg']))
+            ->map(function ($media) {
+                if (! Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) {
                     return $media['preview_url'];
                 }
 
@@ -125,15 +128,16 @@ class Status extends Model
 
     public function url($forceLocal = false)
     {
-        if($this->uri) {
+        if ($this->uri) {
             return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
         } else {
             $id = $this->id;
             $account = AccountService::get($this->profile_id, true);
-            if(!$account || !isset($account['username'])) {
+            if (! $account || ! isset($account['username'])) {
                 return '/404';
             }
             $path = url(config('app.url')."/p/{$account['username']}/{$id}");
+
             return $path;
         }
     }
@@ -157,7 +161,7 @@ class Status extends Model
         $media = $this->firstMedia();
         $path = $media->media_path;
         $hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
-        $url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
+        $url = $media->cdn_url ? $media->cdn_url."?v={$hash}" : url(Storage::url($path)."?v={$hash}");
 
         return $url;
     }
@@ -167,9 +171,9 @@ class Status extends Model
         return $this->hasMany(Like::class);
     }
 
-    public function liked() : bool
+    public function liked(): bool
     {
-        if(!Auth::check()) {
+        if (! Auth::check()) {
             return false;
         }
 
@@ -200,7 +204,7 @@ class Status extends Model
 
     public function bookmarked()
     {
-        if (!Auth::check()) {
+        if (! Auth::check()) {
             return false;
         }
         $profile = Auth::user()->profile;
@@ -213,9 +217,9 @@ class Status extends Model
         return $this->hasMany(self::class, 'reblog_of_id');
     }
 
-    public function shared() : bool
+    public function shared(): bool
     {
-        if(!Auth::check()) {
+        if (! Auth::check()) {
             return false;
         }
         $pid = Auth::user()->profile_id;
@@ -241,7 +245,7 @@ class Status extends Model
     public function parent()
     {
         $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
-        if (!empty($parent)) {
+        if (! empty($parent)) {
             return $this->findOrFail($parent);
         } else {
             return false;
@@ -256,25 +260,25 @@ class Status extends Model
     public function hashtags()
     {
         return $this->hasManyThrough(
-        Hashtag::class,
-        StatusHashtag::class,
-        'status_id',
-        'id',
-        'id',
-        'hashtag_id'
-      );
+            Hashtag::class,
+            StatusHashtag::class,
+            'status_id',
+            'id',
+            'id',
+            'hashtag_id'
+        );
     }
 
     public function mentions()
     {
         return $this->hasManyThrough(
-        Profile::class,
-        Mention::class,
-        'status_id',
-        'id',
-        'id',
-        'profile_id'
-      );
+            Profile::class,
+            Mention::class,
+            'status_id',
+            'id',
+            'id',
+            'profile_id'
+        );
     }
 
     public function reportUrl()
@@ -288,17 +292,17 @@ class Status extends Model
         $mediaCollection = [];
         foreach ($media as $image) {
             $mediaCollection[] = [
-          'type'      => 'Link',
-          'href'      => $image->url(),
-          'mediaType' => $image->mime,
-        ];
+                'type' => 'Link',
+                'href' => $image->url(),
+                'mediaType' => $image->mime,
+            ];
         }
         $obj = [
-        '@context' => 'https://www.w3.org/ns/activitystreams',
-        'type'     => 'Image',
-        'name'     => null,
-        'url'      => $mediaCollection,
-      ];
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            'type' => 'Document',
+            'name' => null,
+            'url' => $mediaCollection,
+        ];
 
         return $obj;
     }
@@ -310,7 +314,7 @@ class Status extends Model
 
     public function scopeToAudience($audience)
     {
-        if(!in_array($audience, ['to', 'cc']) || $this->local == false) { 
+        if (! in_array($audience, ['to', 'cc']) || $this->local == false) {
             return;
         }
         $res = [];
@@ -321,9 +325,9 @@ class Status extends Model
             return $mention->permalink();
         })->toArray();
 
-        if($this->in_reply_to_id != null) {
+        if ($this->in_reply_to_id != null) {
             $parent = $this->parent();
-            if($parent) {
+            if ($parent) {
                 $mentions = array_merge([$parent->profile->permalink()], $mentions);
             }
         }
@@ -331,7 +335,7 @@ class Status extends Model
         switch ($scope) {
             case 'public':
                 $res['to'] = [
-                    "https://www.w3.org/ns/activitystreams#Public"
+                    'https://www.w3.org/ns/activitystreams#Public',
                 ];
                 $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
                 break;
@@ -339,7 +343,7 @@ class Status extends Model
             case 'unlisted':
                 $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
                 $res['cc'] = [
-                    "https://www.w3.org/ns/activitystreams#Public"
+                    'https://www.w3.org/ns/activitystreams#Public',
                 ];
                 break;
 
@@ -348,12 +352,13 @@ class Status extends Model
                 $res['cc'] = [];
                 break;
 
-            // TODO: Update scope when DMs are supported
+                // TODO: Update scope when DMs are supported
             case 'direct':
                 $res['to'] = [];
                 $res['cc'] = [];
                 break;
         }
+
         return $res[$audience];
     }
 

+ 3 - 20
app/Transformer/ActivityPub/Verb/CreateNote.php

@@ -3,6 +3,7 @@
 namespace App\Transformer\ActivityPub\Verb;
 
 use App\Models\CustomEmoji;
+use App\Services\MediaService;
 use App\Status;
 use App\Util\Lexer\Autolink;
 use Illuminate\Support\Str;
@@ -52,7 +53,7 @@ class CreateNote extends Fractal\TransformerAbstract
         $emojis = CustomEmoji::scan($status->caption, true) ?? [];
         $emoji = array_merge($emojis, $mentions);
         $tags = array_merge($emoji, $hashtags);
-        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
+        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
 
         return [
             '@context' => [
@@ -106,25 +107,7 @@ class CreateNote extends Fractal\TransformerAbstract
                 'to' => $status->scopeToAudience('to'),
                 'cc' => $status->scopeToAudience('cc'),
                 'sensitive' => (bool) $status->is_nsfw,
-                'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
-                    $res = [
-                        'type' => $media->activityVerb(),
-                        'mediaType' => $media->mime,
-                        'url' => $media->url(),
-                        'name' => $media->caption,
-                    ];
-                    if ($media->blurhash) {
-                        $res['blurhash'] = $media->blurhash;
-                    }
-                    if ($media->width) {
-                        $res['width'] = $media->width;
-                    }
-                    if ($media->height) {
-                        $res['height'] = $media->height;
-                    }
-
-                    return $res;
-                })->toArray(),
+                'attachment' => MediaService::activitypub($status->id, true),
                 'tag' => $tags,
                 'commentsEnabled' => (bool) ! $status->comments_disabled,
                 'capabilities' => [

+ 3 - 20
app/Transformer/ActivityPub/Verb/Note.php

@@ -3,6 +3,7 @@
 namespace App\Transformer\ActivityPub\Verb;
 
 use App\Models\CustomEmoji;
+use App\Services\MediaService;
 use App\Status;
 use App\Util\Lexer\Autolink;
 use Illuminate\Support\Str;
@@ -53,7 +54,7 @@ class Note extends Fractal\TransformerAbstract
         $emojis = CustomEmoji::scan($status->caption, true) ?? [];
         $emoji = array_merge($emojis, $mentions);
         $tags = array_merge($emoji, $hashtags);
-        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
+        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
 
         return [
             '@context' => [
@@ -100,25 +101,7 @@ class Note extends Fractal\TransformerAbstract
             'to' => $status->scopeToAudience('to'),
             'cc' => $status->scopeToAudience('cc'),
             'sensitive' => (bool) $status->is_nsfw,
-            'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
-                $res = [
-                    'type' => $media->activityVerb(),
-                    'mediaType' => $media->mime,
-                    'url' => $media->url(),
-                    'name' => $media->caption,
-                ];
-                if ($media->blurhash) {
-                    $res['blurhash'] = $media->blurhash;
-                }
-                if ($media->width) {
-                    $res['width'] = $media->width;
-                }
-                if ($media->height) {
-                    $res['height'] = $media->height;
-                }
-
-                return $res;
-            })->toArray(),
+            'attachment' => MediaService::activitypub($status->id),
             'tag' => $tags,
             'commentsEnabled' => (bool) ! $status->comments_disabled,
             'capabilities' => [

+ 3 - 9
app/Transformer/ActivityPub/Verb/UpdateNote.php

@@ -3,6 +3,7 @@
 namespace App\Transformer\ActivityPub\Verb;
 
 use App\Models\CustomEmoji;
+use App\Services\MediaService;
 use App\Status;
 use App\Util\Lexer\Autolink;
 use Illuminate\Support\Str;
@@ -53,7 +54,7 @@ class UpdateNote extends Fractal\TransformerAbstract
         $emoji = array_merge($emojis, $mentions);
         $tags = array_merge($emoji, $hashtags);
 
-        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
+        $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
         $latestEdit = $status->edits()->latest()->first();
 
         return [
@@ -107,14 +108,7 @@ class UpdateNote extends Fractal\TransformerAbstract
                 'to' => $status->scopeToAudience('to'),
                 'cc' => $status->scopeToAudience('cc'),
                 'sensitive' => (bool) $status->is_nsfw,
-                'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
-                    return [
-                        'type' => $media->activityVerb(),
-                        'mediaType' => $media->mime,
-                        'url' => $media->url(),
-                        'name' => $media->caption,
-                    ];
-                })->toArray(),
+                'attachment' => MediaService::activitypub($status->id, true),
                 'tag' => $tags,
                 'commentsEnabled' => (bool) ! $status->comments_disabled,
                 'updated' => $latestEdit->created_at->toAtomString(),

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

@@ -4,6 +4,7 @@ namespace App\Util\ActivityPub;
 
 use App\Models\InstanceActor;
 use App\Profile;
+use Artisan;
 use Cache;
 use DateTime;
 
@@ -71,14 +72,15 @@ class HttpSignature
     public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
     {
         $keyId = config('app.url').'/i/actor#main-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');
+        $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
+            $instance = InstanceActor::first() ?: tap(Artisan::call('instance:actor'), fn () => sleep(10)) && InstanceActor::first();
+            if (! $instance) {
+                throw new \Exception('Failed to generate or retrieve InstanceActor.');
+            }
+
+            return $instance->private_key;
+        });
+        abort_if(! $privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor');
         if ($body) {
             $digest = self::_digest($body);
         }

+ 1 - 2
composer.json

@@ -19,7 +19,6 @@
         "doctrine/dbal": "^3.0",
         "endroid/qr-code": "^6.0",
         "intervention/image": "^3.11.2",
-        "intervention/image-driver-vips": "^1.0",
         "jenssegers/agent": "^2.6",
         "laravel-notification-channels/expo": "^2.0.0",
         "laravel-notification-channels/webpush": "^10.2",
@@ -53,7 +52,7 @@
         "laravel/pint": "^1.13",
         "laravel/telescope": "^5.5",
         "mockery/mockery": "^1.6",
-        "nunomaduro/collision": "^8.1",
+        "nunomaduro/collision": "^8.8",
         "phpunit/phpunit": "^11.0.1"
     },
     "autoload": {

+ 18 - 153
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": "80e41279435abaff339524fb0a150bc6",
+    "content-hash": "f65f184f0c7fabfe8938012287604e36",
     "packages": [
         {
             "name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.343.9",
+            "version": "3.343.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "6ca5eb1c60b879cf516e5fadefec87afc6219e74"
+                "reference": "473d632d03a78b19f9f75a2126c5ba8c21f09346"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6ca5eb1c60b879cf516e5fadefec87afc6219e74",
-                "reference": "6ca5eb1c60b879cf516e5fadefec87afc6219e74",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/473d632d03a78b19f9f75a2126c5ba8c21f09346",
+                "reference": "473d632d03a78b19f9f75a2126c5ba8c21f09346",
                 "shasum": ""
             },
             "require": {
@@ -153,9 +153,9 @@
             "support": {
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.9"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.10"
             },
-            "time": "2025-05-12T18:11:31+00:00"
+            "time": "2025-05-13T18:09:50+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -2216,80 +2216,6 @@
             ],
             "time": "2025-02-27T13:08:55+00:00"
         },
-        {
-            "name": "intervention/image-driver-vips",
-            "version": "1.0.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Intervention/image-driver-vips.git",
-                "reference": "080de0e638bcf508b5e79c2d88e82b0fd91b12b0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Intervention/image-driver-vips/zipball/080de0e638bcf508b5e79c2d88e82b0fd91b12b0",
-                "reference": "080de0e638bcf508b5e79c2d88e82b0fd91b12b0",
-                "shasum": ""
-            },
-            "require": {
-                "intervention/image": "^3.11.0",
-                "jcupitt/vips": "^2.4",
-                "php": "^8.1"
-            },
-            "require-dev": {
-                "ext-fileinfo": "*",
-                "phpstan/phpstan": "^2",
-                "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
-                "slevomat/coding-standard": "~8.0",
-                "squizlabs/php_codesniffer": "^3.8"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Intervention\\Image\\Drivers\\Vips\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Oliver Vogel",
-                    "email": "oliver@intervention.io",
-                    "homepage": "https://intervention.io/"
-                },
-                {
-                    "name": "Thomas Picquet",
-                    "email": "thomas@sctr.net"
-                }
-            ],
-            "description": "libvips driver for Intervention Image",
-            "homepage": "https://image.intervention.io/",
-            "keywords": [
-                "image",
-                "libvips",
-                "vips"
-            ],
-            "support": {
-                "issues": "https://github.com/Intervention/image-driver-vips/issues",
-                "source": "https://github.com/Intervention/image-driver-vips/tree/1.0.5"
-            },
-            "funding": [
-                {
-                    "url": "https://paypal.me/interventionio",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/Intervention",
-                    "type": "github"
-                },
-                {
-                    "url": "https://ko-fi.com/interventionphp",
-                    "type": "ko_fi"
-                }
-            ],
-            "time": "2025-05-05T13:53:52+00:00"
-        },
         {
             "name": "jaybizzle/crawler-detect",
             "version": "v1.3.4",
@@ -2342,67 +2268,6 @@
             },
             "time": "2025-03-05T23:12:10+00:00"
         },
-        {
-            "name": "jcupitt/vips",
-            "version": "v2.5.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/libvips/php-vips.git",
-                "reference": "a54c1cceea581b592a199edd61a7c06f44a24c08"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/libvips/php-vips/zipball/a54c1cceea581b592a199edd61a7c06f44a24c08",
-                "reference": "a54c1cceea581b592a199edd61a7c06f44a24c08",
-                "shasum": ""
-            },
-            "require": {
-                "ext-ffi": "*",
-                "php": ">=7.4",
-                "psr/log": "^1.1.3|^2.0|^3.0"
-            },
-            "require-dev": {
-                "php-parallel-lint/php-parallel-lint": "^1.3",
-                "phpdocumentor/shim": "^3.3",
-                "phpunit/phpunit": "^9.5",
-                "squizlabs/php_codesniffer": "^3.7"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Jcupitt\\Vips\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "John Cupitt",
-                    "email": "jcupitt@gmail.com",
-                    "homepage": "https://github.com/jcupitt",
-                    "role": "Developer"
-                }
-            ],
-            "description": "A high-level interface to the libvips image processing library.",
-            "homepage": "https://github.com/libvips/php-vips",
-            "keywords": [
-                "image",
-                "libvips",
-                "processing"
-            ],
-            "support": {
-                "issues": "https://github.com/libvips/php-vips/issues",
-                "source": "https://github.com/libvips/php-vips/tree/v2.5.0"
-            },
-            "time": "2025-04-04T17:10:13+00:00"
-        },
         {
             "name": "jenssegers/agent",
             "version": "v2.6.4",
@@ -2618,16 +2483,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v12.13.0",
+            "version": "v12.14.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269"
+                "reference": "84b142958d1638a7e89de94ce75c2821c601d3d7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/52b588bcd8efc6d01bc1493d2d67848f8065f269",
-                "reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/84b142958d1638a7e89de94ce75c2821c601d3d7",
+                "reference": "84b142958d1638a7e89de94ce75c2821c601d3d7",
                 "shasum": ""
             },
             "require": {
@@ -2829,7 +2694,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2025-05-07T17:29:01+00:00"
+            "time": "2025-05-13T17:50:51+00:00"
         },
         {
             "name": "laravel/helpers",
@@ -2890,16 +2755,16 @@
         },
         {
             "name": "laravel/horizon",
-            "version": "v5.31.2",
+            "version": "v5.32.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/horizon.git",
-                "reference": "e6068c65be6c02a01e34531abf3143fab91f0de0"
+                "reference": "7686a8e1996472cc341dfd6f1d437065698594ad"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/horizon/zipball/e6068c65be6c02a01e34531abf3143fab91f0de0",
-                "reference": "e6068c65be6c02a01e34531abf3143fab91f0de0",
+                "url": "https://api.github.com/repos/laravel/horizon/zipball/7686a8e1996472cc341dfd6f1d437065698594ad",
+                "reference": "7686a8e1996472cc341dfd6f1d437065698594ad",
                 "shasum": ""
             },
             "require": {
@@ -2964,9 +2829,9 @@
             ],
             "support": {
                 "issues": "https://github.com/laravel/horizon/issues",
-                "source": "https://github.com/laravel/horizon/tree/v5.31.2"
+                "source": "https://github.com/laravel/horizon/tree/v5.32.0"
             },
-            "time": "2025-04-18T12:57:39+00:00"
+            "time": "2025-05-09T14:58:32+00:00"
         },
         {
             "name": "laravel/passport",

+ 2 - 2
config/image.php

@@ -7,11 +7,11 @@ return [
     | Image Driver
     |--------------------------------------------------------------------------
     |
-    | Intervention Image supports "GD Library", "Imagick" and "libvips" to process
+    | Intervention Image supports "GD Library" or "Imagick" to process
     | images internally. You may choose one of them according to your PHP
     | configuration. By default PHP's "GD Library" implementation is used.
     |
-    | Supported: "gd", "imagick", "libvips"
+    | Supported: "gd", "imagick"
     |
     */
     'driver' => env('IMAGE_DRIVER', 'gd'),

+ 23 - 29
phpunit.xml

@@ -1,31 +1,25 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    backupGlobals="false"
-    backupStaticProperties="false"
-    bootstrap="vendor/autoload.php"
-    colors="true"
-    processIsolation="false"
-    stopOnFailure="false"
-    cacheDirectory=".phpunit.cache"
-    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd">
-  <source>
-    <include>
-      <directory suffix=".php">./app</directory>
-    </include>
-  </source>
-  <testsuites>
-    <testsuite name="Feature">
-      <directory suffix="Test.php">./tests/Feature</directory>
-    </testsuite>
-    <testsuite name="Unit">
-      <directory suffix="Test.php">./tests/Unit</directory>
-    </testsuite>
-  </testsuites>
-  <php>
-    <env name="APP_ENV" value="testing"/>
-    <env name="CACHE_DRIVER" value="array"/>
-    <env name="SESSION_DRIVER" value="array"/>
-    <env name="QUEUE_DRIVER" value="sync"/>
-    <env name="MAIL_DRIVER" value="array"/>
-  </php>
+<phpunit backupGlobals="false"
+         beStrictAboutTestsThatDoNotTestAnything="false"
+         colors="true"
+         processIsolation="false"
+         stopOnError="false"
+         stopOnFailure="false"
+         cacheDirectory=".phpunit.cache"
+         backupStaticProperties="false">
+    <testsuites>
+        <testsuite name="Laravel Test Suite">
+            <directory suffix="Test.php">./tests</directory>
+        </testsuite>
+    </testsuites>
+    <php>
+        <env name="APP_ENV" value="testing" />
+        <env name="CACHE_DRIVER" value="array"/>
+        <env name="SESSION_DRIVER" value="array"/>
+        <env name="QUEUE_DRIVER" value="sync"/>
+        <env name="MAIL_DRIVER" value="array"/>
+        <ini name="date.timezone" value="UTC" />
+        <ini name="intl.default_locale" value="C.UTF-8" />
+        <ini name="memory_limit" value="2048M" />
+    </php>
 </phpunit>