ソースを参照

Merge pull request #2571 from pixelfed/staging

v0.10.10
daniel 4 年 前
コミット
4deec1f6c1
92 ファイル変更15004 行追加1075 行削除
  1. 48 7
      CHANGELOG.md
  2. 13 1
      app/Avatar.php
  3. 21 1
      app/Console/Commands/UserCreate.php
  4. 6 0
      app/Http/Controllers/AdminController.php
  5. 43 22
      app/Http/Controllers/Api/ApiV1Controller.php
  6. 3 104
      app/Http/Controllers/Api/BaseApiController.php
  7. 1 10
      app/Http/Controllers/AvatarController.php
  8. 515 0
      app/Http/Controllers/ComposeController.php
  9. 7 3
      app/Http/Controllers/DiscoverController.php
  10. 1 34
      app/Http/Controllers/MediaController.php
  11. 1 38
      app/Http/Controllers/MediaTagController.php
  12. 228 10
      app/Http/Controllers/SeasonalController.php
  13. 3 0
      app/Http/Controllers/SiteController.php
  14. 22 1
      app/Http/Controllers/StatusController.php
  15. 0 1
      app/Jobs/AvatarPipeline/AvatarOptimize.php
  16. 0 1
      app/Jobs/AvatarPipeline/CreateAvatar.php
  17. 98 0
      app/Jobs/AvatarPipeline/RemoteAvatarFetch.php
  18. 1 1
      app/Jobs/ImageOptimizePipeline/ImageOptimize.php
  19. 1 1
      app/Jobs/ImageOptimizePipeline/ImageResize.php
  20. 7 15
      app/Jobs/ImageOptimizePipeline/ImageUpdate.php
  21. 31 0
      app/Jobs/MediaPipeline/MediaStoragePipeline.php
  22. 20 1
      app/Jobs/StatusPipeline/StatusDelete.php
  23. 3 18
      app/Jobs/VideoPipeline/VideoThumbnail.php
  24. 15 12
      app/Media.php
  25. 1 1
      app/Models/InstanceActor.php
  26. 9 6
      app/Observers/AvatarObserver.php
  27. 9 0
      app/Profile.php
  28. 10 41
      app/Services/ActivityPubFetchService.php
  29. 0 0
      app/Services/EmailService.php
  30. 26 0
      app/Services/MediaPathService.php
  31. 230 0
      app/Services/MediaStorageService.php
  32. 28 0
      app/Services/StatusLabelService.php
  33. 20 2
      app/Transformer/Api/Mastodon/v1/MediaTransformer.php
  34. 19 1
      app/Transformer/Api/MediaTransformer.php
  35. 4 1
      app/Transformer/Api/StatusTransformer.php
  36. 1 1
      app/Transformer/Api/StoryItemTransformer.php
  37. 119 107
      app/Util/ActivityPub/Helpers.php
  38. 2 2
      app/Util/ActivityPub/HttpSignature.php
  39. 9 2
      app/Util/Lexer/RestrictedNames.php
  40. 10 8
      app/Util/Media/Image.php
  41. 54 5
      app/Util/Sentiment/Bouncer.php
  42. 8 1
      app/Util/Site/Config.php
  43. 1 0
      composer.json
  44. 194 102
      composer.lock
  45. 9 2
      config/backup.php
  46. 1 1
      config/cache.php
  47. 10 1
      config/instance.php
  48. 1 1
      config/pixelfed.php
  49. 38 0
      database/migrations/2021_01_25_011355_add_cdn_url_to_avatars_table.php
  50. 12113 1
      package-lock.json
  51. BIN
      public/js/app.js
  52. BIN
      public/js/components.js
  53. BIN
      public/js/compose.js
  54. BIN
      public/js/my2020.js
  55. BIN
      public/js/profile-directory.js
  56. BIN
      public/js/profile.js
  57. BIN
      public/js/quill.js
  58. BIN
      public/js/rempos.js
  59. BIN
      public/js/rempro.js
  60. BIN
      public/js/search.js
  61. BIN
      public/js/status.js
  62. BIN
      public/js/story-compose.js
  63. BIN
      public/js/theme-monokai.js
  64. BIN
      public/js/timeline.js
  65. BIN
      public/js/vendor.js
  66. BIN
      public/mix-manifest.json
  67. 18 0
      resources/assets/js/app.js
  68. 4 1
      resources/assets/js/components.js
  69. 153 15
      resources/assets/js/components/ComposeModal.vue
  70. 239 0
      resources/assets/js/components/My2020.vue
  71. 17 23
      resources/assets/js/components/PostComponent.vue
  72. 8 4
      resources/assets/js/components/Profile.vue
  73. 4 18
      resources/assets/js/components/RemotePost.vue
  74. 1 1
      resources/assets/js/components/RemoteProfile.vue
  75. 3 3
      resources/assets/js/components/SearchResults.vue
  76. 405 388
      resources/assets/js/components/Timeline.vue
  77. 29 9
      resources/assets/js/components/presenter/PhotoPresenter.vue
  78. 4 0
      resources/assets/js/my2020.js
  79. 10 0
      resources/views/account/yir.blade.php
  80. 0 1
      resources/views/admin/instances/home.blade.php
  81. 8 10
      resources/views/layouts/app.blade.php
  82. 3 7
      resources/views/layouts/partial/nav.blade.php
  83. 26 0
      resources/views/site/help/instance-actor.blade.php
  84. 1 1
      resources/views/site/help/partial/template.blade.php
  85. 22 6
      resources/views/status/compose.blade.php
  86. 4 1
      routes/api.php
  87. 23 19
      routes/web.php
  88. 1 0
      storage/app/.gitignore
  89. 2 1
      storage/app/public/.gitignore
  90. 3 0
      storage/app/public/textimg/.gitignore
  91. BIN
      storage/app/public/textimg/bg_1.jpg
  92. 2 0
      storage/app/remcache/.gitignore

+ 48 - 7
CHANGELOG.md

@@ -1,16 +1,19 @@
 # Release Notes
 
-## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.9...dev)
+## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.10.10...dev)
+### Added
+
+
+## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)
 ### Added
 - Direct Messages ([d63569c](https://github.com/pixelfed/pixelfed/commit/d63569c))
-- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5))
+- ActivityPubFetchService for signed GET requests ([8763bfc5](https://github.com/pixelfed/pixelfed/commit/8763bfc5)) ([3ee1215a](https://github.com/pixelfed/pixelfed/commit/3ee1215a))
 - Custom content warnings for remote posts ([6afc61a4](https://github.com/pixelfed/pixelfed/commit/6afc61a4))
 - Thai translations ([74cd536](https://github.com/pixelfed/pixelfed/commit/74cd536))
 - Added Bookmarks to v1 api ([99cb48c5](https://github.com/pixelfed/pixelfed/commit/99cb48c5))
 - Added New Post notification to Timeline ([a0e7c4d5](https://github.com/pixelfed/pixelfed/commit/a0e7c4d5))
 - Add Instagram Import ([e2a6bdd0](https://github.com/pixelfed/pixelfed/commit/e2a6bdd0))
 - Add notification preview to NotificationCard ([28445e27](https://github.com/pixelfed/pixelfed/commit/28445e27))
-- Add Grid Mode to Timelines ([c1853ca8](https://github.com/pixelfed/pixelfed/commit/c1853ca8))
 - Add MediaPathService ([c54b29c5](https://github.com/pixelfed/pixelfed/commit/c54b29c5))
 - Add Media Tags ([711fc020](https://github.com/pixelfed/pixelfed/commit/711fc020))
 - Add MediaTagService ([524c6d45](https://github.com/pixelfed/pixelfed/commit/524c6d45))
@@ -24,6 +27,7 @@
 - Add autospam feature ([b892bcf0](https://github.com/pixelfed/pixelfed/commit/b892bcf0))
 - Add hCaptcha ([082c1ccb](https://github.com/pixelfed/pixelfed/commit/082c1ccb))
 - Add StatusView model to store views for discover algorithm ([7a68ee94](https://github.com/pixelfed/pixelfed/commit/7a68ee94))
+- Add Year in Review feature (mysql only) ([f32072a3](https://github.com/pixelfed/pixelfed/commit/f32072a3))
 
 ### Updated
 - Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@@ -145,8 +149,8 @@
 - Updated avatars, use jpeg default. ([f6528c84](https://github.com/pixelfed/pixelfed/commit/f6528c84))
 - Updated antispam bouncer, change recent from 1 week to 3 months. ([7d818197](https://github.com/pixelfed/pixelfed/commit/7d818197))
 - Updated Post components, fix remote post and profile urls. ([cfcf17f3](https://github.com/pixelfed/pixelfed/commit/cfcf17f3))
-- Update migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
-- Update LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
+- Updated migrations, fix broken oauth change. ([4a885c88](https://github.com/pixelfed/pixelfed/commit/4a885c88))
+- Updated LikeController, store status_profile_id and is_comment attributes. ([799a4cba](https://github.com/pixelfed/pixelfed/commit/799a4cba))
 - Updated Profile, fix status count. ([6dcd472b](https://github.com/pixelfed/pixelfed/commit/6dcd472b))
 - Updated StatusService, cast response to array. ([0fbde91e](https://github.com/pixelfed/pixelfed/commit/0fbde91e))
 - Updated status model, use scope over deprecated visibility attribute. ([f70826e1](https://github.com/pixelfed/pixelfed/commit/f70826e1))
@@ -155,7 +159,44 @@
 - Updated AP helpers, fixed federation bug. ([a52564f3](https://github.com/pixelfed/pixelfed/commit/a52564f3))
 - Updated Helpers, cache profiles. ([1f672ecf](https://github.com/pixelfed/pixelfed/commit/1f672ecf))
 - Updated DiscoverController, improve trending api performance. ([d8d3331f](https://github.com/pixelfed/pixelfed/commit/d8d3331f))
-- Update InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
+- Updated InboxWorker, fix race condition in account deletes. ([4a4d8f00](https://github.com/pixelfed/pixelfed/commit/4a4d8f00))
+- Updated StoryItemTransformer, increase story duration from 5 seconds to 10 seconds. ([5b0b14fc](https://github.com/pixelfed/pixelfed/commit/5b0b14fc))
+- Updated StatusController, add view method. ([0cfc12c5](https://github.com/pixelfed/pixelfed/commit/0cfc12c5))
+- Updated MediaPathService, add story method. ([aac44309](https://github.com/pixelfed/pixelfed/commit/aac44309))
+- Updated StatusDelete job, handle cloud storage media deletes. ([4b1a0fd7](https://github.com/pixelfed/pixelfed/commit/4b1a0fd7))
+- Updated ImageOptimizePipeline, add skip_optimize and MediaStorageService support. ([234f72f3](https://github.com/pixelfed/pixelfed/commit/234f72f3))
+- Updated Media model, add cdn support to url and thumbnailUrl methods. ([57fa889d](https://github.com/pixelfed/pixelfed/commit/57fa889d))
+- Updated MediaController, remove deprecated endpoint. ([8132db74](https://github.com/pixelfed/pixelfed/commit/8132db74))
+- Updated api controllers, deprecate old endpoints. ([4415af1b](https://github.com/pixelfed/pixelfed/commit/4415af1b))
+- Updated mobile apis, add blurhash. ([cf40526e](https://github.com/pixelfed/pixelfed/commit/cf40526e))
+- Updated Image media util, store dimensions of media not thumbnail. ([40bd64aa](https://github.com/pixelfed/pixelfed/commit/40bd64aa))
+- Updated MediaTransformers, include meta attribute with focus and dimensions. ([f8cbe1e4](https://github.com/pixelfed/pixelfed/commit/f8cbe1e4))
+- Updated storage, add remote media cache directory. ([0eabbfdd](https://github.com/pixelfed/pixelfed/commit/0eabbfdd))
+- Updated backup config, prevents gateway timeouts for large databases using mysql. ([9cd4bd74](https://github.com/pixelfed/pixelfed/commit/9cd4bd74))
+- Updated MediaPipeline, handle cloud object storage. ([be6d12fc](https://github.com/pixelfed/pixelfed/commit/be6d12fc))
+- Updated AP Helpers, use MediaStoragePipeline. ([01a1ffd6](https://github.com/pixelfed/pixelfed/commit/01a1ffd6))
+- Updated RemoteProfile component, change thumbnail url. ([c1118956](https://github.com/pixelfed/pixelfed/commit/c1118956))
+- Updated blade views. ([9683e846](https://github.com/pixelfed/pixelfed/commit/9683e846))
+- Updated cache config, use phpredis by default. ([ed6877df](https://github.com/pixelfed/pixelfed/commit/ed6877df))
+- Updated components, fix url rewriter. Closes #2538. ([e8cc66dc](https://github.com/pixelfed/pixelfed/commit/e8cc66dc))
+- Updated UserCreate command, closes #2581. ([b2b8c9f9](https://github.com/pixelfed/pixelfed/commit/b2b8c9f9))
+- Updated AvatarController, remove deprecated thumb_path. ([889c3d87](https://github.com/pixelfed/pixelfed/commit/889c3d87))
+- Updated VideoThumbnail, add MediaStoragePipeline. ([98c44f7b](https://github.com/pixelfed/pixelfed/commit/98c44f7b))
+- Updated StatusDelete pipeline, fix object storage thumbnail deletion. ([f930c4bd](https://github.com/pixelfed/pixelfed/commit/f930c4bd))
+- Updated MediaStorageService, clear transformer cache after storing media. ([ce6ab80d](https://github.com/pixelfed/pixelfed/commit/ce6ab80d))
+- Updated MediaTransformer, remove cache busting. ([258b2729](https://github.com/pixelfed/pixelfed/commit/258b2729))
+- Updated AP helpers, only run MediaStoragePipeline if using cloud storage. ([77f21b4b](https://github.com/pixelfed/pixelfed/commit/77f21b4b))
+- Updated AvatarObserver, add logic to delete avatars stored in S3. ([9eafc31e](https://github.com/pixelfed/pixelfed/commit/9eafc31e))
+- Updated Profile model, use cdn_url for avatars. ([ea8e4261](https://github.com/pixelfed/pixelfed/commit/ea8e4261))
+- Updated ActivityPubFetchService, add url validation. ([654b08d3](https://github.com/pixelfed/pixelfed/commit/654b08d3))
+- Updated MediaStorageService, add avatar method. ([94a9f685](https://github.com/pixelfed/pixelfed/commit/94a9f685))
+- Updated AvatarPipeline, add remote avatar fetch. ([4c148055](https://github.com/pixelfed/pixelfed/commit/4c148055))
+- Updated ComposeController, update media version. ([cc2d4bf8](https://github.com/pixelfed/pixelfed/commit/cc2d4bf8))
+- Updated AP Helpers, add blurhash and RemoteAvatarFetch. ([de8828e8](https://github.com/pixelfed/pixelfed/commit/de8828e8))
+- Updated Timeline, prevent nextTick() when reloading same comment modal. Fixes #2584. ([cc84125b](https://github.com/pixelfed/pixelfed/commit/cc84125b))
+- Updated site config, add labels to config. ([abe9cb3d](https://github.com/pixelfed/pixelfed/commit/abe9cb3d))
+- Update StatusLabelService, change config key. ([4abfe76a](https://github.com/pixelfed/pixelfed/commit/4abfe76a))
+
 
 ## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
 ### Added
@@ -213,7 +254,7 @@
 - Updated StatusTransformer, fixes #[2113](https://github.com/pixelfed/pixelfed/issues/2113) ([eefa6e0d](https://github.com/pixelfed/pixelfed/commit/eefa6e0d))
 - Updated InternalApiController, limit remote profile ui to remote profiles ([d918a68e](https://github.com/pixelfed/pixelfed/commit/d918a68e))
 - Updated NotificationCard, fix pagination bug #[2019](https://github.com/pixelfed/pixelfed/issues/2019) ([32beaad5](https://github.com/pixelfed/pixelfed/commit/32beaad5))
-- 
+
 
 ## [v0.10.8 (2020-01-29)](https://github.com/pixelfed/pixelfed/compare/v0.10.7...v0.10.8)
 ### Added

+ 13 - 1
app/Avatar.php

@@ -14,9 +14,21 @@ class Avatar extends Model
      *
      * @var array
      */
-    protected $dates = ['deleted_at'];
+    protected $dates = [
+        'deleted_at',
+        'last_fetched_at',
+        'last_processed_at'
+    ];
+    
     protected $fillable = ['profile_id'];
 
+    protected $visible = [
+        'id',
+        'profile_id',
+        'media_path',
+        'size',
+    ];
+
     public function profile()
     {
     	return $this->belongsTo(Profile::class);

+ 21 - 1
app/Console/Commands/UserCreate.php

@@ -12,7 +12,7 @@ class UserCreate extends Command
      *
      * @var string
      */
-    protected $signature = 'user:create';
+    protected $signature = 'user:create {--name=} {--username=} {--email=} {--password=} {--is_admin=0} {--confirm_email=0}';
 
     /**
      * The console command description.
@@ -40,6 +40,26 @@ class UserCreate extends Command
     {
         $this->info('Creating a new user...');
 
+        $o = $this->options();
+
+        if( $o['name'] &&
+            $o['username'] &&
+            $o['email'] &&
+            $o['password']
+        ) {
+            $user = new User;
+            $user->username = $o['username'];
+            $user->name = $o['name'];
+            $user->email = $o['email'];
+            $user->password = bcrypt($o['password']);
+            $user->is_admin = (bool) $o['is_admin'];
+            $user->email_verified_at = (bool) $o['confirm_email'] ? now() : null;
+            $user->save();
+
+            $this->info('Successfully created user!');
+            return;
+        }
+
         $name = $this->ask('Name');
 
         $username = $this->ask('Username');

+ 6 - 0
app/Http/Controllers/AdminController.php

@@ -139,6 +139,9 @@ class AdminController extends Controller
 			$appeal->appeal_handled_at = now();
 			$appeal->save();
 
+			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
+			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
+
 			return redirect('/i/admin/reports/autospam');
 		}
 
@@ -151,6 +154,9 @@ class AdminController extends Controller
 		$appeal->appeal_handled_at = now();
 		$appeal->save();
 
+		Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
+		Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
+
 		return redirect('/i/admin/reports/autospam');
 	}
 

+ 43 - 22
app/Http/Controllers/Api/ApiV1Controller.php

@@ -53,7 +53,6 @@ use App\Services\{
     MediaBlocklistService
 };
 
-
 class ApiV1Controller extends Controller 
 {
 	protected $fractal;
@@ -98,6 +97,7 @@ class ApiV1Controller extends Controller
         	'client_secret' => $client->secret,
         	'vapid_key' => null
         ];
+        
         return response()->json($res, 200, [
             'Access-Control-Allow-Origin' => '*'
         ]);
@@ -113,14 +113,18 @@ class ApiV1Controller extends Controller
     {
         abort_if(!$request->user(), 403);
         $id = $request->user()->id;
-        $key = 'user:last_active_at:id:'.$id;
-        $ttl = now()->addMinutes(5);
-        Cache::remember($key, $ttl, function() use($id) {
-            $user = User::findOrFail($id);
-            $user->last_active_at = now();
-            $user->save();
-            return;
-        });
+
+        if($request->user()->last_active_at) {
+            $key = 'user:last_active_at:id:'.$id;
+            $ttl = now()->addMinutes(5);
+            Cache::remember($key, $ttl, function() use($id) {
+                $user = User::findOrFail($id);
+                $user->last_active_at = now();
+                $user->save();
+                return;
+            });
+        }
+
         $profile = Profile::whereNull('status')->whereUserId($id)->firstOrFail();
         $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
         $res = $this->fractal->createData($resource)->toArray();
@@ -1031,6 +1035,11 @@ class ApiV1Controller extends Controller
         ]);
 
         $user = $request->user();
+
+        if($user->last_active_at == null) {
+            return [];
+        }
+
         $profile = $user->profile;
 
         if(config('pixelfed.enforce_account_limit') == true) {
@@ -1087,8 +1096,8 @@ class ApiV1Controller extends Controller
 
         $resource = new Fractal\Resource\Item($media, new MediaTransformer());
         $res = $this->fractal->createData($resource)->toArray();
-        $res['preview_url'] = url('/storage/no-preview.png');
-        $res['url'] = url('/storage/no-preview.png');
+        $res['preview_url'] = $media->url(). '?cb=1&_v=' . time();
+        $res['url'] = $media->url(). '?cb=1&_v=' . time();
         return response()->json($res);
     }
 
@@ -1322,13 +1331,15 @@ class ApiV1Controller extends Controller
         $limit = $request->input('limit') ?? 3;
         $user = $request->user();
         
-        $key = 'user:last_active_at:id:'.$user->id;
-        $ttl = now()->addMinutes(5);
-        Cache::remember($key, $ttl, function() use($user) {
-            $user->last_active_at = now();
-            $user->save();
-            return;
-        });
+        if($user->last_active_at) {
+            $key = 'user:last_active_at:id:'.$user->id;
+            $ttl = now()->addMinutes(5);
+            Cache::remember($key, $ttl, function() use($user) {
+                $user->last_active_at = now();
+                $user->save();
+                return;
+            });
+        }
 
         $pid = $request->user()->profile_id;
 
@@ -1739,6 +1750,10 @@ class ApiV1Controller extends Controller
         $in_reply_to_id = $request->input('in_reply_to_id');
         $user = $request->user();
 
+        if($user->last_active_at == null) {
+            return [];
+        }
+
         if($in_reply_to_id) {
             $parent = Status::findOrFail($in_reply_to_id);
 
@@ -1752,6 +1767,13 @@ class ApiV1Controller extends Controller
             $status->in_reply_to_profile_id = $parent->profile_id;
             $status->save();
         } else if($ids) {
+            if(Media::whereUserId($user->id)
+                ->whereNull('status_id')
+                ->find($ids)
+                ->count() == 0
+            ) {
+                abort(400, 'Invalid media_ids');
+            }
             $status = new Status;
             $status->caption = strip_tags($request->input('status'));
             $status->profile_id = $user->profile_id;
@@ -1765,7 +1787,7 @@ class ApiV1Controller extends Controller
                 if($k + 1 > config('pixelfed.max_album_length')) {
                     continue;
                 }
-                $m = Media::findOrFail($v);
+                $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);
                 if($m->profile_id !== $user->profile_id || $m->status_id) {
                     abort(403, 'Invalid media id');
                 }
@@ -1776,7 +1798,7 @@ class ApiV1Controller extends Controller
 
             if(empty($mimes)) {
                 $status->delete();
-                abort(500, 'Invalid media ids');
+                abort(400, 'Invalid media ids');
             }
 
             $status->scope = $request->input('visibility', 'public');
@@ -1786,8 +1808,7 @@ class ApiV1Controller extends Controller
         }
 
         if(!$status) {
-            $oops = 'An error occured. RefId: '.time().'-'.$user->profile_id.':'.Str::random(5).':'.Str::random(10);
-            abort(500, $oops);
+            abort(500, 'An error occured.');
         }
 
         NewStatusPipeline::dispatch($status);

+ 3 - 104
app/Http/Controllers/Api/BaseApiController.php

@@ -183,7 +183,6 @@ class BaseApiController extends Controller
             $avatar = Avatar::whereProfileId($profile->id)->firstOrFail();
             $opath = $avatar->media_path;
             $avatar->media_path = "$public/$name";
-            $avatar->thumb_path = null;
             $avatar->change_count = ++$avatar->change_count;
             $avatar->last_processed_at = null;
             $avatar->save();
@@ -201,117 +200,17 @@ class BaseApiController extends Controller
 
     public function showTempMedia(Request $request, $profileId, $mediaId, $timestamp)
     {
-        abort_if(!$request->user(), 403);
-        abort_if(!$request->hasValidSignature(), 404); 
-        abort_if(Auth::user()->profile_id != $profileId, 404); 
-        $media = Media::whereProfileId(Auth::user()->profile_id)->findOrFail($mediaId);
-        $path = storage_path('app/'.$media->media_path);
-        return response()->file($path);
+        abort(400, 'Endpoint deprecated');
     }
 
     public function uploadMedia(Request $request)
     {
-        abort_if(!$request->user(), 403);
-        $this->validate($request, [
-              'file.*'      => function() {
-                return [
-                    'required',
-                    'mimes:' . config('pixelfed.media_types'),
-                    'max:' . config('pixelfed.max_photo_size'),
-                ];
-              },
-              'filter_name' => 'nullable|string|max:24',
-              'filter_class' => 'nullable|alpha_dash|max:24'
-        ]);
-
-        $user = Auth::user();
-        $profile = $user->profile;
-
-        if(config('pixelfed.enforce_account_limit') == true) {
-            $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
-                return Media::whereUserId($user->id)->sum('size') / 1000;
-            }); 
-            $limit = (int) config('pixelfed.max_account_size');
-            if ($size >= $limit) {
-               abort(403, 'Account size limit reached.');
-            }
-        }
-
-        $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
-        $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
-
-        $photo = $request->file('file');
-
-        $mimes = explode(',', config('pixelfed.media_types'));
-        if(in_array($photo->getMimeType(), $mimes) == false) {
-            return;
-        }
-
-        $storagePath = MediaPathService::get($user, 2);
-        $path = $photo->store($storagePath);
-        $hash = \hash_file('sha256', $photo);
-
-        abort_if(MediaBlocklistService::exists($hash) == true, 451);
-
-        $media = new Media();
-        $media->status_id = null;
-        $media->profile_id = $profile->id;
-        $media->user_id = $user->id;
-        $media->media_path = $path;
-        $media->original_sha256 = $hash;
-        $media->size = $photo->getSize();
-        $media->mime = $photo->getMimeType();
-        $media->filter_class = $filterClass;
-        $media->filter_name = $filterName;
-        $media->save();
-
-        $url = URL::temporarySignedRoute(
-            'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
-        );
-
-        switch ($media->mime) {
-            case 'image/jpeg':
-            case 'image/png':
-                ImageOptimize::dispatch($media);
-                break;
-
-            case 'video/mp4':
-                VideoThumbnail::dispatch($media);
-                $preview_url = '/storage/no-preview.png';
-                $url = '/storage/no-preview.png';
-                break;
-            
-            default:
-                break;
-        }
-
-        $resource = new Fractal\Resource\Item($media, new MediaTransformer());
-        $res = $this->fractal->createData($resource)->toArray();
-        $res['preview_url'] = $url;
-        $res['url'] = $url;
-        return response()->json($res);
+        abort(400, 'Endpoint deprecated');
     }
 
     public function deleteMedia(Request $request)
     {
-        abort_if(!$request->user(), 403);
-        $this->validate($request, [
-            'id' => 'required|integer|min:1|exists:media,id'
-        ]);
-
-        $media = Media::whereNull('status_id')
-            ->whereUserId(Auth::id())
-            ->findOrFail($request->input('id'));
-
-        Storage::delete($media->media_path);
-        Storage::delete($media->thumbnail_path);
-
-        $media->forceDelete();
-
-        return response()->json([
-            'msg' => 'Successfully deleted',
-            'code' => 200
-        ]);
+        abort(400, 'Endpoint deprecated');
     }
 
     public function verifyCredentials(Request $request)

+ 1 - 10
app/Http/Controllers/AvatarController.php

@@ -35,7 +35,6 @@ class AvatarController extends Controller
             $avatar = Avatar::firstOrNew(['profile_id' => $profile->id]);
             $currentAvatar = $avatar->recentlyCreated ? null : storage_path('app/'.$profile->avatar->media_path);
             $avatar->media_path = "$public/$name";
-            $avatar->thumb_path = null;
             $avatar->change_count = ++$avatar->change_count;
             $avatar->last_processed_at = null;
             $avatar->save();
@@ -121,10 +120,7 @@ class AvatarController extends Controller
         $avatar = $profile->avatar;
 
         if( $avatar->media_path == 'public/avatars/default.png' || 
-            $avatar->thumb_path == 'public/avatars/default.png' ||
-            $avatar->media_path == 'public/avatars/default.jpg' || 
-            $avatar->thumb_path == 'public/avatars/default.jpg'
-
+            $avatar->media_path == 'public/avatars/default.jpg'
         ) {
             return;
         }
@@ -133,12 +129,7 @@ class AvatarController extends Controller
             @unlink(storage_path('app/' . $avatar->media_path));
         }
 
-        if(is_file(storage_path('app/' . $avatar->thumb_path))) {
-            @unlink(storage_path('app/' . $avatar->thumb_path));
-        }
-
         $avatar->media_path = 'public/avatars/default.jpg';
-        $avatar->thumb_path = 'public/avatars/default.jpg';
         $avatar->change_count = $avatar->change_count + 1;
         $avatar->save();
 

+ 515 - 0
app/Http/Controllers/ComposeController.php

@@ -0,0 +1,515 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Auth, Cache, Storage, URL;
+use Carbon\Carbon;
+use App\{
+	Avatar,
+	Like,
+	Media,
+	MediaTag,
+	Notification,
+	Profile,
+	Place,
+	Status,
+	UserFilter
+};
+use App\Transformer\Api\{
+	MediaTransformer,
+	MediaDraftTransformer,
+	StatusTransformer,
+	StatusStatelessTransformer
+};
+use League\Fractal;
+use App\Util\Media\Filter;
+use League\Fractal\Serializer\ArraySerializer;
+use League\Fractal\Pagination\IlluminatePaginatorAdapter;
+use App\Jobs\AvatarPipeline\AvatarOptimize;
+use App\Jobs\ImageOptimizePipeline\ImageOptimize;
+use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
+use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\Jobs\VideoPipeline\{
+	VideoOptimize,
+	VideoPostProcess,
+	VideoThumbnail
+};
+use App\Services\NotificationService;
+use App\Services\MediaPathService;
+use App\Services\MediaBlocklistService;
+use App\Services\MediaTagService;
+use Illuminate\Support\Str;
+use App\Util\Lexer\Autolink;
+use App\Util\Lexer\Extractor;
+
+class ComposeController extends Controller
+{
+	protected $fractal;
+
+	public function __construct()
+	{
+		$this->middleware('auth');
+		$this->fractal = new Fractal\Manager();
+		$this->fractal->setSerializer(new ArraySerializer());
+	}
+
+	public function show(Request $request)
+	{
+		return view('status.compose');
+	}
+
+	public function mediaUpload(Request $request)
+	{
+		abort_if(!$request->user(), 403);
+
+		$this->validate($request, [
+			'file.*' => function() {
+				return [
+					'required',
+					'mimes:' . config('pixelfed.media_types'),
+					'max:' . config('pixelfed.max_photo_size'),
+				];
+			},
+			'filter_name' => 'nullable|string|max:24',
+			'filter_class' => 'nullable|alpha_dash|max:24'
+		]);
+
+		$user = Auth::user();
+		$profile = $user->profile;
+
+		if(config('pixelfed.enforce_account_limit') == true) {
+			$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
+				return Media::whereUserId($user->id)->sum('size') / 1000;
+			}); 
+			$limit = (int) config('pixelfed.max_account_size');
+			if ($size >= $limit) {
+				abort(403, 'Account size limit reached.');
+			}
+		}
+
+		$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
+		$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
+
+		$photo = $request->file('file');
+
+		$mimes = explode(',', config('pixelfed.media_types'));
+		if(in_array($photo->getMimeType(), $mimes) == false) {
+			return;
+		}
+
+		$storagePath = MediaPathService::get($user, 2);
+		$path = $photo->store($storagePath);
+		$hash = \hash_file('sha256', $photo);
+
+		abort_if(MediaBlocklistService::exists($hash) == true, 451);
+
+		$media = new Media();
+		$media->status_id = null;
+		$media->profile_id = $profile->id;
+		$media->user_id = $user->id;
+		$media->media_path = $path;
+		$media->original_sha256 = $hash;
+		$media->size = $photo->getSize();
+		$media->mime = $photo->getMimeType();
+		$media->filter_class = $filterClass;
+		$media->filter_name = $filterName;
+		$media->version = 3;
+		$media->save();
+
+		// $url = URL::temporarySignedRoute(
+		// 	'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id, 'timestamp' => time()]
+		// );
+
+		switch ($media->mime) {
+			case 'image/jpeg':
+			case 'image/png':
+			ImageOptimize::dispatch($media);
+			break;
+
+			case 'video/mp4':
+			VideoThumbnail::dispatch($media);
+			$preview_url = '/storage/no-preview.png';
+			$url = '/storage/no-preview.png';
+			break;
+
+			default:
+			break;
+		}
+
+		$resource = new Fractal\Resource\Item($media, new MediaTransformer());
+		$res = $this->fractal->createData($resource)->toArray();
+		$res['preview_url'] = $media->url() . '?v=' . time();
+		$res['url'] = $media->url() . '?v=' . time();
+		return response()->json($res);
+	}
+
+	public function mediaUpdate(Request $request)
+	{
+		$this->validate($request, [
+			'id' => 'required',
+			'file' => function() {
+				return [
+					'required',
+					'mimes:' . config('pixelfed.media_types'),
+					'max:' . config('pixelfed.max_photo_size'),
+				];
+			},
+		]);
+
+		$user = Auth::user();
+
+		$photo = $request->file('file');
+		$id = $request->input('id');
+
+		$media = Media::whereUserId($user->id)
+		->whereProfileId($user->profile_id)
+		->whereNull('status_id')
+		->findOrFail($id);
+
+		$media->save();
+
+		$fragments = explode('/', $media->media_path);
+		$name = last($fragments);
+		array_pop($fragments);
+		$dir = implode('/', $fragments);
+		$path = $photo->storeAs($dir, $name);
+		$res = [
+			'url' => $media->url() . '?v=' . time()
+		];
+		ImageOptimize::dispatch($media);
+		return $res;
+	}
+
+	public function mediaDelete(Request $request)
+	{
+		abort_if(!$request->user(), 403);
+
+		$this->validate($request, [
+			'id' => 'required|integer|min:1|exists:media,id'
+		]);
+
+		$media = Media::whereNull('status_id')
+		->whereUserId(Auth::id())
+		->findOrFail($request->input('id'));
+
+		Storage::delete($media->media_path);
+		Storage::delete($media->thumbnail_path);
+
+		$media->forceDelete();
+
+		return response()->json([
+			'msg' => 'Successfully deleted',
+			'code' => 200
+		]);
+	}
+
+	public function searchTag(Request $request)
+	{
+		abort_if(!$request->user(), 403);
+
+		$this->validate($request, [
+			'q' => 'required|string|min:1|max:50'
+		]);
+
+		$q = $request->input('q');
+
+		if(Str::of($q)->startsWith('@')) {
+			if(strlen($q) < 3) {
+				return [];
+			}
+			$q = mb_substr($q, 1);
+		}
+
+		$blocked = UserFilter::whereFilterableType('App\Profile')
+			->whereFilterType('block')
+			->whereFilterableId($request->user()->profile_id)
+			->pluck('user_id');
+
+		$blocked->push($request->user()->profile_id);
+
+		$results = Profile::select('id','domain','username')
+			->whereNotIn('id', $blocked)
+			->whereNull('domain')
+			->where('username','like','%'.$q.'%')
+			->limit(15)
+			->get()
+			->map(function($r) {
+				return [
+					'id' => (string) $r->id,
+					'name' => $r->username,
+					'privacy' => true,
+					'avatar' => $r->avatarUrl()
+				];
+		});
+
+		return $results;
+	}
+
+    public function searchUntag(Request $request)
+    {
+        abort_if(!$request->user(), 403);
+
+        $this->validate($request, [
+            'status_id' => 'required',
+            'profile_id' => 'required'
+        ]);
+
+        $user = $request->user();
+        $status_id = $request->input('status_id');
+        $profile_id = (int) $request->input('profile_id');
+
+        abort_if((int) $user->profile_id !== $profile_id, 400);
+
+        $tag = MediaTag::whereStatusId($status_id)
+            ->whereProfileId($profile_id)
+            ->first();
+
+        if(!$tag) {
+            return [];
+        }
+        Notification::whereItemType('App\MediaTag')
+            ->whereItemId($tag->id)
+            ->whereProfileId($profile_id)
+            ->whereAction('tagged')
+            ->delete();
+
+        MediaTagService::untag($status_id, $profile_id);
+
+        return [200];
+    }
+
+	public function searchLocation(Request $request)
+	{
+		abort_if(!Auth::check(), 403);
+		$this->validate($request, [
+			'q' => 'required|string|max:100'
+		]);
+		$q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
+		$hash = hash('sha256', $q);
+		$key = 'search:location:id:' . $hash;
+		$places = Cache::remember($key, now()->addMinutes(15), function() use($q) {
+			$q = '%' . $q . '%';
+			return Place::where('name', 'like', $q)
+			->take(80)
+			->get()
+			->map(function($r) {
+				return [
+					'id' => $r->id,
+					'name' => $r->name,
+					'country' => $r->country,
+					'url'   => $r->url()
+				];
+			});
+		});
+		return $places;
+	}
+
+	public function store(Request $request)
+	{
+		$this->validate($request, [
+			'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+			'media.*'   => 'required',
+			'media.*.id' => 'required|integer|min:1',
+			'media.*.filter_class' => 'nullable|alpha_dash|max:30',
+			'media.*.license' => 'nullable|string|max:140',
+			'media.*.alt' => 'nullable|string|max:140',
+			'cw' => 'nullable|boolean',
+			'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
+			'place' => 'nullable',
+			'comments_disabled' => 'nullable',
+			'tagged' => 'nullable',
+			// 'optimize_media' => 'nullable'
+		]);
+
+		if(config('costar.enabled') == true) {
+			$blockedKeywords = config('costar.keyword.block');
+			if($blockedKeywords !== null && $request->caption) {
+				$keywords = config('costar.keyword.block');
+				foreach($keywords as $kw) {
+					if(Str::contains($request->caption, $kw) == true) {
+						abort(400, 'Invalid object');
+					}
+				}
+			}
+		}
+
+		$user = Auth::user();
+		$profile = $user->profile;
+		$visibility = $request->input('visibility');
+		$medias = $request->input('media');
+		$attachments = [];
+		$status = new Status;
+		$mimes = [];
+		$place = $request->input('place');
+		$cw = $request->input('cw');
+		$tagged = $request->input('tagged');
+		$optimize_media = (bool) $request->input('optimize_media');
+
+		foreach($medias as $k => $media) {
+			if($k + 1 > config('pixelfed.max_album_length')) {
+				continue;
+			}
+			$m = Media::findOrFail($media['id']);
+			if($m->profile_id !== $profile->id || $m->status_id) {
+				abort(403, 'Invalid media id');
+			}
+			$m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
+			$m->license = $media['license'];
+			$m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
+			$m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
+			// if($optimize_media == false) {
+			// 	$m->skip_optimize = true;
+			// 	ImageThumbnail::dispatch($m);
+			// } else {
+			// 	ImageOptimize::dispatch($m);
+			// }
+			if($cw == true || $profile->cw == true) {
+				$m->is_nsfw = $cw;
+				$status->is_nsfw = $cw;
+			}
+			$m->save();
+			$attachments[] = $m;
+			array_push($mimes, $m->mime);
+		}
+
+		$mediaType = StatusController::mimeTypeCheck($mimes);
+
+		if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
+			abort(400, __('exception.compose.invalid.album'));
+		}
+
+		if($place && is_array($place)) {
+			$status->place_id = $place['id'];
+		}
+
+		if($request->filled('comments_disabled')) {
+			$status->comments_disabled = (bool) $request->input('comments_disabled');
+		}
+
+		$status->caption = strip_tags($request->caption);
+		$status->scope = 'draft';
+		$status->profile_id = $profile->id;
+		$status->save();
+
+		foreach($attachments as $media) {
+			$media->status_id = $status->id;
+			$media->save();
+		}
+
+		$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
+		$cw = $profile->cw == true ? true : $cw;
+		$status->is_nsfw = $cw;
+		$status->visibility = $visibility;
+		$status->scope = $visibility;
+		$status->type = $mediaType;
+		$status->save();
+
+		foreach($tagged as $tg) {
+			$mt = new MediaTag;
+			$mt->status_id = $status->id;
+			$mt->media_id = $status->media->first()->id;
+			$mt->profile_id = $tg['id'];
+			$mt->tagged_username = $tg['name'];
+			$mt->is_public = true;
+			$mt->metadata = json_encode([
+				'_v' => 1,
+			]);
+			$mt->save();
+			MediaTagService::set($mt->status_id, $mt->profile_id);
+			MediaTagService::sendNotification($mt);
+		}
+
+		NewStatusPipeline::dispatch($status);
+		Cache::forget('user:account:id:'.$profile->user_id);
+		Cache::forget('_api:statuses:recent_9:'.$profile->id);
+		Cache::forget('profile:status_count:'.$profile->id);
+		Cache::forget('status:transformer:media:attachments:'.$status->id);
+		Cache::forget($user->storageUsedKey());
+
+		return $status->url();
+	}
+
+	public function storeText(Request $request)
+	{
+		abort_unless(config('exp.top'), 404);
+		$this->validate($request, [
+			'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+			'cw' => 'nullable|boolean',
+			'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
+			'place' => 'nullable',
+			'comments_disabled' => 'nullable',
+			'tagged' => 'nullable',
+		]);
+
+		if(config('costar.enabled') == true) {
+			$blockedKeywords = config('costar.keyword.block');
+			if($blockedKeywords !== null && $request->caption) {
+				$keywords = config('costar.keyword.block');
+				foreach($keywords as $kw) {
+					if(Str::contains($request->caption, $kw) == true) {
+						abort(400, 'Invalid object');
+					}
+				}
+			}
+		}
+
+		$user = Auth::user();
+		$profile = $user->profile;
+		$visibility = $request->input('visibility');
+		$status = new Status;
+		$place = $request->input('place');
+		$cw = $request->input('cw');
+		$tagged = $request->input('tagged');
+
+		if($place && is_array($place)) {
+			$status->place_id = $place['id'];
+		}
+
+		if($request->filled('comments_disabled')) {
+			$status->comments_disabled = (bool) $request->input('comments_disabled');
+		}
+
+		$status->caption = strip_tags($request->caption);
+		$status->profile_id = $profile->id;
+		$entities = Extractor::create()->extract($status->caption);
+		$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
+		$cw = $profile->cw == true ? true : $cw;
+		$status->is_nsfw = $cw;
+		$status->visibility = $visibility;
+		$status->scope = $visibility;
+		$status->type = 'text';
+		$status->rendered = Autolink::create()->autolink($status->caption);
+		$status->entities = json_encode(array_merge([
+			'timg' => [
+				'version' => 0,
+				'bg_id' => 1,
+				'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
+				'length' => strlen($status->caption),
+			]
+		], $entities), JSON_UNESCAPED_SLASHES);
+		$status->save();
+
+		foreach($tagged as $tg) {
+			$mt = new MediaTag;
+			$mt->status_id = $status->id;
+			$mt->media_id = $status->media->first()->id;
+			$mt->profile_id = $tg['id'];
+			$mt->tagged_username = $tg['name'];
+			$mt->is_public = true;
+			$mt->metadata = json_encode([
+				'_v' => 1,
+			]);
+			$mt->save();
+			MediaTagService::set($mt->status_id, $mt->profile_id);
+			MediaTagService::sendNotification($mt);
+		}
+
+
+		Cache::forget('user:account:id:'.$profile->user_id);
+		Cache::forget('_api:statuses:recent_9:'.$profile->id);
+		Cache::forget('profile:status_count:'.$profile->id);
+
+		return $status->url();
+	}
+}

+ 7 - 3
app/Http/Controllers/DiscoverController.php

@@ -144,12 +144,12 @@ class DiscoverController extends Controller
 
     public function profilesDirectoryApi(Request $request)
     {
+      return ['error' => 'Temporarily unavailable.'];
+
       $this->validate($request, [
         'page' => 'integer|max:10'
       ]);
 
-      return ['error' => 'Temporarily unavailable.'];
-
       $page = $request->input('page') ?? 1;
       $key = 'discover:profiles:page:' . $page;
       $ttl = now()->addHours(12);
@@ -214,6 +214,8 @@ class DiscoverController extends Controller
 
     public function trendingHashtags(Request $request)
     {
+      return [];
+      
       $res = StatusHashtag::select('hashtag_id', \DB::raw('count(*) as total'))
         ->groupBy('hashtag_id')
         ->orderBy('total','desc')
@@ -234,6 +236,8 @@ class DiscoverController extends Controller
 
     public function trendingPlaces(Request $request)
     {
+      return [];
+
       $res = Status::select('place_id',DB::raw('count(place_id) as total'))
         ->whereNotNull('place_id')
         ->where('created_at','>',now()->subDays(14))
@@ -250,6 +254,6 @@ class DiscoverController extends Controller
           ];
         });
 
-      return $res;
+      return [];
     }
 }

+ 1 - 34
app/Http/Controllers/MediaController.php

@@ -22,39 +22,6 @@ class MediaController extends Controller
 
 	public function composeUpdate(Request $request, $id)
 	{
-		$this->validate($request, [
-			'file'      => function() {
-				return [
-					'required',
-					'mimes:' . config('pixelfed.media_types'),
-					'max:' . config('pixelfed.max_photo_size'),
-				];
-			},
-		]);
-
-		$user = Auth::user();
-
-		$photo = $request->file('file');
-
-		$media = Media::whereUserId($user->id)
-			->whereProfileId($user->profile_id)
-			->whereNull('status_id')
-			->findOrFail($id);
-
-		$media->version = 2;
-		$media->save();
-
-		$fragments = explode('/', $media->media_path);
-		$name = last($fragments);
-		array_pop($fragments);
-		$dir = implode('/', $fragments);
-		$path = $photo->storeAs($dir, $name);
-        $res = [];
-        $res['url'] =  URL::temporarySignedRoute(
-            'temp-media', now()->addHours(1), ['profileId' => $media->profile_id, 'mediaId' => $media->id, 'timestamp' => time()]
-        );
-        ImageOptimize::dispatch($media);
-		return $res;
-
+        abort(400, 'Endpoint deprecated');
 	}	
 }

+ 1 - 38
app/Http/Controllers/MediaTagController.php

@@ -20,44 +20,7 @@ class MediaTagController extends Controller
 
     public function usernameLookup(Request $request)
     {
-    	abort_if(!$request->user(), 403);
-
-    	$this->validate($request, [
-    		'q' => 'required|string|min:1|max:50'
-    	]);
-
-    	$q = $request->input('q');
-
-    	if(Str::of($q)->startsWith('@')) {
-    		if(strlen($q) < 3) {
-    			return [];
-    		}
-    		$q = mb_substr($q, 1);
-    	}
-
-    	$blocked = UserFilter::whereFilterableType('App\Profile')
-    		->whereFilterType('block')
-    		->whereFilterableId($request->user()->profile_id)
-    		->pluck('user_id');
-
-    	$blocked->push($request->user()->profile_id);
-
-    	$results = Profile::select('id','domain','username')
-    		->whereNotIn('id', $blocked)
-    		->whereNull('domain')
-    		->where('username','like','%'.$q.'%')
-    		->limit(15)
-    		->get()
-    		->map(function($r) {
-    			return [
-    				'id' => (string) $r->id,
-    				'name' => $r->username,
-    				'privacy' => true,
-    				'avatar' => $r->avatarUrl()
-    			];
-    		});
-
-    	return $results;
+        abort(404);
     }
 
     public function untagProfile(Request $request)

+ 228 - 10
app/Http/Controllers/SeasonalController.php

@@ -4,17 +4,235 @@ namespace App\Http\Controllers;
 
 use Illuminate\Http\Request;
 use Auth;
+use App\AccountLog;
+use App\Follower;
+use App\Like;
+use App\Status;
+use App\StatusHashtag;
+use Illuminate\Support\Facades\Cache;
 
 class SeasonalController extends Controller
 {
-    public function __construct()
-    {
-    	$this->middleware('auth');
-    }
-
-    public function yearInReview()
-    {
-    	$profile = Auth::user()->profile;
-    	return view('account.yir', compact('profile'));
-    }
+	public function __construct()
+	{
+		$this->middleware('auth');
+	}
+
+	public function yearInReview()
+	{
+		abort_if(now()->gt('2021-03-01 00:00:00'), 404);
+		abort_if(config('database.default') != 'mysql', 404);
+
+		$profile = Auth::user()->profile;
+		return view('account.yir', compact('profile'));
+	}
+
+	public function getData(Request $request)
+	{
+		abort_if(now()->gt('2021-03-01 00:00:00'), 404);
+		abort_if(config('database.default') != 'mysql', 404);
+
+		$uid = $request->user()->id;
+		$pid = $request->user()->profile_id;
+		$epoch = '2020-01-01 00:00:00';
+		$epochStart = '2020-01-01 00:00:00';
+		$epochEnd = '2020-12-31 23:59:59';
+
+		$siteKey = 'seasonal:my2020:shared';
+		$siteTtl = now()->addMonths(3);
+		$userKey = 'seasonal:my2020:user:' . $uid;
+		$userTtl = now()->addMonths(3);
+
+		$shared = Cache::remember($siteKey, $siteTtl, function() use($epochStart, $epochEnd) {
+			return [
+				'average' => [
+					'posts' => round(Status::selectRaw('*, count(profile_id) as count')
+					->whereNull('uri')
+					->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
+					->where('created_at', '>', $epochStart)
+					->where('created_at', '<', $epochEnd)
+					->groupBy('profile_id')
+					->pluck('count')
+					->avg()),
+
+					'likes' => round(Like::selectRaw('*, count(profile_id) as count')
+					->where('created_at', '>', $epochStart)
+					->where('created_at', '<', $epochEnd)
+					->groupBy('profile_id')
+					->pluck('count')
+					->avg()),
+				],
+
+				'popular' => [
+
+					'hashtag' => StatusHashtag::selectRaw('*,count(hashtag_id) as count')
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->groupBy('hashtag_id')
+						->orderByDesc('count')
+						->take(1)
+						->get()
+						->map(function($sh) {
+							return [
+								'name' => $sh->hashtag->name,
+								'count' => $sh->count
+							];
+						})
+						->first(),
+
+						'post' => Status::whereScope('public')
+						->where('likes_count', '>', 1)
+						->whereIsNsfw(false)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->orderByDesc('likes_count')
+						->take(1)
+						->get()
+						->map(function($status) {
+							return [
+								'id' => (string) $status->id,
+								'username' => (string) $status->profile->username,
+								'created_at' => $status->created_at->format('M d, Y'),
+								'type' => $status->type,
+								'url' => $status->url(),
+								'thumb' => $status->thumb(),
+								'likes_count' => $status->likes_count,
+								'reblogs_count' => $status->reblogs_count,
+								'reply_count' => $status->reply_count ?? 0,
+							];
+						})
+						->first(),
+
+						'places' => Status::selectRaw('*, count(place_id) as count')
+						->whereNotNull('place_id')
+						->having('count', '>', 1)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->groupBy('place_id')
+						->orderByDesc('count')
+						->take(1)
+						->get()
+						->map(function($sh) {
+							return [
+								'name' => $sh->place->getName(),
+								'url' => $sh->place->url(),
+								'count' => $sh->count
+							];
+						})
+					->first()
+				],
+
+			];
+		});
+
+		$res = Cache::remember($userKey, $userTtl, function() use($uid, $pid, $epochStart, $epochEnd, $request) {
+			return [
+				'account' => [
+					'user_id' => $request->user()->id,
+					'created_at' => $request->user()->created_at->format('M d, Y'),
+					'created_this_year' => $request->user()->created_at->gt('2020-01-01 00:00:00'),
+					'created_months_ago' => $request->user()->created_at->diffInMonths(now()),
+					'followers_this_year' => Follower::whereFollowingId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->count(),
+					'followed_this_year' => Follower::whereProfileId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->count(),
+					'most_popular' => Status::whereProfileId($pid)
+						->where('likes_count', '>', 1)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->orderByDesc('likes_count')
+						->take(1)
+						->get()
+						->map(function($status) {
+							return [
+								'id' => (string) $status->id,
+								'username' => (string) $status->profile->username,
+								'created_at' => $status->created_at->format('M d, Y'),
+								'type' => $status->type,
+								'url' => $status->url(),
+								'thumb' => $status->thumb(),
+								'likes_count' => $status->likes_count,
+								'reblogs_count' => $status->reblogs_count,
+								'reply_count' => $status->reply_count ?? 0,
+							];
+						})
+					->first(),
+					'posts_count' => Status::whereProfileId($pid)
+						->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->count(),
+					'likes_count' => Like::whereProfileId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->count(),
+					'hashtag' => StatusHashtag::selectRaw('*, count(hashtag_id) as count')
+						->whereProfileId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->groupBy('profile_id')
+						->orderByDesc('count')
+						->take(1)
+						->get()
+						->map(function($sh) {
+							return [
+								'name' => $sh->hashtag->name,
+								'count' => $sh->count
+							];
+						})
+					->first(),
+					'places' => Status::selectRaw('*, count(place_id) as count')
+						->whereNotNull('place_id')
+						->having('count', '>', 1)
+						->whereProfileId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->groupBy('place_id')
+						->orderByDesc('count')
+						->take(1)
+						->get()
+						->map(function($sh) {
+							return [
+								'name' => $sh->place->getName(),
+								'url' => $sh->place->url(),
+								'count' => $sh->count
+							];
+						})
+					->first(),
+					'places_total' => Status::whereProfileId($pid)
+						->where('created_at', '>', $epochStart)
+						->where('created_at', '<', $epochEnd)
+						->whereNotNull('place_id')
+						->count()
+				]
+			];
+		});
+
+		return response()->json(array_merge($res, $shared));
+	}
+
+	public function store(Request $request)
+	{
+		abort_if(now()->gt('2021-03-01 00:00:00'), 404);
+		abort_if(config('database.default') != 'mysql', 404);
+		$this->validate($request, [
+			'profile_id' => 'required',
+			'type' => 'required|string|in:view,hide'
+		]);
+
+		$user = $request->user();
+
+		$log = new AccountLog();
+		$log->user_id = $user->id;
+		$log->item_type = 'App\User';
+		$log->item_id = $user->id;
+		$log->action = $request->input('type') == 'view' ? 'seasonal.my2020.view' : 'seasonal.my2020.hide';
+		$log->ip_address = $request->ip();
+		$log->user_agent = $request->user_agent();
+		$log->save();
+	}
 }

+ 3 - 0
app/Http/Controllers/SiteController.php

@@ -9,6 +9,7 @@ use App\Util\Lexer\PrettyNumber;
 use App\{Follower, Page, Profile, Status, User, UserFilter};
 use App\Util\Localization\Localization;
 use App\Services\FollowerService;
+use App\Util\ActivityPub\Helpers;
 
 class SiteController extends Controller
 {
@@ -108,10 +109,12 @@ class SiteController extends Controller
 
     public function redirectUrl(Request $request)
     {
+        abort_if(!$request->user(), 404);
         $this->validate($request, [
             'url' => 'required|url'
         ]);
         $url = request()->input('url');
+        abort_if(Helpers::validateUrl($url) == false, 404);
         return view('site.redirect', compact('url'));
     }
 

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

@@ -282,7 +282,7 @@ class StatusController extends Controller
         $resource = new Fractal\Resource\Item($status, new Note());
         $res = $fractal->createData($resource)->toArray();
 
-        return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT);
+        return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
     }
 
     public function edit(Request $request, $username, $id)
@@ -408,4 +408,25 @@ class StatusController extends Controller
 
         return response()->json([200]);
     }
+
+    public function storeView(Request $request)
+    {
+        abort_if(!$request->user(), 403);
+
+        $this->validate($request, [
+            'status_id' => 'required|integer|exists:statuses,id',
+            'profile_id' => 'required|integer|exists:profiles,id'
+        ]);
+
+        $sid = (int) $request->input('status_id');
+        $pid = (int) $request->input('profile_id');
+
+        StatusView::firstOrCreate([
+                'status_id' => $sid,
+                'status_profile_id' => $pid,
+                'profile_id' => $request->user()->profile_id
+        ]);
+
+        return response()->json(1);
+    }
 }

+ 0 - 1
app/Jobs/AvatarPipeline/AvatarOptimize.php

@@ -58,7 +58,6 @@ class AvatarOptimize implements ShouldQueue
             $img->save($file, $quality);
 
             $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
-            $avatar->thumb_path = $avatar->media_path;
             $avatar->change_count = ++$avatar->change_count;
             $avatar->last_processed_at = Carbon::now();
             $avatar->save();

+ 0 - 1
app/Jobs/AvatarPipeline/CreateAvatar.php

@@ -45,7 +45,6 @@ class CreateAvatar implements ShouldQueue
         $avatar = new Avatar();
         $avatar->profile_id = $profile->id;
         $avatar->media_path = $path;
-        $avatar->thumb_path = $path;
         $avatar->change_count = 0;
         $avatar->last_processed_at = \Carbon\Carbon::now();
         $avatar->save();

+ 98 - 0
app/Jobs/AvatarPipeline/RemoteAvatarFetch.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace App\Jobs\AvatarPipeline;
+
+use App\Avatar;
+use App\Profile;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use App\Util\ActivityPub\Helpers;
+use Illuminate\Support\Str;
+use Zttp\Zttp;
+use App\Http\Controllers\AvatarController;
+use Storage;
+use Log;
+use Illuminate\Http\File;
+use App\Services\MediaStorageService;
+use App\Services\ActivityPubFetchService;
+
+class RemoteAvatarFetch implements ShouldQueue
+{
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $profile;
+
+	/**
+	* Delete the job if its models no longer exist.
+	*
+	* @var bool
+	*/
+	public $deleteWhenMissingModels = true;
+
+	/**
+	* Create a new job instance.
+	*
+	* @return void
+	*/
+	public function __construct(Profile $profile)
+	{
+		$this->profile = $profile;
+	}
+
+	/**
+	* Execute the job.
+	*
+	* @return void
+	*/
+	public function handle()
+	{
+		$profile = $this->profile;
+
+		if($profile->domain == null || $profile->private_key) {
+			return 1;
+		}
+
+		$avatar = Avatar::firstOrCreate([
+			'profile_id' => $profile->id
+		]);
+
+		if($avatar->media_path == null && $avatar->remote_url == null) {
+			$avatar->media_path = 'public/avatars/default.jpg';
+			$avatar->is_remote = true;
+			$avatar->save();
+		}
+
+		$person = Helpers::fetchFromUrl($profile->remote_url);
+
+		if(!$person || !isset($person['@context'])) {
+			return 1;
+		}
+
+		if( !isset($person['icon']) || 
+			!isset($person['icon']['type']) ||
+			!isset($person['icon']['url'])
+		) {
+			return 1;
+		}
+
+		if($person['icon']['type'] !== 'Image') {
+			return 1;
+		}
+
+		if(!Helpers::validateUrl($person['icon']['url'])) {
+			return 1;
+		}
+
+		$icon = $person['icon'];
+
+		$avatar->remote_url = $icon['url'];
+		$avatar->save();
+
+		MediaStorageService::avatar($avatar);
+
+		return 1;
+	}
+}

+ 1 - 1
app/Jobs/ImageOptimizePipeline/ImageOptimize.php

@@ -41,7 +41,7 @@ class ImageOptimize implements ShouldQueue
     {
         $media = $this->media;
         $path = storage_path('app/'.$media->media_path);
-        if (!is_file($path)) {
+        if (!is_file($path) || $media->skip_optimize) {
             return;
         }
 

+ 1 - 1
app/Jobs/ImageOptimizePipeline/ImageResize.php

@@ -45,7 +45,7 @@ class ImageResize implements ShouldQueue
             return;
         }
         $path = storage_path('app/'.$media->media_path);
-        if (!is_file($path)) {
+        if (!is_file($path) || $media->skip_optimize) {
             return;
         }
 

+ 7 - 15
app/Jobs/ImageOptimizePipeline/ImageUpdate.php

@@ -11,6 +11,8 @@ use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use ImageOptimizer;
 use Illuminate\Http\File;
+use App\Services\MediaPathService;
+use App\Jobs\MediaPipeline\MediaStoragePipeline;
 
 class ImageUpdate implements ShouldQueue
 {
@@ -60,7 +62,9 @@ class ImageUpdate implements ShouldQueue
 
         if (in_array($media->mime, $this->protectedMimes) == true) {
             ImageOptimizer::optimize($thumb);
-            ImageOptimizer::optimize($path);
+            if(!$media->skip_optimize) {
+                ImageOptimizer::optimize($path);
+            }
         }
 
         if (!is_file($path) || !is_file($thumb)) {
@@ -73,19 +77,7 @@ class ImageUpdate implements ShouldQueue
         $media->size = $total;
         $media->save();
 
-        if(config('pixelfed.cloud_storage') == true) {
-            $p = explode('/', $media->media_path);
-            $monthHash = $p[2];
-            $userHash = $p[3];
-            $storagePath = "public/m/{$monthHash}/{$userHash}";
-            $file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
-            $url = Storage::disk(config('filesystems.cloud'))->url($file);
-            $thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
-            $thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
-            $media->thumbnail_url = $thumbUrl;
-            $media->cdn_url = $url;
-            $media->optimized_url = $url;
-            $media->save();
-        }
+        MediaStoragePipeline::dispatch($media);
+
     }
 }

+ 31 - 0
app/Jobs/MediaPipeline/MediaStoragePipeline.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Jobs\MediaPipeline;
+
+use App\Media;
+use Cache;
+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\Redis;
+use App\Services\MediaStorageService;
+
+class MediaStoragePipeline implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $media;
+
+    public function __construct(Media $media)
+    {
+    	$this->media = $media;
+    }
+
+    public function handle()
+    {
+    	MediaStorageService::store($this->media);
+    }
+
+}

+ 20 - 1
app/Jobs/StatusPipeline/StatusDelete.php

@@ -2,7 +2,7 @@
 
 namespace App\Jobs\StatusPipeline;
 
-use DB;
+use DB, Storage;
 use App\{
     AccountInterstitial,
     MediaTag,
@@ -17,6 +17,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use League\Fractal;
+use Illuminate\Support\Str;
 use League\Fractal\Serializer\ArraySerializer;
 use App\Transformer\ActivityPub\Verb\DeleteNote;
 use App\Util\ActivityPub\Helpers;
@@ -89,6 +90,24 @@ class StatusDelete implements ShouldQueue
                 if (is_file($photo)) {
                     unlink($photo);
                 }
+                if( config('pixelfed.cloud_storage') == true) {
+                    if( Str::of($media->media_path)
+                        ->startsWith('public/') && 
+                        Storage::disk(config('filesystems.cloud'))
+                        ->exists($media->media_path)
+                    ) {
+                        Storage::disk(config('filesystems.cloud'))
+                        ->delete($media->media_path);
+                    }
+                    if( Str::of($media->thumbnail_path)
+                        ->startsWith('public/') && 
+                        Storage::disk(config('filesystems.cloud'))
+                        ->exists($media->thumbnail_path)
+                    ) {
+                        Storage::disk(config('filesystems.cloud'))
+                        ->delete($media->thumbnail_path);
+                    }
+                }
                 $media->delete();
             } catch (Exception $e) {
             }

+ 3 - 18
app/Jobs/VideoPipeline/VideoThumbnail.php

@@ -12,6 +12,7 @@ use Cache;
 use FFMpeg;
 use Storage;
 use App\Media;
+use App\Jobs\MediaPipeline\MediaStoragePipeline;
 
 class VideoThumbnail implements ShouldQueue
 {
@@ -62,26 +63,10 @@ class VideoThumbnail implements ShouldQueue
             
         }
 
-        if(config('pixelfed.cloud_storage') == true) {
-            $path = storage_path('app/'.$media->media_path);
-            $thumb = storage_path('app/'.$media->thumbnail_path);
-            $p = explode('/', $media->media_path);
-            $monthHash = $p[2];
-            $userHash = $p[3];
-            $storagePath = "public/m/{$monthHash}/{$userHash}";
-            $file = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($path), 'public');
-            $url = Storage::disk(config('filesystems.cloud'))->url($file);
-            $thumbFile = Storage::disk(config('filesystems.cloud'))->putFile($storagePath, new File($thumb), 'public');
-            $thumbUrl = Storage::disk(config('filesystems.cloud'))->url($thumbFile);
-            $media->thumbnail_url = $thumbUrl;
-            $media->cdn_url = $url;
-            $media->optimized_url = $url;
-            $media->save();
-
-        }
-        
         if($media->status_id) {
             Cache::forget('status:transformer:media:attachments:' . $media->status_id);
         }
+
+        MediaStoragePipeline::dispatch($media);
     }
 }

+ 15 - 12
app/Media.php

@@ -29,25 +29,28 @@ class Media extends Model
 
     public function url()
     {
-        if(!empty($this->remote_media) && $this->remote_url) {
-            //$url = \App\Services\MediaProxyService::get($this->remote_url, $this->mime);
-            $url = $this->remote_url;
-        } else {
-            $path = $this->media_path;
-            $url = $this->cdn_url ?? config('app.url') . Storage::url($path);
+        if($this->cdn_url) {
+            return $this->cdn_url;
         }
 
-        return $url;
+        if($this->remote_media && $this->remote_url) {
+            return $this->remote_url;
+        }
+
+        return url(Storage::url($this->media_path));
     }
 
     public function thumbnailUrl()
     {
-        if($this->remote_media == true) {
-            return $this->remote_url;
-        } else {
-            $path = $this->thumbnail_path ?? 'public/no-preview.png';
-            return url(Storage::url($path));
+        if($this->thumbnail_url) {
+            return $this->thumbnail_url;
         }
+
+        if(!$this->remote_media && $this->thumbnail_path) {
+            return url(Storage::url($this->thumbnail_path));
+        }
+
+        return url(Storage::url('public/no-preview.png'));
     }
 
     public function thumb()

+ 1 - 1
app/Models/InstanceActor.php

@@ -35,7 +35,7 @@ class InstanceActor extends Model
 				'publicKeyPem' => $this->public_key
 			],
 			'manuallyApprovesFollowers' => true,
-			'url' => route('help.instance-actor')
+			'url' => url('/site/kb/instance-actor')
 		];
 	}
 }

+ 9 - 6
app/Observers/AvatarObserver.php

@@ -3,6 +3,8 @@
 namespace App\Observers;
 
 use App\Avatar;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 
 class AvatarObserver
 {
@@ -54,12 +56,13 @@ class AvatarObserver
         ) {
             @unlink($path);
         }
-        $path = storage_path('app/'.$avatar->thumb_path);
-        if( is_file($path) && 
-            $avatar->thumb_path != 'public/avatars/default.png' &&
-            $avatar->media_path != 'public/avatars/default.jpg'
-        ) {
-            @unlink($path);
+
+        if($avatar->cdn_url) {
+            $disk = Storage::disk(config('filesystems.cloud'));
+            $base = Str::startsWith($avatar->media_path, 'cache/avatars/');
+            if($base && $disk->exists($avatar->media_path)) {
+                $disk->delete($avatar->media_path);
+            }
         }
     }
 

+ 9 - 0
app/Profile.php

@@ -151,6 +151,15 @@ class Profile extends Model
     {
         $url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
             $avatar = $this->avatar;
+
+            if($avatar->cdn_url) {
+                return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
+            }
+
+            if($avatar->is_remote) {
+                return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
+            }
+            
             $path = $avatar->media_path;
             $path = "{$path}?v={$avatar->change_count}";
 

+ 10 - 41
app/Services/ActivityPubFetchService.php

@@ -9,51 +9,20 @@ use App\Util\ActivityPub\HttpSignature;
 
 class ActivityPubFetchService
 {
-	public $signed = true;
-	public $actor;
-	public $url;
-	public $headers = [
-		'Accept'		=> 'application/activity+json, application/json',
-		'User-Agent'	=> '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
-	];
-
-	public static function queue()
-	{
-		return new self;
-	}
-
-	public function signed($signed = true)
-	{
-		$this->signed = $signed;
-		return $this;
-	}
-
-	public function actor($profile)
-	{
-		$this->actor = $profile;
-		return $this;
-	}
-
-	public function url($url)
+	public static function get($url)
 	{
 		if(!Helpers::validateUrl($url)) {
-			throw new \Exception('Invalid URL');
+			return 0;
 		}
-		$this->url = $url;
-		return $this;
-	}
 
-	public function get()
-	{
-		if($this->signed == true && $this->actor == null) {
-			throw new \Exception('Cannot sign request without actor');
-		}
-		return $this->signedRequest();
-	}
+		$headers = HttpSignature::instanceActorSign($url, false, [
+			'Accept'		=> 'application/activity+json, application/json',
+			'User-Agent'	=> '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
+		]);
 
-	protected function signedRequest()
-	{
-		$this->headers = HttpSignature::sign($this->actor, $this->url, false, $this->headers);
-		return Zttp::withHeaders($this->headers)->get($this->url)->body();
+		return Zttp::withHeaders($headers)
+			->timeout(30)
+			->get($url)
+			->body();
 	}
 }

ファイルの差分が大きいため隠しています
+ 0 - 0
app/Services/EmailService.php


+ 26 - 0
app/Services/MediaPathService.php

@@ -48,4 +48,30 @@ class MediaPathService {
 		return $path;
 	}
 
+	public static function story($account, $version = 1)
+	{
+		$mh = hash('sha256', date('Y').'-.-'.date('m'));
+		$monthHash = date('Y').date('m').substr($mh, 0, 6).substr($mh, 58, 6);
+		$random = '03'.Str::random(random_int(6,9)).'_'.Str::random(random_int(6,17));
+
+		if($account instanceOf User) {
+			switch ($version) {
+				case 1:
+					$userHash = $account->profile_id;
+					$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
+					break;
+				
+				default:
+					$userHash = $account->profile_id;
+					$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
+					break;
+			}
+		} 
+		if($account instanceOf Profile) {
+			$userHash = $account->id;
+			$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
+		}
+		return $path;
+	}
+
 }

+ 230 - 0
app/Services/MediaStorageService.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace App\Services;
+
+use App\Util\ActivityPub\Helpers;
+use Illuminate\Http\File;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Redis;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use App\Media;
+use App\Profile;
+use App\User;
+use GuzzleHttp\Client;
+use App\Http\Controllers\AvatarController;
+use GuzzleHttp\Exception\RequestException;
+
+class MediaStorageService {
+
+	public static function store(Media $media)
+	{
+		if(config('pixelfed.cloud_storage') == true) {
+			(new self())->cloudStore($media);
+		}
+
+		return;
+	}
+
+	public static function avatar($avatar)
+	{
+		return (new self())->fetchAvatar($avatar);
+	}
+
+	public static function head($url)
+	{
+		$c = new Client();
+		try {
+			$r = $c->request('HEAD', $url);
+		} catch (RequestException $e) {
+			return false;
+		}
+		$h = $r->getHeaders();
+		return [
+			'length' => $h['Content-Length'][0],
+			'mime' => $h['Content-Type'][0]
+		];
+	}
+
+	protected function cloudStore($media)
+	{
+		if($media->remote_media == true) {
+			(new self())->remoteToCloud($media);
+		} else {
+			(new self())->localToCloud($media);
+		}
+	}
+
+	protected function localToCloud($media)
+	{
+		$path = storage_path('app/'.$media->media_path);
+        $thumb = storage_path('app/'.$media->thumbnail_path);
+
+		$p = explode('/', $media->media_path);
+		$name = array_pop($p);
+		$pt = explode('/', $media->thumbnail_path);
+		$thumbname = array_pop($pt);
+		$storagePath = implode('/', $p);
+		
+		$disk = Storage::disk(config('filesystems.cloud'));
+		$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
+		$url = $disk->url($file);
+		$thumbFile = $disk->putFileAs($storagePath, new File($thumb), $thumbname, 'public');
+		$thumbUrl = $disk->url($thumbFile);
+		$media->thumbnail_url = $thumbUrl;
+		$media->cdn_url = $url;
+		$media->optimized_url = $url;
+		$media->replicated_at = now();
+		$media->save();
+		if($media->status_id) {
+			Cache::forget('status:transformer:media:attachments:' . $media->status_id);
+		}
+	}
+
+	protected function remoteToCloud($media)
+	{
+		$url = $media->remote_url;
+
+		if(!Helpers::validateUrl($url)) {
+			return;
+		}
+
+		$head = $this->head($media->remote_url);
+		
+		if(!$head) {
+			return;
+		}
+		
+		$mimes = [
+			'image/jpeg',
+			'image/png',
+			'video/mp4'
+		];
+
+		$mime = $head['mime'];
+		$max_size = (int) config('pixelfed.max_photo_size') * 1000;
+		$media->size = $head['length'];
+		$media->remote_media = true;
+		$media->save();
+
+		if(!in_array($mime, $mimes)) {
+			return;
+		}
+
+		if($head['length'] >= $max_size) {
+			return;
+		}
+
+		switch ($mime) {
+			case 'image/png':
+				$ext = '.png';
+				break;
+
+			case 'image/gif':
+				$ext = '.gif';
+				break;
+
+			case 'image/jpeg':
+				$ext = '.jpg';
+				break;
+
+			case 'video/mp4':
+				$ext = '.mp4';
+				break;
+		}
+
+		$base = MediaPathService::get($media->profile);
+		$path = Str::random(40) . $ext;
+		$tmpBase = storage_path('app/remcache/');
+		$tmpPath = $media->profile_id . '-' . $path;
+		$tmpName = $tmpBase . $tmpPath;
+		$data = file_get_contents($url, false, null, 0, $head['length']);
+		file_put_contents($tmpName, $data);
+		$hash = hash_file('sha256', $tmpName);
+
+		$disk = Storage::disk(config('filesystems.cloud'));
+		$file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
+		$permalink = $disk->url($file);
+
+		$media->media_path = $base . $path;
+		$media->cdn_url = $permalink;
+		$media->original_sha256 = $hash;
+		$media->replicated_at = now();
+		$media->save();
+
+		if($media->status_id) {
+			Cache::forget('status:transformer:media:attachments:' . $media->status_id);
+		}
+
+		unlink($tmpName);
+	}
+
+	protected function fetchAvatar($avatar)
+	{
+		$url = $avatar->remote_url;
+
+		if($url == null || Helpers::validateUrl($url) == false) {
+			return;
+		}
+
+		$head = $this->head($url);
+
+		if($head == false) {
+			return;
+		}
+
+		$mimes = [
+			'image/jpeg',
+			'image/png',
+		];
+
+		$mime = $head['mime'];
+		$max_size = (int) config('pixelfed.max_avatar_size') * 1000;
+
+		if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subDay())) {
+			return;
+		}
+
+		// handle pleroma edge case
+		if(Str::endsWith($mime, '; charset=utf-8')) {
+			$mime = str_replace('; charset=utf-8', '', $mime);
+		}
+
+		if(!in_array($mime, $mimes)) {
+			return;
+		}
+
+		if($head['length'] >= $max_size) {
+			return;
+		}
+
+		if($avatar->size && $head['length'] == $avatar->size) {
+			return;
+		}
+
+		$base = 'cache/avatars/' . $avatar->profile_id;
+		$ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
+		$path = Str::random(20) . '_avatar.' . $ext;
+		$tmpBase = storage_path('app/remcache/');
+		$tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path;
+		$tmpName = $tmpBase . $tmpPath;
+		$data = file_get_contents($url, false, null, 0, $head['length']);
+		file_put_contents($tmpName, $data);
+
+		$disk = Storage::disk(config('filesystems.cloud'));
+		$file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
+		$permalink = $disk->url($file);
+
+		$avatar->media_path = $base . $path;
+		$avatar->is_remote = true;
+		$avatar->cdn_url = $permalink;
+		$avatar->size = $head['length'];
+		$avatar->change_count = $avatar->change_count + 1;
+		$avatar->last_fetched_at = now();
+		$avatar->save();
+
+		Cache::forget('avatar:' . $avatar->profile_id);
+
+		unlink($tmpName);
+	}
+}

+ 28 - 0
app/Services/StatusLabelService.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Cache;
+use App\Status;
+use Illuminate\Support\Str;
+
+class StatusLabelService
+{
+	const CACHE_KEY = 'pf:services:status_label:_v0:';
+
+	public static function get(Status $status)
+	{
+		if(config('instance.label.covid.enabled') == false || !$status) {
+			return [
+				'covid' => false
+			];
+		}
+		
+		return Cache::remember(self::CACHE_KEY . $status->id, now()->addDays(7), function() use($status) {
+			return [
+				'covid' => Str::of(strtolower($status->caption))->contains(['covid','corona', 'coronavirus', 'vaccine', 'vaxx', 'vaccination'])
+			];
+		});
+	}
+
+}

+ 20 - 2
app/Transformer/Api/Mastodon/v1/MediaTransformer.php

@@ -9,7 +9,7 @@ class MediaTransformer extends Fractal\TransformerAbstract
 {
     public function transform(Media $media)
     {
-        return [
+        $res = [
             'id'            => (string) $media->id,
             'type'          => lcfirst($media->activityVerb()),
             'url'           => $media->url(),
@@ -17,7 +17,25 @@ class MediaTransformer extends Fractal\TransformerAbstract
             'preview_url'   => $media->thumbnailUrl(),
             'text_url'      => null,
             'meta'          => null,
-            'description'   => $media->caption
+            'description'   => $media->caption,
+            'blurhash'      => $media->blurhash
         ];
+
+        if($media->width && $media->height) {
+            $res['meta'] = [
+                'focus' => [
+                    'x' => 0,
+                    'y' => 0
+                ],
+                'original' => [
+                    'width' => $media->width,
+                    'height' => $media->height,
+                    'size' => "{$media->width}x{$media->height}",
+                    'aspect' => $media->width / $media->height
+                ]
+            ];
+        }
+
+        return $res;
     }
 }

+ 19 - 1
app/Transformer/Api/MediaTransformer.php

@@ -9,7 +9,7 @@ class MediaTransformer extends Fractal\TransformerAbstract
 {
     public function transform(Media $media)
     {
-        return [
+        $res = [
             'id'            => (string) $media->id,
             'type'          => $media->activityVerb(),
             'url'           => $media->url(),
@@ -24,6 +24,24 @@ class MediaTransformer extends Fractal\TransformerAbstract
             'filter_name'   => $media->filter_name,
             'filter_class'  => $media->version == 1 ? $media->filter_class : null,
             'mime'          => $media->mime,
+            'blurhash'      => $media->blurhash
         ];
+
+        if($media->width && $media->height) {
+            $res['meta'] = [
+                'focus' => [
+                    'x' => 0,
+                    'y' => 0
+                ],
+                'original' => [
+                    'width' => $media->width,
+                    'height' => $media->height,
+                    'size' => "{$media->width}x{$media->height}",
+                    'aspect' => $media->width / $media->height
+                ]
+            ];
+        }
+
+        return $res;
     }
 }

+ 4 - 1
app/Transformer/Api/StatusTransformer.php

@@ -7,6 +7,8 @@ use League\Fractal;
 use Cache;
 use App\Services\HashidService;
 use App\Services\MediaTagService;
+use App\Services\StatusLabelService;
+use Illuminate\Support\Str;
 
 class StatusTransformer extends Fractal\TransformerAbstract
 {
@@ -55,7 +57,8 @@ class StatusTransformer extends Fractal\TransformerAbstract
             'parent'                    => [],
             'place'                     => $status->place,
             'local'                     => (bool) $status->local,
-            'taggedPeople'              => $taggedPeople
+            'taggedPeople'              => $taggedPeople,
+            'label'                     => StatusLabelService::get($status)
         ];
     }
 

+ 1 - 1
app/Transformer/Api/StoryItemTransformer.php

@@ -14,7 +14,7 @@ class StoryItemTransformer extends Fractal\TransformerAbstract
         return [
             'id'                        => (string) $item->id,
             'type'                      => $item->type,
-            'length'                    => 5,
+            'length'                    => 10,
             'src'                       => $item->url(),
             'preview'                   => null,
             'link'                      => null,

+ 119 - 107
app/Util/ActivityPub/Helpers.php

@@ -23,9 +23,12 @@ use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use App\Util\ActivityPub\HttpSignature;
 use Illuminate\Support\Str;
+use App\Services\ActivityPubFetchService;
 use App\Services\ActivityPubDeliveryService;
 use App\Services\MediaPathService;
 use App\Services\MediaStorageService;
+use App\Jobs\MediaPipeline\MediaStoragePipeline;
+use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
 
 class Helpers {
 
@@ -214,8 +217,8 @@ class Helpers {
 		$ttl = now()->addMinutes(5);
 
 		return Cache::remember($key, $ttl, function() use($url) {
-			$res = Zttp::withoutVerifying()->withHeaders(self::zttpUserAgent())->get($url);
-			$res = json_decode($res->body(), true, 8);
+			$res = ActivityPubFetchService::get($url);
+			$res = json_decode($res, true, 8);
 			if(json_last_error() == JSON_ERROR_NONE) {
 				return $res;
 			} else {
@@ -242,129 +245,132 @@ class Helpers {
 		if($local) {
 			$id = (int) last(explode('/', $url));
 			return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
-		} else {
-			$cached = Status::whereNotIn('scope', ['draft','archived'])
-				->whereUri($url)
-				->orWhere('object_url', $url)
-				->first();
+		}
 
-			if($cached) {
-				return $cached;
-			}
+		$cached = Status::whereNotIn('scope', ['draft','archived'])
+			->whereUri($url)
+			->orWhere('object_url', $url)
+			->first();
 
-			$res = self::fetchFromUrl($url);
-			
-			if(!$res || empty($res)) {
-				return;
-			}
+		if($cached) {
+			return $cached;
+		}
 
-			if(isset($res['object'])) {
-				$activity = $res;
-			} else {
-				$activity = ['object' => $res];
-			}
+		$res = self::fetchFromUrl($url);
+		
+		if(!$res || empty($res)) {
+			return;
+		}
 
-			$scope = 'private';
-			
-			$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
+		if(isset($res['object'])) {
+			$activity = $res;
+		} else {
+			$activity = ['object' => $res];
+		}
 
-			if(isset($res['to']) == true) {
-				if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
-					$scope = 'public';
-				}
-				if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
-					$scope = 'public';
-				}
+		$scope = 'private';
+		
+		$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
+
+		if(isset($res['to']) == true) {
+			if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
+				$scope = 'public';
+			}
+			if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
+				$scope = 'public';
 			}
+		}
 
-			if(isset($res['cc']) == true) {
-				if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
-					$scope = 'unlisted';
-				}
-				if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
-					$scope = 'unlisted';
-				}
+		if(isset($res['cc']) == true) {
+			if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
+				$scope = 'unlisted';
 			}
+			if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
+				$scope = 'unlisted';
+			}
+		}
 
-			if(config('costar.enabled') == true) {
-				$blockedKeywords = config('costar.keyword.block');
-				if($blockedKeywords !== null) {
-					$keywords = config('costar.keyword.block');
-					foreach($keywords as $kw) {
-						if(Str::contains($res['content'], $kw) == true) {
-							abort(400, 'Invalid object');
-						}
+		if(config('costar.enabled') == true) {
+			$blockedKeywords = config('costar.keyword.block');
+			if($blockedKeywords !== null) {
+				$keywords = config('costar.keyword.block');
+				foreach($keywords as $kw) {
+					if(Str::contains($res['content'], $kw) == true) {
+						return;
 					}
 				}
+			}
 
-				$unlisted = config('costar.domain.unlisted');
-				if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
-					$unlisted = true;
-					$scope = 'unlisted';
-				} else {
-					$unlisted = false;
-				}
-
-				$cwDomains = config('costar.domain.cw');
-				if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
-					$cw = true;
-				} 
+			$unlisted = config('costar.domain.unlisted');
+			if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
+				$unlisted = true;
+				$scope = 'unlisted';
+			} else {
+				$unlisted = false;
 			}
 
-			$id = isset($res['id']) ? $res['id'] : $url;
+			$cwDomains = config('costar.domain.cw');
+			if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
+				$cw = true;
+			} 
+		}
 
-			if(!self::validateUrl($id) ||
-			   !self::validateUrl($activity['object']['attributedTo'])
-			) {
-				return;
-			}
+		$id = isset($res['id']) ? $res['id'] : $url;
+		$idDomain = parse_url($id, PHP_URL_HOST);
+		$urlDomain = parse_url($url, PHP_URL_HOST);
 
-			$idDomain = parse_url($id, PHP_URL_HOST);
-			$urlDomain = parse_url($url, PHP_URL_HOST);
-			$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
+		if(!self::validateUrl($id)) {
+			return;
+		}
 
-			if(
-				$idDomain !== $urlDomain || 
-				$actorDomain !== $urlDomain || 
-				$idDomain !== $actorDomain
-			) {
+		if(isset($activity['object']['attributedTo'])) {
+			$actorDomain = parse_url($activity['object']['attributedTo'], PHP_URL_HOST);
+			if(!self::validateUrl($activity['object']['attributedTo']) ||
+				$idDomain !== $actorDomain)
+			{
 				return;
 			}
+		}
 
-			$profile = self::profileFirstOrNew($activity['object']['attributedTo']);
-			if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) && $replyTo == true) {
-				$reply_to = self::statusFirstOrFetch($activity['object']['inReplyTo'], false);
-				$reply_to = optional($reply_to)->id;
-			} else {
-				$reply_to = null;
-			}
-			$ts = is_array($res['published']) ? $res['published'][0] : $res['published'];
-			$status = DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) {
-				$status = new Status;
-				$status->profile_id = $profile->id;
-				$status->url = isset($res['url']) ? $res['url'] : $url;
-				$status->uri = isset($res['url']) ? $res['url'] : $url;
-				$status->object_url = $id;
-				$status->caption = strip_tags($res['content']);
-				$status->rendered = Purify::clean($res['content']);
-				$status->created_at = Carbon::parse($ts);
-				$status->in_reply_to_id = $reply_to;
-				$status->local = false;
-				$status->is_nsfw = $cw;
-				$status->scope = $scope;
-				$status->visibility = $scope;
-				$status->cw_summary = $cw == true && isset($res['summary']) ?
-					Purify::clean(strip_tags($res['summary'])) : null;
-				$status->save();
-				if($reply_to == null) {
-					self::importNoteAttachment($res, $status);
-				}
-				return $status;
-			});
-
+		if(
+			$idDomain !== $urlDomain || 
+			$actorDomain !== $urlDomain
+		) {
+			return;
+		}
 
-			return $status;
+		$profile = self::profileFirstOrNew($activity['object']['attributedTo']);
+		if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) && $replyTo == true) {
+			$reply_to = self::statusFirstOrFetch($activity['object']['inReplyTo'], false);
+			$reply_to = optional($reply_to)->id;
+		} else {
+			$reply_to = null;
 		}
+		$ts = is_array($res['published']) ? $res['published'][0] : $res['published'];
+		$status = DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) {
+			$status = new Status;
+			$status->profile_id = $profile->id;
+			$status->url = isset($res['url']) ? $res['url'] : $url;
+			$status->uri = isset($res['url']) ? $res['url'] : $url;
+			$status->object_url = $id;
+			$status->caption = strip_tags($res['content']);
+			$status->rendered = Purify::clean($res['content']);
+			$status->created_at = Carbon::parse($ts);
+			$status->in_reply_to_id = $reply_to;
+			$status->local = false;
+			$status->is_nsfw = $cw;
+			$status->scope = $scope;
+			$status->visibility = $scope;
+			$status->cw_summary = $cw == true && isset($res['summary']) ?
+				Purify::clean(strip_tags($res['summary'])) : null;
+			$status->save();
+			if($reply_to == null) {
+				self::importNoteAttachment($res, $status);
+			}
+			return $status;
+		});
+
+		return $status;
 	}
 
 	public static function statusFetch($url)
@@ -385,12 +391,14 @@ class Helpers {
 		foreach($attachments as $media) {
 			$type = $media['mediaType'];
 			$url = $media['url'];
+			$blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
 			$valid = self::validateUrl($url);
 			if(in_array($type, $allowed) == false || $valid == false) {
 				continue;
 			}
 
 			$media = new Media();
+			$media->blurhash = $blurhash;
 			$media->remote_media = true;
 			$media->status_id = $status->id;
 			$media->profile_id = $status->profile_id;
@@ -398,7 +406,12 @@ class Helpers {
 			$media->media_path = $url;
 			$media->remote_url = $url;
 			$media->mime = $type;
+			$media->version = 3;
 			$media->save();
+
+			if(config('pixelfed.cloud_storage') == true) {
+				MediaStoragePipeline::dispatch($media);
+			}
 		}
 		
 		$status->viewType();
@@ -425,6 +438,7 @@ class Helpers {
 					->whereUsername($id)
 					->firstOrFail();
 			}
+
 			$res = self::fetchProfileFromUrl($url);
 			if(isset($res['id']) == false) {
 				return;
@@ -460,10 +474,7 @@ class Helpers {
 					$profile->webfinger = strtolower(Purify::clean($webfinger));
 					$profile->last_fetched_at = now();
 					$profile->save();
-					if($runJobs == true) {
-						// RemoteFollowImportRecent::dispatch($res, $profile);
-						CreateAvatar::dispatch($profile);
-					}
+					RemoteAvatarFetch::dispatch($profile);
 					return $profile;
 				});
 			} else {
@@ -477,6 +488,7 @@ class Helpers {
 					$profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) && Helpers::validateUrl($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null;
 					$profile->save();
 				}
+				RemoteAvatarFetch::dispatch($profile);
 			}
 			return $profile;
 		});

+ 2 - 2
app/Util/ActivityPub/HttpSignature.php

@@ -43,7 +43,7 @@ class HttpSignature {
       $digest = self::_digest($body);
     }
     $headers = self::_headersToSign($url, $body ? $digest : false);
-    $headers = array_merge($headers, $addlHeaders);
+    $headers = array_unique(array_merge($headers, $addlHeaders));
     $stringToSign = self::_headersToSigningString($headers);
     $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
     $key = openssl_pkey_get_private($privateKey);
@@ -53,7 +53,7 @@ class HttpSignature {
     unset($headers['(request-target)']);
     $headers['Signature'] = $signatureHeader;
 
-    return self::_headersToCurlArray($headers);
+    return $headers;
   }
 
   public static function parseSignatureHeader($signature) {

+ 9 - 2
app/Util/Lexer/RestrictedNames.php

@@ -87,6 +87,14 @@ class RestrictedNames
 		'assets',
 		'public',
 		'storage',
+		'htaccess',
+		'.htaccess',
+		'favicon.ico',
+		'embed.js',
+		'index.php',
+		'manifest.json',
+		'mix-manifest.json',
+		'robots.txt',
 
 		// Laravel Horizon
 		'horizon',
@@ -147,7 +155,6 @@ class RestrictedNames
 		'driver',
 		'e',
 		'embed',
-		'embed.js',
 		'email',
 		'emails',
 		'error',
@@ -191,7 +198,6 @@ class RestrictedNames
 		'invites',
 		'import',
 		'imports',
-		'index.php',
 		'j',
 		'js',
 		'k',
@@ -329,6 +335,7 @@ class RestrictedNames
 		$reserved = self::$reserved;
 
 		$res = array_merge($additional, $reserved, $banned);
+		$res = array_unique($res);
 		sort($res);
 		
 		return $res;

+ 10 - 8
app/Util/Media/Image.php

@@ -4,7 +4,7 @@ namespace App\Util\Media;
 
 use App\Media;
 use Image as Intervention;
-use Cache, Storage;
+use Cache, Log, Storage;
 
 class Image
 {
@@ -165,30 +165,32 @@ class Image
 
 			$quality = config('pixelfed.image_quality');
 			$img->save($newPath, $quality);
-			$media->width = $img->width();
-			$media->height = $img->height();
-			$img->destroy();
-			if (!$thumbnail) {
-				$media->orientation = $orientation;
-			}
 
 			if ($thumbnail == true) {
 				$media->thumbnail_path = $converted['path'];
 				$media->thumbnail_url = url(Storage::url($converted['path']));
 			} else {
+				$media->width = $img->width();
+				$media->height = $img->height();
+				$media->orientation = $orientation;
 				$media->media_path = $converted['path'];
 				$media->mime = $img->mime;
 			}
 
-
+			$img->destroy();
 			$media->save();
 
 			if($thumbnail) {
 				$this->generateBlurhash($media);
 			}
+
 			Cache::forget('status:transformer:media:attachments:'.$media->status_id);
 			Cache::forget('status:thumb:'.$media->status_id);
+
 		} catch (Exception $e) {
+			$media->processed_at = now();
+			$media->save();
+			Log::info('MediaResizeException: Could not process media id: ' . $media->id);
 		}
 	}
 

+ 54 - 5
app/Util/Sentiment/Bouncer.php

@@ -15,12 +15,50 @@ class Bouncer {
 			return;
 		}
 
-		$recentKey = 'pf:bouncer:recent_by_pid:' . $status->profile_id;
-		$recentTtl = now()->addMinutes(5);
-		$recent = Cache::remember($recentKey, $recentTtl, function() use($status) {
-			return $status->profile->created_at->gt(now()->subMonths(2)) || $status->profile->statuses()->count() == 0;
+		$exemptionKey = 'pf:bouncer_v0:exemption_by_pid:' . $status->profile_id;
+		$exemptionTtl = now()->addDays(12);
+
+		$exemption = Cache::remember($exemptionKey, $exemptionTtl, function() use($status) {
+			$uid = $status->profile->user_id;
+			$ids = AccountInterstitial::whereUserId($uid)
+				->whereType('post.autospam')
+				->whereItemType('App\Status')
+				->whereNotNull('appeal_handled_at')
+				->latest()
+				->take(5)
+				->pluck('item_id');
+
+			if($ids->count() == 0) {
+				return false;
+			}
+
+			$count = Status::select('id', 'scope')
+				->whereScope('public')
+				->find($ids)
+				->count();
+
+			return $count >= 1 ? true : false;
 		});
 
+		if($exemption == true) {
+			return;
+		}
+
+		$recentKey = 'pf:bouncer_v0:recent_by_pid:' . $status->profile_id;
+		$recentTtl = now()->addHours(28);
+
+		$recent = Cache::remember($recentKey, $recentTtl, function() use($status) {
+			return $status
+				->profile
+				->created_at
+				->gt(now()->subMonths(6)) || 
+			$status
+				->profile
+				->statuses()
+				->whereScope('public')
+				->count() == 0;
+		});
+		
 		if(!$recent) {
 			return;
 		}
@@ -29,7 +67,16 @@ class Bouncer {
 			return;
 		}
 
-		if(!Str::contains($status->caption, ['https://', 'http://', 'hxxps://', 'hxxp://', 'www.', '.com', '.net', '.org'])) {
+		if(!Str::contains($status->caption, [
+			'https://', 
+			'http://', 
+			'hxxps://', 
+			'hxxp://', 
+			'www.', 
+			'.com', 
+			'.net', 
+			'.org'
+		])) {
 			return;
 		}
 
@@ -74,6 +121,8 @@ class Bouncer {
 		$status->is_nsfw = true;
 		$status->save();
 
+		Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
+		Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
 	}
 
 }

+ 8 - 1
app/Util/Site/Config.php

@@ -8,7 +8,7 @@ use Illuminate\Support\Str;
 class Config {
 
 	public static function get() {
-		return Cache::remember('api:site:configuration:_v0.1', now()->addHours(30), function() {
+		return Cache::remember('api:site:configuration:_v0.2', now()->addHours(30), function() {
 			return [
 				'open_registration' => config('pixelfed.open_registration'),
 				'uploader' => [
@@ -62,6 +62,13 @@ class Config {
 						'instagram' => config('pixelfed.import.instagram.enabled'),
 						'mastodon' => false,
 						'pixelfed' => false
+					],
+					'label' => [
+						'covid' => [
+							'enabled' => config('instance.label.covid.enabled'),
+							'org' => config('instance.label.covid.org'),
+							'url' => config('instance.label.covid.url'),
+						]
 					]
 				]
 			];

+ 1 - 0
composer.json

@@ -44,6 +44,7 @@
         "symfony/http-kernel": "5.1.5"
     },
     "require-dev": {
+        "brianium/paratest": "^6.1",
         "facade/ignition": "^2.3.6",
         "fzaninotto/faker": "^1.4",
         "mockery/mockery": "^1.0",

+ 194 - 102
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": "b4d25a7ba9e07f08e9ddacc2ddf5cfc1",
+    "content-hash": "eab416feda81875b20d5df2399f9ed86",
     "packages": [
         {
             "name": "alchemy/binary-driver",
@@ -130,16 +130,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.171.16",
+            "version": "3.172.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "216ff33ce238c30cf793973262ea727f2ce41224"
+                "reference": "5a5e66c4d54c392042820703eeb8a6bd3d222924"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/216ff33ce238c30cf793973262ea727f2ce41224",
-                "reference": "216ff33ce238c30cf793973262ea727f2ce41224",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5a5e66c4d54c392042820703eeb8a6bd3d222924",
+                "reference": "5a5e66c4d54c392042820703eeb8a6bd3d222924",
                 "shasum": ""
             },
             "require": {
@@ -214,9 +214,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.171.16"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.172.0"
             },
-            "time": "2021-01-12T19:12:49+00:00"
+            "time": "2021-01-22T19:21:38+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -1971,23 +1971,23 @@
         },
         {
             "name": "jaybizzle/crawler-detect",
-            "version": "v1.2.103",
+            "version": "v1.2.104",
             "source": {
                 "type": "git",
                 "url": "https://github.com/JayBizzle/Crawler-Detect.git",
-                "reference": "3efa2860959cc971f17624b40bf0699823f9d0f3"
+                "reference": "a581e89a9212c4e9d18049666dc735718c29de9c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/3efa2860959cc971f17624b40bf0699823f9d0f3",
-                "reference": "3efa2860959cc971f17624b40bf0699823f9d0f3",
+                "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/a581e89a9212c4e9d18049666dc735718c29de9c",
+                "reference": "a581e89a9212c4e9d18049666dc735718c29de9c",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8|^5.5|^6.5"
+                "phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4"
             },
             "type": "library",
             "autoload": {
@@ -2017,9 +2017,9 @@
             ],
             "support": {
                 "issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
-                "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.103"
+                "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.104"
             },
-            "time": "2020-11-23T19:49:25+00:00"
+            "time": "2021-01-13T15:25:20+00:00"
         },
         {
             "name": "jenssegers/agent",
@@ -2106,16 +2106,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v8.22.1",
+            "version": "v8.25.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "5c70991b96c5722afed541a996479b5112654c8b"
+                "reference": "05da44d6823c2923597519ac10151f5827a24f80"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/5c70991b96c5722afed541a996479b5112654c8b",
-                "reference": "5c70991b96c5722afed541a996479b5112654c8b",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/05da44d6823c2923597519ac10151f5827a24f80",
+                "reference": "05da44d6823c2923597519ac10151f5827a24f80",
                 "shasum": ""
             },
             "require": {
@@ -2202,6 +2202,7 @@
             },
             "suggest": {
                 "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
+                "brianium/paratest": "Required to run tests in parallel (^6.0).",
                 "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).",
                 "ext-ftp": "Required to use the Flysystem FTP driver.",
                 "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
@@ -2269,7 +2270,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2021-01-13T13:37:56+00:00"
+            "time": "2021-01-26T14:40:21+00:00"
         },
         {
             "name": "laravel/helpers",
@@ -3097,16 +3098,16 @@
         },
         {
             "name": "league/mime-type-detection",
-            "version": "1.5.1",
+            "version": "1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/mime-type-detection.git",
-                "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa"
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa",
-                "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa",
+                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
                 "shasum": ""
             },
             "require": {
@@ -3114,8 +3115,9 @@
                 "php": "^7.2 || ^8.0"
             },
             "require-dev": {
-                "phpstan/phpstan": "^0.12.36",
-                "phpunit/phpunit": "^8.5.8"
+                "friendsofphp/php-cs-fixer": "^2.18",
+                "phpstan/phpstan": "^0.12.68",
+                "phpunit/phpunit": "^8.5.8 || ^9.3"
             },
             "type": "library",
             "autoload": {
@@ -3136,7 +3138,7 @@
             "description": "Mime-type detection for Flysystem",
             "support": {
                 "issues": "https://github.com/thephpleague/mime-type-detection/issues",
-                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.5.1"
+                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
             },
             "funding": [
                 {
@@ -3148,7 +3150,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-10-18T11:50:25+00:00"
+            "time": "2021-01-18T20:58:21+00:00"
         },
         {
             "name": "league/oauth2-server",
@@ -3239,16 +3241,16 @@
         },
         {
             "name": "mobiledetect/mobiledetectlib",
-            "version": "2.8.34",
+            "version": "2.8.35",
             "source": {
                 "type": "git",
                 "url": "https://github.com/serbanghita/Mobile-Detect.git",
-                "reference": "6f8113f57a508494ca36acbcfa2dc2d923c7ed5b"
+                "reference": "68a35170fdf36e7b35f9c125e5102338dbc3ff65"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/6f8113f57a508494ca36acbcfa2dc2d923c7ed5b",
-                "reference": "6f8113f57a508494ca36acbcfa2dc2d923c7ed5b",
+                "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/68a35170fdf36e7b35f9c125e5102338dbc3ff65",
+                "reference": "68a35170fdf36e7b35f9c125e5102338dbc3ff65",
                 "shasum": ""
             },
             "require": {
@@ -3289,9 +3291,9 @@
             ],
             "support": {
                 "issues": "https://github.com/serbanghita/Mobile-Detect/issues",
-                "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.34"
+                "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.35"
             },
-            "time": "2019-09-18T18:44:20+00:00"
+            "time": "2021-01-25T19:09:34+00:00"
         },
         {
             "name": "monolog/monolog",
@@ -3904,16 +3906,16 @@
         },
         {
             "name": "pbmedia/laravel-ffmpeg",
-            "version": "7.5.4",
+            "version": "7.5.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/protonemedia/laravel-ffmpeg.git",
-                "reference": "72bb005b4be13710663e7de9077d32c7a76158a3"
+                "reference": "460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/72bb005b4be13710663e7de9077d32c7a76158a3",
-                "reference": "72bb005b4be13710663e7de9077d32c7a76158a3",
+                "url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0",
+                "reference": "460b879f7b1b6333ee02fe1fa35d6ff5bc4c0ea0",
                 "shasum": ""
             },
             "require": {
@@ -3977,7 +3979,7 @@
             ],
             "support": {
                 "issues": "https://github.com/protonemedia/laravel-ffmpeg/issues",
-                "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.4"
+                "source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.5"
             },
             "funding": [
                 {
@@ -3985,7 +3987,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-01-07T08:06:09+00:00"
+            "time": "2021-01-18T14:48:50+00:00"
         },
         {
             "name": "php-ffmpeg/php-ffmpeg",
@@ -4976,16 +4978,16 @@
         },
         {
             "name": "psy/psysh",
-            "version": "v0.10.5",
+            "version": "v0.10.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "7c710551d4a2653afa259c544508dc18a9098956"
+                "reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7c710551d4a2653afa259c544508dc18a9098956",
-                "reference": "7c710551d4a2653afa259c544508dc18a9098956",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6f990c19f91729de8b31e639d6e204ea59f19cf3",
+                "reference": "6f990c19f91729de8b31e639d6e204ea59f19cf3",
                 "shasum": ""
             },
             "require": {
@@ -5014,7 +5016,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "0.10.x-dev"
+                    "dev-main": "0.10.x-dev"
                 }
             },
             "autoload": {
@@ -5046,9 +5048,9 @@
             ],
             "support": {
                 "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.10.5"
+                "source": "https://github.com/bobthecow/psysh/tree/v0.10.6"
             },
-            "time": "2020-12-04T02:51:30+00:00"
+            "time": "2021-01-18T15:53:43+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
@@ -5096,16 +5098,16 @@
         },
         {
             "name": "ramsey/collection",
-            "version": "1.1.1",
+            "version": "1.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/collection.git",
-                "reference": "24d93aefb2cd786b7edd9f45b554aea20b28b9b1"
+                "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/collection/zipball/24d93aefb2cd786b7edd9f45b554aea20b28b9b1",
-                "reference": "24d93aefb2cd786b7edd9f45b554aea20b28b9b1",
+                "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
+                "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1",
                 "shasum": ""
             },
             "require": {
@@ -5115,19 +5117,19 @@
                 "captainhook/captainhook": "^5.3",
                 "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
                 "ergebnis/composer-normalize": "^2.6",
-                "fzaninotto/faker": "^1.5",
+                "fakerphp/faker": "^1.5",
                 "hamcrest/hamcrest-php": "^2",
-                "jangregor/phpstan-prophecy": "^0.6",
+                "jangregor/phpstan-prophecy": "^0.8",
                 "mockery/mockery": "^1.3",
                 "phpstan/extension-installer": "^1",
                 "phpstan/phpstan": "^0.12.32",
                 "phpstan/phpstan-mockery": "^0.12.5",
                 "phpstan/phpstan-phpunit": "^0.12.11",
-                "phpunit/phpunit": "^8.5",
+                "phpunit/phpunit": "^8.5 || ^9",
                 "psy/psysh": "^0.10.4",
                 "slevomat/coding-standard": "^6.3",
                 "squizlabs/php_codesniffer": "^3.5",
-                "vimeo/psalm": "^3.12.2"
+                "vimeo/psalm": "^4.4"
             },
             "type": "library",
             "autoload": {
@@ -5157,15 +5159,19 @@
             ],
             "support": {
                 "issues": "https://github.com/ramsey/collection/issues",
-                "source": "https://github.com/ramsey/collection/tree/1.1.1"
+                "source": "https://github.com/ramsey/collection/tree/1.1.3"
             },
             "funding": [
                 {
                     "url": "https://github.com/ramsey",
                     "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2020-09-10T20:58:17+00:00"
+            "time": "2021-01-21T17:40:04+00:00"
         },
         {
             "name": "ramsey/uuid",
@@ -5261,16 +5267,16 @@
         },
         {
             "name": "spatie/db-dumper",
-            "version": "2.18.0",
+            "version": "2.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/db-dumper.git",
-                "reference": "eddb2b7c6877817d97bbdc1c60d1a800bf5a267a"
+                "reference": "6a9004885b6de8417c2a5e1aa9e3712b49c1c59d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/db-dumper/zipball/eddb2b7c6877817d97bbdc1c60d1a800bf5a267a",
-                "reference": "eddb2b7c6877817d97bbdc1c60d1a800bf5a267a",
+                "url": "https://api.github.com/repos/spatie/db-dumper/zipball/6a9004885b6de8417c2a5e1aa9e3712b49c1c59d",
+                "reference": "6a9004885b6de8417c2a5e1aa9e3712b49c1c59d",
                 "shasum": ""
             },
             "require": {
@@ -5309,7 +5315,7 @@
             ],
             "support": {
                 "issues": "https://github.com/spatie/db-dumper/issues",
-                "source": "https://github.com/spatie/db-dumper/tree/2.18.0"
+                "source": "https://github.com/spatie/db-dumper/tree/2.20.0"
             },
             "funding": [
                 {
@@ -5317,7 +5323,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-11-10T09:20:18+00:00"
+            "time": "2021-01-26T07:44:13+00:00"
         },
         {
             "name": "spatie/image-optimizer",
@@ -5375,16 +5381,16 @@
         },
         {
             "name": "spatie/laravel-backup",
-            "version": "6.14.2",
+            "version": "6.14.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/laravel-backup.git",
-                "reference": "3374e1eeb09ef32c6bfd495ae1f2f4de4b594922"
+                "reference": "8a4c95bffffde831edaca64bdef55aac213d0eef"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/3374e1eeb09ef32c6bfd495ae1f2f4de4b594922",
-                "reference": "3374e1eeb09ef32c6bfd495ae1f2f4de4b594922",
+                "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/8a4c95bffffde831edaca64bdef55aac213d0eef",
+                "reference": "8a4c95bffffde831edaca64bdef55aac213d0eef",
                 "shasum": ""
             },
             "require": {
@@ -5448,7 +5454,7 @@
             ],
             "support": {
                 "issues": "https://github.com/spatie/laravel-backup/issues",
-                "source": "https://github.com/spatie/laravel-backup/tree/6.14.2"
+                "source": "https://github.com/spatie/laravel-backup/tree/6.14.3"
             },
             "funding": [
                 {
@@ -5460,7 +5466,7 @@
                     "type": "other"
                 }
             ],
-            "time": "2020-12-23T10:13:12+00:00"
+            "time": "2021-01-15T13:25:43+00:00"
         },
         {
             "name": "spatie/laravel-image-optimizer",
@@ -8069,16 +8075,16 @@
         },
         {
             "name": "vlucas/phpdotenv",
-            "version": "v5.2.0",
+            "version": "v5.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vlucas/phpdotenv.git",
-                "reference": "fba64139db67123c7a57072e5f8d3db10d160b66"
+                "reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/fba64139db67123c7a57072e5f8d3db10d160b66",
-                "reference": "fba64139db67123c7a57072e5f8d3db10d160b66",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
+                "reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
                 "shasum": ""
             },
             "require": {
@@ -8093,7 +8099,7 @@
             "require-dev": {
                 "bamarni/composer-bin-plugin": "^1.4.1",
                 "ext-filter": "*",
-                "phpunit/phpunit": "^7.5.20 || ^8.5.2 || ^9.0"
+                "phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5.1"
             },
             "suggest": {
                 "ext-filter": "Required to use the boolean validator."
@@ -8101,7 +8107,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.2-dev"
+                    "dev-master": "5.3-dev"
                 }
             },
             "autoload": {
@@ -8133,7 +8139,7 @@
             ],
             "support": {
                 "issues": "https://github.com/vlucas/phpdotenv/issues",
-                "source": "https://github.com/vlucas/phpdotenv/tree/v5.2.0"
+                "source": "https://github.com/vlucas/phpdotenv/tree/v5.3.0"
             },
             "funding": [
                 {
@@ -8145,7 +8151,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-14T15:57:31+00:00"
+            "time": "2021-01-20T15:23:13+00:00"
         },
         {
             "name": "voku/portable-ascii",
@@ -8226,12 +8232,12 @@
             "version": "1.9.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/webmozart/assert.git",
+                "url": "https://github.com/webmozarts/assert.git",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "shasum": ""
             },
@@ -8269,13 +8275,93 @@
                 "validate"
             ],
             "support": {
-                "issues": "https://github.com/webmozart/assert/issues",
-                "source": "https://github.com/webmozart/assert/tree/master"
+                "issues": "https://github.com/webmozarts/assert/issues",
+                "source": "https://github.com/webmozarts/assert/tree/1.9.1"
             },
             "time": "2020-07-08T17:02:28+00:00"
         }
     ],
     "packages-dev": [
+        {
+            "name": "brianium/paratest",
+            "version": "v6.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paratestphp/paratest.git",
+                "reference": "235db99a43401d68fdc4495b20b49291ea2e767d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paratestphp/paratest/zipball/235db99a43401d68fdc4495b20b49291ea2e767d",
+                "reference": "235db99a43401d68fdc4495b20b49291ea2e767d",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-pcre": "*",
+                "ext-reflection": "*",
+                "ext-simplexml": "*",
+                "php": "^7.3 || ^8.0",
+                "phpunit/php-code-coverage": "^9.2.5",
+                "phpunit/php-file-iterator": "^3.0.5",
+                "phpunit/php-timer": "^5.0.3",
+                "phpunit/phpunit": "^9.5.0",
+                "sebastian/environment": "^5.1.3",
+                "symfony/console": "^4.4 || ^5.2",
+                "symfony/process": "^4.4 || ^5.2"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^8.2.0",
+                "ekino/phpstan-banned-code": "^0.3.1",
+                "ergebnis/phpstan-rules": "^0.15.3",
+                "ext-posix": "*",
+                "infection/infection": "^0.18.2",
+                "phpstan/phpstan": "^0.12.58",
+                "phpstan/phpstan-deprecation-rules": "^0.12.5",
+                "phpstan/phpstan-phpunit": "^0.12.16",
+                "phpstan/phpstan-strict-rules": "^0.12.5",
+                "squizlabs/php_codesniffer": "^3.5.8",
+                "symfony/filesystem": "^5.2.0",
+                "thecodingmachine/phpstan-strict-rules": "^0.12.1",
+                "vimeo/psalm": "^4.3.1"
+            },
+            "bin": [
+                "bin/paratest"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "ParaTest\\": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Brian Scaturro",
+                    "email": "scaturrob@gmail.com",
+                    "homepage": "http://brianscaturro.com",
+                    "role": "Lead"
+                }
+            ],
+            "description": "Parallel testing for PHP",
+            "homepage": "https://github.com/paratestphp/paratest",
+            "keywords": [
+                "concurrent",
+                "parallel",
+                "phpunit",
+                "testing"
+            ],
+            "support": {
+                "issues": "https://github.com/paratestphp/paratest/issues",
+                "source": "https://github.com/paratestphp/paratest/tree/v6.1.2"
+            },
+            "time": "2020-12-15T11:41:54+00:00"
+        },
         {
             "name": "doctrine/instantiator",
             "version": "1.4.0",
@@ -8412,16 +8498,16 @@
         },
         {
             "name": "facade/ignition",
-            "version": "2.5.8",
+            "version": "2.5.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/ignition.git",
-                "reference": "8e907d81244649c5ea746e2ec30c32c5f59df472"
+                "reference": "66b3138ecce38024723fb3bfc66ef8852a779ea9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/ignition/zipball/8e907d81244649c5ea746e2ec30c32c5f59df472",
-                "reference": "8e907d81244649c5ea746e2ec30c32c5f59df472",
+                "url": "https://api.github.com/repos/facade/ignition/zipball/66b3138ecce38024723fb3bfc66ef8852a779ea9",
+                "reference": "66b3138ecce38024723fb3bfc66ef8852a779ea9",
                 "shasum": ""
             },
             "require": {
@@ -8485,7 +8571,7 @@
                 "issues": "https://github.com/facade/ignition/issues",
                 "source": "https://github.com/facade/ignition"
             },
-            "time": "2020-12-29T09:12:55+00:00"
+            "time": "2021-01-26T14:45:19+00:00"
         },
         {
             "name": "facade/ignition-contracts",
@@ -8542,16 +8628,16 @@
         },
         {
             "name": "filp/whoops",
-            "version": "2.9.1",
+            "version": "2.9.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771"
+                "reference": "df7933820090489623ce0be5e85c7e693638e536"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771",
-                "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536",
+                "reference": "df7933820090489623ce0be5e85c7e693638e536",
                 "shasum": ""
             },
             "require": {
@@ -8601,9 +8687,15 @@
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.9.1"
+                "source": "https://github.com/filp/whoops/tree/2.9.2"
             },
-            "time": "2020-11-01T12:00:00+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/denis-sokolov",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-01-24T12:00:00+00:00"
         },
         {
             "name": "fzaninotto/faker",
@@ -8843,16 +8935,16 @@
         },
         {
             "name": "nunomaduro/collision",
-            "version": "v5.2.0",
+            "version": "v5.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/collision.git",
-                "reference": "aca954fd03414ba0dd85d7d8e42ba9b251893d1f"
+                "reference": "aca63581f380f63a492b1e3114604e411e39133a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/aca954fd03414ba0dd85d7d8e42ba9b251893d1f",
-                "reference": "aca954fd03414ba0dd85d7d8e42ba9b251893d1f",
+                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/aca63581f380f63a492b1e3114604e411e39133a",
+                "reference": "aca63581f380f63a492b1e3114604e411e39133a",
                 "shasum": ""
             },
             "require": {
@@ -8927,7 +9019,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2021-01-13T10:00:08+00:00"
+            "time": "2021-01-25T15:34:13+00:00"
         },
         {
             "name": "phar-io/manifest",
@@ -9585,16 +9677,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.5.0",
+            "version": "9.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe"
+                "reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe",
-                "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7bdf4085de85a825f4424eae52c99a1cec2f360",
+                "reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360",
                 "shasum": ""
             },
             "require": {
@@ -9672,7 +9764,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.0"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.1"
             },
             "funding": [
                 {
@@ -9684,7 +9776,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-12-04T05:05:53+00:00"
+            "time": "2021-01-17T07:42:25+00:00"
         },
         {
             "name": "sebastian/cli-parser",

+ 9 - 2
config/backup.php

@@ -37,6 +37,13 @@ return [
                 'followLinks' => false,
             ],
 
+            'mysql' => [
+                 'dump' => [
+                      'useSingleTransaction' => true,
+                      'useQuick' => true,
+                  ],
+            ],
+
             /*
              * The names of the connections to the databases that should be backed up
              * MySQL, PostgreSQL, SQLite and Mongo databases are supported.
@@ -49,7 +56,7 @@ return [
         /*
          * The database dump can be gzipped to decrease diskspace usage.
          */
-        'gzip_database_dump' => false,
+        'gzip_database_dump' => true,
 
         'destination' => [
 
@@ -62,7 +69,7 @@ return [
              * The disk names on which the backups will be stored.
              */
             'disks' => [
-                'local',
+                'local'
             ],
         ],
     ],

+ 1 - 1
config/cache.php

@@ -70,7 +70,7 @@ return [
 
         'redis' => [
             'driver' => 'redis',
-            'client' => env('REDIS_CLIENT', 'predis'),
+            'client' => env('REDIS_CLIENT', 'phpredis'),
 
             'default' => [
                 'scheme'   => env('REDIS_SCHEME', 'tcp'),

+ 10 - 1
config/instance.php

@@ -36,6 +36,7 @@ return [
 			'body' => env('PAGE_503_BODY', 'Our service is in maintenance mode, please try again later.')
 		]
 	],
+
 	'username' => [
 		'banned' => env('BANNED_USERNAMES'),
 		'remote' => [
@@ -61,5 +62,13 @@ return [
 			'enabled' => env('OAUTH_PAT_ENABLED', false),
 			'id' 	  => env('OAUTH_PAT_ID'),
 		]
-	]
+	],
+
+	'label' => [
+		'covid' => [
+			'enabled' => env('ENABLE_COVID_LABEL', true),
+			'url' => env('COVID_LABEL_URL', 'https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public'),
+			'org' => env('COVID_LABEL_ORG', 'visit the WHO website')
+		]
+	],
 ];

+ 1 - 1
config/pixelfed.php

@@ -23,7 +23,7 @@ return [
     | This value is the version of your Pixelfed instance.
     |
     */
-    'version' => '0.10.9',
+    'version' => '0.10.10',
 
     /*
     |--------------------------------------------------------------------------

+ 38 - 0
database/migrations/2021_01_25_011355_add_cdn_url_to_avatars_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddCdnUrlToAvatarsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('avatars', function (Blueprint $table) {
+            $table->string('cdn_url')->unique()->index()->nullable()->after('remote_url');
+            $table->unsignedInteger('size')->nullable()->after('cdn_url');
+            $table->boolean('is_remote')->nullable()->index()->after('cdn_url');
+            $table->dropColumn('thumb_path');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('avatars', function (Blueprint $table) {
+            $table->dropColumn('cdn_url');
+            $table->dropColumn('size');
+            $table->dropColumn('is_remote');
+            $table->string('thumb_path')->nullable();
+        });
+    }
+}

ファイルの差分が大きいため隠しています
+ 12113 - 1
package-lock.json


BIN
public/js/app.js


BIN
public/js/components.js


BIN
public/js/compose.js


BIN
public/js/my2020.js


BIN
public/js/profile-directory.js


BIN
public/js/profile.js


BIN
public/js/quill.js


BIN
public/js/rempos.js


BIN
public/js/rempro.js


BIN
public/js/search.js


BIN
public/js/status.js


BIN
public/js/story-compose.js


BIN
public/js/theme-monokai.js


BIN
public/js/timeline.js


BIN
public/js/vendor.js


BIN
public/mix-manifest.json


+ 18 - 0
resources/assets/js/app.js

@@ -96,6 +96,24 @@ window.App.util = {
 				return interval + "m";
 			}
 			return Math.floor(seconds) + "s";
+		}),
+		rewriteLinks: (function(i) {
+
+			let tag = i.innerText;
+
+			if(i.href.startsWith(window.location.origin)) {
+				return i.href;
+			}
+
+			if(tag.startsWith('#') == true) {
+				tag = '/discover/tags/' + tag.substr(1) +'?src=rph';
+			} else if(tag.startsWith('@') == true) {
+				tag = '/' + i.innerText + '?src=rpp';
+			} else {
+				tag = '/i/redirect?url=' + encodeURIComponent(tag);
+			}
+
+			return tag; 
 		})
 	}, 
 	filters: [

+ 4 - 1
resources/assets/js/components.js

@@ -4,7 +4,10 @@ import InfiniteLoading from 'vue-infinite-loading';
 import Loading from 'vue-loading-overlay';
 import VueTimeago from 'vue-timeago';
 import VueCarousel from 'vue-carousel';
-
+import VueBlurHash from 'vue-blurhash'
+import 'vue-blurhash/dist/vue-blurhash.css'
+  
+Vue.use(VueBlurHash);
 Vue.use(VueCarousel);
 Vue.use(BootstrapVue);
 Vue.use(InfiniteLoading);

+ 153 - 15
resources/assets/js/components/ComposeModal.vue

@@ -86,10 +86,46 @@
 						<span v-else>
 							<a v-if="!pageLoading && (page > 1 && page <= 2) || (page == 1 && ids.length != 0) || page == 'cropPhoto'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="nextPage">Next</a>
 							<a v-if="!pageLoading && page == 3" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
+							<a v-if="!pageLoading && page == 'addText'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="composeTextPost()">Post</a>
 						</span>
 					</div>
 				</div>
 				<div class="card-body p-0 border-top">
+					<div v-if="page == 'textOptions'" class="w-100 h-100" style="min-height: 280px;">
+						test
+					</div>
+					<div v-if="page == 'addText'" class="w-100 h-100" style="min-height: 280px;">
+						<div class="mt-2">
+							<div class="media px-3">
+								<div class="media-body">
+									<div class="form-group">
+										<label class="font-weight-bold text-muted small d-none">Body</label>
+										<textarea class="form-control border-0 rounded-0 no-focus" rows="7" placeholder="What's happening?" style="font-size:18px;resize:none" v-model="composeText" v-on:keyup="composeTextLength = composeText.length"></textarea>
+										<div class="border-bottom"></div>
+										<p class="help-text small text-right text-muted mb-0 font-weight-bold">{{composeTextLength}}/{{config.uploader.max_caption_length}}</p>
+										<p class="mb-0 mt-2">
+											<a class="btn btn-primary rounded-pill mr-2" href="#" style="height: 37px;" @click.prevent="showTextOptions()">
+												<i class="fas fa-palette px-3 text-white"></i>
+											</a>
+											<!-- <a class="btn btn-outline-lighter rounded-pill ml-3" href="#" @click.prevent="showLocationCard()">
+												<i class="fas fa-map-marker-alt px-3"></i>
+											</a>
+											<a class="btn btn-outline-lighter rounded-pill mx-3" href="#" @click.prevent="showTagCard()">
+												<i class="fas fa-user-plus px-3"></i>
+											</a> -->
+											<a class="btn rounded-pill mx-3 d-inline-flex align-items-center" href="#" :class="[nsfw ? 'btn-danger' : 'btn-outline-lighter']" style="height: 37px;" @click.prevent="nsfw = !nsfw" title="Mark as sensitive/not safe for work">
+												<i class="far fa-flag px-3"></i> <span class="text-muted small font-weight-bold"></span>
+											</a>
+											<a class="btn btn-outline-lighter rounded-pill d-inline-flex align-items-center" href="#" style="height: 37px;" @click.prevent="showVisibilityCard()">
+												<i class="fas fa-eye mr-2"></i> <span class="text-muted small font-weight-bold">{{visibilityTag}}</span>
+											</a>
+										</p>
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+
 					<div v-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;">
 						<div class="text-center">
 							<div v-if="media.length == 0" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark">
@@ -107,6 +143,26 @@
 									</div>
 								</div>
 							</div>
+
+							<div v-if="config.ab.top == true && media.length == 0" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark">
+								<div @click.prevent="addText" class="card-body">
+									<div class="media">
+										<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
+											<i class="far fa-edit text-primary fa-lg"></i>
+										</div>	
+										<div class="media-body text-left">
+											<p class="mb-0">
+												<span class="h5 mt-0 font-weight-bold text-primary">New Text Post</span>
+												<sup class="float-right mt-2">
+													<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
+												</sup>
+											</p>
+											<p class="mb-0 text-muted">Share a text only post</p>
+										</div>
+									</div>
+								</div>
+							</div>
+
 							<a v-if="config.features.stories == true" class="card mx-md-5 my-md-3 shadow-none border compose-action text-decoration-none text-dark" href="/i/stories/new">
 								<div class="card-body">
 									<div class="media">
@@ -349,6 +405,19 @@
 
 					<div v-if="page == 'advancedSettings'" class="w-100 h-100">
 						<div class="list-group list-group-flush">
+							<!-- <div class="d-none list-group-item d-flex justify-content-between">
+								<div>
+									<div class="text-dark ">Optimize Media</div>
+									<p v-if="mediaCropped" class="text-muted small mb-0">Media was cropped or filtered, it must be optimized.</p>
+									<p v-else class="text-muted small mb-0">Compress media for smaller file size.</p>
+								</div>
+								<div>
+									<div class="custom-control custom-switch" style="z-index: 9999;">
+										<input type="checkbox" class="custom-control-input" id="asoptimizemedia" v-model="optimizeMedia" :disabled="mediaCropped">
+										<label class="custom-control-label" for="asoptimizemedia"></label>
+									</div>
+								</div>
+							</div> -->
 							<div class="list-group-item d-flex justify-content-between">
 								<div>
 									<div class="text-dark ">Turn off commenting</div>
@@ -591,6 +660,8 @@ export default {
 			nsfw: false,
 			place: false,
 			commentsDisabled: false,
+			optimizeMedia: true,
+			mediaCropped: false,
 			pageTitle: '',
 
 			cropper: {
@@ -613,11 +684,13 @@ export default {
 				'addToStory',
 				'editMedia',
 				'cameraRoll',
-				'tagPeopleHelp'
+				'tagPeopleHelp',
+				'textOptions'
 			],
 			cameraRollMedia: [],
 			taggedUsernames: [],
-			taggedPeopleSearch: null
+			taggedPeopleSearch: null,
+			textMode: false
 		}
 	},
 
@@ -664,6 +737,12 @@ export default {
 			el.removeAttr('disabled');
 		},
 
+		addText(event) {
+			this.pageTitle = 'New Text Post';
+			this.page = 'addText';
+			this.textMode = true;
+		},
+
 		mediaWatcher() {
 			let self = this;
 			$(document).on('change', '#pf-dz', function(e) {
@@ -705,7 +784,7 @@ export default {
 					}
 				};
 
-				axios.post('/api/pixelfed/v1/media', form, xhrConfig)
+				axios.post('/api/compose/v0/media/upload', form, xhrConfig)
 				.then(function(e) {
 					self.uploadProgress = 100;
 					self.ids.push(e.data.id);
@@ -747,7 +826,7 @@ export default {
 			}
 			let id = this.media[this.carouselCursor].id;
 			
-			axios.delete('/api/pixelfed/v1/media', {
+			axios.delete('/api/compose/v0/media/delete', {
 				params: {
 					id: id
 				}
@@ -794,9 +873,51 @@ export default {
 						cw: this.nsfw,
 						comments_disabled: this.commentsDisabled,
 						place: this.place,
-						tagged: this.taggedUsernames
+						tagged: this.taggedUsernames,
+						optimize_media: this.optimizeMedia
 					};
-					axios.post('/api/local/status/compose', data)
+					axios.post('/api/compose/v0/publish', data)
+					.then(res => {
+						let data = res.data;
+						window.location.href = data;
+					}).catch(err => {
+						let msg = err.response.data.message ? err.response.data.message : 'An unexpected error occured.'
+						swal('Oops, something went wrong!', msg, 'error');
+					});
+					return;
+				break;
+
+				case 'delete' :
+					this.ids = [];
+					this.media = [];
+					this.carouselCursor = 0;
+					this.composeText = '';
+					this.composeTextLength = 0;
+					$('#composeModal').modal('hide');
+					return;
+				break;
+			}
+		},
+
+		composeTextPost() {
+			let state = this.composeState;
+
+			if(this.composeText.length > this.config.uploader.max_caption_length) {
+				swal('Error', 'Caption is too long', 'error');
+				return;
+			}
+
+			switch(state) {
+				case 'publish' :
+					let data = {
+						caption: this.composeText,
+						visibility: this.visibility,
+						cw: this.nsfw,
+						comments_disabled: this.commentsDisabled,
+						place: this.place,
+						tagged: this.taggedUsernames,
+					};
+					axios.post('/api/compose/v0/publish/text', data)
 					.then(res => {
 						let data = res.data;
 						window.location.href = data;
@@ -828,6 +949,14 @@ export default {
 			this.pageTitle = '';
 			
 			switch(this.page) {
+				case 'addText':
+					this.page = 1;
+				break;
+
+				case 'textOptions':
+					this.page = 'addText';
+				break;
+
 				case 'cropPhoto':
 				case 'editMedia':
 					this.page = 2;
@@ -838,7 +967,9 @@ export default {
 				break;
 
 				default:
-					this.namedPages.indexOf(this.page) != -1 ? this.page = 3 : this.page--;
+					this.namedPages.indexOf(this.page) != -1 ? 
+					this.page = (this.textMode ? 'addText' : 3) : 
+					(this.textMode ? 'addText' : this.page--);
 				break;
 			}
 		},
@@ -860,10 +991,11 @@ export default {
 							imageSmoothingEnabled: false,
 							imageSmoothingQuality: 'high',
 						}).toBlob(function(blob) {
+						self.mediaCropped = true;
 						let data = new FormData();
 						data.append('file', blob);
-						let url = '/api/local/compose/media/update/' + self.ids[self.carouselCursor];
-
+						data.append('id', self.ids[self.carouselCursor]);
+						let url = '/api/compose/v0/media/update';
 						axios.post(url, data).then(res => {
 							self.media[self.carouselCursor].url = res.data.url;
 							self.pageLoading = false;
@@ -921,7 +1053,7 @@ export default {
 		locationSearch(input) {
 			if (input.length < 1) { return []; };
 			let results = [];
-			return axios.get('/api/local/compose/location/search', {
+			return axios.get('/api/compose/v0/search/location', {
 				params: {
 					q: input
 				}
@@ -936,8 +1068,8 @@ export default {
 
 		onSubmitLocation(result) {
 			this.place = result;
-			this.pageTitle = '';
-			this.page = 3;
+			this.pageTitle = this.textMode ? 'New Text Post' : '';
+			this.page = (this.textMode ? 'addText' : 3);
 			return;
 		},
 
@@ -965,7 +1097,7 @@ export default {
 			this.visibility = state;
 			this.visibilityTag = tags[state];
 			this.pageTitle = '';
-			this.page = 3;
+			this.page = this.textMode ? 'addText' : 3;
 		},
 
 		showMediaDescriptionsCard() {
@@ -1024,7 +1156,8 @@ export default {
 						canvas.toBlob(function(blob) {
 							data = new FormData();
 							data.append('file', blob);
-							axios.post('/api/local/compose/media/update/'+media.id, data).then(res => {
+							data.append('id', media.id);
+							axios.post('/api/compose/v0/media/update', data).then(res => {
 							}).catch(err => {
 							});
 						});
@@ -1039,7 +1172,7 @@ export default {
 			if (input.length < 1) { return []; };
 			let self = this;
 			let results = [];
-			return axios.get('/api/local/compose/tag/search', {
+			return axios.get('/api/compose/v0/search/tag', {
 				params: {
 					q: input
 				}
@@ -1070,6 +1203,11 @@ export default {
 
 		untagUsername(index) {
 			this.taggedUsernames.splice(index, 1);
+		},
+
+		showTextOptions() {
+			this.page = 'textOptions';
+			this.pageTitle = 'Text Post Options';
 		}
 	}
 }

+ 239 - 0
resources/assets/js/components/My2020.vue

@@ -0,0 +1,239 @@
+<template>
+<div class="bg-dark text-white">
+	<div v-if="!loaded" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
+		<div class="text-center">
+			<div class="spinner-border text-light" role="status">
+				<span class="sr-only">Loading...</span>
+			</div>
+			<p class="mb-0 lead mt-2">Loading</p>
+		</div>
+	</div>
+	<div v-if="loaded && notEnoughData" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
+		<div class="text-center">
+			<p class="display-4">Oops!</p>
+			<p class="h3 font-weight-light py-3">We don't have enough data to display your <span class="font-weight-bold">#my2020</span>.</p>
+			<p class="mb-0 h5 font-weight-light">We hope to see you next year!</p>
+		</div>
+	</div>
+	<div v-if="loaded && !notEnoughData" class="d-flex justify-content-center align-items-center" style="width:100%;height:100vh;min-height:500px; padding: 0 15px;">
+
+		<div v-if="page == 1" class="text-center">
+			<p class="h1 font-weight-light">Hello {{user.username}}!</p>
+			<p class="h1 py-4">Your 2020 on Pixelfed.</p>
+			<p class="h4 font-weight-light mb-0 animate__animated animate__bounceInDown">Use the buttons below to navigate.</p>
+		</div>
+
+		<div v-if="page == 2" class="text-center mw-500">
+			<p class="display-4">User #<span class="font-weight-bold">{{stats.account.user_id}}</span></p>
+			<p class="h3 font-weight-light mb-0">You joined Pixelfed on {{stats.account.created_at}}</p>
+		</div>
+
+		<div v-if="page == 3" class="text-center mw-500">
+			<p class="display-4">You created <span class="font-weight-bold">{{stats.account.posts_count}}</span> posts</p>
+			<p class="h3 font-weight-light mb-0">The average user created <span class="font-weight-bold">{{stats.average.posts}}</span> posts this year.</p>
+		</div>
+
+		<div v-if="page == 4" class="text-center mw-500">
+			<p class="display-4">You liked <span class="font-weight-bold">{{stats.account.likes_count}}</span> posts</p>
+			<p class="h3 font-weight-light mb-0">The average user liked <span class="font-weight-bold">{{stats.average.likes}}</span> posts this year.</p>
+		</div>
+
+		<div v-if="page == 5" class="text-center mw-500">
+			<div v-if="stats.account.most_popular">
+				<p class="h1 font-weight-light mb-0 text-break md-line-height">Your most popular post of 2020 was created on <span class="font-weight-bold">{{stats.account.most_popular.created_at}}</span> with <span class="font-weight-bold">{{stats.account.most_popular.likes_count}}</span> likes.</p>
+				<p class="mt-4 mb-0">
+					<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.account.most_popular.url">View Post</a>
+				</p>
+			</div>
+			<div v-else>
+				<p class="h1 font-weight-light mb-0 text-break md-line-height">The most popular post of 2020 was created by <span class="font-weight-bold">{{stats.popular.post.username}}</span> on <span class="font-weight-bold">{{stats.popular.post.created_at}}</span> with <span class="font-weight-bold">{{stats.popular.post.likes_count}}</span> likes.</p>
+				<p class="mt-4 mb-0">
+					<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.popular.post.url">View Post</a>
+				</p>
+			</div>
+		</div>
+
+		<div v-if="page == 6" class="text-center mw-500">
+			<p class="display-4"><span class="font-weight-bold">{{stats.account.followers_this_year}}</span> New Followers</p>
+			<p class="h3 font-weight-light mb-0">You followed <span class="font-weight-bold">{{stats.account.followed_this_year}}</span> accounts this year!</p>
+		</div>
+
+		<div v-if="page == 7" class="text-center mw-500">
+			<div v-if="stats.account.hashtag">
+				<p class="h1 text-break">Your favourite hashtag was <span class="font-weight-bold">#{{stats.account.hashtag.name}}</span>.</p>
+				<p class="h3 font-weight-light mb-0">You used it <span class="font-weight-bold">{{stats.account.hashtag.count}}</span> times!</p>
+			</div>
+			<div v-else>
+				<p class="h1 text-break">The most popular hashtag was <span class="font-weight-bold">#{{stats.popular.hashtag.name}}</span></p>
+				<p class="h3 font-weight-light mb-0">It was used <span class="font-weight-bold">{{stats.popular.hashtag.count}}</span> times!</p>
+			</div>
+		</div>
+
+		<div v-if="page == 8" class="text-center mw-500">
+			<p class="display-4">You tagged <span class="font-weight-bold">{{stats.account.places_total}}</span> places.</p>
+			<p v-if="stats.account.places_total" class="h3 font-weight-light mb-0">You tagged <span class="font-weight-bold">{{stats.account.places.name}}</span> a total of <span class="font-weight-bold">{{stats.account.places.count}}</span> times!</p>
+			<p v-else class="h3 font-weight-light mb-0">The most tagged place was <span class="font-weight-bold">{{stats.popular.places.name}}</span> that was tagged a total of <span class="font-weight-bold">{{stats.popular.places.count}}</span> times!</p>
+		</div>
+
+		<div v-if="page == 9" class="text-center">
+			<p class="display-4">Happy 2021!</p>
+			<p class="h3 font-weight-light mb-0">We wish you the best in the new year.</p>
+		</div>
+
+	</div>
+	<div v-if="loaded" class="fixed-top">
+		<p class="text-center mt-3 d-flex justify-content-center align-items-center mb-0">
+			<img src="/img/pixelfed-icon-grey.svg" width="60" height="60">
+			<span class="text-light font-weight-bold ml-3" style="font-size: 22px;">#my2020</span>
+		</p>
+	</div>
+	<div v-if="loaded" class="fixed-bottom">
+		<p class="text-center">
+			<a v-if="!notEnoughData" :class="prevClass()" href="#" @click.prevent="prevPage()" :disabled="page == 1"><i class="fas fa-chevron-left"></i> Back</a>
+			<a class="btn btn-outline-light rounded-pill mx-3" href="/">Back to Pixelfed</a>
+			<a v-if="!notEnoughData" :class="nextClass()" href="#" @click.prevent="nextPage()">Next <i class="fas fa-chevron-right"></i></a>
+		</p>
+	</div>
+</div>
+</template>
+
+<style type="text/css" scoped>
+	.md-line-height {
+		line-height: 1.65 !important;
+	}
+	.mw-500 {
+		max-width: 500px;
+	}
+</style>
+
+<script type="text/javascript">
+	
+export default {
+	data() {
+		return {
+			config: window.App.config,
+			user: {},
+			loggedIn: false,
+			loaded: false,
+			page: 1,
+			stats: [],
+			notEnoughData: false,
+			reportedView: false
+		}
+	},
+
+	mounted() {
+		let u = new URLSearchParams(window.location.search);
+		if( u.has('v') && 
+			u.has('ned') && 
+			u.has('sl') && 
+			u.get('v') == 20 && 
+			u.get('sl') >= 1 && 
+			u.get('sl') <= 9
+		) {
+			if(u.get('ned') == 0) {
+				this.page = u.get('sl');
+			} else {
+				this.notEnoughData = true;
+			}
+		}
+
+		axios.get('/api/pixelfed/v1/accounts/verify_credentials')
+		.then(res => {
+			this.user = res.data;
+			window._sharedData.curUser = res.data;
+		});
+
+		this.fetchData();
+	}, 
+
+	updated() {
+	},
+
+	methods: {
+		fetchData() {
+			axios.get('/api/pixelfed/v2/seasonal/yir')
+			.then(res => {
+				this.stats = res.data;
+				this.loaded = true;
+				this.shortcuts();
+			})
+		},
+
+		nextPage() {
+			if(this.page == 9) {
+				return;
+			}
+
+			if(this.page == 7 && this.stats.popular.places == null) {
+				this.page = 9;
+				window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=9');
+				return;
+			}
+
+			if(this.page == 8) {
+				axios.post('/api/pixelfed/v2/seasonal/yir', {
+					'profile_id' : this.user.profile_id
+				})
+			}
+			++this.page;
+			window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
+		},
+
+		prevPage() {
+			if(this.page == 1) {
+				return;
+			}
+			if(this.page == 9 && this.stats.popular.places == null) {
+				this.page = 7;
+				window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=7');
+				return;
+			}
+			--this.page;
+			if(this.page == 1) {
+				window.history.pushState({}, {}, '/i/my2020');
+			} else {
+				window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
+			}
+		},
+
+		prevClass() {
+			return this.page == 1
+				? 'btn btn-outline-muted rounded-pill'
+				: 'btn btn-outline-light rounded-pill';
+		},
+
+		nextClass() {
+			return this.page == 9
+				? 'btn btn-outline-muted rounded-pill'
+				: 'btn btn-outline-light rounded-pill';
+		},
+
+		dateFormat(d) {
+		},
+
+		shortcuts() {
+			let self = this;
+			window.addEventListener("keydown", function(event) {
+				if (event.defaultPrevented) {
+					return;
+				}
+
+				switch(event.code) {
+					case "KeyA":
+					case "ArrowLeft":
+					self.prevPage();
+					break;
+					case "KeyD":
+					case "ArrowRight":
+					self.nextPage();
+					break;
+				}
+
+				event.preventDefault();
+				}, true);
+		}
+	}
+}
+
+</script>

+ 17 - 23
resources/assets/js/components/PostComponent.vue

@@ -45,7 +45,14 @@
          </div>
           <div class="col-12 col-md-8 px-0 mx-0">
               <div class="postPresenterContainer d-none d-flex justify-content-center align-items-center" style="background: #000;">
-                <div v-if="status.pf_type === 'photo'" class="w-100">
+                <div v-if="status.pf_type === 'text'" class="w-100">
+                  <div class="w-100 card-img-top border-bottom rounded-0" style="background-image: url(/storage/textimg/bg_1.jpg);background-size: cover;width: 100%;height: 540px;">
+                    <div class="w-100 h-100 d-flex justify-content-center align-items-center">
+                      <p class="text-center text-break h3 px-5 font-weight-bold" v-html="status.content"></p>
+                    </div>
+                  </div>
+                </div>
+                <div v-else-if="status.pf_type === 'photo'" class="w-100">
                   <photo-presenter :status="status" v-on:lightbox="lightbox"></photo-presenter>
                 </div>
 
@@ -104,7 +111,7 @@
             </div>
             <div class="d-flex flex-md-column flex-column-reverse h-100" style="overflow-y: auto;">
               <div class="card-body status-comments pt-0">
-                <div class="status-comment">
+                <div v-if="status.pf_type != 'text'" class="status-comment">
                   <div v-if="status.content.length" class="pt-3">
                     <div v-if="showCaption != true">
                       <span class="py-3">
@@ -839,11 +846,12 @@ export default {
     beforeMount() {
       let u = new URLSearchParams(window.location.search);
       let forceMetro = localStorage.getItem('pf_metro_ui.exp.forceMetro') == 'true';
-      if(forceMetro == true || u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
+      if(this.statusTemplate == 'text') {
         this.layout = 'metro';
+        return;
       }
-      if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
-        this.layout = 'moment';
+      if(forceMetro == true || u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
+        this.layout = 'metro';
       }
     },
 
@@ -897,15 +905,8 @@ export default {
                 }, 3000);
                 setTimeout(function() {
                   self.fetchState();
-                  document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) { 
-                    if(i.href.startsWith(window.location.origin)) {
-                      return;
-                    }
-                    let tag = i.innerText;
-                    if(tag.startsWith('#')) {
-                      tag = tag.substr(1);
-                    }
-                    i.href = '/discover/tags/'+tag+'?src=rph'; 
+                  document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) { 
+                    i.href = App.util.format.rewriteLinks(i);
                   });
                 }, 500);
             }).catch(error => {
@@ -1252,15 +1253,8 @@ export default {
                 $('.postCommentsLoader').addClass('d-none');
                 $('.postCommentsContainer').removeClass('d-none');
                 setTimeout(function() {
-                  document.querySelectorAll('.comments .comment-body a').forEach(function(i, e) { 
-                      if(i.href.startsWith(window.location.origin)) {
-                        return;
-                      }
-                      let tag = i.innerText;
-                      if(tag.startsWith('#')) {
-                        tag = tag.substr(1);
-                      }
-                      i.href = '/discover/tags/'+tag+'?src=rph'; 
+                  document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) { 
+                    i.href = App.util.format.rewriteLinks(i);
                   });
                 }, 500);
             }).catch(error => {

+ 8 - 4
resources/assets/js/components/Profile.vue

@@ -1,5 +1,12 @@
 <template>
 <div class="w-100 h-100">
+	<div v-if="owner && layout == 'moment'">
+		<div class="bg-primary shadow">
+			<p class="text-center text-white mb-0 py-3 font-weight-bold border-bottom border-info">
+				<i class="fas fa-exclamation-triangle fa-lg mr-2"></i> The Moment UI layout has been deprecated and will be removed in a future release.
+			</p>
+		</div>
+	</div>
 	<div v-if="isMobile" class="bg-white p-3 border-bottom">
 		<div class="d-flex justify-content-between align-items-center">
 			<div @click="goBack" class="cursor-pointer">
@@ -679,10 +686,7 @@
 			if(forceMetro == true || u.has('ui') && u.get('ui') == 'metro' && this.layout != 'metro') {
 				this.layout = 'metro';
 			}
-			if(u.has('ui') && u.get('ui') == 'moment' && this.layout != 'moment') {
-				Vue.use(VueMasonry);
-				this.layout = 'moment';
-			}
+			
 			if(this.layout == 'metro' && u.has('t')) {
 				if(this.modes.indexOf(u.get('t')) != -1) {
 					if(u.get('t') == 'bookmarks') {

+ 4 - 18
resources/assets/js/components/RemotePost.vue

@@ -627,15 +627,8 @@ export default {
                 }, 3000);
                 setTimeout(function() {
                   self.fetchState();
-                  document.querySelectorAll('.status-comment .comment-text a').forEach(function(i, e) { 
-                    if(i.href.startsWith(window.location.origin)) {
-                      return;
-                    }
-                    let tag = i.innerText;
-                    if(tag.startsWith('#')) {
-                      tag = tag.substr(1);
-                    }
-                    i.href = '/discover/tags/'+tag+'?src=rph'; 
+                  document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) { 
+                    i.href = App.util.format.rewriteLinks(i);
                   });
                 }, 500);
             }).catch(error => {
@@ -977,15 +970,8 @@ export default {
                 $('.postCommentsLoader').addClass('d-none');
                 $('.postCommentsContainer').removeClass('d-none');
                 setTimeout(function() {
-                  document.querySelectorAll('.comments .comment-body a').forEach(function(i, e) { 
-                      if(i.href.startsWith(window.location.origin)) {
-                        return;
-                      }
-                      let tag = i.innerText;
-                      if(tag.startsWith('#')) {
-                        tag = tag.substr(1);
-                      }
-                      i.href = '/discover/tags/'+tag+'?src=rph'; 
+                  document.querySelectorAll('.status-comment .postCommentsContainer .comment-body a').forEach(function(i, e) { 
+                    i.href = App.util.format.rewriteLinks(i);
                   });
                 }, 500);
             }).catch(error => {

+ 1 - 1
resources/assets/js/components/RemoteProfile.vue

@@ -253,7 +253,7 @@
 									shares: status.reblogs_count,
 									comments: status.reply_count
 								},
-								thumb: status.media_attachments[0].preview_url,
+								thumb: status.media_attachments[0].url,
 								media: status.media_attachments,
 								timestamp: status.created_at,
 								type: status.pf_type,

+ 3 - 3
resources/assets/js/components/SearchResults.vue

@@ -33,7 +33,7 @@
 						<div class="pb-2">
 							<div class="media align-items-center py-2">
 								<div class="media-body text-truncate">
-									<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
+									<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
 										<i class="fas fa-map-marker-alt text-lighter mr-2"></i> {{hashtag.value}}
 									</p>
 								</div>
@@ -74,7 +74,7 @@
 								<i class="fas fa-hashtag text-muted"></i>
 								</span>
 								<div class="media-body text-truncate">
-									<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
+									<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
 										#{{hashtag.value}}
 									</p>
 									<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
@@ -99,7 +99,7 @@
 							<div class="media align-items-center py-2 pr-3">
 								<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
 								<div class="media-body">
-									<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
+									<p class="mb-0 text-break text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
 										{{profile.value}}
 									</p>
 									<p class="mb-0 small font-weight-bold text-muted text-uppercase">

ファイルの差分が大きいため隠しています
+ 405 - 388
resources/assets/js/components/Timeline.vue


+ 29 - 9
resources/assets/js/components/presenter/PhotoPresenter.vue

@@ -1,14 +1,26 @@
 <template>
 	<div v-if="status.sensitive == true">
-		<details class="details-animated">
-			<summary>
-				<p class="mb-0 lead font-weight-bold">{{ status.spoiler_text ? status.spoiler_text : 'CW / NSFW / Hidden Media'}}</p>
-				<p class="font-weight-light">(click to show)</p>
-			</summary>
-			<div class="max-hide-overflow" :title="status.media_attachments[0].description">
-				<img :class="status.media_attachments[0].filter_class + ' card-img-top'" :src="status.media_attachments[0].url" loading="lazy" :alt="altText(status)" onerror="this.onerror=null;this.src='/storage/no-preview.png'">
-			</div>
-		</details>
+		<div class="text-light content-label">
+			<p class="text-center">
+				<i class="far fa-eye-slash fa-2x"></i>
+			</p>
+			<p class="h4 font-weight-bold text-center">
+				Sensitive Content
+			</p>
+			<p class="text-center py-2">
+				This photo contains sensitive content which <br/>
+				some people may find offsensive or disturbing.
+			</p>
+			<p class="mb-0">
+				<button @click="status.sensitive = false" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Photo</button>
+			</p>
+		</div>
+		<blur-hash-image
+			width="32"
+			height="32"
+			punch="1"
+			:hash="status.media_attachments[0].blurhash"
+			:alt="altText(status)"/>
 	</div>
 	<div v-else>
 		<div :title="status.media_attachments[0].description">
@@ -22,6 +34,14 @@
     border-top-left-radius: 0 !important;
     border-top-right-radius: 0 !important;
   }
+  .content-label {
+  	margin: 0;
+  	position: absolute;
+  	top:45%;
+  	left:50%;
+  	z-index: 999;
+  	transform: translate(-50%, -50%);
+  }
 </style>
 
 <script type="text/javascript">

+ 4 - 0
resources/assets/js/my2020.js

@@ -0,0 +1,4 @@
+Vue.component(
+	'my-yearreview',
+	require('./components/My2020.vue').default
+);

+ 10 - 0
resources/views/account/yir.blade.php

@@ -0,0 +1,10 @@
+@extends('layouts.blank')
+
+@section('content')
+<my-yearreview></my-yearreview>
+@endsection
+
+@push('scripts')
+<script type="text/javascript" src="{{mix('js/my2020.js')}}"></script>
+	<script type="text/javascript">App.boot();</script>
+@endpush

+ 0 - 1
resources/views/admin/instances/home.blade.php

@@ -118,7 +118,6 @@
 						value: "unlisted",
 					},
 					cw: {
-						text: autocw == 0 ? "CW Media" : "Remove AutoCW",
 						text: autocw == 0 ? "CW Media" : "Remove AutoCW",
 						className: "bg-warning",
 						value: "autocw",

+ 8 - 10
resources/views/layouts/app.blade.php

@@ -56,26 +56,24 @@
     @stack('scripts')
     <div class="d-block d-sm-none mt-5"></div>
     <div class="d-block d-sm-none fixed-bottom">
-        <div class="card card-body rounded-0 py-2 d-flex align-items-middle box-shadow" style="border-top:1px solid #F1F5F8">
-            <ul class="nav nav-pills nav-fill">
+        <div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8">
+            <ul class="nav nav-pills nav-fill d-flex align-items-middle">
               <li class="nav-item">
-                <a class="nav-link {{request()->is('/')?'text-dark':'text-lighter'}}" href="/"><i class="fas fa-home fa-lg"></i></a>
+                <a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a>
               </li>
               <li class="nav-item">
-                <a class="nav-link {{request()->is('discover')?'text-dark':'text-lighter'}}" href="/discover"><i class="fas fa-search fa-lg"></i></a>
+                <a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a>
               </li>
               <li class="nav-item">
-                <div class="nav-link text-primary cursor-pointer" onclick="App.util.compose.post()">
-                    <span class="border border-primary rounded p-2 bg-primary">
-                        <i class="fas fa-camera fa-lg text-white" style="color:#fff !important;"></i>
-                    </span>
+                <div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()">
+                    <i class="far fa-plus-square fa-2x"></i>
                 </div>
               </li>
               <li class="nav-item">
-                <a class="nav-link {{request()->is('account/activity')?'text-dark':'text-lighter'}}" href="/account/activity"><i class="far fa-heart fa-lg"></i></a>
+                <a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
               </li>
               <li class="nav-item">
-                <a class="nav-link text-lighter" href="/i/me"><i class="far fa-user fa-lg"></i></a>
+                <a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a>
               </li>
             </ul>
         </div>

+ 3 - 7
resources/views/layouts/partial/nav.blade.php

@@ -39,10 +39,10 @@
                             </a>
                         </li>
                         <li class="nav-item px-md-2 d-none d-md-block">
-                            <a class="nav-link font-weight-bold text-dark" href="/?a=co" title="Compose" data-toggle="tooltip" data-placement="bottom">
+                            <div class="nav-link font-weight-bold text-dark cursor-pointer" title="Compose" data-toggle="tooltip" data-placement="bottom" onclick="App.util.compose.post()">
                                 <i class="far fa-plus-square fa-lg"></i>
                                 <span class="sr-only">Compose</span>
-                            </a>
+                            </div>
                         </li>
                         <li class="nav-item px-md-2">
                             <a class="nav-link font-weight-bold text-dark" href="/account/direct" title="Direct" data-toggle="tooltip" data-placement="bottom">
@@ -52,7 +52,7 @@
                         </li>
                         <li class="nav-item px-md-2 d-none d-md-block">
                             <a class="nav-link font-weight-bold text-dark" href="/account/activity" title="Notifications" data-toggle="tooltip" data-placement="bottom">
-                                <i class="far fa-bell fa-lg"></i>
+                                <i class="far fa-bell fa-lg" style="vertical-align: middle;"></i>
                                 <span class="sr-only">Notifications</span>
                             </a>
                         </li>
@@ -64,10 +64,6 @@
                             </a>
 
                             <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
-                                <a class="d-block d-md-none dropdown-item font-weight-bold" href="/">
-                                    <span class="fas fa-home pr-2 text-lighter"></span>
-                                    Home
-                                </a>
                                 <a class="dropdown-item font-weight-bold" href="{{route('discover')}}">
                                     <span class="far fa-compass pr-2 text-lighter"></span>
                                     {{__('navmenu.discover')}}

+ 26 - 0
resources/views/site/help/instance-actor.blade.php

@@ -0,0 +1,26 @@
+@extends('site.help.partial.template', ['breadcrumb'=>'Instance Actor'])
+
+@section('section')
+
+  <div class="title">
+    <h3 class="font-weight-bold">Instance Actor</h3>
+  </div>
+  <hr>
+  <p class="lead">We use a special account type known as an Instance Actor to fetch content securely with other servers in the fediverse.</p>
+  <div class="py-4">
+    <p class="font-weight-bold h5 pb-3">For Instance Admins</p>
+    <p class="mb-0">If you are an instance admin that found this URL in a request or profile, this account is used to fetch content from remote instances using signed requests (HTTP Signatures) to enforce domain block compatibility with other instances.</p>
+  </div>
+  <hr>
+  <div class="card bg-primary border-primary" style="box-shadow: none !important;border: 3px solid #08d!important;">
+    <div class="card-header text-light font-weight-bold h4 p-4 bg-primary">Instance Actor Tips</div>
+    <div class="card-body bg-white p-3">
+      <ul class="pt-3">
+        <li class="lead  mb-4">The Instance Actor will not appear in search results.</li>
+        <li class="lead  mb-4">You cannot follow an Instance Actor.</li>
+        <li class="lead  mb-4">The Instance Actor does not follow accounts.</li>
+        <li class="lead">The Instance Actor account does not post or share content from users.</li>
+      </ul>
+    </div>
+  </div>
+@endsection

+ 1 - 1
resources/views/site/help/partial/template.blade.php

@@ -4,7 +4,7 @@
 
 <div class="container px-0 mt-0 mt-md-4 mb-md-5 pb-md-5">
   <div class="col-12 px-0">
-    <div class="card mt-md-5 px-0 mx-md-3">
+    <div class="card mt-md-5 px-0 mx-md-3 shadow-none border">
       <div class="card-header font-weight-bold text-muted bg-white py-4">
         <a href="{{route('site.help')}}" class="text-muted">{{__('helpcenter.helpcenter')}}</a>
         <span class="px-2 font-weight-light">&mdash;</span>

+ 22 - 6
resources/views/status/compose.blade.php

@@ -2,12 +2,28 @@
 
 @section('content')
 
-<div class="alert alert-info text-center rounded-0">
 <div class="container">
-<span class="font-weight-bold">ComposeUI v3 is deprecated</span>
-<br>
-Please use the <a href="#" onclick="event.preventDefault();window.App.util.compose.post()" class="font-weight-bold">new UI</a> to compose a post.
-</div>
+	<div class="row">
+		<div class="col-12 col-md-6 offset-md-3 mt-md-3 px-0">
+			<compose-modal></compose-modal>
+		</div>
+	</div>
 </div>
+@endsection
+
+@push('styles')
+<style type="text/css">
+	.card {
+		box-shadow: none;
+		border: 1px solid #ddd;
+	}
+	.card .card-header .fas.fa-times {
+		color: #fff;
+	}
+</style>
+@endpush
 
-@endsection
+@push('scripts')
+<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
+<script type="text/javascript">window.App.boot()</script>
+@endpush

+ 4 - 1
routes/api.php

@@ -2,10 +2,13 @@
 
 use Illuminate\Http\Request;
 
-$middleware = ['auth:api','twofactor','validemail','localization', 'throttle:60,1'];
+$middleware = ['auth:api','twofactor','validemail','throttle:60,1','interstitial'];
 
 Route::post('/f/inbox', 'FederationController@sharedInbox');
 Route::post('/users/{username}/inbox', 'FederationController@userInbox');
+Route::get('i/actor', 'InstanceActorController@profile');
+Route::post('i/actor/inbox', 'InstanceActorController@inbox');
+Route::get('i/actor/outbox', 'InstanceActorController@outbox');
 
 Route::group(['prefix' => 'api'], function() use($middleware) {
 

+ 23 - 19
routes/web.php

@@ -98,11 +98,28 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
     Route::get('discover/loops', 'DiscoverController@showLoops');
     Route::get('discover/profiles', 'DiscoverController@profilesDirectory')->name('discover.profiles');
     
-    
     Route::group(['prefix' => 'api'], function () {
         Route::get('search', 'SearchController@searchAPI');
         Route::get('nodeinfo/2.0.json', 'FederationController@nodeinfo');
 
+        Route::group(['prefix' => 'compose'], function() {
+            Route::group(['prefix' => 'v0'], function() {
+                Route::post('/media/upload', 'ComposeController@mediaUpload');
+                Route::post('/media/update', 'ComposeController@mediaUpdate')
+                    ->middleware('throttle:maxComposeMediaUpdatesPerHour,60')
+                    ->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')
+                    ->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
+                Route::delete('/media/delete', 'ComposeController@mediaDelete');
+                Route::get('/search/tag', 'ComposeController@searchTag');
+                Route::get('/search/location', 'ComposeController@searchLocation');
+
+                Route::post('/publish', 'ComposeController@store')
+                    ->middleware('throttle:maxPostsPerHour,60')
+                    ->middleware('throttle:maxPostsPerDay,1440');
+                Route::post('/publish/text', 'ComposeController@storeText');
+            });
+        });
+
         Route::group(['prefix' => 'direct'], function () {
             Route::get('browse', 'DirectMessageController@browse');
             Route::post('create', 'DirectMessageController@create');
@@ -130,7 +147,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::get('loops', 'DiscoverController@loopsApi');
             Route::post('loops/watch', 'DiscoverController@loopWatch');
             Route::get('discover/tag', 'DiscoverController@getHashtags');
-            Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
         });
         
         Route::group(['prefix' => 'pixelfed'], function() {
@@ -176,25 +192,13 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
                 Route::get('discover/posts/trending', 'DiscoverController@trendingApi');
                 Route::get('discover/posts/hashtags', 'DiscoverController@trendingHashtags');
                 Route::get('discover/posts/places', 'DiscoverController@trendingPlaces');
+                Route::get('seasonal/yir', 'SeasonalController@getData');
+                Route::post('seasonal/yir', 'SeasonalController@store');
             });
         });
 
         Route::group(['prefix' => 'local'], function () {
-            // Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
-            // Route::get('accounts/relationships', 'PublicApiController@relationships');
-            // Route::get('accounts/{id}/statuses', 'PublicApiController@accountStatuses');
-            // Route::get('accounts/{id}/following', 'PublicApiController@accountFollowing');
-            // Route::get('accounts/{id}/followers', 'PublicApiController@accountFollowers');
-            // Route::get('accounts/{id}', 'PublicApiController@account');
-            // Route::post('avatar/update', 'ApiController@avatarUpdate');
-            // Route::get('likes', 'ApiController@hydrateLikes');
-            // Route::post('media', 'ApiController@uploadMedia');
-            // Route::delete('media', 'ApiController@deleteMedia');
-            // Route::get('notifications', 'ApiController@notifications');
-            // Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
-            // Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
-
-            Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
+            // Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
             Route::get('exp/rec', 'ApiController@userRecommendations');
             Route::post('discover/tag/subscribe', 'HashtagFollowController@store')->middleware('throttle:maxHashtagFollowsPerHour,60')->middleware('throttle:maxHashtagFollowsPerDay,1440');
             Route::get('discover/tag/list', 'HashtagFollowController@getTags');
@@ -209,9 +213,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::post('collection/{id}/publish', 'CollectionController@publish')->middleware('throttle:maxCollectionsPerHour,60')->middleware('throttle:maxCollectionsPerDay,1440')->middleware('throttle:maxCollectionsPerMonth,43800');
             Route::get('profile/collections/{id}', 'CollectionController@getUserCollections');
 
-            Route::post('compose/media/update/{id}', 'MediaController@composeUpdate')->middleware('throttle:maxComposeMediaUpdatesPerHour,60')->middleware('throttle:maxComposeMediaUpdatesPerDay,1440')->middleware('throttle:maxComposeMediaUpdatesPerMonth,43800');
             Route::get('compose/location/search', 'ApiController@composeLocationSearch');
-            Route::get('compose/tag/search', 'MediaTagController@usernameLookup');
             Route::post('compose/tag/untagme', 'MediaTagController@untagProfile');
         });
         Route::group(['prefix' => 'admin'], function () {
@@ -308,6 +310,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
         Route::get('warning', 'AccountInterstitialController@get');
         Route::post('warning', 'AccountInterstitialController@read');
+        Route::get('my2020', 'SeasonalController@yearInReview');
     });
 
     Route::group(['prefix' => 'account'], function () {
@@ -440,6 +443,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
             Route::view('stories', 'site.help.stories')->name('help.stories');
             Route::view('embed', 'site.help.embed')->name('help.embed');
             Route::view('hashtags', 'site.help.hashtags')->name('help.hashtags');
+            Route::view('instance-actor', 'site.help.instance-actor')->name('help.instance-actor');
             Route::view('discover', 'site.help.discover')->name('help.discover');
             Route::view('direct-messages', 'site.help.dm')->name('help.dm');
             Route::view('timelines', 'site.help.timelines')->name('help.timelines');

+ 1 - 0
storage/app/.gitignore

@@ -1,4 +1,5 @@
 *
 !public/
+!remcache/
 !cities.json
 !.gitignore

+ 2 - 1
storage/app/public/.gitignore

@@ -1,4 +1,5 @@
 *
 !.gitignore
 !no-preview.png
-!m/
+!m/
+!textimg/

+ 3 - 0
storage/app/public/textimg/.gitignore

@@ -0,0 +1,3 @@
+*
+!.gitignore
+!bg_1.jpg

BIN
storage/app/public/textimg/bg_1.jpg


+ 2 - 0
storage/app/remcache/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません