Răsfoiți Sursa

Merge pull request #3532 from pixelfed/staging

Staging
daniel 3 ani în urmă
părinte
comite
255c41fb83
47 a modificat fișierele cu 1061 adăugiri și 394 ștergeri
  1. 1 0
      CHANGELOG.md
  2. 2 4
      app/Console/Commands/GenerateInstanceActor.php
  3. 201 86
      app/Console/Commands/Installer.php
  4. 15 5
      app/FollowRequest.php
  5. 31 10
      app/Http/Controllers/AccountController.php
  6. 120 26
      app/Http/Controllers/Api/ApiV1Controller.php
  7. 16 0
      app/Http/Controllers/LiveStreamController.php
  8. 91 57
      app/Http/Controllers/PublicApiController.php
  9. 5 1
      app/Http/Middleware/TrustProxies.php
  10. 69 0
      app/Jobs/FollowPipeline/FollowAcceptPipeline.php
  11. 0 5
      app/Jobs/FollowPipeline/FollowPipeline.php
  12. 69 0
      app/Jobs/FollowPipeline/FollowRejectPipeline.php
  13. 15 0
      app/Models/LiveStream.php
  14. 22 1
      app/Profile.php
  15. 1 1
      app/Services/FollowerService.php
  16. 95 0
      app/Services/NetworkTimelineService.php
  17. 2 0
      app/Services/RelationshipService.php
  18. 21 18
      app/Services/UserFilterService.php
  19. 25 0
      app/Transformer/ActivityPub/Verb/AcceptFollow.php
  20. 25 0
      app/Transformer/ActivityPub/Verb/RejectFollow.php
  21. 11 0
      app/Util/ActivityPub/Helpers.php
  22. 4 9
      app/Util/ActivityPub/Inbox.php
  23. 165 163
      composer.lock
  24. 6 0
      config/instance.php
  25. 12 6
      config/livestreaming.php
  26. 1 1
      config/pixelfed.php
  27. 34 0
      database/migrations/2022_06_03_051308_add_object_column_to_follow_requests_table.php
  28. BIN
      public/css/app.css
  29. BIN
      public/css/appdark.css
  30. BIN
      public/js/compose-ojtjadoml.js
  31. BIN
      public/js/daci-ojtjadoml.js
  32. BIN
      public/js/dffc-ojtjadoml.js
  33. BIN
      public/js/dmsg-ojtjadoml.js
  34. BIN
      public/js/dmyh-ojtjadoml.js
  35. BIN
      public/js/dmym-ojtjadoml.js
  36. BIN
      public/js/dsfc-ojtjadoml.js
  37. BIN
      public/js/dssc-ojtjadoml.js
  38. BIN
      public/js/home-ojtjadoml.js
  39. BIN
      public/js/manifest.js
  40. BIN
      public/js/notifications-ojtjadoml.js
  41. BIN
      public/js/post-ojtjadoml.js
  42. BIN
      public/js/profile-ojtjadoml.js
  43. BIN
      public/js/spa.js
  44. BIN
      public/js/vendor.js
  45. 1 1
      public/js/vendor.js.LICENSE.txt
  46. BIN
      public/mix-manifest.json
  47. 1 0
      routes/api.php

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@
 - Add ffmpeg config, disable logging by default ([108e3803](https://github.com/pixelfed/pixelfed/commit/108e3803))
 - Refactor AP profileFetch logic to fix race conditions and improve updating fields and avatars ([505261da](https://github.com/pixelfed/pixelfed/commit/505261da))
 - Update network timeline api, limit falloff to 2 days ([13a66303](https://github.com/pixelfed/pixelfed/commit/13a66303))
+- Update Inbox, store follow request activity ([c82f2085](https://github.com/pixelfed/pixelfed/commit/c82f2085))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.3 (2022-05-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.2...v0.11.3)

+ 2 - 4
app/Console/Commands/GenerateInstanceActor.php

@@ -29,9 +29,6 @@ class GenerateInstanceActor extends Command
 		}
 
 		if(InstanceActor::exists()) {
-			$this->line(' ');
-			$this->error('Instance actor already exists!');
-			$this->line(' ');
 			$actor = InstanceActor::whereNotNull('public_key')
 				->whereNotNull('private_key')
 				->firstOrFail();
@@ -42,7 +39,8 @@ class GenerateInstanceActor extends Command
 			Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() use($actor) {
 				return $actor->private_key;
 			});
-			exit;
+			$this->info('Instance actor succesfully generated. You do not need to run this command again.');
+			return;
 		}
 
 		$pkiConfig = [

+ 201 - 86
app/Console/Commands/Installer.php

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Redis;
+use \PDO;
 
 class Installer extends Command
 {
@@ -12,7 +13,7 @@ class Installer extends Command
      *
      * @var string
      */
-    protected $signature = 'install';
+    protected $signature = 'install {--dangerously-overwrite-env : Re-run installation and overwrite current .env }';
 
     /**
      * The console command description.
@@ -21,6 +22,8 @@ class Installer extends Command
      */
     protected $description = 'CLI Installer';
 
+    public $installType = 'Simple';
+
     /**
      * Create a new command instance.
      *
@@ -54,23 +57,48 @@ class Installer extends Command
         $this->info(' ');
         $this->info('Pixelfed version: ' . config('pixelfed.version'));
         $this->line(' ');
-        $this->info('Scanning system...');                               
+        $this->envCheck();
+    }
+
+    protected function envCheck()
+    {
+        if( file_exists(base_path('.env')) &&
+        	filesize(base_path('.env')) !== 0 &&
+        	!$this->option('dangerously-overwrite-env')
+        ) {
+            $this->line('');
+            $this->error('Installation aborted, found existing .env file');
+            $this->line('Run the following command to re-run the installer:');
+            $this->line('');
+            $this->info('php artisan install --dangerously-overwrite-env');
+            $this->line('');
+            exit;
+        }
+        $this->installType();
+    }
+
+    protected function installType()
+    {
+    	$type = $this->choice('Select installation type', ['Simple', 'Advanced'], 0);
+		$this->installType = $type;
         $this->preflightCheck();
     }
+
     protected function preflightCheck()
     {
-        $this->line(' ');
-        $this->info('Checking for installed dependencies...');
-        $redis = Redis::connection();
-        if($redis->ping()) {
-            $this->info('- Found redis!');
-        } else {
-            $this->error('- Redis not found, aborting installation');
-            exit;
+        if($this->installType === 'Advanced') {
+			$this->info('Scanning system...');
+			$this->line(' ');
+			$this->info('Checking for installed dependencies...');
+	        $redis = Redis::connection();
+	        if($redis->ping()) {
+	            $this->info('- Found redis!');
+	        } else {
+	            $this->error('- Redis not found, aborting installation');
+	            exit;
+	        }
         }
         $this->checkPhpDependencies();
-        $this->checkPermissions();
-        $this->envCheck();
     }
 
     protected function checkPhpDependencies()
@@ -81,31 +109,39 @@ class Installer extends Command
             'curl',
             'json',
             'mbstring',
-            'openssl'
+            'openssl',
         ];
-        $ffmpeg = exec('which ffmpeg');
-        if(empty($ffmpeg)) {
-            $this->error('FFmpeg not found, please install it.');
-            $this->error('Cancelling installation.');
-            exit;
-        } else {
-            $this->info('- Found FFmpeg!');
-        }
-        $this->line('');
-        $this->info('Checking for required php extensions...');
+        if($this->installType === 'Advanced') {
+	        $ffmpeg = exec('which ffmpeg');
+	        if(empty($ffmpeg)) {
+	            $this->error('FFmpeg not found, please install it.');
+	            $this->error('Cancelling installation.');
+	            exit;
+	        } else {
+	            $this->info('- Found FFmpeg!');
+	        }
+	        $this->line('');
+        	$this->info('Checking for required php extensions...');
+	    }
         foreach($extensions as $ext) {
             if(extension_loaded($ext) == false) {
-                $this->error("- {$ext} extension not found, aborting installation");
+                $this->error("\"{$ext}\" PHP extension not found, aborting installation");
                 exit;
             }
         }
-        $this->info("- Required PHP extensions found!");
+        if($this->installType === 'Advanced') {
+	        $this->info("- Required PHP extensions found!");
+	    }
+
+	    $this->checkPermissions();
     }
 
     protected function checkPermissions()
     {
-        $this->line('');
-        $this->info('Checking for proper filesystem permissions...');
+    	if($this->installType === 'Advanced') {
+	        $this->line('');
+	        $this->info('Checking for proper filesystem permissions...');
+	    }
 
         $paths = [
             base_path('bootstrap'),
@@ -119,100 +155,152 @@ class Installer extends Command
                 $this->error("  $path");
                 exit;
             } else {
-                $this->info("- Found valid permissions for {$path}");
+            	if($this->installType === 'Advanced') {
+	                $this->info("- Found valid permissions for {$path}");
+	            }
             }
         }
-    }
 
-    protected function envCheck()
-    {
-        if(!file_exists(base_path('.env')) || filesize(base_path('.env')) == 0) {
-            $this->line('');
-            $this->info('No .env configuration file found. We will create one now!');
-            $this->createEnv();
-        } else {
-            $confirm = $this->confirm('Found .env file, do you want to overwrite it?');
-            if(!$confirm) {
-                $this->info('Cancelling installation.');
-                exit;
-            }
-            $confirm = $this->confirm('Are you really sure you want to overwrite it?');
-            if(!$confirm) {
-                $this->info('Cancelling installation.');
-                exit;
-            }
-            $this->error('Warning ... if you did not backup your .env before its overwritten it will be permanently deleted.');
-            $confirm = $this->confirm('The application may be installed already, are you really sure you want to overwrite it?');
-            if(!$confirm) {
-                $this->info('Cancelling installation.');
-                exit;
-            }
-        }
-        $this->postInstall();
+        $this->createEnv();
     }
 
     protected function createEnv()
     {
         $this->line('');
-        // copy env
         if(!file_exists(app()->environmentFilePath())) {
             exec('cp .env.example .env');
-            $this->call('key:generate');            
+            $this->updateEnvFile('APP_ENV', 'setup');
+            $this->call('key:generate');
         }
 
         $name = $this->ask('Site name [ex: Pixelfed]');
         $this->updateEnvFile('APP_NAME', $name ?? 'pixelfed');
 
         $domain = $this->ask('Site Domain [ex: pixelfed.com]');
+        if(empty($domain)) {
+        	$this->error('You must set the site domain');
+        	exit;
+        }
+        if(starts_with($domain, 'http')) {
+        	$this->error('The site domain cannot start with https://, you must use the FQDN (eg: example.org)');
+        	exit;
+        }
+        if(strpos($domain, '.') == false) {
+        	$this->error('You must enter a valid site domain');
+        	exit;
+        }
         $this->updateEnvFile('APP_DOMAIN', $domain ?? 'example.org');
         $this->updateEnvFile('ADMIN_DOMAIN', $domain ?? 'example.org');
         $this->updateEnvFile('SESSION_DOMAIN', $domain ?? 'example.org');
-        $this->updateEnvFile('APP_URL', 'https://' . $domain ?? 'https://example.org');
+        $this->updateEnvFile('APP_URL', 'https://' . $domain);
 
         $database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
         $this->updateEnvFile('DB_CONNECTION', $database ?? 'mysql');
-        switch ($database) {
-            case 'mysql':
-                $database_host = $this->ask('Select database host', '127.0.0.1');
-                $this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
 
-                $database_port = $this->ask('Select database port', 3306);
-                $this->updateEnvFile('DB_PORT', $database_port ?? 3306);
+        $database_host = $this->ask('Select database host', '127.0.0.1');
+        $this->updateEnvFile('DB_HOST', $database_host ?? 'mysql');
+
+        $database_port_default = $database === 'mysql' ? 3306 : 5432;
+        $database_port = $this->ask('Select database port', $database_port_default);
+        $this->updateEnvFile('DB_PORT', $database_port ?? $database_port_default);
 
-                $database_db = $this->ask('Select database', 'pixelfed');
-                $this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
+        $database_db = $this->ask('Select database', 'pixelfed');
+        $this->updateEnvFile('DB_DATABASE', $database_db ?? 'pixelfed');
 
-                $database_username = $this->ask('Select database username', 'pixelfed');
-                $this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
+        $database_username = $this->ask('Select database username', 'pixelfed');
+        $this->updateEnvFile('DB_USERNAME', $database_username ?? 'pixelfed');
 
-                $db_pass = str_random(64);
-                $database_password = $this->secret('Select database password', $db_pass);
-                $this->updateEnvFile('DB_PASSWORD', $database_password);
-            break;
-            
+        $db_pass = str_random(64);
+        $database_password = $this->secret('Select database password', $db_pass);
+        $this->updateEnvFile('DB_PASSWORD', $database_password);
+
+        $dsn = "{$database}:dbname={$database_db};host={$database_host};port={$database_port};";
+        try {
+        	$dbh = new PDO($dsn, $database_username, $database_password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
+        } catch (\PDOException $e) {
+        	$this->error('Cannot connect to database, check your credentials and try again');
+        	exit;
         }
 
-        $cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
-        $this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
+        if($this->installType === 'Advanced') {
+	        $cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
+	        $this->updateEnvFile('CACHE_DRIVER', $cache ?? 'redis');
 
-        $session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
-        $this->updateEnvFile('SESSION_DRIVER', $session ?? 'redis');
+	        $session = $this->choice('Select session driver', ["redis", "file", "cookie", "database", "apc", "memcached", "array"], 0);
+	        $this->updateEnvFile('SESSION_DRIVER', $session ?? 'redis');
 
-        $redis_host = $this->ask('Set redis host', 'localhost');
-        $this->updateEnvFile('REDIS_HOST', $redis_host);
+	        $redis_host = $this->ask('Set redis host', 'localhost');
+	        $this->updateEnvFile('REDIS_HOST', $redis_host);
 
-        $redis_password = $this->ask('Set redis password', 'null');
-        $this->updateEnvFile('REDIS_PASSWORD', $redis_password);
+	        $redis_password = $this->ask('Set redis password', 'null');
+	        $this->updateEnvFile('REDIS_PASSWORD', $redis_password);
 
-        $redis_port = $this->ask('Set redis port', 6379);
-        $this->updateEnvFile('REDIS_PORT', $redis_port);
+	        $redis_port = $this->ask('Set redis port', 6379);
+	        $this->updateEnvFile('REDIS_PORT', $redis_port);
+	    }
 
-        $open_registration = $this->choice('Allow new registrations?', ['true', 'false'], 1);
+        $open_registration = $this->choice('Allow new registrations?', ['false', 'true'], 0);
         $this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
 
-        $enforce_email_verification = $this->choice('Enforce email verification?', ['true', 'false'], 0);
+        $activitypub_federation = $this->choice('Enable ActivityPub federation?', ['false', 'true'], 1);
+        $this->updateEnvFile('ACTIVITY_PUB', $activitypub_federation);
+        $this->updateEnvFile('AP_INBOX', $activitypub_federation);
+        $this->updateEnvFile('AP_SHAREDINBOX', $activitypub_federation);
+        $this->updateEnvFile('AP_REMOTE_FOLLOW', $activitypub_federation);
+
+        $enforce_email_verification = $this->choice('Enforce email verification?', ['false', 'true'], 1);
         $this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
 
+        $enable_mobile_apis = $this->choice('Enable mobile app/apis support?', ['false', 'true'], 1);
+        $this->updateEnvFile('OAUTH_ENABLED', $enable_mobile_apis);
+        $this->updateEnvFile('EXP_EMC', $enable_mobile_apis);
+
+    	$optimize_media = $this->choice('Optimize media uploads? Requires jpegoptim and other dependencies!', ['false', 'true'], 0);
+    	$this->updateEnvFile('PF_OPTIMIZE_IMAGES', $optimize_media);
+
+        if($this->installType === 'Advanced') {
+
+        	if($optimize_media === 'true') {
+	        	$image_quality = $this->ask('Set image optimization quality between 1-100. Default is 80%, lower values use less disk space at the expense of image quality.', '80');
+	        	if($image_quality < 1) {
+	        		$this->error('Min image quality is 1. You should avoid such a low value, 60 at minimum is recommended.');
+	        		exit;
+	        	}
+	        	if($image_quality > 100) {
+	        		$this->error('Max image quality is 100');
+	        		exit;
+	        	}
+	    		$this->updateEnvFile('IMAGE_QUALITY', $image_quality);
+        	}
+
+        	$max_photo_size = $this->ask('Max photo upload size in kilobytes. Default 15000 which is equal to 15MB', '15000');
+        	if($max_photo_size * 1024 > $this->parseSize(ini_get('post_max_size'))) {
+        		$this->error('Max photo size (' . (round($max_photo_size / 1000)) . 'M) cannot exceed php.ini `post_max_size` of ' . ini_get('post_max_size'));
+        		exit;
+        	}
+        	$this->updateEnvFile('MAX_PHOTO_SIZE', $max_photo_size);
+
+        	$max_caption_length = $this->ask('Max caption limit. Default to 500, max 5000.', '500');
+        	if($max_caption_length > 5000) {
+        		$this->error('Max caption length is 5000 characters.');
+        		exit;
+        	}
+        	$this->updateEnvFile('MAX_CAPTION_LENGTH', $max_caption_length);
+
+        	$max_album_length = $this->ask('Max photos allowed per album. Choose a value between 1 and 10.', '4');
+        	if($max_album_length < 1) {
+        		$this->error('Min album length is 1 photos per album.');
+        		exit;
+        	}
+        	if($max_album_length > 10) {
+        		$this->error('Max album length is 10 photos per album.');
+        		exit;
+        	}
+        	$this->updateEnvFile('MAX_ALBUM_LENGTH', $max_album_length);
+        }
+
+        $this->updateEnvFile('APP_ENV', 'production');
+        $this->postInstall();
     }
 
     protected function updateEnvFile($key, $value)
@@ -247,8 +335,35 @@ class Installer extends Command
 
     protected function postInstall()
     {
-        $this->callSilent('config:cache');
-        //$this->callSilent('route:cache');
+        $this->line('');
+        $this->info('We recommend running database migrations now, or you can do it manually later.');
+        $confirm = $this->choice('Do you want to run the database migrations?', ['No', 'Yes'], 0);
+        if($confirm === 'Yes') {
+        	$this->callSilently('config:clear');
+        	sleep(3);
+        	$this->call('migrate', ['--force' => true]);
+	        $this->callSilently('instance:actor');
+	        $this->callSilently('passport:install');
+
+	        $confirm = $this->choice('Do you want to create an admin account?', ['No', 'Yes'], 0);
+	        if($confirm === 'Yes') {
+	        	$this->call('user:create');
+	        }
+        } else {
+        	$this->callSilently('config:cache');
+        }
+
         $this->info('Pixelfed has been successfully installed!');
     }
+
+    protected function parseSize($size) {
+    	$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
+    	$size = preg_replace('/[^0-9\.]/', '', $size);
+    	if ($unit) {
+    		return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
+    	}
+    	else {
+    		return round($size);
+    	}
+    }
 }

+ 15 - 5
app/FollowRequest.php

@@ -6,7 +6,16 @@ use Illuminate\Database\Eloquent\Model;
 
 class FollowRequest extends Model
 {
-	protected $fillable = ['follower_id', 'following_id'];
+	protected $fillable = ['follower_id', 'following_id', 'activity', 'handled_at'];
+
+	protected $casts = [
+		'activity' => 'array',
+	];
+
+    public function actor()
+    {
+        return $this->belongsTo(Profile::class, 'follower_id', 'id');
+    }
 	
     public function follower()
     {
@@ -18,13 +27,14 @@ class FollowRequest extends Model
         return $this->belongsTo(Profile::class, 'following_id', 'id');
     }
 
-    public function actor()
+    public function target()
     {
-        return $this->belongsTo(Profile::class, 'follower_id', 'id');
+        return $this->belongsTo(Profile::class, 'following_id', 'id');
     }
 
-    public function target()
+    public function permalink($append = null, $namespace = '#accepts')
     {
-        return $this->belongsTo(Profile::class, 'following_id', 'id');
+        $path = $this->target->permalink("{$namespace}/follows/{$this->id}{$append}");
+        return url($path);
     }
 }

+ 31 - 10
app/Http/Controllers/AccountController.php

@@ -29,6 +29,8 @@ use App\Transformer\Api\Mastodon\v1\AccountTransformer;
 use App\Services\AccountService;
 use App\Services\UserFilterService;
 use App\Services\RelationshipService;
+use App\Jobs\FollowPipeline\FollowAcceptPipeline;
+use App\Jobs\FollowPipeline\FollowRejectPipeline;
 
 class AccountController extends Controller
 {
@@ -363,12 +365,13 @@ class AccountController extends Controller
 			'accounts' => $followers->take(10)->map(function($a) {
 				$actor = $a->actor;
 				return [
-					'id' => $actor->id,
+					'rid' => (string) $a->id,
+					'id' => (string) $actor->id,
 					'username' => $actor->username,
 					'avatar' => $actor->avatarUrl(),
 					'url' => $actor->url(),
 					'local' => $actor->domain == null,
-					'following' => $actor->followedBy(Auth::user()->profile)
+					'account' => AccountService::get($actor->id)
 				];
 			})
 		];
@@ -390,17 +393,35 @@ class AccountController extends Controller
 
 		switch ($action) {
 			case 'accept':
-			$follow = new Follower();
-			$follow->profile_id = $follower->id;
-			$follow->following_id = $pid;
-			$follow->save();
-			FollowPipeline::dispatch($follow);
-			$followRequest->delete();
+				$follow = new Follower();
+				$follow->profile_id = $follower->id;
+				$follow->following_id = $pid;
+				$follow->save();
+
+				$profile = Profile::findOrFail($pid);
+				$profile->followers_count++;
+				$profile->save();
+				AccountService::del($profile->id);
+
+				$profile = Profile::findOrFail($follower->id);
+				$profile->following_count++;
+				$profile->save();
+				AccountService::del($profile->id);
+
+				if($follower->domain != null && $follower->private_key === null) {
+					FollowAcceptPipeline::dispatch($followRequest);
+				} else {
+					FollowPipeline::dispatch($follow);
+					$followRequest->delete();
+				}
 			break;
 
 			case 'reject':
-			$followRequest->is_rejected = true;
-			$followRequest->save();
+				if($follower->domain != null && $follower->private_key === null) {
+					FollowRejectPipeline::dispatch($followRequest);
+				} else {
+					$followRequest->delete();
+				}
 			break;
 		}
 

+ 120 - 26
app/Http/Controllers/Api/ApiV1Controller.php

@@ -62,6 +62,7 @@ use App\Services\{
 	FollowerService,
 	InstanceService,
 	LikeService,
+	NetworkTimelineService,
 	NotificationService,
 	MediaPathService,
 	PublicTimelineService,
@@ -82,6 +83,8 @@ use App\Services\DiscoverService;
 use App\Services\CustomEmojiService;
 use App\Services\MarkerService;
 use App\Models\Conversation;
+use App\Jobs\FollowPipeline\FollowAcceptPipeline;
+use App\Jobs\FollowPipeline\FollowRejectPipeline;
 
 class ApiV1Controller extends Controller
 {
@@ -441,7 +444,7 @@ class ApiV1Controller extends Controller
 
 		if($pid != $account['id']) {
 			if($account['locked']) {
-				if(FollowerService::follows($pid, $account['id'])) {
+				if(!FollowerService::follows($pid, $account['id'])) {
 					return [];
 				}
 			}
@@ -488,7 +491,7 @@ class ApiV1Controller extends Controller
 
 		if($pid != $account['id']) {
 			if($account['locked']) {
-				if(FollowerService::follows($pid, $account['id'])) {
+				if(!FollowerService::follows($pid, $account['id'])) {
 					return [];
 				}
 			}
@@ -722,6 +725,13 @@ class ApiV1Controller extends Controller
 			->exists();
 
 		if($isFollowing == false) {
+			$followRequest = FollowRequest::whereFollowerId($user->profile_id)
+				->whereFollowingId($target->id)
+				->first();
+			if($followRequest) {
+				$followRequest->delete();
+				RelationshipService::refresh($target->id, $user->profile_id);
+			}
 			$resource = new Fractal\Resource\Item($target, new RelationshipTransformer());
 			$res = $this->fractal->createData($resource)->toArray();
 
@@ -1149,15 +1159,22 @@ class ApiV1Controller extends Controller
 	public function accountFollowRequests(Request $request)
 	{
 		abort_if(!$request->user(), 403);
-
+		$this->validate($request, [
+			'limit' => 'sometimes|integer|min:1|max:100'
+		]);
 		$user = $request->user();
 
-		$followRequests = FollowRequest::whereFollowingId($user->profile->id)->pluck('follower_id');
-
-		$profiles = Profile::find($followRequests);
+		$res = FollowRequest::whereFollowingId($user->profile->id)
+			->limit($request->input('limit', 40))
+			->pluck('follower_id')
+			->map(function($id) {
+				return AccountService::getMastodon($id, true);
+			})
+			->filter(function($acct) {
+				return $acct && isset($acct['id']);
+			})
+			->values();
 
-		$resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
-		$res = $this->fractal->createData($resource)->toArray();
 		return $this->json($res);
 	}
 
@@ -1171,10 +1188,46 @@ class ApiV1Controller extends Controller
 	public function accountFollowRequestAccept(Request $request, $id)
 	{
 		abort_if(!$request->user(), 403);
+		$pid = $request->user()->profile_id;
+		$target = AccountService::getMastodon($id);
 
-		// todo
+		if(!$target) {
+			return response()->json(['error' => 'Record not found'], 404);
+		}
 
-		return response()->json([]);
+		$followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
+
+		if(!$followRequest) {
+			return response()->json(['error' => 'Record not found'], 404);
+		}
+
+		$follower = $followRequest->follower;
+		$follow = new Follower();
+		$follow->profile_id = $follower->id;
+		$follow->following_id = $pid;
+		$follow->save();
+
+		$profile = Profile::findOrFail($pid);
+		$profile->followers_count++;
+		$profile->save();
+		AccountService::del($profile->id);
+
+		$profile = Profile::findOrFail($follower->id);
+		$profile->following_count++;
+		$profile->save();
+		AccountService::del($profile->id);
+
+		if($follower->domain != null && $follower->private_key === null) {
+			FollowAcceptPipeline::dispatch($followRequest);
+		} else {
+			FollowPipeline::dispatch($follow);
+			$followRequest->delete();
+		}
+
+		RelationshipService::refresh($pid, $id);
+		$res = RelationshipService::get($pid, $id);
+		$res['followed_by'] = true;
+		return $this->json($res);
 	}
 
 	/**
@@ -1187,10 +1240,30 @@ class ApiV1Controller extends Controller
 	public function accountFollowRequestReject(Request $request, $id)
 	{
 		abort_if(!$request->user(), 403);
+		$pid = $request->user()->profile_id;
+		$target = AccountService::getMastodon($id);
 
-		// todo
+		if(!$target) {
+			return response()->json(['error' => 'Record not found'], 404);
+		}
 
-		return response()->json([]);
+		$followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
+
+		if(!$followRequest) {
+			return response()->json(['error' => 'Record not found'], 404);
+		}
+
+		$follower = $followRequest->follower;
+
+		if($follower->domain != null && $follower->private_key === null) {
+			FollowRejectPipeline::dispatch($followRequest);
+		} else {
+			$followRequest->delete();
+		}
+
+		RelationshipService::refresh($pid, $id);
+		$res = RelationshipService::get($pid, $id);
+		return $this->json($res);
 	}
 
 	/**
@@ -1811,7 +1884,7 @@ class ApiV1Controller extends Controller
 			->take(($limit * 2))
 			->get()
 			->map(function($s) use($pid) {
-				$status = StatusService::getMastodon($s['id']);
+				$status = StatusService::getMastodon($s['id'], false);
 				if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
 					return false;
 				}
@@ -1842,7 +1915,7 @@ class ApiV1Controller extends Controller
 			->take(($limit * 2))
 			->get()
 			->map(function($s) use($pid) {
-				$status = StatusService::getMastodon($s['id']);
+				$status = StatusService::getMastodon($s['id'], false);
 				if(!$status || !isset($status['account']) || !isset($status['account']['id'])) {
 					return false;
 				}
@@ -1899,28 +1972,46 @@ class ApiV1Controller extends Controller
 		$this->validate($request,[
 		  'min_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
 		  'max_id'      => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
-		  'limit'       => 'nullable|integer|max:100'
+		  'limit'       => 'nullable|integer|max:100',
+		  'remote'		=> 'sometimes'
 		]);
 
 		$min = $request->input('min_id');
 		$max = $request->input('max_id');
 		$limit = $request->input('limit') ?? 20;
 		$user = $request->user();
+		$remote = $request->has('remote');
         $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
 
-		Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
-			if(PublicTimelineService::count() == 0) {
-				PublicTimelineService::warmCache(true, 400);
+        if($remote && config('instance.timeline.network.cached')) {
+			Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() {
+				if(NetworkTimelineService::count() == 0) {
+					NetworkTimelineService::warmCache(true, config('instance.timeline.network.cache_dropoff'));
+				}
+			});
+
+			if ($max) {
+				$feed = NetworkTimelineService::getRankedMaxId($max, $limit + 5);
+			} else if ($min) {
+				$feed = NetworkTimelineService::getRankedMinId($min, $limit + 5);
+			} else {
+				$feed = NetworkTimelineService::get(0, $limit + 5);
 			}
-		});
+        } else {
+			Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() {
+				if(PublicTimelineService::count() == 0) {
+					PublicTimelineService::warmCache(true, 400);
+				}
+			});
 
-		if ($max) {
-			$feed = PublicTimelineService::getRankedMaxId($max, $limit + 5);
-		} else if ($min) {
-			$feed = PublicTimelineService::getRankedMinId($min, $limit + 5);
-		} else {
-			$feed = PublicTimelineService::get(0, $limit + 5);
-		}
+			if ($max) {
+				$feed = PublicTimelineService::getRankedMaxId($max, $limit + 5);
+			} else if ($min) {
+				$feed = PublicTimelineService::getRankedMinId($min, $limit + 5);
+			} else {
+				$feed = PublicTimelineService::get(0, $limit + 5);
+			}
+        }
 
 		$res = collect($feed)
 		->map(function($k) use($user) {
@@ -1943,6 +2034,9 @@ class ApiV1Controller extends Controller
 		// ->toArray();
 
 		$baseUrl = config('app.url') . '/api/v1/timelines/public?limit=' . $limit . '&';
+		if($remote) {
+			$baseUrl .= 'remote=1&';
+		}
 		$minId = $res->map(function($s) {
 			return ['id' => $s['id']];
 		})->min('id');

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

@@ -217,4 +217,20 @@ class LiveStreamController extends Controller
 
     	return;
     }
+
+    public function getConfig(Request $request)
+    {
+    	abort_if(!config('livestreaming.enabled'), 400);
+    	abort_if(!$request->user(), 403);
+
+    	$res = [
+    		'enabled' => config('livestreaming.enabled'),
+    		'broadcast' => [
+    			'sources' => config('livestreaming.broadcast.sources'),
+    			'limits' => config('livestreaming.broadcast.limits')
+    		],
+    	];
+
+    	return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
+    }
 }

+ 91 - 57
app/Http/Controllers/PublicApiController.php

@@ -32,6 +32,7 @@ use App\Services\{
     LikeService,
     PublicTimelineService,
     ProfileService,
+    NetworkTimelineService,
     ReblogService,
     RelationshipService,
     StatusService,
@@ -521,7 +522,7 @@ class PublicApiController extends Controller
                       ->limit($limit)
                       ->get()
                       ->map(function($s) use ($user) {
-                           $status = StatusService::get($s->id);
+                           $status = StatusService::get($s->id, false);
                            if(!$status) {
                            		return false;
                            }
@@ -567,7 +568,7 @@ class PublicApiController extends Controller
                       ->limit($limit)
                       ->get()
                       ->map(function($s) use ($user) {
-                           $status = StatusService::get($s->id);
+                           $status = StatusService::get($s->id, false);
                            if(!$status) {
                            		return false;
                            }
@@ -608,59 +609,92 @@ class PublicApiController extends Controller
 
         $filtered = $user ? UserFilterService::filters($user->profile_id) : [];
 
-        if($min || $max) {
-            $dir = $min ? '>' : '<';
-            $id = $min ?? $max;
-            $timeline = Status::select(
-                        'id',
-                        'uri',
-                        'type',
-                        'scope',
-                        'created_at',
-                      )
-                      ->where('id', $dir, $id)
-                      ->whereNull(['in_reply_to_id', 'reblog_of_id'])
-                      ->whereNotIn('profile_id', $filtered)
-                      ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
-                      ->whereNotNull('uri')
-                      ->whereScope('public')
-                      ->where('id', '>', $amin)
-                      ->orderBy('created_at', 'desc')
-                      ->limit($limit)
-                      ->get()
-                     ->map(function($s) use ($user) {
-                            $status = StatusService::get($s->id);
-                            $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
-                            $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
-                            $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
-                            return $status;
-                      });
-            $res = $timeline->toArray();
-        } else {
-                $timeline = Status::select(
-                            'id',
-                            'uri',
-                            'type',
-                            'scope',
-                            'created_at',
-                          )
-                      	  ->whereNull(['in_reply_to_id', 'reblog_of_id'])
-                          ->whereNotIn('profile_id', $filtered)
-                          ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
-                          ->whereNotNull('uri')
-                          ->whereScope('public')
-                          ->where('id', '>', $amin)
-                          ->orderBy('created_at', 'desc')
-                          ->limit($limit)
-                          ->get()
-                          ->map(function($s) use ($user) {
-                                $status = StatusService::get($s->id);
-                                $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
-                                $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
-                                $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
-                                return $status;
-                          });
-                $res = $timeline->toArray();
+        if(config('instance.timeline.network.cached') == false) {
+	        if($min || $max) {
+	            $dir = $min ? '>' : '<';
+	            $id = $min ?? $max;
+	            $timeline = Status::select(
+	                        'id',
+	                        'uri',
+	                        'type',
+	                        'scope',
+	                        'created_at',
+	                      )
+	                      ->where('id', $dir, $id)
+	                      ->whereNull(['in_reply_to_id', 'reblog_of_id'])
+	                      ->whereNotIn('profile_id', $filtered)
+	                      ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+	                      ->whereNotNull('uri')
+	                      ->whereScope('public')
+	                      ->where('id', '>', $amin)
+	                      ->orderBy('created_at', 'desc')
+	                      ->limit($limit)
+	                      ->get()
+	                     ->map(function($s) use ($user) {
+	                            $status = StatusService::get($s->id);
+	                            $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
+	                            $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
+	                            $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
+	                            return $status;
+	                      });
+	            $res = $timeline->toArray();
+	        } else {
+	                $timeline = Status::select(
+	                            'id',
+	                            'uri',
+	                            'type',
+	                            'scope',
+	                            'created_at',
+	                          )
+	                      	  ->whereNull(['in_reply_to_id', 'reblog_of_id'])
+	                          ->whereNotIn('profile_id', $filtered)
+	                          ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+	                          ->whereNotNull('uri')
+	                          ->whereScope('public')
+	                          ->where('id', '>', $amin)
+	                          ->orderBy('created_at', 'desc')
+	                          ->limit($limit)
+	                          ->get()
+	                          ->map(function($s) use ($user) {
+	                                $status = StatusService::get($s->id);
+	                                $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
+	                                $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $s->id);
+	                                $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $s->id);
+	                                return $status;
+	                          });
+	                $res = $timeline->toArray();
+	        }
+	    } else {
+            Cache::remember('api:v1:timelines:network:cache_check', 10368000, function() {
+                if(NetworkTimelineService::count() == 0) {
+                    NetworkTimelineService::warmCache(true, 400);
+                }
+            });
+
+            if ($max) {
+                $feed = NetworkTimelineService::getRankedMaxId($max, $limit);
+            } else if ($min) {
+                $feed = NetworkTimelineService::getRankedMinId($min, $limit);
+            } else {
+                $feed = NetworkTimelineService::get(0, $limit);
+            }
+
+            $res = collect($feed)
+            ->map(function($k) use($user) {
+                $status = StatusService::get($k);
+                if($status && isset($status['account']) && $user) {
+                    $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
+                    $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $k);
+                    $status['reblogged'] = (bool) ReblogService::get($user->profile_id, $k);
+                    $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']);
+                }
+                return $status;
+            })
+            ->filter(function($s) use($filtered) {
+                return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false;
+            })
+            ->values()
+            ->toArray();
         }
 
         return response()->json($res);
@@ -704,7 +738,7 @@ class PublicApiController extends Controller
 
 		if($pid != $account['id']) {
 			if($account['locked']) {
-				if(FollowerService::follows($pid, $account['id'])) {
+				if(!FollowerService::follows($pid, $account['id'])) {
 					return [];
 				}
 			}
@@ -744,7 +778,7 @@ class PublicApiController extends Controller
 
 		if($pid != $account['id']) {
 			if($account['locked']) {
-				if(FollowerService::follows($pid, $account['id'])) {
+				if(!FollowerService::follows($pid, $account['id'])) {
 					return [];
 				}
 			}

+ 5 - 1
app/Http/Middleware/TrustProxies.php

@@ -19,5 +19,9 @@ class TrustProxies extends Middleware
      *
      * @var int
      */
-    protected $headers = Request::HEADER_X_FORWARDED_ALL;
+    protected $headers = Request::HEADER_X_FORWARDED_FOR |
+		Request::HEADER_X_FORWARDED_HOST |
+		Request::HEADER_X_FORWARDED_PORT |
+		Request::HEADER_X_FORWARDED_PROTO |
+		Request::HEADER_X_FORWARDED_AWS_ELB;
 }

+ 69 - 0
app/Jobs/FollowPipeline/FollowAcceptPipeline.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Jobs\FollowPipeline;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+
+use Cache, Log;
+use Illuminate\Support\Facades\Redis;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
+use App\FollowRequest;
+use App\Util\ActivityPub\Helpers;
+use App\Transformer\ActivityPub\Verb\AcceptFollow;
+
+class FollowAcceptPipeline implements ShouldQueue
+{
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $followRequest;
+
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
+
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(FollowRequest $followRequest)
+	{
+		$this->followRequest = $followRequest;
+	}
+
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$follow = $this->followRequest;
+		$actor = $follow->actor;
+		$target = $follow->target;
+
+		if($actor->domain == null || $actor->inbox_url == null || !$target->private_key) {
+			return;
+		}
+
+		$fractal = new Fractal\Manager();
+		$fractal->setSerializer(new ArraySerializer());
+		$resource = new Fractal\Resource\Item($follow, new AcceptFollow());
+		$activity = $fractal->createData($resource)->toArray();
+		$url = $actor->sharedInbox ?? $actor->inbox_url;
+
+		Helpers::sendSignedObject($target, $url, $activity);
+
+		$follow->delete();
+
+		return;
+	}
+}

+ 0 - 5
app/Jobs/FollowPipeline/FollowPipeline.php

@@ -63,11 +63,6 @@ class FollowPipeline implements ShouldQueue
 			$notification->item_id = $target->id;
 			$notification->item_type = "App\Profile";
 			$notification->save();
-
-			$redis = Redis::connection();
-
-			$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
-			$redis->lpush($nkey, $notification->id);
 		} catch (Exception $e) {
 			Log::error($e);
 		}

+ 69 - 0
app/Jobs/FollowPipeline/FollowRejectPipeline.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Jobs\FollowPipeline;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+
+use Cache, Log;
+use Illuminate\Support\Facades\Redis;
+use League\Fractal;
+use League\Fractal\Serializer\ArraySerializer;
+use App\FollowRequest;
+use App\Util\ActivityPub\Helpers;
+use App\Transformer\ActivityPub\Verb\RejectFollow;
+
+class FollowRejectPipeline implements ShouldQueue
+{
+	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+	protected $followRequest;
+
+	/**
+	 * Delete the job if its models no longer exist.
+	 *
+	 * @var bool
+	 */
+	public $deleteWhenMissingModels = true;
+
+	/**
+	 * Create a new job instance.
+	 *
+	 * @return void
+	 */
+	public function __construct(FollowRequest $followRequest)
+	{
+		$this->followRequest = $followRequest;
+	}
+
+	/**
+	 * Execute the job.
+	 *
+	 * @return void
+	 */
+	public function handle()
+	{
+		$follow = $this->followRequest;
+		$actor = $follow->actor;
+		$target = $follow->target;
+
+		if($actor->domain == null || $actor->inbox_url == null || !$target->private_key) {
+			return;
+		}
+
+		$fractal = new Fractal\Manager();
+		$fractal->setSerializer(new ArraySerializer());
+		$resource = new Fractal\Resource\Item($follow, new RejectFollow());
+		$activity = $fractal->createData($resource)->toArray();
+		$url = $actor->sharedInbox ?? $actor->inbox_url;
+
+		Helpers::sendSignedObject($target, $url, $activity);
+
+		$follow->delete();
+
+		return;
+	}
+}

+ 15 - 0
app/Models/LiveStream.php

@@ -22,6 +22,21 @@ class LiveStream extends Model
     	$host = config('livestreaming.server.host');
     	$port = ':' . config('livestreaming.server.port');
     	$path = '/' . config('livestreaming.server.path') . '?';
+    	$query = http_build_query([
+    		'name' => $this->stream_id,
+    		'key' => $this->stream_key,
+    		'ts' => time()
+    	]);
+
+    	return $proto . $host . $port . $path . $query;
+    }
+
+    public function getStreamRtmpUrl()
+    {
+    	$proto = 'rtmp://';
+    	$host = config('livestreaming.server.host');
+    	$port = ':' . config('livestreaming.server.port');
+    	$path = '/' . config('livestreaming.server.path') . '/'. $this->stream_id . '?';
     	$query = http_build_query([
     		'key' => $this->stream_key,
     		'ts' => time()

+ 22 - 1
app/Profile.php

@@ -271,7 +271,28 @@ class Profile extends Model
 						$this->permalink('/followers')
 					]
 				];
-				break;
+			break;
+
+			case 'unlisted':
+				$audience = [
+					'to' => [
+					],
+					'cc' => [
+						'https://www.w3.org/ns/activitystreams#Public',
+						$this->permalink('/followers')
+					]
+				];
+			break;
+
+			case 'private':
+				$audience = [
+					'to' => [
+						$this->permalink('/followers')
+					],
+					'cc' => [
+					]
+				];
+			break;
 		}
 		return $audience;
 	}

+ 1 - 1
app/Services/FollowerService.php

@@ -78,11 +78,11 @@ class FollowerService
 			}
 			return $profile
 				->followers()
-				->whereLocalProfile(false)
 				->get()
 				->map(function($follow) {
 					return $follow->sharedInbox ?? $follow->inbox_url;
 				})
+				->filter()
 				->unique()
 				->values()
 				->toArray();

+ 95 - 0
app/Services/NetworkTimelineService.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Redis;
+use App\{
+	Profile,
+	Status,
+	UserFilter
+};
+
+class NetworkTimelineService
+{
+	const CACHE_KEY = 'pf:services:timeline:network';
+
+	public static function get($start = 0, $stop = 10)
+	{
+		if($stop > 100) {
+			$stop = 100;
+		}
+
+		return Redis::zrevrange(self::CACHE_KEY, $start, $stop);
+	}
+
+	public static function getRankedMaxId($start = null, $limit = 10)
+	{
+		if(!$start) {
+			return [];
+		}
+
+		return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY, $start, '-inf', [
+			'withscores' => true,
+			'limit' => [1, $limit]
+		]));
+	}
+
+	public static function getRankedMinId($end = null, $limit = 10)
+	{
+		if(!$end) {
+			return [];
+		}
+
+		return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY, '+inf', $end, [
+			'withscores' => true,
+			'limit' => [0, $limit]
+		]));
+	}
+
+	public static function add($val)
+	{
+		if(self::count() > config('instance.timeline.network.cache_dropoff')) {
+			if(config('database.redis.client') === 'phpredis') {
+				Redis::zpopmin(self::CACHE_KEY);
+			}
+		}
+
+		return Redis::zadd(self::CACHE_KEY, $val, $val);
+	}
+
+	public static function rem($val)
+	{
+		return Redis::zrem(self::CACHE_KEY, $val);
+	}
+
+	public static function del($val)
+	{
+		return self::rem($val);
+	}
+
+	public static function count()
+	{
+		return Redis::zcard(self::CACHE_KEY);
+	}
+
+	public static function warmCache($force = false, $limit = 100)
+	{
+		if(self::count() == 0 || $force == true) {
+			Redis::del(self::CACHE_KEY);
+			$ids = Status::whereNotNull('uri')
+				->whereScope('public')
+				->whereNull('in_reply_to_id')
+				->whereNull('reblog_of_id')
+				->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
+				->where('created_at', '>', now()->subHours(config('instance.timeline.network.max_hours_old')))
+				->orderByDesc('created_at')
+				->limit($limit)
+				->pluck('id');
+			foreach($ids as $id) {
+				self::add($id);
+			}
+			return 1;
+		}
+		return 0;
+	}
+}

+ 2 - 0
app/Services/RelationshipService.php

@@ -57,6 +57,8 @@ class RelationshipService
 
 	public static function refresh($aid, $tid)
 	{
+		Cache::forget('pf:services:follow:audience:' . $aid);
+		Cache::forget('pf:services:follow:audience:' . $tid);
 		self::delete($tid, $aid);
 		self::delete($aid, $tid);
 		self::get($tid, $aid);

+ 21 - 18
app/Services/UserFilterService.php

@@ -3,26 +3,24 @@
 namespace App\Services;
 
 use Cache;
+use App\UserFilter;
 use Illuminate\Support\Facades\Redis;
 
-use App\{
-	Follower,
-	Profile,
-	UserFilter
-};
-
-class UserFilterService {
-
+class UserFilterService
+{
 	const USER_MUTES_KEY = 'pf:services:mutes:ids:';
 	const USER_BLOCKS_KEY = 'pf:services:blocks:ids:';
 
-	public static function mutes(int $profile_id) : array
+	public static function mutes(int $profile_id)
 	{
 		$key = self::USER_MUTES_KEY . $profile_id;
-		$cached = Redis::zrevrange($key, 0, -1);
-		if($cached) {
-			return $cached;
+		$warm = Cache::has($key . ':cached');
+		if($warm) {
+			return Redis::zrevrange($key, 0, -1) ?? [];
 		} else {
+			if(Redis::zrevrange($key, 0, -1)) {
+				return Redis::zrevrange($key, 0, -1);
+			}
 			$ids = UserFilter::whereFilterType('mute')
 				->whereUserId($profile_id)
 				->pluck('filterable_id')
@@ -30,29 +28,34 @@ class UserFilterService {
 			foreach ($ids as $muted_id) {
 				Redis::zadd($key, (int) $muted_id, (int) $muted_id);
 			}
+			Cache::set($key . ':cached', 1, 7776000);
 			return $ids;
 		}
 	}
 
-	public static function blocks(int $profile_id) : array
+	public static function blocks(int $profile_id)
 	{
 		$key = self::USER_BLOCKS_KEY . $profile_id;
-		$cached = Redis::zrevrange($key, 0, -1);
-		if($cached) {
-			return $cached;
+		$warm = Cache::has($key . ':cached');
+		if($warm) {
+			return Redis::zrevrange($key, 0, -1) ?? [];
 		} else {
+			if(Redis::zrevrange($key, 0, -1)) {
+				return Redis::zrevrange($key, 0, -1);
+			}
 			$ids = UserFilter::whereFilterType('block')
 				->whereUserId($profile_id)
 				->pluck('filterable_id')
 				->toArray();
 			foreach ($ids as $blocked_id) {
-				Redis::zadd($key, $blocked_id, $blocked_id);
+				Redis::zadd($key, (int) $blocked_id, (int) $blocked_id);
 			}
+			Cache::set($key . ':cached', 1, 7776000);
 			return $ids;
 		}
 	}
 
-	public static function filters(int $profile_id) : array
+	public static function filters(int $profile_id)
 	{
 		return array_unique(array_merge(self::mutes($profile_id), self::blocks($profile_id)));
 	}

+ 25 - 0
app/Transformer/ActivityPub/Verb/AcceptFollow.php

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

+ 25 - 0
app/Transformer/ActivityPub/Verb/RejectFollow.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Transformer\ActivityPub\Verb;
+
+use App\FollowRequest;
+use League\Fractal;
+
+class RejectFollow extends Fractal\TransformerAbstract
+{
+	public function transform(FollowRequest $follow)
+	{
+		return [
+			'@context'  => 'https://www.w3.org/ns/activitystreams',
+			'type'      => 'Reject',
+			'id'		=> $follow->permalink(null, '#rejects'),
+			'actor'     => $follow->target->permalink(),
+			'object' 	=> [
+				'type' 		=> 'Follow',
+				'id'        => $follow->activity && isset($follow->activity['id']) ? $follow->activity['id'] : null,
+				'actor'		=> $follow->actor->permalink(),
+				'object'	=> $follow->target->permalink()
+			]
+		];
+	}
+}

+ 11 - 0
app/Util/ActivityPub/Helpers.php

@@ -32,6 +32,7 @@ use App\Services\CustomEmojiService;
 use App\Services\InstanceService;
 use App\Services\MediaPathService;
 use App\Services\MediaStorageService;
+use App\Services\NetworkTimelineService;
 use App\Jobs\MediaPipeline\MediaStoragePipeline;
 use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
 use App\Util\Media\License;
@@ -490,6 +491,16 @@ class Helpers {
 			if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) {
 				StatusTagsPipeline::dispatch($activity, $status);
 			}
+
+			if( config('instance.timeline.network.cached') &&
+				$status->in_reply_to_id === null &&
+				$status->reblog_of_id === null &&
+				in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
+				$status->created_at->gt(now()->subHours(config('instance.timeline.network.max_hours_old')))
+			) {
+				NetworkTimelineService::add($status->id);
+			}
+
 			return $status;
 		});
 	}

+ 4 - 9
app/Util/ActivityPub/Inbox.php

@@ -473,17 +473,12 @@ class Inbox
 			return;
 		}
 		if($target->is_private == true) {
-			FollowRequest::firstOrCreate([
+			FollowRequest::updateOrCreate([
 				'follower_id' => $actor->id,
-				'following_id' => $target->id
+				'following_id' => $target->id,
+			],[
+				'activity' => collect($this->payload)->only(['id','actor','object','type'])->toArray()
 			]);
-
-			Cache::forget('profile:follower_count:'.$target->id);
-			Cache::forget('profile:follower_count:'.$actor->id);
-			Cache::forget('profile:following_count:'.$target->id);
-			Cache::forget('profile:following_count:'.$actor->id);
-			FollowerService::add($actor->id, $target->id);
-
 		} else {
 			$follower = new Follower;
 			$follower->profile_id = $actor->id;

Fișier diff suprimat deoarece este prea mare
+ 165 - 163
composer.lock


+ 6 - 0
config/instance.php

@@ -24,6 +24,12 @@ return [
 	'timeline' => [
 		'local' => [
 			'is_public' => env('INSTANCE_PUBLIC_LOCAL_TIMELINE', false)
+		],
+
+		'network' => [
+			'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false,
+			'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100),
+			'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6)
 		]
 	],
 

+ 12 - 6
config/livestreaming.php

@@ -10,14 +10,20 @@ return [
 	],
 
 	'broadcast' => [
-		'max_duration' => env('HLS_LIVE_BROADCAST_MAX_DURATION', 60),
-		'max_active' => env('HLS_LIVE_BROADCAST_MAX_ACTIVE', 10),
+		'delete_token_after_finished' => (bool) env('HLS_LIVE_BROADCAST_DELETE_TOKEN_AFTER', true),
+		'max_duration' => (int) env('HLS_LIVE_BROADCAST_MAX_DURATION', 60),
+		'max_active' => (int) env('HLS_LIVE_BROADCAST_MAX_ACTIVE', 10),
 
 		'limits' => [
-			'enabled' => env('HLS_LIVE_BROADCAST_LIMITS', true),
-			'min_follower_count' => env('HLS_LIVE_BROADCAST_LIMITS_MIN_FOLLOWERS', 100),
-			'min_account_age' => env('HLS_LIVE_BROADCAST_LIMITS_MIN_ACCOUNT_AGE', 14),
-			'admins_only' => env('HLS_LIVE_BROADCAST_LIMITS_ADMINS_ONLY', true)
+			'enabled' => (bool) env('HLS_LIVE_BROADCAST_LIMITS', true),
+			'min_follower_count' => (int) env('HLS_LIVE_BROADCAST_LIMITS_MIN_FOLLOWERS', 100),
+			'min_account_age' => (int) env('HLS_LIVE_BROADCAST_LIMITS_MIN_ACCOUNT_AGE', 14),
+			'admins_only' => (bool) env('HLS_LIVE_BROADCAST_LIMITS_ADMINS_ONLY', true)
+		],
+
+		'sources' => [
+			'app' => (bool) env('HLS_LIVE_BROADCAST_SOURCE_APP', false),
+			'web' => (bool) env('HLS_LIVE_BROADCAST_SOURCE_WEB', false)
 		]
 	],
 

+ 1 - 1
config/pixelfed.php

@@ -239,7 +239,7 @@ return [
 		]
 	],
 
-	'max_collection_length' => (int) env('PF_MAX_COLLECTION_LENGTH', 18),
+	'max_collection_length' => (int) env('PF_MAX_COLLECTION_LENGTH', 100),
 
 	'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'),
 

+ 34 - 0
database/migrations/2022_06_03_051308_add_object_column_to_follow_requests_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddObjectColumnToFollowRequestsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('follow_requests', function (Blueprint $table) {
+            $table->json('activity')->nullable()->after('following_id');
+            $table->timestamp('handled_at')->nullable()->after('is_local');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('follow_requests', function (Blueprint $table) {
+            $table->dropColumn('activity');
+            $table->dropColumn('handled_at');
+        });
+    }
+}

BIN
public/css/app.css


BIN
public/css/appdark.css


BIN
public/js/compose-ojtjadoml.js


BIN
public/js/daci-ojtjadoml.js


BIN
public/js/dffc-ojtjadoml.js


BIN
public/js/dmsg-ojtjadoml.js


BIN
public/js/dmyh-ojtjadoml.js


BIN
public/js/dmym-ojtjadoml.js


BIN
public/js/dsfc-ojtjadoml.js


BIN
public/js/dssc-ojtjadoml.js


BIN
public/js/home-ojtjadoml.js


BIN
public/js/manifest.js


BIN
public/js/notifications-ojtjadoml.js


BIN
public/js/post-ojtjadoml.js


BIN
public/js/profile-ojtjadoml.js


BIN
public/js/spa.js


BIN
public/js/vendor.js


+ 1 - 1
public/js/vendor.js.LICENSE.txt

@@ -16,7 +16,7 @@
  */
 
 /*!
- * BootstrapVue Icons, generated from Bootstrap Icons 1.2.2
+ * BootstrapVue Icons, generated from Bootstrap Icons 1.5.0
  *
  * @link https://icons.getbootstrap.com/
  * @license MIT

BIN
public/mix-manifest.json


+ 1 - 0
routes/api.php

@@ -105,5 +105,6 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 		Route::get('chat/latest', 'LiveStreamController@getLatestChat')->middleware($middleware);
 		Route::post('chat/message', 'LiveStreamController@addChatComment')->middleware($middleware);
 		Route::post('chat/delete', 'LiveStreamController@deleteChatComment')->middleware($middleware);
+		Route::get('config', 'LiveStreamController@getConfig')->middleware($middleware);
 	});
 });

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff