Ver código fonte

Merge branch 'frontend-ui-refactor' into fix-startup-script

Brad Koehn 6 anos atrás
pai
commit
bd822cd5dc
100 arquivos alterados com 3422 adições e 421 exclusões
  1. 1 1
      .circleci/config.yml
  2. 34 0
      app/Console/Commands/UpdateCommand.php
  3. 5 2
      app/Follower.php
  4. 16 1
      app/Http/Controllers/AccountController.php
  5. 32 14
      app/Http/Controllers/Api/BaseApiController.php
  6. 10 0
      app/Http/Controllers/CollectionController.php
  7. 10 0
      app/Http/Controllers/CollectionItemController.php
  8. 55 0
      app/Http/Controllers/DirectMessageController.php
  9. 6 48
      app/Http/Controllers/DiscoverController.php
  10. 51 1
      app/Http/Controllers/FederationController.php
  11. 3 1
      app/Http/Controllers/HomeController.php
  12. 151 0
      app/Http/Controllers/Import/Instagram.php
  13. 13 0
      app/Http/Controllers/Import/Mastodon.php
  14. 16 0
      app/Http/Controllers/ImportController.php
  15. 133 55
      app/Http/Controllers/InternalApiController.php
  16. 4 2
      app/Http/Controllers/ProfileController.php
  17. 103 0
      app/Http/Controllers/PublicApiController.php
  18. 5 2
      app/Http/Controllers/SearchController.php
  19. 5 4
      app/Http/Controllers/Settings/HomeSettings.php
  20. 1 0
      app/Http/Controllers/SettingsController.php
  21. 18 21
      app/Http/Controllers/SiteController.php
  22. 11 35
      app/Http/Controllers/StatusController.php
  23. 12 28
      app/Http/Controllers/TimelineController.php
  24. 1 0
      app/Http/Kernel.php
  25. 23 0
      app/Http/Middleware/Localization.php
  26. 115 0
      app/Jobs/ImportPipeline/ImportInstagram.php
  27. 4 4
      app/Jobs/InboxPipeline/InboxWorker.php
  28. 2 2
      app/Jobs/LikePipeline/LikePipeline.php
  29. 1 0
      app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php
  30. 1 1
      app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php
  31. 79 0
      app/Jobs/SharePipeline/SharePipeline.php
  32. 1 1
      app/Jobs/StatusPipeline/NewStatusPipeline.php
  33. 16 0
      app/Jobs/StatusPipeline/StatusActivityPubDeliver.php
  34. 10 7
      app/Jobs/StatusPipeline/StatusDelete.php
  35. 1 1
      app/Jobs/StatusPipeline/StatusEntityLexer.php
  36. 34 0
      app/Jobs/VideoPipeline/VideoOptimize.php
  37. 34 0
      app/Jobs/VideoPipeline/VideoPostProcess.php
  38. 62 0
      app/Jobs/VideoPipeline/VideoThumbnail.php
  39. 1 0
      app/Like.php
  40. 17 2
      app/Media.php
  41. 43 0
      app/Profile.php
  42. 1 2
      app/Providers/RouteServiceProvider.php
  43. 9 8
      app/Report.php
  44. 43 33
      app/Status.php
  45. 10 0
      app/Story.php
  46. 10 0
      app/StoryReaction.php
  47. 19 0
      app/Transformer/ActivityPub/Verb/Announce.php
  48. 70 0
      app/Transformer/ActivityPub/Verb/CreateNote.php
  49. 19 0
      app/Transformer/ActivityPub/Verb/Follow.php
  50. 19 0
      app/Transformer/ActivityPub/Verb/Like.php
  51. 7 2
      app/Transformer/Api/MediaTransformer.php
  52. 25 0
      app/Transformer/Api/RelationshipTransformer.php
  53. 1 1
      app/Transformer/Api/StatusTransformer.php
  54. 18 22
      app/Util/ActivityPub/Helpers.php
  55. 266 27
      app/Util/ActivityPub/Inbox.php
  56. 34 0
      app/Util/HTTPSignatures/Algorithm.php
  57. 7 0
      app/Util/HTTPSignatures/AlgorithmException.php
  58. 19 0
      app/Util/HTTPSignatures/AlgorithmInterface.php
  59. 119 0
      app/Util/HTTPSignatures/Context.php
  60. 7 0
      app/Util/HTTPSignatures/Exception.php
  61. 41 0
      app/Util/HTTPSignatures/GuzzleHttpSignatures.php
  62. 48 0
      app/Util/HTTPSignatures/HeaderList.php
  63. 36 0
      app/Util/HTTPSignatures/HmacAlgorithm.php
  64. 260 0
      app/Util/HTTPSignatures/Key.php
  65. 7 0
      app/Util/HTTPSignatures/KeyException.php
  66. 36 0
      app/Util/HTTPSignatures/KeyStore.php
  67. 7 0
      app/Util/HTTPSignatures/KeyStoreException.php
  68. 15 0
      app/Util/HTTPSignatures/KeyStoreInterface.php
  69. 64 0
      app/Util/HTTPSignatures/RsaAlgorithm.php
  70. 38 0
      app/Util/HTTPSignatures/Signature.php
  71. 49 0
      app/Util/HTTPSignatures/SignatureParameters.php
  72. 111 0
      app/Util/HTTPSignatures/SignatureParametersParser.php
  73. 7 0
      app/Util/HTTPSignatures/SignatureParseException.php
  74. 7 0
      app/Util/HTTPSignatures/SignedHeaderNotPresentException.php
  75. 104 0
      app/Util/HTTPSignatures/Signer.php
  76. 89 0
      app/Util/HTTPSignatures/SigningString.php
  77. 202 0
      app/Util/HTTPSignatures/Verification.php
  78. 31 0
      app/Util/HTTPSignatures/Verifier.php
  79. 0 28
      app/Util/Lexer/Extractor.php
  80. 6 2
      app/Util/Media/Image.php
  81. 4 4
      composer.json
  82. 4 2
      config/app.php
  83. 2 2
      config/debugbar.php
  84. 7 6
      config/filesystems.php
  85. 1 1
      config/pixelfed.php
  86. 141 0
      config/purify.php
  87. 1 1
      config/queue.php
  88. 4 4
      contrib/docker/Dockerfile.apache
  89. 59 26
      contrib/docker/Dockerfile.fpm
  90. 4 4
      contrib/docker/start.sh
  91. 4 3
      database/factories/UserFactory.php
  92. 1 1
      docker-compose.yml
  93. 186 8
      package-lock.json
  94. 3 1
      package.json
  95. BIN
      public/css/app.css
  96. 0 0
      public/img/help/what_is_the_fediverse.svg
  97. BIN
      public/img/pixelfed-icon-black.svg
  98. BIN
      public/img/pixelfed-icon-grey.svg
  99. BIN
      public/js/app.js
  100. BIN
      public/js/components.js

+ 1 - 1
.circleci/config.yml

@@ -22,7 +22,7 @@ jobs:
       - checkout
       - checkout
 
 
       - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
       - run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
-      - run: sudo docker-php-ext-install bcmath pcntl zip
+      - run: sudo docker-php-ext-install pcntl
 
 
       # Download and cache dependencies
       # Download and cache dependencies
 
 

+ 34 - 0
app/Console/Commands/UpdateCommand.php

@@ -33,6 +33,7 @@ class UpdateCommand extends Command
         '0.1.7',
         '0.1.7',
         '0.1.8',
         '0.1.8',
         '0.1.9',
         '0.1.9',
+        '0.2.1',
     ];
     ];
 
 
     protected $version;
     protected $version;
@@ -76,10 +77,15 @@ class UpdateCommand extends Command
             case '0.1.8':
             case '0.1.8':
                 $this->info('You are running an older version that doesn\'t require any updates!');
                 $this->info('You are running an older version that doesn\'t require any updates!');
                 break;
                 break;
+
             case '0.1.9':
             case '0.1.9':
                 $this->update019();
                 $this->update019();
                 break;
                 break;
 
 
+            case '0.2.1':
+                $this->update021();
+                break;
+
             default:
             default:
                 # code...
                 # code...
                 break;
                 break;
@@ -127,6 +133,34 @@ class UpdateCommand extends Command
         $bar->finish();
         $bar->finish();
     }
     }
 
 
+    public function update021()
+    {
+        $this->buildVersionFile();
+        $v = $this->getVersionFile();
+        if($v['updated'] == true) {
+            $this->info('Already up to date!');
+            exit;
+        }
+
+        $statusCount = Status::count();
+        $this->info('Running updates ...');
+        $bar = $this->output->createProgressBar($statusCount);
+        Status::has('media')->chunk(200, function($statuses) use ($bar) {
+
+            foreach($statuses as $status) {
+                if($status->firstMedia()) {
+                    $media = $status->firstMedia();
+                    if(in_array($media->mime, ['image/jpeg', 'image/png'])) {
+                        ImageThumbnail::dispatch($media);
+                    }
+                }
+                $bar->advance();
+            }
+        });
+        $this->updateVersionFile('0.2.1', true);
+        $bar->finish();
+    }
+
     protected function buildVersionFile()
     protected function buildVersionFile()
     {
     {
         $path = storage_path('app/version.json');
         $path = storage_path('app/version.json');

+ 5 - 2
app/Follower.php

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
 
 
 class Follower extends Model
 class Follower extends Model
 {
 {
+
+    protected $fillable = ['profile_id', 'following_id', 'local_profile'];
+
     public function actor()
     public function actor()
     {
     {
         return $this->belongsTo(Profile::class, 'profile_id', 'id');
         return $this->belongsTo(Profile::class, 'profile_id', 'id');
@@ -21,9 +24,9 @@ class Follower extends Model
         return $this->belongsTo(Profile::class, 'following_id', 'id');
         return $this->belongsTo(Profile::class, 'following_id', 'id');
     }
     }
 
 
-    public function permalink()
+    public function permalink($append = null)
     {
     {
-        $path = $this->actor->permalink("/follow/{$this->id}");
+        $path = $this->actor->permalink("/follow/{$this->id}{$append}");
         return url($path);
         return url($path);
     }
     }
 
 

+ 16 - 1
app/Http/Controllers/AccountController.php

@@ -64,10 +64,13 @@ class AccountController extends Controller
       ]);
       ]);
         $profile = Auth::user()->profile;
         $profile = Auth::user()->profile;
         $action = $request->input('a');
         $action = $request->input('a');
+        $allowed = ['like', 'follow'];
         $timeago = Carbon::now()->subMonths(3);
         $timeago = Carbon::now()->subMonths(3);
         $following = $profile->following->pluck('id');
         $following = $profile->following->pluck('id');
         $notifications = Notification::whereIn('actor_id', $following)
         $notifications = Notification::whereIn('actor_id', $following)
-          ->where('profile_id', '!=', $profile->id)
+          ->whereIn('action', $allowed)
+          ->where('actor_id', '<>', $profile->id)
+          ->where('profile_id', '<>', $profile->id)
           ->whereDate('created_at', '>', $timeago)
           ->whereDate('created_at', '>', $timeago)
           ->orderBy('notifications.created_at', 'desc')
           ->orderBy('notifications.created_at', 'desc')
           ->simplePaginate(30);
           ->simplePaginate(30);
@@ -200,6 +203,11 @@ class AccountController extends Controller
           'filter_type'     => 'mute',
           'filter_type'     => 'mute',
         ]);
         ]);
 
 
+        $pid = $user->id;
+        Cache::forget("user:filter:list:$pid");
+        Cache::forget("feature:discover:people:$pid");
+        Cache::forget("feature:discover:posts:$pid");
+
         return redirect()->back();
         return redirect()->back();
     }
     }
 
 
@@ -221,6 +229,9 @@ class AccountController extends Controller
         switch ($type) {
         switch ($type) {
           case 'user':
           case 'user':
             $profile = Profile::findOrFail($item);
             $profile = Profile::findOrFail($item);
+            if ($profile->id == $user->id) {
+                return abort(403);
+            }
             $class = get_class($profile);
             $class = get_class($profile);
             $filterable['id'] = $profile->id;
             $filterable['id'] = $profile->id;
             $filterable['type'] = $class;
             $filterable['type'] = $class;
@@ -241,6 +252,10 @@ class AccountController extends Controller
           'filter_type'     => 'block',
           'filter_type'     => 'block',
         ]);
         ]);
 
 
+        $pid = $user->id;
+        Cache::forget("user:filter:list:$pid");
+        Cache::forget("feature:discover:people:$pid");
+        Cache::forget("feature:discover:posts:$pid");
         return redirect()->back();
         return redirect()->back();
     }
     }
 
 

+ 32 - 14
app/Http/Controllers/Api/BaseApiController.php

@@ -2,22 +2,27 @@
 
 
 namespace App\Http\Controllers\Api;
 namespace App\Http\Controllers\Api;
 
 
-use App\Avatar;
-use App\Http\Controllers\AvatarController;
-use App\Http\Controllers\Controller;
-use App\Jobs\AvatarPipeline\AvatarOptimize;
-use App\Jobs\ImageOptimizePipeline\ImageOptimize;
-use App\Media;
-use App\Profile;
-use App\Transformer\Api\AccountTransformer;
-use App\Transformer\Api\MediaTransformer;
-use App\Transformer\Api\StatusTransformer;
-use Auth;
-use Cache;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\URL;
+use App\Http\Controllers\{
+    Controller,
+    AvatarController
+};
+use Auth, Cache, URL;
+use App\{Avatar,Media,Profile};
+use App\Transformer\Api\{
+    AccountTransformer,
+    MediaTransformer,
+    StatusTransformer
+};
 use League\Fractal;
 use League\Fractal;
 use League\Fractal\Serializer\ArraySerializer;
 use League\Fractal\Serializer\ArraySerializer;
+use App\Jobs\AvatarPipeline\AvatarOptimize;
+use App\Jobs\ImageOptimizePipeline\ImageOptimize;
+use App\Jobs\VideoPipeline\{
+    VideoOptimize,
+    VideoPostProcess,
+    VideoThumbnail
+};
 
 
 class BaseApiController extends Controller
 class BaseApiController extends Controller
 {
 {
@@ -187,7 +192,20 @@ class BaseApiController extends Controller
         $url = URL::temporarySignedRoute(
         $url = URL::temporarySignedRoute(
             'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id]
             'temp-media', now()->addHours(1), ['profileId' => $profile->id, 'mediaId' => $media->id]
         );
         );
-        ImageOptimize::dispatch($media);
+
+        switch ($media->mime) {
+            case 'image/jpeg':
+            case 'image/png':
+                ImageOptimize::dispatch($media);
+                break;
+
+            case 'video/mp4':
+                VideoThumbnail::dispatch($media);
+                break;
+            
+            default:
+                break;
+        }
 
 
         $res = [
         $res = [
             'id'          => $media->id,
             'id'          => $media->id,

+ 10 - 0
app/Http/Controllers/CollectionController.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class CollectionController extends Controller
+{
+    //
+}

+ 10 - 0
app/Http/Controllers/CollectionItemController.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class CollectionItemController extends Controller
+{
+    //
+}

+ 55 - 0
app/Http/Controllers/DirectMessageController.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Auth;
+use Illuminate\Http\Request;
+use App\{
+	DirectMessage,
+	Profile,
+	Status
+};
+
+class DirectMessageController extends Controller
+{
+    public function __construct()
+    {
+    	$this->middleware('auth');
+    }
+
+    public function inbox(Request $request)
+    {
+    	$profile = Auth::user()->profile;
+    	$inbox = DirectMessage::whereToId($profile->id)
+    		->with(['author','status'])
+    		->orderBy('created_at', 'desc')
+    		->groupBy('from_id')
+    		->paginate(10);
+    	return view('account.messages', compact('inbox'));
+
+    }
+
+    public function show(Request $request, int $pid, $mid)
+    {
+    	$profile = Auth::user()->profile;
+
+    	if($pid !== $profile->id) { 
+    		abort(403); 
+    	}
+
+    	$msg = DirectMessage::whereToId($profile->id)
+    		->findOrFail($mid);
+
+    	$thread = DirectMessage::whereToId($profile->id)
+    		->orWhere([['from_id', $profile->id],['to_id', $msg->from_id]])
+    		->orderBy('created_at', 'desc')
+    		->paginate(10);
+
+    	return view('account.message', compact('msg', 'profile', 'thread'));
+    }
+
+    public function compose(Request $request)
+    {
+        $profile = Auth::user()->profile;
+    }
+}

+ 6 - 48
app/Http/Controllers/DiscoverController.php

@@ -21,53 +21,7 @@ class DiscoverController extends Controller
 
 
     public function home(Request $request)
     public function home(Request $request)
     {
     {
-        $this->validate($request, [
-          'page' => 'nullable|integer|max:50'
-        ]);
-
-        $pid = Auth::user()->profile->id;
-
-        $following = Cache::remember('feature:discover:following:'.$pid, 720, function() use($pid) {
-          $following = Follower::select('following_id')
-                      ->whereProfileId($pid)
-                      ->pluck('following_id');
-          $filtered = UserFilter::select('filterable_id')
-                    ->whereUserId($pid)
-                    ->whereFilterableType('App\Profile')
-                    ->whereIn('filter_type', ['mute', 'block'])
-                    ->pluck('filterable_id');
-          $following->push($pid);
-          
-          if($filtered->count() > 0) {
-            $following->push($filtered);
-          }
-          return $following;
-        });
-
-        $people = Cache::remember('feature:discover:people:'.$pid, 15, function() use($following) {
-            return Profile::select('id', 'name', 'username')->inRandomOrder()
-                ->whereHas('statuses')
-                ->whereNull('domain')
-                ->whereNotIn('id', $following)
-                ->whereIsPrivate(false)
-                ->take(3)
-                ->get();
-        });
-
-        $posts = Status::select('id', 'caption', 'profile_id')
-          ->whereHas('media')
-          ->whereHas('profile', function($q) {
-            $q->where('is_private', false);
-          })
-          ->whereIsNsfw(false)
-          ->whereVisibility('public')
-          ->where('profile_id', '<>', $pid)
-          ->whereNotIn('profile_id', $following)
-          ->withCount(['comments', 'likes'])
-          ->orderBy('created_at', 'desc')
-          ->simplePaginate(21);
-
-        return view('discover.home', compact('people', 'posts'));
+        return view('discover.home');
     }
     }
 
 
     public function showTags(Request $request, $hashtag)
     public function showTags(Request $request, $hashtag)
@@ -82,13 +36,17 @@ class DiscoverController extends Controller
           ->firstOrFail();
           ->firstOrFail();
 
 
         $posts = $tag->posts()
         $posts = $tag->posts()
+          ->whereHas('media')
           ->withCount(['likes', 'comments'])
           ->withCount(['likes', 'comments'])
           ->whereIsNsfw(false)
           ->whereIsNsfw(false)
           ->whereVisibility('public')
           ->whereVisibility('public')
-          ->has('media')
           ->orderBy('id', 'desc')
           ->orderBy('id', 'desc')
           ->simplePaginate(12);
           ->simplePaginate(12);
 
 
+        if($posts->count() == 0) {
+          abort(404);
+        }
+        
         return view('discover.tags.show', compact('tag', 'posts'));
         return view('discover.tags.show', compact('tag', 'posts'));
     }
     }
 }
 }

+ 51 - 1
app/Http/Controllers/FederationController.php

@@ -13,6 +13,7 @@ use Cache;
 use Carbon\Carbon;
 use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use League\Fractal;
 use League\Fractal;
+use App\Util\ActivityPub\Helpers;
 
 
 class FederationController extends Controller
 class FederationController extends Controller
 {
 {
@@ -133,6 +134,19 @@ class FederationController extends Controller
         return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
         return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
     }
     }
 
 
+    public function hostMeta(Request $request)
+    {
+        $path = route('well-known.webfinger');
+        $xml = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+  <Link rel="lrdd" type="application/xrd+xml" template="{$path}?resource={uri}"/>
+</XRD>
+XML;
+
+        return response($xml)->header('Content-Type', 'application/xrd+xml');
+    }
+
     public function userOutbox(Request $request, $username)
     public function userOutbox(Request $request, $username)
     {
     {
         if (config('pixelfed.activitypub_enabled') == false) {
         if (config('pixelfed.activitypub_enabled') == false) {
@@ -153,6 +167,42 @@ class FederationController extends Controller
 
 
     public function userInbox(Request $request, $username)
     public function userInbox(Request $request, $username)
     {
     {
-        return;
+        // todo
+    }
+
+    public function userFollowing(Request $request, $username)
+    {
+        if (config('pixelfed.activitypub_enabled') == false) {
+            abort(403);
+        }
+        $profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
+        $obj = [
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            'id'       => $request->getUri(),
+            'type'     => 'OrderedCollectionPage',
+            'totalItems' => $profile->following()->count(),
+            'orderedItems' => $profile->following->map(function($f) {
+                return $f->permalink();
+            })
+        ];
+        return response()->json($obj); 
+    }
+
+    public function userFollowers(Request $request, $username)
+    {
+        if (config('pixelfed.activitypub_enabled') == false) {
+            abort(403);
+        }
+        $profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
+        $obj = [
+            '@context' => 'https://www.w3.org/ns/activitystreams',
+            'id'       => $request->getUri(),
+            'type'     => 'OrderedCollectionPage',
+            'totalItems' => $profile->followers()->count(),
+            'orderedItems' => $profile->followers->map(function($f) {
+                return $f->permalink();
+            })
+        ];
+        return response()->json($obj); 
     }
     }
 }
 }

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

@@ -2,6 +2,8 @@
 
 
 namespace App\Http\Controllers;
 namespace App\Http\Controllers;
 
 
+use Illuminate\Http\Request;
+
 class HomeController extends Controller
 class HomeController extends Controller
 {
 {
     /**
     /**
@@ -19,7 +21,7 @@ class HomeController extends Controller
      *
      *
      * @return \Illuminate\Http\Response
      * @return \Illuminate\Http\Response
      */
      */
-    public function index()
+    public function index(Request $request)
     {
     {
         return view('home');
         return view('home');
     }
     }

+ 151 - 0
app/Http/Controllers/Import/Instagram.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Http\Controllers\Import;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use Auth, DB;
+use App\{
+	ImportData,
+	ImportJob,
+	Profile, 
+	User
+};
+
+trait Instagram
+{
+    public function instagram()
+    {
+      return view('settings.import.instagram.home');
+    }
+
+    public function instagramStart(Request $request)
+    {	
+    	$job = $this->instagramRedirectOrNew();
+    	return redirect($job->url());
+    }
+
+    protected function instagramRedirectOrNew()
+    {
+    	$profile = Auth::user()->profile;
+    	$exists = ImportJob::whereProfileId($profile->id)
+    		->whereService('instagram')
+    		->whereNull('completed_at')
+    		->exists();
+    	if($exists) {
+    		$job = ImportJob::whereProfileId($profile->id)
+    		->whereService('instagram')
+    		->whereNull('completed_at')
+    		->first();
+    	} else {
+    		$job = new ImportJob;
+    		$job->profile_id = $profile->id;
+    		$job->service = 'instagram';
+    		$job->uuid = (string) Str::uuid();
+    		$job->stage = 1;
+    		$job->save();
+    	}
+    	return $job;
+    }
+
+    public function instagramStepOne(Request $request, $uuid)
+    {
+    	$profile = Auth::user()->profile;
+    	$job = ImportJob::whereProfileId($profile->id)
+    		->whereNull('completed_at')
+    		->whereUuid($uuid)
+    		->whereStage(1)
+    		->firstOrFail();
+    	return view('settings.import.instagram.step-one', compact('profile', 'job'));
+    }
+
+    public function instagramStepOneStore(Request $request, $uuid)
+    {
+    	$this->validate($request, [
+    		'media.*' => 'required|mimes:bin,jpeg,png,gif|max:500',
+    		//'mediajson' => 'required|file|mimes:json'
+    	]);
+    	$media = $request->file('media');
+
+    	$profile = Auth::user()->profile;
+    	$job = ImportJob::whereProfileId($profile->id)
+    		->whereNull('completed_at')
+    		->whereUuid($uuid)
+    		->whereStage(1)
+    		->firstOrFail();
+    		
+        foreach ($media as $k => $v) {
+        	$original = $v->getClientOriginalName();
+    		if(strlen($original) < 32 || $k > 100) {
+    			continue;
+    		}
+            $storagePath = "import/{$job->uuid}";
+            $path = $v->store($storagePath);
+            DB::transaction(function() use ($profile, $job, $path, $original) {
+		        $data = new ImportData;
+		        $data->profile_id = $profile->id;
+		        $data->job_id = $job->id;
+		        $data->service = 'instagram';
+		        $data->path = $path;
+		        $data->stage = $job->stage;
+		        $data->original_name = $original;
+		        $data->save();
+            });
+        }
+        DB::transaction(function() use ($profile, $job) {
+        	$job->stage = 2;
+        	$job->save();
+    	});
+        return redirect($job->url());
+    	return view('settings.import.instagram.step-one', compact('profile', 'job'));
+    }
+
+    public function instagramStepTwo(Request $request, $uuid)
+    {
+    	$profile = Auth::user()->profile;
+    	$job = ImportJob::whereProfileId($profile->id)
+    		->whereNull('completed_at')
+    		->whereUuid($uuid)
+    		->whereStage(2)
+    		->firstOrFail();
+    	return view('settings.import.instagram.step-two', compact('profile', 'job'));
+    }
+
+    public function instagramStepTwoStore(Request $request, $uuid)
+    {
+    	$this->validate($request, [
+    		'media' => 'required|file|max:1000'
+    	]);
+    	$profile = Auth::user()->profile;
+    	$job = ImportJob::whereProfileId($profile->id)
+    		->whereNull('completed_at')
+    		->whereUuid($uuid)
+    		->whereStage(2)
+    		->firstOrFail();
+    	$media = $request->file('media');
+    	$file = file_get_contents($media);
+		$json = json_decode($file, true);
+		if(!$json || !isset($json['photos'])) {
+			return abort(500);
+		}
+		$storagePath = "import/{$job->uuid}";
+        $path = $media->store($storagePath);
+        $job->media_json = $path;
+        $job->stage = 3;
+        $job->save();
+        return redirect($job->url());
+		return $json;
+
+    }
+
+    public function instagramStepThree(Request $request, $uuid)
+    {
+    	$profile = Auth::user()->profile;
+    	$job = ImportJob::whereProfileId($profile->id)
+    		->whereNull('completed_at')
+    		->whereUuid($uuid)
+    		->whereStage(3)
+    		->firstOrFail();
+    	return view('settings.import.instagram.step-three', compact('profile', 'job'));
+    }
+}

+ 13 - 0
app/Http/Controllers/Import/Mastodon.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Http\Controllers\Import;
+
+use Illuminate\Http\Request;
+
+trait Mastodon
+{
+    public function mastodon()
+    {
+      return view('settings.import.mastodon.home');
+    }
+}

+ 16 - 0
app/Http/Controllers/ImportController.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class ImportController extends Controller
+{
+    use Import\Instagram, Import\Mastodon;
+
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+
+}

+ 133 - 55
app/Http/Controllers/InternalApiController.php

@@ -4,12 +4,19 @@ namespace App\Http\Controllers;
 
 
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use App\{
 use App\{
+    DirectMessage,
+    Hashtag,
+    Follower,
     Like,
     Like,
     Media,
     Media,
+    Notification,
     Profile,
     Profile,
+    StatusHashtag,
     Status,
     Status,
+    UserFilter,
 };
 };
 use Auth,Cache;
 use Auth,Cache;
+use Carbon\Carbon;
 use League\Fractal;
 use League\Fractal;
 use App\Transformer\Api\{
 use App\Transformer\Api\{
     AccountTransformer,
     AccountTransformer,
@@ -30,60 +37,6 @@ class InternalApiController extends Controller
         $this->fractal->setSerializer(new ArraySerializer());
         $this->fractal->setSerializer(new ArraySerializer());
     }
     }
 
 
-    public function status(Request $request, $username, int $postid)
-    {
-        $auth = Auth::user()->profile;
-        $profile = Profile::whereUsername($username)->first();
-        $status = Status::whereProfileId($profile->id)->find($postid);
-        $status = new Fractal\Resource\Item($status, new StatusTransformer());
-        $user = new Fractal\Resource\Item($auth, new AccountTransformer());
-        $res = [];
-        $res['status'] = $this->fractal->createData($status)->toArray();
-        $res['user'] = $this->fractal->createData($user)->toArray();
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT);
-    }
-
-    public function statusComments(Request $request, $username, int $postId)
-    {
-        $this->validate($request, [
-            'min_id'    => 'nullable|integer|min:1',
-            'max_id'    => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
-            'limit'     => 'nullable|integer|min:5|max:50'
-        ]);
-        $limit = $request->limit ?? 10;
-        $auth = Auth::user()->profile;
-        $profile = Profile::whereUsername($username)->first();
-        $status = Status::whereProfileId($profile->id)->find($postId);
-        if($request->filled('min_id') || $request->filled('max_id')) {
-            $q = false;
-            $limit = 50;
-            if($request->filled('min_id')) {
-                $replies = $status->comments()
-                ->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
-                ->where('id', '>=', $request->min_id)
-                ->orderBy('id', 'desc')
-                ->paginate($limit);
-            }
-            if($request->filled('max_id')) {
-                $replies = $status->comments()
-                ->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
-                ->where('id', '<=', $request->max_id)
-                ->orderBy('id', 'desc')
-                ->paginate($limit);
-            }
-        } else {
-            $replies = $status->comments()
-            ->select('id', 'caption', 'rendered', 'profile_id', 'created_at')
-            ->orderBy('id', 'desc')
-            ->paginate($limit);
-        }
-
-        $resource = new Fractal\Resource\Collection($replies, new StatusTransformer(), 'data');
-        $resource->setPaginator(new IlluminatePaginatorAdapter($replies));
-        $res = $this->fractal->createData($resource)->toArray();
-        return response()->json($res, 200, [], JSON_PRETTY_PRINT);
-    }
-
     public function compose(Request $request)
     public function compose(Request $request)
     {
     {
         $this->validate($request, [
         $this->validate($request, [
@@ -101,13 +54,15 @@ class InternalApiController extends Controller
         $attachments = [];
         $attachments = [];
         $status = new Status;
         $status = new Status;
 
 
-        foreach($medias as $media) {
+        foreach($medias as $k => $media) {
             $m = Media::findOrFail($media['id']);
             $m = Media::findOrFail($media['id']);
             if($m->profile_id !== $profile->id || $m->status_id) {
             if($m->profile_id !== $profile->id || $m->status_id) {
                 abort(403, 'Invalid media id');
                 abort(403, 'Invalid media id');
             }
             }
             $m->filter_class = $media['filter'];
             $m->filter_class = $media['filter'];
             $m->license = $media['license'];
             $m->license = $media['license'];
+            $m->caption = strip_tags($media['alt']);
+            $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
             if($media['cw'] == true) {
             if($media['cw'] == true) {
                 $m->is_nsfw = true;
                 $m->is_nsfw = true;
                 $status->is_nsfw = true;
                 $status->is_nsfw = true;
@@ -135,4 +90,127 @@ class InternalApiController extends Controller
 
 
         return $status->url();
         return $status->url();
     }
     }
+
+    public function notifications(Request $request)
+    {
+        $this->validate($request, [
+          'page' => 'nullable|min:1|max:3',
+        ]);
+        $profile = Auth::user()->profile;
+        $timeago = Carbon::now()->subMonths(6);
+        $notifications = Notification::with('actor')
+        ->whereProfileId($profile->id)
+        ->whereDate('created_at', '>', $timeago)
+        ->orderBy('id', 'desc')
+        ->simplePaginate(30);
+        $notifications = $notifications->map(function($k, $v) {
+            return [
+                'id' => $k->id,
+                'action' => $k->action,
+                'message' => $k->message,
+                'rendered' => $k->rendered,
+                'actor' => [
+                    'avatar' => $k->actor->avatarUrl(),
+                    'username' => $k->actor->username,
+                    'url' => $k->actor->url(),
+                ],
+                'url' => $k->item->url(),
+                'read_at' => $k->read_at,
+            ];
+        });
+        return response()->json($notifications, 200, [], JSON_PRETTY_PRINT);
+    }
+
+    public function discover(Request $request)
+    {
+        $profile = Auth::user()->profile;
+        $pid = $profile->id;
+        $following = Cache::remember('feature:discover:following:'.$pid, 60, function() use ($pid) {
+            return Follower::whereProfileId($pid)->pluck('following_id')->toArray();
+        });
+        $filters = Cache::remember("user:filter:list:$pid", 60, function() use($pid) {
+            return UserFilter::whereUserId($pid)
+            ->whereFilterableType('App\Profile')
+            ->whereIn('filter_type', ['mute', 'block'])
+            ->pluck('filterable_id')->toArray();
+        });
+        $following = array_merge($following, $filters);
+
+        $people = Cache::remember('feature:discover:people:'.$pid, 15, function() use ($following) {
+            return Profile::select('id', 'name', 'username')
+            ->with('avatar')
+            ->inRandomOrder()
+            ->whereHas('statuses')
+            ->whereNull('domain')
+            ->whereNotIn('id', $following)
+            ->whereIsPrivate(false)
+            ->take(3)
+            ->get();
+        });
+
+        $posts = Cache::remember('feature:discover:posts:'.$pid, 60, function() use ($following) {
+            return Status::select('id', 'caption', 'profile_id')
+              ->whereNull('in_reply_to_id')
+              ->whereNull('reblog_of_id')
+              ->whereIsNsfw(false)
+              ->whereVisibility('public')
+              ->whereNotIn('profile_id', $following)
+              ->withCount(['comments', 'likes'])
+              ->orderBy('created_at', 'desc')
+              ->take(21)
+              ->get();
+          });
+
+        $res = [
+            'people' => $people->map(function($profile) {
+                return [
+                    'avatar' => $profile->avatarUrl(),
+                    'name' => $profile->name,
+                    'username' => $profile->username,
+                    'url'   => $profile->url(),
+                ];
+            }),
+            'posts' => $posts->map(function($post) {
+                return [
+                    'url' => $post->url(),
+                    'thumb' => $post->thumb(),
+                    'comments_count' => $post->comments_count,
+                    'likes_count' => $post->likes_count,
+                ];
+            })
+        ];
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT);
+    }
+    public function directMessage(Request $request, $profileId, $threadId)
+    {
+        $profile = Auth::user()->profile;
+
+        if($profileId != $profile->id) { 
+            abort(403); 
+        }
+
+        $msg = DirectMessage::whereToId($profile->id)
+            ->orWhere('from_id',$profile->id)
+            ->findOrFail($threadId);
+
+        $thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
+            ->whereIn('from_id', [$profile->id,$msg->from_id])
+            ->orderBy('created_at', 'asc')
+            ->paginate(30);
+
+        return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
+    }
+
+    public function notificationMarkAllRead(Request $request)
+    {
+        $profile = Auth::user()->profile;
+
+        $notifications = Notification::whereProfileId($profile->id)->get();
+        foreach($notifications as $n) {
+            $n->read_at = Carbon::now();
+            $n->save();
+        }
+
+        return;
+    }
 }
 }

+ 4 - 2
app/Http/Controllers/ProfileController.php

@@ -34,6 +34,8 @@ class ProfileController extends Controller
         if ($user->remote_url) {
         if ($user->remote_url) {
             $settings = new \StdClass;
             $settings = new \StdClass;
             $settings->crawlable = false;
             $settings->crawlable = false;
+            $settings->show_profile_follower_count = true;
+            $settings->show_profile_following_count = true;
         } else {
         } else {
             $settings = User::whereUsername($username)->firstOrFail()->settings;
             $settings = User::whereUsername($username)->firstOrFail()->settings;
         }
         }
@@ -150,7 +152,7 @@ class ProfileController extends Controller
             $blocked = $this->blockedProfileCheck($profile);
             $blocked = $this->blockedProfileCheck($profile);
             $check = $this->privateProfileCheck($profile, null);
             $check = $this->privateProfileCheck($profile, null);
             if($check || $blocked) {
             if($check || $blocked) {
-                return view('profile.private', compact('user'));
+                return view('profile.private', compact('user', 'is_following'));
             }
             }
         }
         }
         $followers = $profile->followers()->orderBy('created_at', 'desc')->simplePaginate(12);
         $followers = $profile->followers()->orderBy('created_at', 'desc')->simplePaginate(12);
@@ -174,7 +176,7 @@ class ProfileController extends Controller
             $blocked = $this->blockedProfileCheck($profile);
             $blocked = $this->blockedProfileCheck($profile);
             $check = $this->privateProfileCheck($profile, null);
             $check = $this->privateProfileCheck($profile, null);
             if($check || $blocked) {
             if($check || $blocked) {
-                return view('profile.private', compact('user'));
+                return view('profile.private', compact('user', 'is_following'));
             }
             }
         }
         }
         $following = $profile->following()->orderBy('created_at', 'desc')->simplePaginate(12);
         $following = $profile->following()->orderBy('created_at', 'desc')->simplePaginate(12);

+ 103 - 0
app/Http/Controllers/PublicApiController.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\{
+    Hashtag,
+    Like,
+    Media,
+    Notification,
+    Profile,
+    StatusHashtag,
+    Status,
+};
+use Auth,Cache;
+use Carbon\Carbon;
+use League\Fractal;
+use App\Transformer\Api\{
+    AccountTransformer,
+    StatusTransformer,
+};
+use App\Jobs\StatusPipeline\NewStatusPipeline;
+use League\Fractal\Serializer\ArraySerializer;
+use League\Fractal\Pagination\IlluminatePaginatorAdapter;
+
+
+class PublicApiController extends Controller
+{
+    protected $fractal;
+
+    public function __construct()
+    {
+        $this->middleware('throttle:200, 15');
+        $this->fractal = new Fractal\Manager();
+        $this->fractal->setSerializer(new ArraySerializer());
+    }
+
+    protected function getUserData()
+    {
+    	if(false == Auth::check()) {
+    		return [];
+    	} else {
+	        $profile = Auth::user()->profile;
+	        $user = new Fractal\Resource\Item($profile, new AccountTransformer());
+        	return $this->fractal->createData($user)->toArray();
+    	}
+    }
+
+    public function status(Request $request, $username, int $postid)
+    {
+        $profile = Profile::whereUsername($username)->first();
+        $status = Status::whereProfileId($profile->id)->find($postid);
+        $item = new Fractal\Resource\Item($status, new StatusTransformer());
+        $res = [
+        	'status' => $this->fractal->createData($item)->toArray(),
+        	'user' => $this->getUserData(),
+            'reactions' => [
+                'liked' => $status->liked(),
+                'shared' => $status->shared(),
+                'bookmarked' => $status->bookmarked(),
+            ],
+        ];
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT);
+    }
+
+    public function statusComments(Request $request, $username, int $postId)
+    {
+        $this->validate($request, [
+            'min_id'    => 'nullable|integer|min:1',
+            'max_id'    => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
+            'limit'     => 'nullable|integer|min:5|max:50'
+        ]);
+        $limit = $request->limit ?? 10;
+        $profile = Profile::whereUsername($username)->first();
+        $status = Status::whereProfileId($profile->id)->find($postId);
+        if($request->filled('min_id') || $request->filled('max_id')) {
+            if($request->filled('min_id')) {
+                $replies = $status->comments()
+                ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
+                ->where('id', '>=', $request->min_id)
+                ->orderBy('id', 'desc')
+                ->paginate($limit);
+            }
+            if($request->filled('max_id')) {
+                $replies = $status->comments()
+                ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
+                ->where('id', '<=', $request->max_id)
+                ->orderBy('id', 'desc')
+                ->paginate($limit);
+            }
+        } else {
+            $replies = $status->comments()
+            ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at')
+            ->orderBy('id', 'desc')
+            ->paginate($limit);
+        }
+
+        $resource = new Fractal\Resource\Collection($replies, new StatusTransformer(), 'data');
+        $resource->setPaginator(new IlluminatePaginatorAdapter($replies));
+        $res = $this->fractal->createData($resource)->toArray();
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT);
+    }
+}

+ 5 - 2
app/Http/Controllers/SearchController.php

@@ -38,7 +38,10 @@ class SearchController extends Controller
                 });
                 });
                 $tokens->push($tags);
                 $tokens->push($tags);
             }
             }
-            $users = Profile::select('username', 'name', 'id')->where('username', 'like', '%'.$tag.'%')->limit(20)->get();
+            $users = Profile::select('username', 'name', 'id')
+                ->where('username', 'like', '%'.$tag.'%')
+                ->limit(20)
+                ->get();
 
 
             if($users->count() > 0) {
             if($users->count() > 0) {
                 $profiles = $users->map(function ($item, $key) {
                 $profiles = $users->map(function ($item, $key) {
@@ -71,7 +74,7 @@ class SearchController extends Controller
                     'count'  => 0,
                     'count'  => 0,
                     'url'    => $item->url(),
                     'url'    => $item->url(),
                     'type'   => 'status',
                     'type'   => 'status',
-                    'value'  => 'Posted '.$item->created_at->diffForHumans(),
+                    'value'  => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
                     'tokens' => [$item->caption],
                     'tokens' => [$item->caption],
                     'name'   => $item->caption,
                     'name'   => $item->caption,
                     'thumb'  => $item->thumb(),
                     'thumb'  => $item->thumb(),

+ 5 - 4
app/Http/Controllers/Settings/HomeSettings.php

@@ -11,6 +11,7 @@ use App\UserFilter;
 use App\Util\Lexer\PrettyNumber;
 use App\Util\Lexer\PrettyNumber;
 use Auth;
 use Auth;
 use DB;
 use DB;
+use Purify;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 
 
 trait HomeSettings
 trait HomeSettings
@@ -40,8 +41,8 @@ trait HomeSettings
       ]);
       ]);
 
 
         $changes = false;
         $changes = false;
-        $name = $request->input('name');
-        $bio = $request->input('bio');
+        $name = strip_tags($request->input('name'));
+        $bio = $request->filled('bio') ? Purify::clean($request->input('bio')) : null;
         $website = $request->input('website');
         $website = $request->input('website');
         $email = $request->input('email');
         $email = $request->input('email');
         $user = Auth::user();
         $user = Auth::user();
@@ -79,12 +80,12 @@ trait HomeSettings
                 $profile->name = $name;
                 $profile->name = $name;
             }
             }
 
 
-            if (!$profile->website || $profile->website != $website) {
+            if ($profile->website != $website) {
                 $changes = true;
                 $changes = true;
                 $profile->website = $website;
                 $profile->website = $website;
             }
             }
 
 
-            if (!$profile->bio || !$profile->bio != $bio) {
+            if ($profile->bio != $bio) {
                 $changes = true;
                 $changes = true;
                 $profile->bio = $bio;
                 $profile->bio = $bio;
             }
             }

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

@@ -8,6 +8,7 @@ use App\UserFilter;
 use Auth;
 use Auth;
 use DB;
 use DB;
 use Cache;
 use Cache;
+use Purify;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use App\Http\Controllers\Settings\{
 use App\Http\Controllers\Settings\{
     HomeSettings,
     HomeSettings,

+ 18 - 21
app/Http/Controllers/SiteController.php

@@ -33,12 +33,15 @@ class SiteController extends Controller
     {
     {
         $pid = Auth::user()->profile->id;
         $pid = Auth::user()->profile->id;
         // TODO: Use redis for timelines
         // TODO: Use redis for timelines
-        $following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
-        $following->push(Auth::user()->profile->id);
+
+        $following = Follower::whereProfileId($pid)->pluck('following_id');
+        $following->push($pid)->toArray();
+
         $filtered = UserFilter::whereUserId($pid)
         $filtered = UserFilter::whereUserId($pid)
-                  ->whereFilterableType('App\Profile')
-                  ->whereIn('filter_type', ['mute', 'block'])
-                  ->pluck('filterable_id');
+                    ->whereFilterableType('App\Profile')
+                    ->whereIn('filter_type', ['mute', 'block'])
+                    ->pluck('filterable_id')->toArray();
+
         $timeline = Status::whereIn('profile_id', $following)
         $timeline = Status::whereIn('profile_id', $following)
                   ->whereNotIn('profile_id', $filtered)
                   ->whereNotIn('profile_id', $filtered)
                   ->whereHas('media')
                   ->whereHas('media')
@@ -46,6 +49,7 @@ class SiteController extends Controller
                   ->orderBy('created_at', 'desc')
                   ->orderBy('created_at', 'desc')
                   ->withCount(['comments', 'likes', 'shares'])
                   ->withCount(['comments', 'likes', 'shares'])
                   ->simplePaginate(20);
                   ->simplePaginate(20);
+                  
         $type = 'personal';
         $type = 'personal';
 
 
         return view('timeline.template', compact('timeline', 'type'));
         return view('timeline.template', compact('timeline', 'type'));
@@ -53,29 +57,22 @@ class SiteController extends Controller
 
 
     public function changeLocale(Request $request, $locale)
     public function changeLocale(Request $request, $locale)
     {
     {
-        if (!App::isLocale($locale)) {
-            return redirect()->back();
+        // todo: add other locales after pushing new l10n strings
+        $locales = ['en'];
+        if(in_array($locale, $locales)) {
+          session()->put('locale', $locale);
         }
         }
-        App::setLocale($locale);
 
 
         return redirect()->back();
         return redirect()->back();
     }
     }
 
 
     public function about()
     public function about()
     {
     {
-        $res = Cache::remember('site:page:about', 15, function () {
-            $statuses = Status::whereHas('media')
-              ->whereNull('in_reply_to_id')
-              ->whereNull('reblog_of_id')
-              ->count();
-            $statusCount = PrettyNumber::convert($statuses);
-            $userCount = PrettyNumber::convert(User::count());
-            $remoteCount = PrettyNumber::convert(Profile::whereNotNull('remote_url')->count());
-            $adminContact = User::whereIsAdmin(true)->first();
-
-            return view('site.about')->with(compact('statusCount', 'userCount', 'remoteCount', 'adminContact'))->render();
-        });
+        return view('site.about');
+    }
 
 
-        return $res;
+    public function language()
+    {
+      return view('site.language');
     }
     }
 }
 }

+ 11 - 35
app/Http/Controllers/StatusController.php

@@ -22,8 +22,7 @@ class StatusController extends Controller
         $user = Profile::whereUsername($username)->firstOrFail();
         $user = Profile::whereUsername($username)->firstOrFail();
 
 
         $status = Status::whereProfileId($user->id)
         $status = Status::whereProfileId($user->id)
-                ->where('visibility', '!=', 'draft')
-                ->withCount(['likes', 'comments', 'media'])
+                ->whereNotIn('visibility',['draft','direct'])
                 ->findOrFail($id);
                 ->findOrFail($id);
 
 
         if($status->visibility == 'private' || $user->is_private) {
         if($status->visibility == 'private' || $user->is_private) {
@@ -40,34 +39,8 @@ class StatusController extends Controller
             return $this->showActivityPub($request, $status);
             return $this->showActivityPub($request, $status);
         }
         }
 
 
-        $template = $this->detectTemplate($status);
-
-        $replies = Status::whereInReplyToId($status->id)->orderBy('created_at', 'desc')->simplePaginate(30);
-
-        return view($template, compact('user', 'status', 'replies'));
-    }
-
-    protected function detectTemplate($status)
-    {
-        $template = Cache::rememberForever('template:status:type:'.$status->id, function () use ($status) {
-            $template = 'status.show.photo';
-            if (!$status->media_path && $status->in_reply_to_id) {
-                $template = 'status.reply';
-            }
-            if ($status->media->count() > 1) {
-                $template = 'status.show.album';
-            }
-            if ($status->viewType() == 'video') {
-                $template = 'status.show.video';
-            }
-            if ($status->viewType() == 'video-album') {
-                $template = 'status.show.video-album';
-            }
-
-            return $template;
-        });
-
-        return $template;
+        $template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
+        return view($template, compact('user', 'status'));
     }
     }
 
 
     public function compose()
     public function compose()
@@ -148,9 +121,7 @@ class StatusController extends Controller
 
 
     public function delete(Request $request)
     public function delete(Request $request)
     {
     {
-        if (!Auth::check()) {
-            abort(403);
-        }
+        $this->authCheck();
 
 
         $this->validate($request, [
         $this->validate($request, [
           'type'  => 'required|string',
           'type'  => 'required|string',
@@ -162,12 +133,17 @@ class StatusController extends Controller
         if ($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
         if ($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
             StatusDelete::dispatch($status);
             StatusDelete::dispatch($status);
         }
         }
-
-        return redirect(Auth::user()->url());
+        if($request->wantsJson()) {
+            return response()->json(['Status successfully deleted.']);
+        } else {
+            return redirect(Auth::user()->url());
+        }
     }
     }
 
 
     public function storeShare(Request $request)
     public function storeShare(Request $request)
     {
     {
+        $this->authCheck();
+        
         $this->validate($request, [
         $this->validate($request, [
           'item'    => 'required|integer',
           'item'    => 'required|integer',
         ]);
         ]);

+ 12 - 28
app/Http/Controllers/TimelineController.php

@@ -2,12 +2,13 @@
 
 
 namespace App\Http\Controllers;
 namespace App\Http\Controllers;
 
 
+use Auth, Cache;
 use App\Follower;
 use App\Follower;
 use App\Profile;
 use App\Profile;
 use App\Status;
 use App\Status;
 use App\User;
 use App\User;
 use App\UserFilter;
 use App\UserFilter;
-use Auth;
+use Illuminate\Http\Request;
 
 
 class TimelineController extends Controller
 class TimelineController extends Controller
 {
 {
@@ -17,39 +18,22 @@ class TimelineController extends Controller
         $this->middleware('twofactor');
         $this->middleware('twofactor');
     }
     }
 
 
-    public function personal()
-    {
-        $pid = Auth::user()->profile->id;
-        // TODO: Use redis for timelines
-        $following = Follower::whereProfileId($pid)->pluck('following_id');
-        $following->push($pid);
-        $filtered = UserFilter::whereUserId($pid)
-                  ->whereFilterableType('App\Profile')
-                  ->whereIn('filter_type', ['mute', 'block'])
-                  ->pluck('filterable_id');
-        $timeline = Status::whereIn('profile_id', $following)
-                  ->whereNotIn('profile_id', $filtered)
-                  ->whereVisibility('public')
-                  ->orderBy('created_at', 'desc')
-                  ->withCount(['comments', 'likes'])
-                  ->simplePaginate(20);
-        $type = 'personal';
-
-        return view('timeline.template', compact('timeline', 'type'));
-    }
-
-    public function local()
+    public function local(Request $request)
     {
     {
+        $this->validate($request,[
+          'page' => 'nullable|integer|max:20'
+        ]);
         // TODO: Use redis for timelines
         // TODO: Use redis for timelines
         // $timeline = Timeline::build()->local();
         // $timeline = Timeline::build()->local();
         $pid = Auth::user()->profile->id;
         $pid = Auth::user()->profile->id;
 
 
-        $filtered = UserFilter::whereUserId($pid)
+        $private = Profile::whereIsPrivate(true)->where('id', '!=', $pid)->pluck('id');
+        $filters = UserFilter::whereUserId($pid)
                   ->whereFilterableType('App\Profile')
                   ->whereFilterableType('App\Profile')
                   ->whereIn('filter_type', ['mute', 'block'])
                   ->whereIn('filter_type', ['mute', 'block'])
-                  ->pluck('filterable_id');
-        $private = Profile::whereIsPrivate(true)->pluck('id');
-        $filtered = $filtered->merge($private);
+                  ->pluck('filterable_id')->toArray();
+        $filtered = array_merge($private->toArray(), $filters);
+
         $timeline = Status::whereHas('media')
         $timeline = Status::whereHas('media')
                   ->whereNotIn('profile_id', $filtered)
                   ->whereNotIn('profile_id', $filtered)
                   ->whereNull('in_reply_to_id')
                   ->whereNull('in_reply_to_id')
@@ -57,7 +41,7 @@ class TimelineController extends Controller
                   ->whereVisibility('public')
                   ->whereVisibility('public')
                   ->withCount(['comments', 'likes'])
                   ->withCount(['comments', 'likes'])
                   ->orderBy('created_at', 'desc')
                   ->orderBy('created_at', 'desc')
-                  ->simplePaginate(20);
+                  ->simplePaginate(10);
         $type = 'local';
         $type = 'local';
 
 
         return view('timeline.template', compact('timeline', 'type'));
         return view('timeline.template', compact('timeline', 'type'));

+ 1 - 0
app/Http/Kernel.php

@@ -58,6 +58,7 @@ class Kernel extends HttpKernel
         'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
         'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
         'can'           => \Illuminate\Auth\Middleware\Authorize::class,
         'can'           => \Illuminate\Auth\Middleware\Authorize::class,
         'dangerzone'    => \App\Http\Middleware\DangerZone::class,
         'dangerzone'    => \App\Http\Middleware\DangerZone::class,
+        'localization'  => \App\Http\Middleware\Localization::class,
         'guest'         => \App\Http\Middleware\RedirectIfAuthenticated::class,
         'guest'         => \App\Http\Middleware\RedirectIfAuthenticated::class,
         'signed'        => \Illuminate\Routing\Middleware\ValidateSignature::class,
         'signed'        => \Illuminate\Routing\Middleware\ValidateSignature::class,
         'throttle'      => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'throttle'      => \Illuminate\Routing\Middleware\ThrottleRequests::class,

+ 23 - 0
app/Http/Middleware/Localization.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure, Session;
+
+class Localization
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        if(Session::has('locale')) {
+            app()->setLocale(Session::get('locale'));
+        }
+        return $next($request);
+    }
+}

+ 115 - 0
app/Jobs/ImportPipeline/ImportInstagram.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace App\Jobs\ImportPipeline;
+
+use DB;
+use Carbon\Carbon;
+use Illuminate\Bus\Queueable;
+use Illuminate\Filesystem\Filesystem;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use App\Jobs\ImageOptimizePipeline\ImageOptimize;
+use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\{
+    ImportJob,
+    ImportData,
+    Media,
+    Profile,
+    Status,
+};
+
+class ImportInstagram implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+    
+    protected $job;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(ImportJob $job)
+    {
+        $this->job = $job;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $job = $this->job;
+        $profile = $this->job->profile;
+        $json = $job->mediaJson();
+        $collection = $json['photos'];
+        $files = $job->files;
+        $monthHash = hash('sha1', date('Y').date('m'));
+        $userHash = hash('sha1', $profile->id . (string) $profile->created_at);
+        $fs = new Filesystem;
+
+        foreach($collection as $import)
+        {
+            $caption = $import['caption'];
+            try {
+                $min = Carbon::create(2010, 10, 6, 0, 0, 0);
+                $taken_at = Carbon::parse($import['taken_at']);
+                if(!$min->lt($taken_at)) {
+                    $taken_at = Carbon::now();
+                }
+            } catch (Exception $e) {
+                
+            }
+            $filename = last( explode('/', $import['path']) );
+            $importData = ImportData::whereJobId($job->id)
+                ->whereOriginalName($filename)
+                ->firstOrFail();
+
+            if(is_file(storage_path("app/$importData->path")) == false) {
+                continue;
+            }
+
+            DB::transaction(function() use(
+                $fs, $job, $profile, $caption, $taken_at, $filename,
+                $monthHash, $userHash, $importData
+            ) {
+                $status = new Status();
+                $status->profile_id = $profile->id;
+                $status->caption = strip_tags($caption);
+                $status->is_nsfw = false;
+                $status->visibility = 'public';
+                $status->created_at = $taken_at;
+                $status->save();
+
+
+                $path = storage_path("app/$importData->path");
+                $storagePath = "public/m/{$monthHash}/{$userHash}";
+                $newPath = "app/$storagePath/$filename";
+                $fs->move($path,storage_path($newPath));
+                $path = $newPath;
+                $hash = \hash_file('sha256', storage_path($path));
+                $media = new Media();
+                $media->status_id = $status->id;
+                $media->profile_id = $profile->id;
+                $media->user_id = $profile->user->id;
+                $media->media_path = "$storagePath/$filename";
+                $media->original_sha256 = $hash;
+                $media->size = $fs->size(storage_path($path));
+                $media->mime = $fs->mimeType(storage_path($path));
+                $media->filter_class = null;
+                $media->filter_name = null;
+                $media->order = 1;
+                $media->save();
+                ImageOptimize::dispatch($media);
+                NewStatusPipeline::dispatch($status);
+            });
+        }
+
+        $job->completed_at = Carbon::now();
+        $job->save();
+    }
+}

+ 4 - 4
app/Jobs/InboxPipeline/InboxWorker.php

@@ -14,7 +14,7 @@ class InboxWorker implements ShouldQueue
 {
 {
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
 
-    protected $request;
+    protected $headers;
     protected $profile;
     protected $profile;
     protected $payload;
     protected $payload;
 
 
@@ -23,9 +23,9 @@ class InboxWorker implements ShouldQueue
      *
      *
      * @return void
      * @return void
      */
      */
-    public function __construct($request, Profile $profile, $payload)
+    public function __construct($headers, $profile, $payload)
     {
     {
-        $this->request = $request;
+        $this->headers = $headers;
         $this->profile = $profile;
         $this->profile = $profile;
         $this->payload = $payload;
         $this->payload = $payload;
     }
     }
@@ -37,6 +37,6 @@ class InboxWorker implements ShouldQueue
      */
      */
     public function handle()
     public function handle()
     {
     {
-        (new Inbox($this->request, $this->profile, $this->payload))->handle();
+        (new Inbox($this->headers, $this->profile, $this->payload))->handle();
     }
     }
 }
 }

+ 2 - 2
app/Jobs/LikePipeline/LikePipeline.php

@@ -41,8 +41,8 @@ class LikePipeline implements ShouldQueue
         $status = $this->like->status;
         $status = $this->like->status;
         $actor = $this->like->actor;
         $actor = $this->like->actor;
 
 
-        if ($status->url !== null) {
-            // Ignore notifications to remote statuses
+        if (!$status || $status->url !== null) {
+            // Ignore notifications to remote statuses, or deleted statuses
             return;
             return;
         }
         }
 
 

+ 1 - 0
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php

@@ -207,6 +207,7 @@ class RemoteFollowImportRecent implements ShouldQueue
 
 
         try {
         try {
             $info = pathinfo($url);
             $info = pathinfo($url);
+            $url = str_replace(' ', '%20', $url);
             $img = file_get_contents($url);
             $img = file_get_contents($url);
             $file = '/tmp/'.str_random(12).$info['basename'];
             $file = '/tmp/'.str_random(12).$info['basename'];
             file_put_contents($file, $img);
             file_put_contents($file, $img);

+ 1 - 1
app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php

@@ -83,7 +83,7 @@ class RemoteFollowPipeline implements ShouldQueue
         $profile->domain = $domain;
         $profile->domain = $domain;
         $profile->username = $remoteUsername;
         $profile->username = $remoteUsername;
         $profile->name = $res['name'];
         $profile->name = $res['name'];
-        $profile->bio = str_limit($res['summary'], 125);
+        $profile->bio = Purify::clean($res['summary']);
         $profile->sharedInbox = $res['endpoints']['sharedInbox'];
         $profile->sharedInbox = $res['endpoints']['sharedInbox'];
         $profile->remote_url = $res['url'];
         $profile->remote_url = $res['url'];
         $profile->save();
         $profile->save();

+ 79 - 0
app/Jobs/SharePipeline/SharePipeline.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Jobs\SharePipeline;
+
+use App\Status;
+use App\Notification;
+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 Log;
+use Redis;
+
+class SharePipeline implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $like;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Status $status)
+    {
+        $this->status = $status;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $status = $this->status;
+        $actor = $this->status->profile;
+        $target = $this->status->parent()->profile;
+
+        if ($status->url !== null) {
+            // Ignore notifications to remote statuses
+            return;
+        }
+
+        $exists = Notification::whereProfileId($status->profile_id)
+                  ->whereActorId($actor->id)
+                  ->whereAction('like')
+                  ->whereItemId($status->id)
+                  ->whereItemType('App\Status')
+                  ->count();
+
+        if ($actor->id === $status->profile_id || $exists !== 0) {
+            return true;
+        }
+
+        try {
+            $notification = new Notification();
+            $notification->profile_id = $status->profile_id;
+            $notification->actor_id = $actor->id;
+            $notification->action = 'like';
+            $notification->message = $like->toText();
+            $notification->rendered = $like->toHtml();
+            $notification->item_id = $status->id;
+            $notification->item_type = "App\Status";
+            $notification->save();
+
+            Cache::forever('notification.'.$notification->id, $notification);
+
+            $redis = Redis::connection();
+            $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
+            $redis->lpush($key, $notification->id);
+        } catch (Exception $e) {
+            Log::error($e);
+        }
+    }
+}

+ 1 - 1
app/Jobs/StatusPipeline/NewStatusPipeline.php

@@ -37,7 +37,7 @@ class NewStatusPipeline implements ShouldQueue
         $status = $this->status;
         $status = $this->status;
 
 
         StatusEntityLexer::dispatch($status);
         StatusEntityLexer::dispatch($status);
-        //StatusActivityPubDeliver::dispatch($status);
+        StatusActivityPubDeliver::dispatch($status);
 
 
         Cache::forever('post.'.$status->id, $status);
         Cache::forever('post.'.$status->id, $status);
 
 

+ 16 - 0
app/Jobs/StatusPipeline/StatusActivityPubDeliver.php

@@ -8,6 +8,10 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Queue\SerializesModels;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
+use App\Transformer\ActivityPub\Verb\CreateNote;
+use App\Util\ActivityPub\Helpers;
 
 
 class StatusActivityPubDeliver implements ShouldQueue
 class StatusActivityPubDeliver implements ShouldQueue
 {
 {
@@ -34,6 +38,18 @@ class StatusActivityPubDeliver implements ShouldQueue
     {
     {
         $status = $this->status;
         $status = $this->status;
 
 
+        $audience = $status->profile->getAudienceInbox();
+        $profile = $status->profile;
+
+        $fractal = new Fractal\Manager();
+        $fractal->setSerializer(new ArraySerializer());
+        $resource = new Fractal\Resource\Item($status, new CreateNote());
+        $activity = $fractal->createData($resource)->toArray();
+
+        foreach($audience as $url) {
+            Helpers::sendSignedObject($profile, $url, $activity);
+        }
+
         // todo: fanout on write
         // todo: fanout on write
     }
     }
 }
 }

+ 10 - 7
app/Jobs/StatusPipeline/StatusDelete.php

@@ -2,9 +2,12 @@
 
 
 namespace App\Jobs\StatusPipeline;
 namespace App\Jobs\StatusPipeline;
 
 
-use App\Notification;
-use App\Status;
-use App\StatusHashtag;
+use App\{
+    Notification,
+    Report,
+    Status,
+    StatusHashtag,
+};
 use Illuminate\Bus\Queueable;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Foundation\Bus\Dispatchable;
@@ -35,15 +38,12 @@ class StatusDelete implements ShouldQueue
     public function handle()
     public function handle()
     {
     {
         $status = $this->status;
         $status = $this->status;
+
         $this->unlinkRemoveMedia($status);
         $this->unlinkRemoveMedia($status);
     }
     }
 
 
     public function unlinkRemoveMedia($status)
     public function unlinkRemoveMedia($status)
     {
     {
-        if ($status->media()->count() == 0) {
-            return;
-        }
-
         foreach ($status->media as $media) {
         foreach ($status->media as $media) {
             $thumbnail = storage_path("app/{$media->thumbnail_path}");
             $thumbnail = storage_path("app/{$media->thumbnail_path}");
             $photo = storage_path("app/{$media->media_path}");
             $photo = storage_path("app/{$media->media_path}");
@@ -73,6 +73,9 @@ class StatusDelete implements ShouldQueue
             ->whereItemId($status->id)
             ->whereItemId($status->id)
             ->delete();
             ->delete();
         StatusHashtag::whereStatusId($status->id)->delete();
         StatusHashtag::whereStatusId($status->id)->delete();
+        Report::whereObjectType('App\Status')
+            ->whereObjectId($status->id)
+            ->delete();
         $status->delete();
         $status->delete();
 
 
         return true;
         return true;

+ 1 - 1
app/Jobs/StatusPipeline/StatusEntityLexer.php

@@ -69,7 +69,7 @@ class StatusEntityLexer implements ShouldQueue
         $this->storeMentions();
         $this->storeMentions();
         DB::transaction(function () {
         DB::transaction(function () {
             $status = $this->status;
             $status = $this->status;
-            $status->rendered = $this->autolink;
+            $status->rendered = nl2br($this->autolink);
             $status->entities = json_encode($this->entities);
             $status->entities = json_encode($this->entities);
             $status->save();
             $status->save();
         });
         });

+ 34 - 0
app/Jobs/VideoPipeline/VideoOptimize.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Jobs\VideoPipeline;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+
+class VideoOptimize implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        //
+    }
+}

+ 34 - 0
app/Jobs/VideoPipeline/VideoPostProcess.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Jobs\VideoPipeline;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+
+class VideoPostProcess implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        //
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        //
+    }
+}

+ 62 - 0
app/Jobs/VideoPipeline/VideoThumbnail.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Jobs\VideoPipeline;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use FFMpeg;
+use App\Media;
+
+class VideoThumbnail implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $media;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Media $media)
+    {
+        $this->media = $media;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $media = $this->media;
+        $base = $media->media_path;
+        $path = explode('/', $base);
+        $name = last($path);
+        try {
+            $t = explode('.', $name);
+            $t = $t[0].'_thumb.png';
+            $i = count($path) - 1;
+            $path[$i] = $t;
+            $save = implode('/', $path);
+            $video = FFMpeg::open($base);
+            if($video->getDurationInSeconds() < 1) {
+                $video->getFrameFromSeconds(0);
+            } elseif($video->getDurationInSeconds() < 5) {
+                $video->getFrameFromSeconds(4);
+            }
+                $video->export()
+                ->save($save);
+
+            $media->thumbnail_path = $save;
+            $media->save();
+
+        } catch (Exception $e) {
+            
+        }
+    }
+}

+ 1 - 0
app/Like.php

@@ -15,6 +15,7 @@ class Like extends Model
      * @var array
      * @var array
      */
      */
     protected $dates = ['deleted_at'];
     protected $dates = ['deleted_at'];
+    protected $fillable = ['profile_id', 'status_id'];
 
 
     public function actor()
     public function actor()
     {
     {

+ 17 - 2
app/Media.php

@@ -19,8 +19,12 @@ class Media extends Model
 
 
     public function url()
     public function url()
     {
     {
-        $path = $this->media_path;
-        $url = Storage::url($path);
+        if(!empty($this->remote_media) && $this->remote_url) {
+            $url = $this->remote_url;
+        } else {
+            $path = $this->media_path;
+            $url = Storage::url($path);
+        }
 
 
         return url($url);
         return url($url);
     }
     }
@@ -60,4 +64,15 @@ class Media extends Model
     {
     {
         return json_decode($this->metadata, true, 3);
         return json_decode($this->metadata, true, 3);
     }
     }
+
+    public function getModel()
+    {
+        if(empty($this->metadata)) {
+            return false;
+        }
+        $meta = $this->getMetadata();
+        if($meta && isset($meta['Model'])) {
+            return $meta['Model'];
+        }
+    }
 }
 }

+ 43 - 0
app/Profile.php

@@ -46,6 +46,9 @@ class Profile extends Model
 
 
     public function permalink($suffix = '')
     public function permalink($suffix = '')
     {
     {
+        if($this->remote_url) {
+            return $this->remote_url;
+        }
         return url('users/'.$this->username.$suffix);
         return url('users/'.$this->username.$suffix);
     }
     }
 
 
@@ -248,4 +251,44 @@ class Profile extends Model
     {
     {
         return $this->sharedInbox ?? $this->inboxUrl();
         return $this->sharedInbox ?? $this->inboxUrl();
     }
     }
+
+    public function getDefaultScope()
+    {
+        return $this->is_private == true ? 'private' : 'public';
+    }
+
+    public function getAudience($scope = false)
+    {
+        if($this->remote_url) {
+            return [];
+        }
+        $scope = $scope ?? $this->getDefaultScope();
+        $audience = [];
+        switch ($scope) {
+            case 'public':
+                $audience = [
+                    'to' => [
+                        'https://www.w3.org/ns/activitystreams#Public'
+                    ],
+                    'cc' => [
+                        $this->permalink('/followers')
+                    ]
+                ];
+                break;
+        }
+        return $audience;
+    }
+
+    public function getAudienceInbox($scope = 'public')
+    {
+        return $this
+            ->followers()
+            ->whereLocalProfile(false)
+            ->get()
+            ->map(function($follow) {
+                return $follow->sharedInbox ?? $follow->inbox_url;
+             })
+            ->unique()
+            ->toArray();
+    }
 }
 }

+ 1 - 2
app/Providers/RouteServiceProvider.php

@@ -65,8 +65,7 @@ class RouteServiceProvider extends ServiceProvider
      */
      */
     protected function mapApiRoutes()
     protected function mapApiRoutes()
     {
     {
-        Route::prefix('api')
-             ->middleware('api')
+        Route::middleware('api')
              ->namespace($this->namespace)
              ->namespace($this->namespace)
              ->group(base_path('routes/api.php'));
              ->group(base_path('routes/api.php'));
     }
     }

+ 9 - 8
app/Report.php

@@ -22,14 +22,15 @@ class Report extends Model
     {
     {
         $class = $this->object_type;
         $class = $this->object_type;
         switch ($class) {
         switch ($class) {
-        case 'App\Status':
-         $column = 'id';
-          break;
-
-        default:
-         $column = 'id';
-          break;
-      }
+            case 'App\Status':
+             $column = 'id';
+              break;
+
+            default:
+             $class = 'App\Status';
+             $column = 'id';
+              break;
+        }
 
 
         return (new $class())->where($column, $this->object_id)->firstOrFail();
         return (new $class())->where($column, $this->object_id)->firstOrFail();
     }
     }

+ 43 - 33
app/Status.php

@@ -2,7 +2,7 @@
 
 
 namespace App;
 namespace App;
 
 
-use Auth;
+use Auth, Cache;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Storage;
 use Storage;
@@ -18,7 +18,7 @@ class Status extends Model
      */
      */
     protected $dates = ['deleted_at'];
     protected $dates = ['deleted_at'];
 
 
-    protected $fillable = ['profile_id', 'visibility'];
+    protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id'];
 
 
     public function profile()
     public function profile()
     {
     {
@@ -37,26 +37,29 @@ class Status extends Model
 
 
     public function viewType()
     public function viewType()
     {
     {
-        $media = $this->firstMedia();
-        $mime = explode('/', $media->mime)[0];
-        $count = $this->media()->count();
-        $type = ($mime == 'image') ? 'image' : 'video';
-        if($count > 1) {
-            $type = ($type == 'image') ? 'album' : 'video-album';
-        }
-
-        return $type;
+        return Cache::remember('status:view-type:'.$this->id, 40320, function() {
+            $media = $this->firstMedia();
+            $mime = explode('/', $media->mime)[0];
+            $count = $this->media()->count();
+            $type = ($mime == 'image') ? 'image' : 'video';
+            if($count > 1) {
+                $type = ($type == 'image') ? 'album' : 'video-album';
+            }
+            return $type;
+        });
     }
     }
 
 
     public function thumb($showNsfw = false)
     public function thumb($showNsfw = false)
     {
     {
-        $type = $this->viewType();
-        $is_nsfw = !$showNsfw ? $this->is_nsfw : false;
-        if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album'])) {
-            return 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
-        }
-
-        return url(Storage::url($this->firstMedia()->thumbnail_path));
+        return Cache::remember('status:thumb:'.$this->id, 40320, function() use ($showNsfw) {
+            $type = $this->viewType();
+            $is_nsfw = !$showNsfw ? $this->is_nsfw : false;
+            if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['image', 'album', 'video'])) {
+                return 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
+            }
+
+            return url(Storage::url($this->firstMedia()->thumbnail_path));
+        });
     }
     }
 
 
     public function url()
     public function url()
@@ -64,11 +67,6 @@ class Status extends Model
         $id = $this->id;
         $id = $this->id;
         $username = $this->profile->username;
         $username = $this->profile->username;
         $path = config('app.url')."/p/{$username}/{$id}";
         $path = config('app.url')."/p/{$username}/{$id}";
-        if (!is_null($this->in_reply_to_id)) {
-            $pid = $this->in_reply_to_id;
-            $path = config('app.url')."/p/{$username}/{$pid}/c/{$id}";
-        }
-
         return url($path);
         return url($path);
     }
     }
 
 
@@ -103,8 +101,10 @@ class Status extends Model
 
 
     public function liked() : bool
     public function liked() : bool
     {
     {
+        if(Auth::check() == false) {
+            return false;
+        }
         $profile = Auth::user()->profile;
         $profile = Auth::user()->profile;
-
         return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
         return Like::whereProfileId($profile->id)->whereStatusId($this->id)->count();
     }
     }
 
 
@@ -116,7 +116,7 @@ class Status extends Model
     public function bookmarked()
     public function bookmarked()
     {
     {
         if (!Auth::check()) {
         if (!Auth::check()) {
-            return 0;
+            return false;
         }
         }
         $profile = Auth::user()->profile;
         $profile = Auth::user()->profile;
 
 
@@ -130,6 +130,9 @@ class Status extends Model
 
 
     public function shared() : bool
     public function shared() : bool
     {
     {
+        if(Auth::check() == false) {
+            return false;
+        }
         $profile = Auth::user()->profile;
         $profile = Auth::user()->profile;
 
 
         return self::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
         return self::whereProfileId($profile->id)->whereReblogOfId($this->id)->count();
@@ -139,7 +142,7 @@ class Status extends Model
     {
     {
         $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
         $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
         if (!empty($parent)) {
         if (!empty($parent)) {
-            return self::findOrFail($parent);
+            return $this->findOrFail($parent);
         }
         }
     }
     }
 
 
@@ -254,7 +257,7 @@ class Status extends Model
                         'url' => $media->url(),
                         'url' => $media->url(),
                         'name' => null
                         'name' => null
                     ];
                     ];
-                })
+                })->toArray()
             ]
             ]
         ];
         ];
     }
     }
@@ -268,18 +271,25 @@ class Status extends Model
         $res['to'] = [];
         $res['to'] = [];
         $res['cc'] = [];
         $res['cc'] = [];
         $scope = $this->scope;
         $scope = $this->scope;
+        $mentions = $this->mentions->map(function ($mention) {
+            return $mention->permalink();
+        })->toArray();
+
         switch ($scope) {
         switch ($scope) {
             case 'public':
             case 'public':
                 $res['to'] = [
                 $res['to'] = [
                     "https://www.w3.org/ns/activitystreams#Public"
                     "https://www.w3.org/ns/activitystreams#Public"
                 ];
                 ];
-                $res['cc'] = [
-                    $this->profile->permalink('/followers')
-                ];
+                $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
+                break;
+
+            case 'unlisted':
+                break;
+
+            case 'private':
                 break;
                 break;
-            
-            default:
-                # code...
+
+            case 'direct':
                 break;
                 break;
         }
         }
         return $res[$audience];
         return $res[$audience];

+ 10 - 0
app/Story.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Story extends Model
+{
+    //
+}

+ 10 - 0
app/StoryReaction.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class StoryReaction extends Model
+{
+    //
+}

+ 19 - 0
app/Transformer/ActivityPub/Verb/Announce.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\Status;
+use League\Fractal;
+
+class Announce extends Fractal\TransformerAbstract
+{
+    public function transform(Status $status)
+    {
+    	return [
+    		'@context'  => 'https://www.w3.org/ns/activitystreams',
+    		'type' 		=> 'Announce',
+    		'actor'		=> $status->profile->permalink(),
+    		'object'	=> $status->parent()->url()
+    	];
+    }
+}

+ 70 - 0
app/Transformer/ActivityPub/Verb/CreateNote.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\Status;
+use League\Fractal;
+
+class CreateNote extends Fractal\TransformerAbstract
+{
+	public function transform(Status $status)
+	{
+
+		$mentions = $status->mentions->map(function ($mention) {
+			return [
+				'type' => 'Mention',
+				'href' => $mention->permalink(),
+				'name' => $mention->emailUrl()
+			];
+		})->toArray();
+		$hashtags = $status->hashtags->map(function ($hashtag) {
+			return [
+				'type' => 'Hashtag',
+				'href' => $hashtag->url(),
+				'name' => "#{$hashtag->name}",
+			];
+		})->toArray();
+		$tags = array_merge($mentions, $hashtags);
+
+		return [
+			'@context' => [
+				'https://www.w3.org/ns/activitystreams',
+				'https://w3id.org/security/v1',
+				[
+					'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
+					'featured'                  => [
+						'https://pixelfed.org/ns#featured' => ['@type' => '@id'],
+					],
+				],
+			],
+			'id' 					=> $status->permalink(),
+			'type' 					=> 'Create',
+			'actor' 				=> $status->profile->permalink(),
+			'published' 			=> $status->created_at->toAtomString(),
+			'to' 					=> $status->scopeToAudience('to'),
+			'cc' 					=> $status->scopeToAudience('cc'),
+			'object' => [
+				'id' 				=> $status->url(),
+				'type' 				=> 'Note',
+				'summary'   		=> null,
+				'content'   		=> $status->rendered ?? $status->caption,
+				'inReplyTo' 		=> $status->in_reply_to_id ? $status->parent()->url() : null,
+				'published'    		=> $status->created_at->toAtomString(),
+				'url'          		=> $status->url(),
+				'attributedTo' 		=> $status->profile->permalink(),
+				'to'           		=> $status->scopeToAudience('to'),
+				'cc' 				=> $status->scopeToAudience('cc'),
+				'sensitive'       	=> (bool) $status->is_nsfw,
+				'attachment'      	=> $status->media->map(function ($media) {
+					return [
+						'type'      => 'Document',
+						'mediaType' => $media->mime,
+						'url'       => $media->url(),
+						'name'      => null,
+					];
+				})->toArray(),
+				'tag' 				=> $tags,
+			]
+		];
+	}
+}

+ 19 - 0
app/Transformer/ActivityPub/Verb/Follow.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\Follower;
+use League\Fractal;
+
+class Follow extends Fractal\TransformerAbstract
+{
+    public function transform(Follower $follower)
+    {
+    	return [
+    		'@context'  => 'https://www.w3.org/ns/activitystreams',
+    		'type' 		=> 'Follow',
+    		'actor'		=> $follower->actor->permalink(),
+    		'object'	=> $follower->target->permalink()
+    	];
+    }
+}

+ 19 - 0
app/Transformer/ActivityPub/Verb/Like.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\Like as LikeModel;
+use League\Fractal;
+
+class Like extends Fractal\TransformerAbstract
+{
+    public function transform(LikeModel $like)
+    {
+    	return [
+    		'@context'  => 'https://www.w3.org/ns/activitystreams',
+    		'type' 		=> 'Like',
+    		'actor'		=> $like->actor->permalink(),
+    		'object'	=> $like->status->url()
+    	];
+    }
+}

+ 7 - 2
app/Transformer/Api/MediaTransformer.php

@@ -16,8 +16,13 @@ class MediaTransformer extends Fractal\TransformerAbstract
             'remote_url'  => null,
             'remote_url'  => null,
             'preview_url' => $media->thumbnailUrl(),
             'preview_url' => $media->thumbnailUrl(),
             'text_url'    => null,
             'text_url'    => null,
-            'meta'        => $media->metadata,
-            'description' => null,
+            'meta'        => null,
+            'description' => $media->caption,
+            'license'     => $media->license,
+            'is_nsfw'     => $media->is_nsfw,
+            'orientation' => $media->orientation,
+            'filter_name' => $media->filter_name,
+            'filter_class' => $media->filter_class,
         ];
         ];
     }
     }
 }
 }

+ 25 - 0
app/Transformer/Api/RelationshipTransformer.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Transformer\Api;
+
+use App\Profile;
+use League\Fractal;
+
+class RelationshipTransformer extends Fractal\TransformerAbstract
+{
+    public function transform(Profile $profile)
+    {
+        return [
+            'id' => $profile->id,
+            'following' => null,
+            'followed_by' => null,
+            'blocking' => null,
+            'muting' => null,
+            'muting_notifications' => null,
+            'requested' => null,
+            'domain_blocking' => null,
+            'showing_reblogs' => null,
+            'endorsed' => null
+        ];
+    }
+}

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

@@ -59,7 +59,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
 
 
     public function includeMediaAttachments(Status $status)
     public function includeMediaAttachments(Status $status)
     {
     {
-        $media = $status->media;
+        $media = $status->media()->orderBy('order')->get();
 
 
         return $this->collection($media, new MediaTransformer());
         return $this->collection($media, new MediaTransformer());
     }
     }

+ 18 - 22
app/Util/ActivityPub/Helpers.php

@@ -20,7 +20,7 @@ use App\Jobs\AvatarPipeline\CreateAvatar;
 use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
 use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
 use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
 use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
-use HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
+use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier};
 use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
 use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
 
 
 class Helpers {
 class Helpers {
@@ -309,31 +309,27 @@ class Helpers {
     {
     {
         $profile = $senderProfile;
         $profile = $senderProfile;
         $keyId = $profile->keyId();
         $keyId = $profile->keyId();
-        $privateKey = openssl_pkey_get_private($profile->private_key);
 
 
-
-        $date = date('D, d M Y h:i:s').' GMT';
+        $date = new \DateTime('UTC');
+        $date = $date->format('D, d M Y H:i:s \G\M\T');
         $host = parse_url($url, PHP_URL_HOST);
         $host = parse_url($url, PHP_URL_HOST);
+        $path = parse_url($url, PHP_URL_PATH);
         $headers = [
         $headers = [
-        	'(request-target)' => 'post '.parse_url($url, PHP_URL_PATH),
-            'Date'         => $date,
-            'Host'		   => $host,
-            'Content-Type' => 'application/activity+json',
+            'date'         => $date,
+            'host'		   => $host,
+            'content-type' => 'application/activity+json',
         ];
         ];
-        if($body) {
-        	$payload = is_string($body) ? $body : json_encode($body);
-        	$digest = base64_encode(hash('sha256', $payload, true));
-        	$headers['Digest'] = 'SHA-256=' . $digest;
-        }
-        $stringToSign = self::_headersToSigningString($headers);
-        $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
-		openssl_sign($stringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256);
-    	openssl_free_key($privateKey);
-	    $signature = base64_encode($signature);
-    	$signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
-    	unset($headers['(request-target)']);
-    	$headers['Signature'] = $signatureHeader;
-    	Zttp::withHeaders($headers)->post($url, $body);
+
+        $context = new Context([
+            'keys' => [$profile->keyId() => $profile->private_key],
+            'algorithm' => 'rsa-sha256',
+            'headers' => ['(request-target)', 'date', 'host', 'content-type'],
+        ]);
+
+        $handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
+        $client = new Client(['handler' => $handlerStack]);
+
+        $response = $client->request('POST', $url, ['headers' => $headers, 'json' => $body]);
         return;
         return;
     }
     }
 
 

+ 266 - 27
app/Util/ActivityPub/Inbox.php

@@ -2,18 +2,30 @@
 
 
 namespace App\Util\ActivityPub;
 namespace App\Util\ActivityPub;
 
 
-use App\Like;
-use App\Profile;
+use Cache, DB, Log, Redis, Validator;
+use App\{
+    Activity,
+    Follower,
+    FollowRequest,
+    Like,
+    Notification,
+    Profile,
+    Status
+};
+use Carbon\Carbon;
+use App\Util\ActivityPub\Helpers;
+use App\Jobs\LikePipeline\LikePipeline;
 
 
 class Inbox
 class Inbox
 {
 {
-    protected $request;
+    protected $headers;
     protected $profile;
     protected $profile;
     protected $payload;
     protected $payload;
+    protected $logger;
 
 
-    public function __construct($request, Profile $profile, $payload)
+    public function __construct($headers, $profile, $payload)
     {
     {
-        $this->request = $request;
+        $this->headers = $headers;
         $this->profile = $profile;
         $this->profile = $profile;
         $this->payload = $payload;
         $this->payload = $payload;
     }
     }
@@ -25,15 +37,31 @@ class Inbox
 
 
     public function authenticatePayload()
     public function authenticatePayload()
     {
     {
-        // todo
+        try {
+           $signature = Helpers::validateSignature($this->headers, $this->payload);
+           $payload = Helpers::validateObject($this->payload);
+           if($signature == false) {
+            return;
+           }
+        } catch (Exception $e) {
+           return; 
+        }
+        $this->payloadLogger(); 
+    }
 
 
+    public function payloadLogger()
+    {
+        $logger = new Activity;
+        $logger->data = json_encode($this->payload);
+        $logger->save();
+        $this->logger = $logger;
+        Log::info('AP:inbox:activity:new:'.$this->logger->id);
         $this->handleVerb();
         $this->handleVerb();
     }
     }
 
 
     public function handleVerb()
     public function handleVerb()
     {
     {
         $verb = $this->payload['type'];
         $verb = $this->payload['type'];
-
         switch ($verb) {
         switch ($verb) {
             case 'Create':
             case 'Create':
                 $this->handleCreateActivity();
                 $this->handleCreateActivity();
@@ -43,43 +71,254 @@ class Inbox
                 $this->handleFollowActivity();
                 $this->handleFollowActivity();
                 break;
                 break;
 
 
+            case 'Announce':
+                $this->handleAnnounceActivity();
+                break;
+
+            case 'Accept':
+                $this->handleAcceptActivity();
+                break;
+
+            case 'Delete':
+                $this->handleDeleteActivity();
+                break;
+
+            case 'Like':
+                $this->handleLikeActivity();
+                break;
+
+            case 'Reject':
+                $this->handleRejectActivity();
+                break;
+
+            case 'Undo':
+                $this->handleUndoActivity();
+                break;
+
             default:
             default:
                 // TODO: decide how to handle invalid verbs.
                 // TODO: decide how to handle invalid verbs.
                 break;
                 break;
         }
         }
     }
     }
 
 
+    public function verifyNoteAttachment()
+    {
+        $activity = $this->payload['object'];
+
+        if(isset($activity['inReplyTo']) && 
+            !empty($activity['inReplyTo']) && 
+            Helpers::validateUrl($activity['inReplyTo'])
+        ) {
+            // reply detected, skip attachment check
+            return true;
+        }
+
+        $valid = Helpers::verifyAttachments($activity);
+
+        return $valid;
+    }
+
+    public function actorFirstOrCreate($actorUrl)
+    {
+        return Helpers::profileFirstOrNew($actorUrl);
+    }
+
     public function handleCreateActivity()
     public function handleCreateActivity()
     {
     {
-        // todo
+        $activity = $this->payload['object'];
+        if(!$this->verifyNoteAttachment()) {
+            return;
+        }
+        if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
+            $this->handleNoteReply();
+
+        } elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) {
+            $this->handleNoteCreate();
+        }
+    }
+
+    public function handleNoteReply()
+    {
+        $activity = $this->payload['object'];
+        $actor = $this->actorFirstOrCreate($this->payload['actor']);
+        $inReplyTo = $activity['inReplyTo'];
+        
+        if(!Helpers::statusFirstOrFetch($activity['url'], true)) {
+            $this->logger->delete();
+            return;
+        }
+
+        $this->logger->to_id = $this->profile->id;
+        $this->logger->from_id = $actor->id;
+        $this->logger->processed_at = Carbon::now();
+        $this->logger->save();
+    }
+
+    public function handleNoteCreate()
+    {
+        $activity = $this->payload['object'];
+        $actor = $this->actorFirstOrCreate($this->payload['actor']);
+        if(!$actor || $actor->domain == null) {
+            return;
+        }
+
+        if(Helpers::userInAudience($this->profile, $this->payload) == false) {
+            //Log::error('AP:inbox:userInAudience:false - Activity#'.$this->logger->id);
+            $logger = Activity::find($this->logger->id);
+            $logger->delete();
+            return;
+        }
+
+        if(Status::whereUrl($activity['url'])->exists()) {
+            return;
+        }
+
+        $status = DB::transaction(function() use($activity, $actor) {
+            $status = new Status;
+            $status->profile_id = $actor->id;
+            $status->caption = strip_tags($activity['content']);
+            $status->visibility = $status->scope = 'public';
+            $status->url = $activity['url'];
+            $status->save();
+            return $status;
+        });
+
+        Helpers::importNoteAttachment($activity, $status);
+
+        $logger = Activity::find($this->logger->id);
+        $logger->to_id = $this->profile->id;
+        $logger->from_id = $actor->id;
+        $logger->processed_at = Carbon::now();
+        $logger->save();
     }
     }
 
 
     public function handleFollowActivity()
     public function handleFollowActivity()
     {
     {
-        $actor = $this->payload['object'];
+        $actor = $this->actorFirstOrCreate($this->payload['actor']);
+        if(!$actor || $actor->domain == null) {
+            return;
+        }
         $target = $this->profile;
         $target = $this->profile;
+        if($target->is_private == true) {
+            // make follow request
+            FollowRequest::firstOrCreate([
+                'follower_id' => $actor->id,
+                'following_id' => $target->id
+            ]);
+            // todo: send notification
+        } else {
+            // store new follower
+            $follower = Follower::firstOrCreate([
+                'profile_id' => $actor->id,
+                'following_id' => $target->id,
+                'local_profile' => empty($actor->domain)
+            ]);
+            if($follower->wasRecentlyCreated == false) {
+                $this->logger->delete();
+                return;
+            }
+            // send notification
+            $notification = new Notification();
+            $notification->profile_id = $target->id;
+            $notification->actor_id = $actor->id;
+            $notification->action = 'follow';
+            $notification->message = $follower->toText();
+            $notification->rendered = $follower->toHtml();
+            $notification->item_id = $target->id;
+            $notification->item_type = "App\Profile";
+            $notification->save();
+
+            \Cache::forever('notification.'.$notification->id, $notification);
+
+            $redis = Redis::connection();
+
+            $nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
+            $redis->lpush($nkey, $notification->id);
+            
+            // send Accept to remote profile
+            $accept = [
+                '@context' => 'https://www.w3.org/ns/activitystreams',
+                'id'       => $follower->permalink('/accept'),
+                'type'     => 'Accept',
+                'actor'    => $target->permalink(),
+                'object'   => [
+                    'id' => $this->payload['id'],
+                    'type'  => 'Follow',
+                    'actor' => $target->permalink(),
+                    'object' => $actor->permalink()
+                ]
+            ];
+            Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
+        }
+        $this->logger->to_id = $target->id;
+        $this->logger->from_id = $actor->id;
+        $this->logger->processed_at = Carbon::now();
+        $this->logger->save();
     }
     }
 
 
-    public function actorFirstOrCreate($actorUrl)
+    public function handleAnnounceActivity()
+    {
+
+    }
+
+    public function handleAcceptActivity()
+    {
+
+    }
+
+    public function handleDeleteActivity()
+    {
+
+    }
+
+    public function handleLikeActivity()
     {
     {
-        if (Profile::whereRemoteUrl($actorUrl)->count() !== 0) {
-            return Profile::whereRemoteUrl($actorUrl)->firstOrFail();
+        $actor = $this->payload['actor'];
+        $profile = self::actorFirstOrCreate($actor);
+        $obj = $this->payload['object'];
+        if(Helpers::validateLocalUrl($obj) == false) {
+            return;
+        }
+        $status = Helpers::statusFirstOrFetch($obj);
+        $like = Like::firstOrCreate([
+            'profile_id' => $profile->id,
+            'status_id' => $status->id
+        ]);
+
+        if($like->wasRecentlyCreated == false) {
+            return;
+        }
+        LikePipeline::dispatch($like);
+        $this->logger->to_id = $status->profile_id;
+        $this->logger->from_id = $profile->id;
+        $this->logger->processed_at = Carbon::now();
+        $this->logger->save();
+    }
+
+
+    public function handleRejectActivity()
+    {
+
+    }
+
+    public function handleUndoActivity()
+    {
+        $actor = $this->payload['actor'];
+        $profile = self::actorFirstOrCreate($actor);
+        $obj = $this->payload['object'];
+        $status = Helpers::statusFirstOrFetch($obj['object']);
+
+        switch ($obj['type']) {
+            case 'Like':
+                Like::whereProfileId($profile->id)
+                    ->whereStatusId($status->id)
+                    ->delete();
+                break;
         }
         }
 
 
-        $res = (new DiscoverActor($url))->discover();
-
-        $domain = parse_url($res['url'], PHP_URL_HOST);
-        $username = $res['preferredUsername'];
-        $remoteUsername = "@{$username}@{$domain}";
-
-        $profile = new Profile();
-        $profile->user_id = null;
-        $profile->domain = $domain;
-        $profile->username = $remoteUsername;
-        $profile->name = $res['name'];
-        $profile->bio = str_limit($res['summary'], 125);
-        $profile->sharedInbox = $res['endpoints']['sharedInbox'];
-        $profile->remote_url = $res['url'];
-        $profile->save();
+        $this->logger->to_id = $status->profile_id;
+        $this->logger->from_id = $profile->id;
+        $this->logger->processed_at = Carbon::now();
+        $this->logger->save();
     }
     }
 }
 }

+ 34 - 0
app/Util/HTTPSignatures/Algorithm.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+abstract class Algorithm
+{
+    /**
+     * @param string $name
+     *
+     * @return HmacAlgorithm
+     *
+     * @throws Exception
+     */
+    public static function create($name)
+    {
+        switch ($name) {
+        case 'hmac-sha1':
+            return new HmacAlgorithm('sha1');
+            break;
+        case 'hmac-sha256':
+            return new HmacAlgorithm('sha256');
+            break;
+        case 'rsa-sha1':
+            return new RsaAlgorithm('sha1');
+            break;
+        case 'rsa-sha256':
+            return new RsaAlgorithm('sha256');
+            break;
+        default:
+            throw new AlgorithmException("No algorithm named '$name'");
+            break;
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/AlgorithmException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class AlgorithmException extends Exception
+{
+}

+ 19 - 0
app/Util/HTTPSignatures/AlgorithmInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+interface AlgorithmInterface
+{
+    /**
+     * @return string
+     */
+    public function name();
+
+    /**
+     * @param string $key
+     * @param string $data
+     *
+     * @return string
+     */
+    public function sign($key, $data);
+}

+ 119 - 0
app/Util/HTTPSignatures/Context.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class Context
+{
+    /** @var array */
+    private $headers;
+
+    /** @var KeyStoreInterface */
+    private $keyStore;
+
+    /** @var array */
+    private $keys;
+
+    /** @var string */
+    private $signingKeyId;
+
+    /** @var AlgorithmInterface */
+    private $algorithm;
+
+    /**
+     * @param array $args
+     *
+     * @throws Exception
+     */
+    public function __construct($args)
+    {
+        if (isset($args['keys']) && isset($args['keyStore'])) {
+            throw new Exception(__CLASS__.' accepts keys or keyStore but not both');
+        } elseif (isset($args['keys'])) {
+            // array of keyId => keySecret
+            $this->keys = $args['keys'];
+        } elseif (isset($args['keyStore'])) {
+            $this->setKeyStore($args['keyStore']);
+        }
+
+        // algorithm for signing; not necessary for verifying.
+        if (isset($args['algorithm'])) {
+            $this->algorithm = Algorithm::create($args['algorithm']);
+        }
+        // headers list for signing; not necessary for verifying.
+        if (isset($args['headers'])) {
+            $this->headers = $args['headers'];
+        }
+
+        // signingKeyId specifies the key used for signing messages.
+        if (isset($args['signingKeyId'])) {
+            $this->signingKeyId = $args['signingKeyId'];
+        } elseif (isset($args['keys']) && 1 === count($args['keys'])) {
+            list($this->signingKeyId) = array_keys($args['keys']); // first key
+        }
+    }
+
+    /**
+     * @return Signer
+     *
+     * @throws Exception
+     */
+    public function signer()
+    {
+        return new Signer(
+            $this->signingKey(),
+            $this->algorithm,
+            $this->headerList()
+        );
+    }
+
+    /**
+     * @return Verifier
+     */
+    public function verifier()
+    {
+        return new Verifier($this->keyStore());
+    }
+
+    /**
+     * @return Key
+     *
+     * @throws Exception
+     * @throws KeyStoreException
+     */
+    private function signingKey()
+    {
+        if (isset($this->signingKeyId)) {
+            return $this->keyStore()->fetch($this->signingKeyId);
+        } else {
+            throw new Exception('no implicit or specified signing key');
+        }
+    }
+
+    /**
+     * @return HeaderList
+     */
+    private function headerList()
+    {
+        return new HeaderList($this->headers);
+    }
+
+    /**
+     * @return KeyStore
+     */
+    private function keyStore()
+    {
+        if (empty($this->keyStore)) {
+            $this->keyStore = new KeyStore($this->keys);
+        }
+
+        return $this->keyStore;
+    }
+
+    /**
+     * @param KeyStoreInterface $keyStore
+     */
+    private function setKeyStore(KeyStoreInterface $keyStore)
+    {
+        $this->keyStore = $keyStore;
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/Exception.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class Exception extends \Exception
+{
+}

+ 41 - 0
app/Util/HTTPSignatures/GuzzleHttpSignatures.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\Context;
+
+class GuzzleHttpSignatures
+{
+    /**
+     * @param Context $context
+     * @return HandlerStack
+     */
+    public static function defaultHandlerFromContext(Context $context)
+    {
+        $stack = HandlerStack::create();
+        $stack->push(self::middlewareFromContext($context));
+
+        return $stack;
+    }
+
+    /**
+     * @param Context $context
+     * @return \Closure
+     */
+    public static function middlewareFromContext(Context $context)
+    {
+        return function (callable $handler) use ($context)
+        {
+            return function (
+                Request $request,
+                array $options
+            ) use ($handler, $context)
+            {
+                $request = $context->signer()->sign($request);
+                return $handler($request, $options);
+            };
+        };
+    }
+}

+ 48 - 0
app/Util/HTTPSignatures/HeaderList.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class HeaderList
+{
+    /** @var array */
+    public $names;
+
+    /**
+     * @param array $names
+     */
+    public function __construct(array $names)
+    {
+        $this->names = array_map(
+            [$this, 'normalize'],
+            $names
+        );
+    }
+
+    /**
+     * @param $string
+     *
+     * @return HeaderList
+     */
+    public static function fromString($string)
+    {
+        return new static(explode(' ', $string));
+    }
+
+    /**
+     * @return string
+     */
+    public function string()
+    {
+        return implode(' ', $this->names);
+    }
+
+    /**
+     * @param $name
+     *
+     * @return string
+     */
+    private function normalize($name)
+    {
+        return strtolower($name);
+    }
+}

+ 36 - 0
app/Util/HTTPSignatures/HmacAlgorithm.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class HmacAlgorithm implements AlgorithmInterface
+{
+    /** @var string */
+    private $digestName;
+
+    /**
+     * @param string $digestName
+     */
+    public function __construct($digestName)
+    {
+        $this->digestName = $digestName;
+    }
+
+    /**
+     * @return string
+     */
+    public function name()
+    {
+        return sprintf('hmac-%s', $this->digestName);
+    }
+
+    /**
+     * @param string $key
+     * @param string $data
+     *
+     * @return string
+     */
+    public function sign($secret, $data)
+    {
+        return hash_hmac($this->digestName, $data, $secret, true);
+    }
+}

+ 260 - 0
app/Util/HTTPSignatures/Key.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class Key
+{
+    /** @var string */
+    private $id;
+
+    /** @var string */
+    private $secret;
+
+    /** @var resource */
+    private $certificate;
+
+    /** @var resource */
+    private $publicKey;
+
+    /** @var resource */
+    private $privateKey;
+
+    /** @var string */
+    private $type;
+
+    /**
+     * @param string       $id
+     * @param string|array $secret
+     */
+    public function __construct($id, $item)
+    {
+        $this->id = $id;
+        if (Key::hasX509Certificate($item) || Key::hasPublicKey($item)) {
+            $publicKey = Key::getPublicKey($item);
+        } else {
+            $publicKey = null;
+        }
+        if (Key::hasPrivateKey($item)) {
+            $privateKey = Key::getPrivateKey($item);
+        } else {
+            $privateKey = null;
+        }
+        if (($publicKey || $privateKey)) {
+            $this->type = 'asymmetric';
+            if ($publicKey && $privateKey) {
+                $publicKeyPEM = openssl_pkey_get_details($publicKey)['key'];
+                $privateKeyPublicPEM = openssl_pkey_get_details($privateKey)['key'];
+                if ($privateKeyPublicPEM != $publicKeyPEM) {
+                    throw new KeyException('Supplied Certificate and Key are not related');
+                }
+            }
+            $this->privateKey = $privateKey;
+            $this->publicKey = $publicKey;
+            $this->secret = null;
+        } else {
+            $this->type = 'secret';
+            $this->secret = $item;
+            $this->publicKey = null;
+            $this->privateKey = null;
+        }
+    }
+
+    /**
+     * Retrieves private key resource from a input string or
+     * array of strings.
+     *
+     * @param string|array $object PEM-format Private Key or file path to same
+     *
+     * @return resource|false
+     */
+    public static function getPrivateKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidateKey) {
+                $privateKey = Key::getPrivateKey($candidateKey);
+                if ($privateKey) {
+                    return $privateKey;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                $privateKey = openssl_get_privatekey($object);
+
+                return $privateKey;
+            } catch (\Exception $e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Retrieves public key resource from a input string or
+     * array of strings.
+     *
+     * @param string|array $object PEM-format Public Key or file path to same
+     *
+     * @return resource|false
+     */
+    public static function getPublicKey($object)
+    {
+        if (is_array($object)) {
+            // If we implement key rotation in future, this should add to a collection
+            foreach ($object as $candidateKey) {
+                $publicKey = Key::getPublicKey($candidateKey);
+                if ($publicKey) {
+                    return $publicKey;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                $publicKey = openssl_get_publickey($object);
+
+                return $publicKey;
+            } catch (\Exception $e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Signing HTTP Messages 'keyId' field.
+     *
+     * @return string
+     *
+     * @throws KeyException
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Retrieve Verifying Key - Public Key for Asymmetric/PKI, or shared secret for HMAC.
+     *
+     * @return string Shared Secret or PEM-format Public Key
+     *
+     * @throws KeyException
+     */
+    public function getVerifyingKey()
+    {
+        switch ($this->type) {
+        case 'asymmetric':
+            if ($this->publicKey) {
+                return openssl_pkey_get_details($this->publicKey)['key'];
+            } else {
+                return null;
+            }
+            break;
+        case 'secret':
+            return $this->secret;
+        default:
+            throw new KeyException("Unknown key type $this->type");
+        }
+    }
+
+    /**
+     * Retrieve Signing Key - Private Key for Asymmetric/PKI, or shared secret for HMAC.
+     *
+     * @return string Shared Secret or PEM-format Private Key
+     *
+     * @throws KeyException
+     */
+    public function getSigningKey()
+    {
+        switch ($this->type) {
+        case 'asymmetric':
+            if ($this->privateKey) {
+                openssl_pkey_export($this->privateKey, $pem);
+
+                return $pem;
+            } else {
+                return null;
+            }
+            break;
+        case 'secret':
+            return $this->secret;
+        default:
+            throw new KeyException("Unknown key type $this->type");
+        }
+    }
+
+    /**
+     * @return string 'secret' for HMAC or 'asymmetric'
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Test if $object is, points to or contains, X.509 PEM-format certificate.
+     *
+     * @param string|array $object PEM Format X.509 Certificate or file path to one
+     *
+     * @return bool
+     */
+    public static function hasX509Certificate($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidateCertificate) {
+                $result = Key::hasX509Certificate($candidateCertificate);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                openssl_x509_export($object, $null);
+
+                return true;
+            } catch (\Exception $e) {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Test if $object is, points to or contains, PEM-format Public Key.
+     *
+     * @param string|array $object PEM-format Public Key or file path to one
+     *
+     * @return bool
+     */
+    public static function hasPublicKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidatePublicKey) {
+                $result = Key::hasPublicKey($candidatePublicKey);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            return false == !openssl_pkey_get_public($object);
+        }
+    }
+
+    /**
+     * Test if $object is, points to or contains, PEM-format Private Key.
+     *
+     * @param string|array $object PEM-format Private Key or file path to one
+     *
+     * @return bool
+     */
+    public static function hasPrivateKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidatePrivateKey) {
+                $result = Key::hasPrivateKey($candidatePrivateKey);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            return  false != openssl_pkey_get_private($object);
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/KeyException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class KeyException extends Exception
+{
+}

+ 36 - 0
app/Util/HTTPSignatures/KeyStore.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class KeyStore implements KeyStoreInterface
+{
+    /** @var Key[] */
+    private $keys;
+
+    /**
+     * @param array $keys
+     */
+    public function __construct($keys)
+    {
+        $this->keys = [];
+        foreach ($keys as $id => $key) {
+            $this->keys[$id] = new Key($id, $key);
+        }
+    }
+
+    /**
+     * @param string $keyId
+     *
+     * @return Key
+     *
+     * @throws KeyStoreException
+     */
+    public function fetch($keyId)
+    {
+        if (isset($this->keys[$keyId])) {
+            return $this->keys[$keyId];
+        } else {
+            throw new KeyStoreException("Key '$keyId' not found");
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/KeyStoreException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class KeyStoreException extends Exception
+{
+}

+ 15 - 0
app/Util/HTTPSignatures/KeyStoreInterface.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+interface KeyStoreInterface
+{
+    /**
+     * return the secret for the specified $keyId.
+     *
+     * @param string $keyId
+     *
+     * @return Key
+     */
+    public function fetch($keyId);
+}

+ 64 - 0
app/Util/HTTPSignatures/RsaAlgorithm.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class RsaAlgorithm implements AlgorithmInterface
+{
+    /** @var string */
+    private $digestName;
+
+    /**
+     * @param string $digestName
+     */
+    public function __construct($digestName)
+    {
+        $this->digestName = $digestName;
+    }
+
+    /**
+     * @return string
+     */
+    public function name()
+    {
+        return sprintf('rsa-%s', $this->digestName);
+    }
+
+    /**
+     * @param string $key
+     * @param string $data
+     *
+     * @return string
+     *
+     * @throws \HttpSignatures\AlgorithmException
+     */
+    public function sign($signingKey, $data)
+    {
+        $algo = $this->getRsaHashAlgo($this->digestName);
+        if (!openssl_get_privatekey($signingKey)) {
+            throw new AlgorithmException("OpenSSL doesn't understand the supplied key (not valid or not found)");
+        }
+        $signature = '';
+        openssl_sign($data, $signature, $signingKey, $algo);
+
+        return $signature;
+    }
+
+    public function verify($message, $signature, $verifyingKey)
+    {
+        $algo = $this->getRsaHashAlgo($this->digestName);
+
+        return openssl_verify($message, base64_decode($signature), $verifyingKey, $algo);
+    }
+
+    private function getRsaHashAlgo($digestName)
+    {
+        switch ($digestName) {
+        case 'sha256':
+            return OPENSSL_ALGO_SHA256;
+        case 'sha1':
+            return OPENSSL_ALGO_SHA1;
+        default:
+            throw new HttpSignatures\AlgorithmException($digestName.' is not a supported hash format');
+      }
+    }
+}

+ 38 - 0
app/Util/HTTPSignatures/Signature.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class Signature
+{
+    /** @var Key */
+    private $key;
+
+    /** @var AlgorithmInterface */
+    private $algorithm;
+
+    /** @var SigningString */
+    private $signingString;
+
+    /**
+     * @param RequestInterface   $message
+     * @param Key                $key
+     * @param AlgorithmInterface $algorithm
+     * @param HeaderList         $headerList
+     */
+    public function __construct($message, Key $key, AlgorithmInterface $algorithm, HeaderList $headerList)
+    {
+        $this->key = $key;
+        $this->algorithm = $algorithm;
+        $this->signingString = new SigningString($headerList, $message);
+    }
+
+    public function string()
+    {
+        return $this->algorithm->sign(
+            $this->key->getSigningKey(),
+            $this->signingString->string()
+          );
+    }
+}

+ 49 - 0
app/Util/HTTPSignatures/SignatureParameters.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class SignatureParameters
+{
+    /**
+     * @param Key                $key
+     * @param AlgorithmInterface $algorithm
+     * @param HeaderList         $headerList
+     * @param Signature          $signature
+     */
+    public function __construct($key, $algorithm, $headerList, $signature)
+    {
+        $this->key = $key;
+        $this->algorithm = $algorithm;
+        $this->headerList = $headerList;
+        $this->signature = $signature;
+    }
+
+    /**
+     * @return string
+     */
+    public function string()
+    {
+        return implode(',', $this->parameterComponents());
+    }
+
+    /**
+     * @return array
+     */
+    private function parameterComponents()
+    {
+        return [
+            sprintf('keyId="%s"', $this->key->getId()),
+            sprintf('algorithm="%s"', $this->algorithm->name()),
+            sprintf('headers="%s"', $this->headerList->string()),
+            sprintf('signature="%s"', $this->signatureBase64()),
+        ];
+    }
+
+    /**
+     * @return string
+     */
+    private function signatureBase64()
+    {
+        return base64_encode($this->signature->string());
+    }
+}

+ 111 - 0
app/Util/HTTPSignatures/SignatureParametersParser.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class SignatureParametersParser
+{
+    /** @var string */
+    private $input;
+
+    /**
+     * @param string $input
+     */
+    public function __construct($input)
+    {
+        $this->input = $input;
+    }
+
+    /**
+     * @return array
+     */
+    public function parse()
+    {
+        $result = $this->pairsToAssociative(
+            $this->arrayOfPairs()
+        );
+        $this->validate($result);
+
+        return $result;
+    }
+
+    /**
+     * @param array $pairs
+     *
+     * @return array
+     */
+    private function pairsToAssociative($pairs)
+    {
+        $result = [];
+        foreach ($pairs as $pair) {
+            $result[$pair[0]] = $pair[1];
+        }
+
+        return $result;
+    }
+
+    /**
+     * @return array
+     */
+    private function arrayOfPairs()
+    {
+        return array_map(
+            [$this, 'pair'],
+            $this->segments()
+        );
+    }
+
+    /**
+     * @return array
+     */
+    private function segments()
+    {
+        return explode(',', $this->input);
+    }
+
+    /**
+     * @param $segment
+     *
+     * @return array
+     *
+     * @throws SignatureParseException
+     */
+    private function pair($segment)
+    {
+        $segmentPattern = '/\A(keyId|algorithm|headers|signature)="(.*)"\z/';
+        $matches = [];
+        $result = preg_match($segmentPattern, $segment, $matches);
+        if (1 !== $result) {
+            throw new SignatureParseException("Signature parameters segment '$segment' invalid");
+        }
+        array_shift($matches);
+
+        return $matches;
+    }
+
+    /**
+     * @param $result
+     *
+     * @throws SignatureParseException
+     */
+    private function validate($result)
+    {
+        $this->validateAllKeysArePresent($result);
+    }
+
+    /**
+     * @param $result
+     *
+     * @throws SignatureParseException
+     */
+    private function validateAllKeysArePresent($result)
+    {
+        // Regexp in pair() ensures no unwanted keys exist.
+        // Ensure that all wanted keys exist.
+        $wanted = ['keyId', 'algorithm', 'headers', 'signature'];
+        $missing = array_diff($wanted, array_keys($result));
+        if (!empty($missing)) {
+            $csv = implode(', ', $missing);
+            throw new SignatureParseException("Missing keys $csv");
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/SignatureParseException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class SignatureParseException extends Exception
+{
+}

+ 7 - 0
app/Util/HTTPSignatures/SignedHeaderNotPresentException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class SignedHeaderNotPresentException extends Exception
+{
+}

+ 104 - 0
app/Util/HTTPSignatures/Signer.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class Signer
+{
+    /** @var Key */
+    private $key;
+
+    /** @var HmacAlgorithm */
+    private $algorithm;
+
+    /** @var HeaderList */
+    private $headerList;
+
+    /**
+     * @param Key           $key
+     * @param HmacAlgorithm $algorithm
+     * @param HeaderList    $headerList
+     */
+    public function __construct($key, $algorithm, $headerList)
+    {
+        $this->key = $key;
+        $this->algorithm = $algorithm;
+        $this->headerList = $headerList;
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return RequestInterface
+     */
+    public function sign($message)
+    {
+        $signatureParameters = $this->signatureParameters($message);
+        $message = $message->withAddedHeader('Signature', $signatureParameters->string());
+        $message = $message->withAddedHeader('Authorization', 'Signature '.$signatureParameters->string());
+
+        return $message;
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return RequestInterface
+     */
+    public function signWithDigest($message)
+    {
+        $message = $this->addDigest($message);
+
+        return $this->sign($message);
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return RequestInterface
+     */
+    private function addDigest($message)
+    {
+        if (!array_search('digest', $this->headerList->names)) {
+            $this->headerList->names[] = 'digest';
+        }
+        $message = $message->withoutHeader('Digest')
+            ->withHeader(
+                'Digest',
+                'SHA-256='.base64_encode(hash('sha256', $message->getBody(), true))
+            );
+
+        return $message;
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return SignatureParameters
+     */
+    private function signatureParameters($message)
+    {
+        return new SignatureParameters(
+            $this->key,
+            $this->algorithm,
+            $this->headerList,
+            $this->signature($message)
+        );
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return Signature
+     */
+    private function signature($message)
+    {
+        return new Signature(
+            $message,
+            $this->key,
+            $this->algorithm,
+            $this->headerList
+        );
+    }
+}

+ 89 - 0
app/Util/HTTPSignatures/SigningString.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class SigningString
+{
+    /** @var HeaderList */
+    private $headerList;
+
+    /** @var RequestInterface */
+    private $message;
+
+    /**
+     * @param HeaderList       $headerList
+     * @param RequestInterface $message
+     */
+    public function __construct(HeaderList $headerList, $message)
+    {
+        $this->headerList = $headerList;
+        $this->message = $message;
+    }
+
+    /**
+     * @return string
+     */
+    public function string()
+    {
+        return implode("\n", $this->lines());
+    }
+
+    /**
+     * @return array
+     */
+    private function lines()
+    {
+        return array_map(
+            [$this, 'line'],
+            $this->headerList->names
+        );
+    }
+
+    /**
+     * @param string $name
+     *
+     * @return string
+     *
+     * @throws SignedHeaderNotPresentException
+     */
+    private function line($name)
+    {
+        if ('(request-target)' == $name) {
+            return $this->requestTargetLine();
+        } else {
+            return sprintf('%s: %s', $name, $this->headerValue($name));
+        }
+    }
+
+    /**
+     * @param string $name
+     *
+     * @return string
+     *
+     * @throws SignedHeaderNotPresentException
+     */
+    private function headerValue($name)
+    {
+        if ($this->message->hasHeader($name)) {
+            $header = $this->message->getHeader($name);
+
+            return end($header);
+        } else {
+            throw new SignedHeaderNotPresentException("Header '$name' not in message");
+        }
+    }
+
+    /**
+     * @return string
+     */
+    private function requestTargetLine()
+    {
+        return sprintf(
+            '(request-target): %s %s',
+            strtolower($this->message->getMethod()),
+            $this->message->getRequestTarget()
+        );
+    }
+}

+ 202 - 0
app/Util/HTTPSignatures/Verification.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class Verification
+{
+    /** @var RequestInterface */
+    private $message;
+
+    /** @var KeyStoreInterface */
+    private $keyStore;
+
+    /** @var array */
+    private $_parameters;
+
+    /**
+     * @param RequestInterface  $message
+     * @param KeyStoreInterface $keyStore
+     */
+    public function __construct($message, KeyStoreInterface $keyStore)
+    {
+        $this->message = $message;
+        $this->keyStore = $keyStore;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isValid()
+    {
+        return $this->hasSignatureHeader() && $this->signatureMatches();
+    }
+
+    /**
+     * @return bool
+     */
+    private function signatureMatches()
+    {
+        try {
+            $key = $this->key();
+            switch ($key->getType()) {
+                case 'secret':
+                  $random = random_bytes(32);
+                  $expectedResult = hash_hmac(
+                      'sha256', $this->expectedSignatureBase64(),
+                      $random,
+                      true
+                  );
+                  $providedResult = hash_hmac(
+                      'sha256', $this->providedSignatureBase64(),
+                      $random,
+                      true
+                  );
+
+                  return $expectedResult === $providedResult;
+                case 'asymmetric':
+                    $signedString = new SigningString(
+                        $this->headerList(),
+                        $this->message
+                    );
+                    $hashAlgo = explode('-', $this->parameter('algorithm'))[1];
+                    $algorithm = new RsaAlgorithm($hashAlgo);
+                    $result = $algorithm->verify(
+                        $signedString->string(),
+                        $this->parameter('signature'),
+                        $key->getVerifyingKey());
+
+                    return $result;
+                default:
+                    throw new Exception("Unknown key type '".$key->getType()."', cannot verify");
+            }
+        } catch (SignatureParseException $e) {
+            return false;
+        } catch (KeyStoreException $e) {
+            return false;
+        } catch (SignedHeaderNotPresentException $e) {
+            return false;
+        }
+    }
+
+    /**
+     * @return string
+     */
+    private function expectedSignatureBase64()
+    {
+        return base64_encode($this->expectedSignature()->string());
+    }
+
+    /**
+     * @return Signature
+     */
+    private function expectedSignature()
+    {
+        return new Signature(
+            $this->message,
+            $this->key(),
+            $this->algorithm(),
+            $this->headerList()
+        );
+    }
+
+    /**
+     * @return string
+     */
+    private function providedSignatureBase64()
+    {
+        return $this->parameter('signature');
+    }
+
+    /**
+     * @return Key
+     */
+    private function key()
+    {
+        return $this->keyStore->fetch($this->parameter('keyId'));
+    }
+
+    /**
+     * @return HmacAlgorithm
+     */
+    private function algorithm()
+    {
+        return Algorithm::create($this->parameter('algorithm'));
+    }
+
+    /**
+     * @return HeaderList
+     */
+    private function headerList()
+    {
+        return HeaderList::fromString($this->parameter('headers'));
+    }
+
+    /**
+     * @param string $name
+     *
+     * @return string
+     *
+     * @throws Exception
+     */
+    private function parameter($name)
+    {
+        $parameters = $this->parameters();
+        if (!isset($parameters[$name])) {
+            throw new Exception("Signature parameters does not contain '$name'");
+        }
+
+        return $parameters[$name];
+    }
+
+    /**
+     * @return array
+     */
+    private function parameters()
+    {
+        if (!isset($this->_parameters)) {
+            $parser = new SignatureParametersParser($this->signatureHeader());
+            $this->_parameters = $parser->parse();
+        }
+
+        return $this->_parameters;
+    }
+
+    /**
+     * @return bool
+     */
+    private function hasSignatureHeader()
+    {
+        return $this->message->hasHeader('Signature') || $this->message->hasHeader('Authorization');
+    }
+
+    /**
+     * @return string
+     *
+     * @throws Exception
+     */
+    private function signatureHeader()
+    {
+        if ($signature = $this->fetchHeader('Signature')) {
+            return $signature;
+        } elseif ($authorization = $this->fetchHeader('Authorization')) {
+            return substr($authorization, strlen('Signature '));
+        } else {
+            throw new Exception('HTTP message has no Signature or Authorization header');
+        }
+    }
+
+    /**
+     * @param $name
+     *
+     * @return string|null
+     */
+    private function fetchHeader($name)
+    {
+        // grab the most recently set header.
+        $header = $this->message->getHeader($name);
+
+        return end($header);
+    }
+}

+ 31 - 0
app/Util/HTTPSignatures/Verifier.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class Verifier
+{
+    /** @var KeyStoreInterface */
+    private $keyStore;
+
+    /**
+     * @param KeyStoreInterface $keyStore
+     */
+    public function __construct(KeyStoreInterface $keyStore)
+    {
+        $this->keyStore = $keyStore;
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return bool
+     */
+    public function isValid($message)
+    {
+        $verification = new Verification($message, $this->keyStore);
+
+        return $verification->isValid();
+    }
+}

+ 0 - 28
app/Util/Lexer/Extractor.php

@@ -98,7 +98,6 @@ class Extractor extends Regex
         $entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
         $entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
         $entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
         $entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
         $entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
         $entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
-        $entities = array_merge($entities, $this->extractCashtagsWithIndices($tweet));
         $entities = $this->removeOverlappingEntities($entities);
         $entities = $this->removeOverlappingEntities($entities);
 
 
         return $entities;
         return $entities;
@@ -303,33 +302,6 @@ class Extractor extends Regex
      */
      */
     public function extractCashtagsWithIndices($tweet = null)
     public function extractCashtagsWithIndices($tweet = null)
     {
     {
-        if (is_null($tweet)) {
-            $tweet = $this->tweet;
-        }
-
-        if (!preg_match('/\$/iu', $tweet)) {
-            return [];
-        }
-
-        preg_match_all(self::$patterns['valid_cashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
-        $tags = [];
-
-        foreach ($matches as $match) {
-            list($all, $before, $dollar, $cash_text, $outer) = array_pad($match, 3, ['', 0]);
-            $start_position = $dollar[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $dollar[1])) : $dollar[1];
-            $end_position = $start_position + StringUtils::strlen($dollar[0].$cash_text[0]);
-
-            if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
-                continue;
-            }
-
-            $tags[] = [
-                'cashtag' => $cash_text[0],
-                'indices' => [$start_position, $end_position],
-            ];
-        }
-
-        return $tags;
     }
     }
 
 
     /**
     /**

+ 6 - 2
app/Util/Media/Image.php

@@ -15,7 +15,7 @@ class Image
     public $orientation;
     public $orientation;
     public $acceptedMimes = [
     public $acceptedMimes = [
         'image/png',
         'image/png',
-        'image/jpeg',
+        'image/jpeg'
     ];
     ];
 
 
     public function __construct()
     public function __construct()
@@ -112,11 +112,15 @@ class Image
         try {
         try {
             $img = Intervention::make($file)->orientate();
             $img = Intervention::make($file)->orientate();
             if($thumbnail) {
             if($thumbnail) {
-                $img->crop($aspect['width'], $aspect['height']);
+                $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
+                    $constraint->aspectRatio();
+                });
             } else {
             } else {
+                $metadata = $img->exif();
                 $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
                 $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
                     $constraint->aspectRatio();
                     $constraint->aspectRatio();
                 });
                 });
+                $media->metadata = json_encode($metadata);
             }
             }
             $converted = $this->setBaseName($path, $thumbnail, $img->extension);
             $converted = $this->setBaseName($path, $thumbnail, $img->extension);
             $newPath = storage_path('app/'.$converted['path']);
             $newPath = storage_path('app/'.$converted['path']);

+ 4 - 4
composer.json

@@ -1,8 +1,8 @@
 {
 {
-    "name": "laravel/laravel",
-    "description": "The Laravel Framework.",
-    "keywords": ["framework", "laravel"],
-    "license": "MIT",
+    "name": "pixelfed/pixelfed",
+    "description": "Open and ethical photo sharing platform, powered by ActivityPub federation.",
+    "keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"],
+    "license": "AGPL-3.0-only",
     "type": "project",
     "type": "project",
     "require": {
     "require": {
         "php": "^7.1.3",
         "php": "^7.1.3",

+ 4 - 2
config/app.php

@@ -13,7 +13,7 @@ return [
     |
     |
     */
     */
 
 
-    'name' => env('APP_NAME', 'Laravel'),
+    'name' => env('APP_NAME', 'Pixelfed'),
 
 
     /*
     /*
     |--------------------------------------------------------------------------
     |--------------------------------------------------------------------------
@@ -52,7 +52,7 @@ return [
     |
     |
     */
     */
 
 
-    'url' => env('APP_URL', 'http://localhost'),
+    'url' => env('APP_URL', 'https://localhost'),
 
 
     /*
     /*
     |--------------------------------------------------------------------------
     |--------------------------------------------------------------------------
@@ -214,6 +214,8 @@ return [
         'Recaptcha'    => Greggilbert\Recaptcha\Facades\Recaptcha::class,
         'Recaptcha'    => Greggilbert\Recaptcha\Facades\Recaptcha::class,
         'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
         'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
         'PrettyNumber' => App\Util\Lexer\PrettyNumber::class,
         'PrettyNumber' => App\Util\Lexer\PrettyNumber::class,
+        'Purify'       => Stevebauman\Purify\Facades\Purify::class,
+        'FFMpeg'       => Pbmedia\LaravelFFMpeg\FFMpegFacade::class,
     ],
     ],
 
 
 ];
 ];

+ 2 - 2
config/debugbar.php

@@ -14,7 +14,7 @@ return [
      |
      |
      */
      */
 
 
-    'enabled' => env('DEBUGBAR_ENABLED', false),
+    'enabled' => false,
     'except'  => [
     'except'  => [
         //
         //
     ],
     ],
@@ -32,7 +32,7 @@ return [
      |
      |
      */
      */
     'storage' => [
     'storage' => [
-        'enabled'    => true,
+        'enabled'    => false,
         'driver'     => 'file', // redis, file, pdo, custom
         'driver'     => 'file', // redis, file, pdo, custom
         'path'       => storage_path('debugbar'), // For file driver
         'path'       => storage_path('debugbar'), // For file driver
         'connection' => null,   // Leave null for default connection (Redis/PDO)
         'connection' => null,   // Leave null for default connection (Redis/PDO)

+ 7 - 6
config/filesystems.php

@@ -56,12 +56,13 @@ return [
         ],
         ],
 
 
         's3' => [
         's3' => [
-            'driver' => 's3',
-            'key'    => env('AWS_ACCESS_KEY_ID'),
-            'secret' => env('AWS_SECRET_ACCESS_KEY'),
-            'region' => env('AWS_DEFAULT_REGION'),
-            'bucket' => env('AWS_BUCKET'),
-            'url'    => env('AWS_URL'),
+            'driver'   => 's3',
+            'key'      => env('AWS_ACCESS_KEY_ID'),
+            'secret'   => env('AWS_SECRET_ACCESS_KEY'),
+            'region'   => env('AWS_DEFAULT_REGION'),
+            'bucket'   => env('AWS_BUCKET'),
+            'url'      => env('AWS_URL'),
+            'endpoint' => env('AWS_ENDPOINT'),
         ],
         ],
 
 
     ],
     ],

+ 1 - 1
config/pixelfed.php

@@ -23,7 +23,7 @@ return [
     | This value is the version of your PixelFed instance.
     | This value is the version of your PixelFed instance.
     |
     |
     */
     */
-    'version' => '0.1.9',
+    'version' => '0.2.1',
 
 
     /*
     /*
     |--------------------------------------------------------------------------
     |--------------------------------------------------------------------------

+ 141 - 0
config/purify.php

@@ -0,0 +1,141 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Settings
+    |--------------------------------------------------------------------------
+    |
+    | The configuration settings array is passed directly to HTMLPurifier.
+    |
+    | Feel free to add / remove / customize these attributes as you wish.
+    |
+    | Documentation: http://htmlpurifier.org/live/configdoc/plain.html
+    |
+    */
+
+    'settings' => [
+
+        /*
+        |--------------------------------------------------------------------------
+        | Core.Encoding
+        |--------------------------------------------------------------------------
+        |
+        | The encoding to convert input to.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#Core.Encoding
+        |
+        */
+
+        'Core.Encoding' => 'utf-8',
+
+        /*
+        |--------------------------------------------------------------------------
+        | Core.SerializerPath
+        |--------------------------------------------------------------------------
+        |
+        | The HTML purifier serializer cache path.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#Cache.SerializerPath
+        |
+        */
+
+        'Cache.SerializerPath' => storage_path('purify'),
+
+        /*
+        |--------------------------------------------------------------------------
+        | HTML.Doctype
+        |--------------------------------------------------------------------------
+        |
+        | Doctype to use during filtering.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#HTML.Doctype
+        |
+        */
+
+        'HTML.Doctype' => 'XHTML 1.0 Strict',
+
+        /*
+        |--------------------------------------------------------------------------
+        | HTML.Allowed
+        |--------------------------------------------------------------------------
+        |
+        | The allowed HTML Elements with their allowed attributes.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#HTML.Allowed
+        |
+        */
+
+        'HTML.Allowed' => 'a[href|title|rel],p',
+
+        /*
+        |--------------------------------------------------------------------------
+        | HTML.ForbiddenElements
+        |--------------------------------------------------------------------------
+        |
+        | The forbidden HTML elements. Elements that are listed in
+        | this string will be removed, however their content will remain.
+        |
+        | For example if 'p' is inside the string, the string: '<p>Test</p>',
+        |
+        | Will be cleaned to: 'Test'
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#HTML.ForbiddenElements
+        |
+        */
+
+        'HTML.ForbiddenElements' => '',
+
+        /*
+        |--------------------------------------------------------------------------
+        | CSS.AllowedProperties
+        |--------------------------------------------------------------------------
+        |
+        | The Allowed CSS properties.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#CSS.AllowedProperties
+        |
+        */
+
+        'CSS.AllowedProperties' => '',
+
+        /*
+        |--------------------------------------------------------------------------
+        | AutoFormat.AutoParagraph
+        |--------------------------------------------------------------------------
+        |
+        | The Allowed CSS properties.
+        |
+        | This directive turns on auto-paragraphing, where double
+        | newlines are converted in to paragraphs whenever possible.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#AutoFormat.AutoParagraph
+        |
+        */
+
+        'AutoFormat.AutoParagraph' => false,
+
+        /*
+        |--------------------------------------------------------------------------
+        | AutoFormat.RemoveEmpty
+        |--------------------------------------------------------------------------
+        |
+        | When enabled, HTML Purifier will attempt to remove empty
+        | elements that contribute no semantic information to the document.
+        |
+        | http://htmlpurifier.org/live/configdoc/plain.html#AutoFormat.RemoveEmpty
+        |
+        */
+
+        'AutoFormat.RemoveEmpty' => false,
+
+        'Attr.AllowedRel' => [
+            'noreferrer',
+            'noopener',
+            'nofollow'
+        ],
+
+    ],
+
+];

+ 1 - 1
config/queue.php

@@ -61,7 +61,7 @@ return [
             'driver'      => 'redis',
             'driver'      => 'redis',
             'connection'  => 'default',
             'connection'  => 'default',
             'queue'       => 'default',
             'queue'       => 'default',
-            'retry_after' => 90,
+            'retry_after' => 1800,
             'block_for'   => null,
             'block_for'   => null,
         ],
         ],
 
 

+ 4 - 4
contrib/docker/Dockerfile.apache

@@ -4,16 +4,16 @@ ARG COMPOSER_VERSION="1.6.5"
 ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
 ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
 
 
 RUN apt-get update \
 RUN apt-get update \
- && apt-get install -y --no-install-recommends git \
+ && apt-get install -y --no-install-recommends git gosu \
       optipng pngquant jpegoptim gifsicle \
       optipng pngquant jpegoptim gifsicle \
-      libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
-      libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
+      libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-3 \
+      libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
  && docker-php-source extract \
  && docker-php-source extract \
  && docker-php-ext-configure gd \
  && docker-php-ext-configure gd \
       --with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
       --with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
       --with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
       --with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
       --with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
       --with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
-      --with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-webp-dir=/usr/lib/x86_64-linux-gnu/ \
  && docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
  && docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
  && pecl install imagick \
  && pecl install imagick \
  && docker-php-ext-enable imagick pcntl imagick gd exif \
  && docker-php-ext-enable imagick pcntl imagick gd exif \

+ 59 - 26
contrib/docker/Dockerfile.fpm

@@ -1,31 +1,64 @@
-FROM php:7.2.6-fpm-alpine
+FROM php:7-fpm
 
 
 ARG COMPOSER_VERSION="1.6.5"
 ARG COMPOSER_VERSION="1.6.5"
 ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
 ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
 
 
-RUN apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool && \
-  apk --no-cache add imagemagick git && \
-  docker-php-ext-install pdo_mysql pcntl && \
-  pecl install imagick && \
-  docker-php-ext-enable imagick pcntl imagick && \
-  curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /tmp/composer.phar && \
-  echo "${COMPOSER_CHECKSUM}  /tmp/composer.phar" | sha256sum -c - && \
-  install -m0755 -o root -g root /tmp/composer.phar /usr/bin/composer.phar && \
-  ln -sf /usr/bin/composer.phar /usr/bin/composer && \
-  rm /tmp/composer.phar && \
-  apk --no-cache del --purge .build
-
-COPY . /var/www/html/
-
-WORKDIR /var/www/html
-RUN install -d -m0755 -o www-data -g www-data \
-    /var/www/html/storage \
-    /var/www/html/storage/framework \
-    /var/www/html/storage/logs \
-    /var/www/html/storage/framework/sessions \
-    /var/www/html/storage/framework/views \
-    /var/www/html/storage/framework/cache && \
-  composer install --prefer-source --no-interaction
-
-VOLUME ["/var/www/html"]
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends git \
+      optipng pngquant jpegoptim gifsicle \
+      libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
+      libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
+ && docker-php-source extract \
+ && docker-php-ext-configure gd \
+      --with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
+      --with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
+ && docker-php-ext-install pdo_mysql pcntl gd exif bcmath \
+ && pecl install imagick \
+ && docker-php-ext-enable imagick pcntl imagick gd exif \
+ && curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
+ && echo "${COMPOSER_CHECKSUM}  /usr/bin/composer" | sha256sum -c - \
+ && chmod 755 /usr/bin/composer \
+ && apt-get autoremove --purge -y \
+       libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
+ && rm -rf /var/cache/apt \
+ && docker-php-source delete
+
 ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
 ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
+
+COPY . /var/www/
+
+WORKDIR /var/www/
+RUN cp -r storage storage.skel \
+ && cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
+ && composer install --prefer-source --no-interaction \
+ && rm -rf html && ln -s public html
+
+VOLUME ["/var/www/storage", "/var/www/public.ext"]
+
+ENV APP_ENV=production \
+    APP_DEBUG=false \
+    LOG_CHANNEL=stderr \
+    DB_CONNECTION=mysql \
+    DB_PORT=3306 \
+    DB_HOST=db \
+    BROADCAST_DRIVER=log \
+    QUEUE_DRIVER=redis \
+    HORIZON_PREFIX=horizon-pixelfed \
+    REDIS_HOST=redis \
+    SESSION_SECURE_COOKIE=true \
+    API_BASE="/api/1/" \
+    API_SEARCH="/api/search" \
+    OPEN_REGISTRATION=true \
+    ENFORCE_EMAIL_VERIFICATION=true \
+    REMOTE_FOLLOW=false \
+    ACTIVITY_PUB=false
+
+CMD cp -r storage.skel/* storage/ \
+ && cp -r public/* public.ext/ \
+ && chown -R www-data:www-data storage/ \
+ && php artisan storage:link \
+ && php artisan migrate --force \
+ && php artisan update \
+ && exec php-fpm

+ 4 - 4
contrib/docker/start.sh

@@ -2,18 +2,18 @@
 
 
 # Create the storage tree if needed and fix permissions
 # Create the storage tree if needed and fix permissions
 cp -r storage.skel/* storage/
 cp -r storage.skel/* storage/
-chown -R www-data:www-data storage/
+chown -R www-data:www-data storage/ bootstrap/cache/
 php artisan storage:link
 php artisan storage:link
 
 
 # Migrate database if the app was upgraded
 # Migrate database if the app was upgraded
-php artisan migrate --force
+gosu www-data:www-data php artisan migrate --force
 
 
 # Run other specific migratins if required
 # Run other specific migratins if required
-php artisan update
+gosu www-data:www-data php artisan update
 
 
 # Run a worker if it is set as embedded
 # Run a worker if it is set as embedded
 if [ "$HORIZON_EMBED" = "true" ]; then
 if [ "$HORIZON_EMBED" = "true" ]; then
-	php artisan horizon &
+  gosu www-data:www-data php artisan horizon &
 fi
 fi
 
 
 # Finally run Apache
 # Finally run Apache

+ 4 - 3
database/factories/UserFactory.php

@@ -15,9 +15,10 @@ use Faker\Generator as Faker;
 
 
 $factory->define(App\User::class, function (Faker $faker) {
 $factory->define(App\User::class, function (Faker $faker) {
     return [
     return [
-        'name'           => $faker->name,
-        'email'          => $faker->unique()->safeEmail,
-        'password'       => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
+        'name' => $faker->name,
+        'username' => str_replace('.', '', $faker->unique()->userName),
+        'email' => str_random(8).$faker->unique()->safeEmail,
+        'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
         'remember_token' => str_random(10),
         'remember_token' => str_random(10),
     ];
     ];
 });
 });

+ 1 - 1
docker-compose.yml

@@ -38,7 +38,7 @@ services:
   #     - "app-storage:/var/www/storage"
   #     - "app-storage:/var/www/storage"
   #   networks:
   #   networks:
   #     - internal
   #     - internal
-  #   command: php artisan horizon
+  #   command: gosu www-data php artisan horizon
     
     
   db:
   db:
     image: mysql:5.7
     image: mysql:5.7

+ 186 - 8
package-lock.json

@@ -2,6 +2,20 @@
     "requires": true,
     "requires": true,
     "lockfileVersion": 1,
     "lockfileVersion": 1,
     "dependencies": {
     "dependencies": {
+        "@videojs/http-streaming": {
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-1.2.5.tgz",
+            "integrity": "sha512-kqjx9oc4NiiUwzqt8EI2PcuebC0WlnxsWydUoMSktLmXc/T6qVS0m8d1eyMA2tjlDILvKkjq2YPS7Jl81phbQQ==",
+            "requires": {
+                "aes-decrypter": "3.0.0",
+                "global": "^4.3.0",
+                "m3u8-parser": "4.2.0",
+                "mpd-parser": "0.6.1",
+                "mux.js": "4.5.1",
+                "url-toolkit": "^2.1.3",
+                "video.js": "^6.8.0 || ^7.0.0"
+            }
+        },
         "abbrev": {
         "abbrev": {
             "version": "1.1.1",
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
             "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -87,6 +101,16 @@
                 }
                 }
             }
             }
         },
         },
+        "aes-decrypter": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.0.0.tgz",
+            "integrity": "sha1-eEihwUW5/b9Xrj4rWxvHzwZEqPs=",
+            "requires": {
+                "commander": "^2.9.0",
+                "global": "^4.3.2",
+                "pkcs7": "^1.0.2"
+            }
+        },
         "after": {
         "after": {
             "version": "0.8.2",
             "version": "0.8.2",
             "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
             "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@@ -1933,8 +1957,7 @@
         "commander": {
         "commander": {
             "version": "2.17.1",
             "version": "2.17.1",
             "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
             "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-            "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
-            "dev": true
+            "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
         },
         },
         "commondir": {
         "commondir": {
             "version": "1.0.1",
             "version": "1.0.1",
@@ -2764,6 +2787,11 @@
                 "buffer-indexof": "^1.0.0"
                 "buffer-indexof": "^1.0.0"
             }
             }
         },
         },
+        "dom-walk": {
+            "version": "0.1.1",
+            "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
+            "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg="
+        },
         "domain-browser": {
         "domain-browser": {
             "version": "1.2.0",
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
             "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -3596,6 +3624,14 @@
                 "debug": "^3.1.0"
                 "debug": "^3.1.0"
             }
             }
         },
         },
+        "for-each": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+            "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+            "requires": {
+                "is-callable": "^1.1.3"
+            }
+        },
         "for-in": {
         "for-in": {
             "version": "1.0.2",
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
             "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -4424,6 +4460,22 @@
                 }
                 }
             }
             }
         },
         },
+        "global": {
+            "version": "4.3.2",
+            "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz",
+            "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=",
+            "requires": {
+                "min-document": "^2.19.0",
+                "process": "~0.5.1"
+            },
+            "dependencies": {
+                "process": {
+                    "version": "0.5.2",
+                    "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz",
+                    "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8="
+                }
+            }
+        },
         "globals": {
         "globals": {
             "version": "9.18.0",
             "version": "9.18.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
             "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
@@ -4991,6 +5043,11 @@
             "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
             "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
             "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
             "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
         },
         },
+        "individual": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
+            "integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
+        },
         "infinite-scroll": {
         "infinite-scroll": {
             "version": "3.0.5",
             "version": "3.0.5",
             "resolved": "https://registry.npmjs.org/infinite-scroll/-/infinite-scroll-3.0.5.tgz",
             "resolved": "https://registry.npmjs.org/infinite-scroll/-/infinite-scroll-3.0.5.tgz",
@@ -5161,8 +5218,7 @@
         "is-callable": {
         "is-callable": {
             "version": "1.1.4",
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
-            "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
-            "dev": true
+            "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
         },
         },
         "is-data-descriptor": {
         "is-data-descriptor": {
             "version": "0.1.4",
             "version": "0.1.4",
@@ -5256,6 +5312,11 @@
             "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
             "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
             "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
             "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
         },
         },
+        "is-function": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz",
+            "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU="
+        },
         "is-glob": {
         "is-glob": {
             "version": "4.0.0",
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
             "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
@@ -5421,8 +5482,7 @@
         "jquery": {
         "jquery": {
             "version": "3.3.1",
             "version": "3.3.1",
             "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
             "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
-            "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==",
-            "dev": true
+            "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
         },
         },
         "js-base64": {
         "js-base64": {
             "version": "2.4.9",
             "version": "2.4.9",
@@ -5836,6 +5896,11 @@
                 "yallist": "^2.1.2"
                 "yallist": "^2.1.2"
             }
             }
         },
         },
+        "m3u8-parser": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.2.0.tgz",
+            "integrity": "sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg=="
+        },
         "make-dir": {
         "make-dir": {
             "version": "1.3.0",
             "version": "1.3.0",
             "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
             "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@@ -6019,6 +6084,14 @@
             "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
             "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
             "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
             "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
         },
         },
+        "min-document": {
+            "version": "2.19.0",
+            "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+            "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
+            "requires": {
+                "dom-walk": "^0.1.0"
+            }
+        },
         "minimalistic-assert": {
         "minimalistic-assert": {
             "version": "1.0.1",
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
             "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -6126,6 +6199,15 @@
                 "run-queue": "^1.0.3"
                 "run-queue": "^1.0.3"
             }
             }
         },
         },
+        "mpd-parser": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.6.1.tgz",
+            "integrity": "sha512-3ucsY5NJMABltTLtYMSDfqZpvKV4yF8YvMx91hZFrHiblseuoKq4XUQ5IkcdtFAIRBAkPhXMU3/eunTFNCNsHw==",
+            "requires": {
+                "global": "^4.3.0",
+                "url-toolkit": "^2.1.1"
+            }
+        },
         "ms": {
         "ms": {
             "version": "2.0.0",
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -6152,6 +6234,11 @@
             "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
             "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
             "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
             "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
         },
         },
+        "mux.js": {
+            "version": "4.5.1",
+            "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-4.5.1.tgz",
+            "integrity": "sha512-j4rEyZKCRinGaSiBxPx9YD9B782TMPHPOlKyaMY07vIGTNYg4ouCEBvL6zX9Hh1k1fKZ5ZF3S7c+XVk6PB+Igw=="
+        },
         "nan": {
         "nan": {
             "version": "2.11.0",
             "version": "2.11.0",
             "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
             "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
@@ -6786,6 +6873,15 @@
                 }
                 }
             }
             }
         },
         },
+        "parse-headers": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz",
+            "integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=",
+            "requires": {
+                "for-each": "^0.3.2",
+                "trim": "0.0.1"
+            }
+        },
         "parse-json": {
         "parse-json": {
             "version": "2.2.0",
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
             "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@@ -6928,6 +7024,11 @@
                 "pinkie": "^2.0.0"
                 "pinkie": "^2.0.0"
             }
             }
         },
         },
+        "pkcs7": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.2.tgz",
+            "integrity": "sha1-ttulJ1KMKUK/wSLOLa/NteWQdOc="
+        },
         "pkg-dir": {
         "pkg-dir": {
             "version": "2.0.0",
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
             "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
@@ -9329,6 +9430,14 @@
                 "readable-stream": "^2.0.2"
                 "readable-stream": "^2.0.2"
             }
             }
         },
         },
+        "readmore-js": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/readmore-js/-/readmore-js-2.2.1.tgz",
+            "integrity": "sha512-hbPP0nQpYYkAywCEZ8ozHivvhWyHic37KJ2IXrHES4qzjp0+nmw8R33MeyMAtXBZfXX4Es8cpd5JBVf9qj47+Q==",
+            "requires": {
+                "jquery": ">2.1.4"
+            }
+        },
         "recast": {
         "recast": {
             "version": "0.11.23",
             "version": "0.11.23",
             "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
             "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
@@ -9712,6 +9821,14 @@
                 "aproba": "^1.1.1"
                 "aproba": "^1.1.1"
             }
             }
         },
         },
+        "rust-result": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
+            "integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
+            "requires": {
+                "individual": "^2.0.0"
+            }
+        },
         "rx": {
         "rx": {
             "version": "4.1.0",
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
             "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
@@ -9722,6 +9839,14 @@
             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
             "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
             "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
         },
         },
+        "safe-json-parse": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
+            "integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
+            "requires": {
+                "rust-result": "^1.0.0"
+            }
+        },
         "safe-regex": {
         "safe-regex": {
             "version": "1.1.0",
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
             "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@@ -10755,6 +10880,11 @@
                 "punycode": "^1.4.1"
                 "punycode": "^1.4.1"
             }
             }
         },
         },
+        "trim": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+            "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
+        },
         "trim-newlines": {
         "trim-newlines": {
             "version": "1.0.0",
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
             "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
@@ -10776,6 +10906,11 @@
                 "glob": "^7.1.2"
                 "glob": "^7.1.2"
             }
             }
         },
         },
+        "tsml": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/tsml/-/tsml-1.0.1.tgz",
+            "integrity": "sha1-ifghi52eJX9H1/a1bQHFpNLGj8M="
+        },
         "tty-browserify": {
         "tty-browserify": {
             "version": "0.0.0",
             "version": "0.0.0",
             "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
             "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@@ -11135,6 +11270,11 @@
                 "requires-port": "^1.0.0"
                 "requires-port": "^1.0.0"
             }
             }
         },
         },
+        "url-toolkit": {
+            "version": "2.1.6",
+            "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.1.6.tgz",
+            "integrity": "sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw=="
+        },
         "use": {
         "use": {
             "version": "3.1.1",
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
             "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@@ -11209,6 +11349,34 @@
                 "extsprintf": "^1.2.0"
                 "extsprintf": "^1.2.0"
             }
             }
         },
         },
+        "video.js": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.2.3.tgz",
+            "integrity": "sha512-oiRGXew1yKk3ILh9+8cvnV0PQp8oqs/2XtkoO46j7BMsFvhgl9L+dy+hS//MUSh1JNgDGUkM/K+E6WTTLlwN7w==",
+            "requires": {
+                "@videojs/http-streaming": "1.2.5",
+                "babel-runtime": "^6.9.2",
+                "global": "4.3.2",
+                "safe-json-parse": "4.0.0",
+                "tsml": "1.0.1",
+                "videojs-font": "3.0.0",
+                "videojs-vtt.js": "0.14.1",
+                "xhr": "2.4.0"
+            }
+        },
+        "videojs-font": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.0.0.tgz",
+            "integrity": "sha512-XS6agz2T7p2cFuuXulJD70md8XMlAN617SJkMWjoTPqZWv+RU8NcZCKsE3Tk73inzxnQdihOp0cvI7NGz2ngHg=="
+        },
+        "videojs-vtt.js": {
+            "version": "0.14.1",
+            "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz",
+            "integrity": "sha512-YxOiywx6N9t3J5nqsE5WN2Sw4CSqVe3zV+AZm2T4syOc2buNJaD6ZoexSdeszx2sHLU/RRo2r4BJAXFDQ7Qo2Q==",
+            "requires": {
+                "global": "^4.3.1"
+            }
+        },
         "vm-browserify": {
         "vm-browserify": {
             "version": "0.0.4",
             "version": "0.0.4",
             "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
             "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
@@ -11694,6 +11862,17 @@
                 "ultron": "~1.1.0"
                 "ultron": "~1.1.0"
             }
             }
         },
         },
+        "xhr": {
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz",
+            "integrity": "sha1-4W5mpF+GmGHu76tBbV7/ci3ECZM=",
+            "requires": {
+                "global": "~4.3.0",
+                "is-function": "^1.0.1",
+                "parse-headers": "^2.0.0",
+                "xtend": "^4.0.0"
+            }
+        },
         "xmlhttprequest": {
         "xmlhttprequest": {
             "version": "1.8.0",
             "version": "1.8.0",
             "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
             "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
@@ -11707,8 +11886,7 @@
         "xtend": {
         "xtend": {
             "version": "4.0.1",
             "version": "4.0.1",
             "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
             "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
-            "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
-            "dev": true
+            "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
         },
         },
         "y18n": {
         "y18n": {
             "version": "3.2.1",
             "version": "3.2.1",

+ 3 - 1
package.json

@@ -25,8 +25,10 @@
         "infinite-scroll": "^3.0.4",
         "infinite-scroll": "^3.0.4",
         "laravel-echo": "^1.4.0",
         "laravel-echo": "^1.4.0",
         "pusher-js": "^4.2.2",
         "pusher-js": "^4.2.2",
+        "readmore-js": "^2.2.1",
         "socket.io-client": "^2.1.1",
         "socket.io-client": "^2.1.1",
         "sweetalert": "^2.1.0",
         "sweetalert": "^2.1.0",
-        "twitter-text": "^2.0.5"
+        "twitter-text": "^2.0.5",
+        "video.js": "^7.2.3"
     }
     }
 }
 }

BIN
public/css/app.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
public/img/help/what_is_the_fediverse.svg


BIN
public/img/pixelfed-icon-black.svg


BIN
public/img/pixelfed-icon-grey.svg


BIN
public/js/app.js


BIN
public/js/components.js


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff