Browse Source

Merge pull request #4902 from pixelfed/staging

Staging
daniel 1 year ago
parent
commit
6ea20716bc

+ 6 - 0
CHANGELOG.md

@@ -98,6 +98,12 @@
 - Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
 - Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
 - Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
+- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
+- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
+- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
+- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
+- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
+- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 
 ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)

+ 298 - 0
app/Console/Commands/InstanceManager.php

@@ -0,0 +1,298 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Instance;
+use App\Profile;
+use App\Services\InstanceService;
+use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
+use function Laravel\Prompts\select;
+use function Laravel\Prompts\confirm;
+use function Laravel\Prompts\progress;
+use function Laravel\Prompts\search;
+use function Laravel\Prompts\table;
+
+class InstanceManager extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:instance-manager';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Manage Instances';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $action = select(
+            'What action do you want to perform?',
+            [
+                'Recalculate Stats',
+                'Ban Instance',
+                'Unlist Instance',
+                'Unlisted Instances',
+                'Banned Instances',
+                'Unban Instance',
+                'Relist Instance',
+            ],
+        );
+
+        switch($action) {
+            case 'Recalculate Stats':
+                return $this->recalculateStats();
+            break;
+
+            case 'Unlisted Instances':
+                return $this->viewUnlistedInstances();
+            break;
+
+            case 'Banned Instances':
+                return $this->viewBannedInstances();
+            break;
+
+            case 'Unlist Instance':
+                return $this->unlistInstance();
+            break;
+
+            case 'Ban Instance':
+                return $this->banInstance();
+            break;
+
+            case 'Unban Instance':
+                return $this->unbanInstance();
+            break;
+
+            case 'Relist Instance':
+                return $this->relistInstance();
+            break;
+        }
+    }
+
+    protected function recalculateStats()
+    {
+        $instanceCount = Instance::count();
+        $confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
+        if(!$confirmed) {
+            $this->error('Aborting...');
+            exit;
+        }
+
+        $users = progress(
+            label: 'Updating instance stats...',
+            steps: Instance::all(),
+            callback: fn ($instance) => $this->updateInstanceStats($instance),
+        );
+    }
+
+    protected function updateInstanceStats($instance)
+    {
+        FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
+    }
+
+    protected function unlistInstance()
+    {
+        $id = search(
+            'Search by domain',
+            fn (string $value) => strlen($value) > 0
+                ? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
+                : []
+        );
+
+        $instance = Instance::find($id);
+        if(!$instance) {
+            $this->error('Oops, an error occured');
+            exit;
+        }
+
+        $tbl = [
+            [
+                $instance->domain,
+                number_format($instance->status_count),
+                number_format($instance->user_count),
+            ]
+        ];
+        table(
+            ['Domain', 'Status Count', 'User Count'],
+            $tbl
+        );
+
+        $confirmed = confirm('Are you sure you want to unlist this instance?');
+        if(!$confirmed) {
+            $this->error('Aborting instance unlisting');
+            exit;
+        }
+
+        $instance->unlisted = true;
+        $instance->save();
+        InstanceService::refresh();
+        $this->info('Successfully unlisted ' . $instance->domain . '!');
+        exit;
+    }
+
+    protected function relistInstance()
+    {
+        $id = search(
+            'Search by domain',
+            fn (string $value) => strlen($value) > 0
+                ? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
+                : []
+        );
+
+        $instance = Instance::find($id);
+        if(!$instance) {
+            $this->error('Oops, an error occured');
+            exit;
+        }
+
+        $tbl = [
+            [
+                $instance->domain,
+                number_format($instance->status_count),
+                number_format($instance->user_count),
+            ]
+        ];
+        table(
+            ['Domain', 'Status Count', 'User Count'],
+            $tbl
+        );
+
+        $confirmed = confirm('Are you sure you want to re-list this instance?');
+        if(!$confirmed) {
+            $this->error('Aborting instance re-listing');
+            exit;
+        }
+
+        $instance->unlisted = false;
+        $instance->save();
+        InstanceService::refresh();
+        $this->info('Successfully re-listed ' . $instance->domain . '!');
+        exit;
+    }
+
+    protected function banInstance()
+    {
+        $id = search(
+            'Search by domain',
+            fn (string $value) => strlen($value) > 0
+                ? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
+                : []
+        );
+
+        $instance = Instance::find($id);
+        if(!$instance) {
+            $this->error('Oops, an error occured');
+            exit;
+        }
+
+        $tbl = [
+            [
+                $instance->domain,
+                number_format($instance->status_count),
+                number_format($instance->user_count),
+            ]
+        ];
+        table(
+            ['Domain', 'Status Count', 'User Count'],
+            $tbl
+        );
+
+        $confirmed = confirm('Are you sure you want to ban this instance?');
+        if(!$confirmed) {
+            $this->error('Aborting instance ban');
+            exit;
+        }
+
+        $instance->banned = true;
+        $instance->save();
+        InstanceService::refresh();
+        $this->info('Successfully banned ' . $instance->domain . '!');
+        exit;
+    }
+
+    protected function unbanInstance()
+    {
+        $id = search(
+            'Search by domain',
+            fn (string $value) => strlen($value) > 0
+                ? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
+                : []
+        );
+
+        $instance = Instance::find($id);
+        if(!$instance) {
+            $this->error('Oops, an error occured');
+            exit;
+        }
+
+        $tbl = [
+            [
+                $instance->domain,
+                number_format($instance->status_count),
+                number_format($instance->user_count),
+            ]
+        ];
+        table(
+            ['Domain', 'Status Count', 'User Count'],
+            $tbl
+        );
+
+        $confirmed = confirm('Are you sure you want to unban this instance?');
+        if(!$confirmed) {
+            $this->error('Aborting instance unban');
+            exit;
+        }
+
+        $instance->banned = false;
+        $instance->save();
+        InstanceService::refresh();
+        $this->info('Successfully un-banned ' . $instance->domain . '!');
+        exit;
+    }
+
+    protected function viewBannedInstances()
+    {
+        $data = Instance::whereBanned(true)
+            ->get(['domain', 'user_count', 'status_count'])
+            ->map(function($d) {
+                return [
+                    'domain' => $d->domain,
+                    'user_count' => number_format($d->user_count),
+                    'status_count' => number_format($d->status_count),
+                ];
+            })
+            ->toArray();
+        table(
+            ['Domain', 'User Count', 'Status Count'],
+            $data
+        );
+    }
+
+    protected function viewUnlistedInstances()
+    {
+        $data = Instance::whereUnlisted(true)
+            ->get(['domain', 'user_count', 'status_count', 'banned'])
+            ->map(function($d) {
+                return [
+                    'domain' => $d->domain,
+                    'user_count' => number_format($d->user_count),
+                    'status_count' => number_format($d->status_count),
+                    'banned' => $d->banned ? '✅' : null
+                ];
+            })
+            ->toArray();
+        table(
+            ['Domain', 'User Count', 'Status Count', 'Banned'],
+            $data
+        );
+    }
+}

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

@@ -219,6 +219,10 @@ class ApiV1Controller extends Controller
         if(!$res) {
             return response()->json(['error' => 'Record not found'], 404);
         }
+        if($res && strpos($res['acct'], '@') != -1) {
+            $domain = parse_url($res['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
         return $this->json($res);
     }
 
@@ -483,6 +487,11 @@ class ApiV1Controller extends Controller
         $limit = $request->input('limit', 10);
         $napi = $request->has(self::PF_API_ENTITY_KEY);
 
+        if($account && strpos($account['acct'], '@') != -1) {
+            $domain = parse_url($account['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         if(intval($pid) !== intval($account['id'])) {
             if($account['locked']) {
                 if(!FollowerService::follows($pid, $account['id'])) {
@@ -575,6 +584,11 @@ class ApiV1Controller extends Controller
         $limit = $request->input('limit', 10);
         $napi = $request->has(self::PF_API_ENTITY_KEY);
 
+        if($account && strpos($account['acct'], '@') != -1) {
+            $domain = parse_url($account['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         if(intval($pid) !== intval($account['id'])) {
             if($account['locked']) {
                 if(!FollowerService::follows($pid, $account['id'])) {
@@ -676,6 +690,11 @@ class ApiV1Controller extends Controller
             return $this->json(['error' => 'Account not found'], 404);
         }
 
+        if($profile && strpos($profile['acct'], '@') != -1) {
+            $domain = parse_url($profile['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $limit = $request->limit ?? 20;
         $max_id = $request->max_id;
         $min_id = $request->min_id;
@@ -766,6 +785,11 @@ class ApiV1Controller extends Controller
             ->whereNull('status')
             ->findOrFail($id);
 
+        if($target && $target->domain) {
+            $domain = $target->domain;
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $private = (bool) $target->is_private;
         $remote = (bool) $target->domain;
         $blocked = UserFilter::whereUserId($target->id)
@@ -1252,14 +1276,19 @@ class ApiV1Controller extends Controller
         $user = $request->user();
         abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
 
-        AccountService::setLastActive($user->id);
-
         $status = StatusService::getMastodon($id, false);
 
-        abort_unless($status, 400);
+        abort_unless($status, 404);
+
+        if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) {
+            $domain = parse_url($status['account']['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
 
         $spid = $status['account']['id'];
 
+        AccountService::setLastActive($user->id);
+
         if(intval($spid) !== intval($user->profile_id)) {
             if($status['visibility'] == 'private') {
                 abort_if(!FollowerService::follows($user->profile_id, $spid), 403);
@@ -1404,6 +1433,11 @@ class ApiV1Controller extends Controller
             return response()->json(['error' => 'Record not found'], 404);
         }
 
+        if($target && strpos($target['acct'], '@') != -1) {
+            $domain = parse_url($target['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
 
         if(!$followRequest) {
@@ -2011,6 +2045,11 @@ class ApiV1Controller extends Controller
 
         $account = Profile::findOrFail($id);
 
+        if($account && $account->domain) {
+            $domain = $account->domain;
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $count = UserFilterService::muteCount($pid);
         $maxLimit = intval(config('instance.user_filters.max_user_mutes'));
         if($count == 0) {
@@ -2653,6 +2692,11 @@ class ApiV1Controller extends Controller
             abort(404);
         }
 
+        if($res && isset($res['account'], $res['account']['acct'], $res['account']['url']) && strpos($res['account']['acct'], '@') != -1) {
+            $domain = parse_url($res['account']['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $scope = $res['visibility'];
         if(!in_array($scope, ['public', 'unlisted'])) {
             if($scope === 'private') {
@@ -2697,6 +2741,11 @@ class ApiV1Controller extends Controller
             return response('', 404);
         }
 
+        if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) {
+            $domain = parse_url($status['account']['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         if(intval($status['account']['id']) !== intval($user->profile_id)) {
             if($status['visibility'] == 'private') {
                 if(!FollowerService::follows($user->profile_id, $status['account']['id'])) {
@@ -2780,6 +2829,10 @@ class ApiV1Controller extends Controller
         $status = Status::findOrFail($id);
         $account = AccountService::get($status->profile_id, true);
         abort_if(!$account, 404);
+        if($account && strpos($account['acct'], '@') != -1) {
+            $domain = parse_url($account['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
         $author = intval($status->profile_id) === intval($pid) || $user->is_admin;
         $napi = $request->has(self::PF_API_ENTITY_KEY);
 
@@ -2871,6 +2924,10 @@ class ApiV1Controller extends Controller
         $pid = $user->profile_id;
         $status = Status::findOrFail($id);
         $account = AccountService::get($status->profile_id, true);
+        if($account && strpos($account['acct'], '@') != -1) {
+            $domain = parse_url($account['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
         abort_if(!$account, 404);
         $author = intval($status->profile_id) === intval($pid) || $user->is_admin;
         $napi = $request->has(self::PF_API_ENTITY_KEY);
@@ -3200,7 +3257,11 @@ class ApiV1Controller extends Controller
         abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action');
         AccountService::setLastActive($user->id);
         $status = Status::whereScope('public')->findOrFail($id);
-
+        if($status && ($status->uri || $status->url || $status->object_url)) {
+            $url = $status->uri ?? $status->url ?? $status->object_url;
+            $domain = parse_url($url, PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
         if(intval($status->profile_id) !== intval($user->profile_id)) {
             if($status->scope == 'private') {
                 abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);

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

@@ -42,6 +42,7 @@ use App\Services\{
 use App\Jobs\StatusPipeline\NewStatusPipeline;
 use League\Fractal\Serializer\ArraySerializer;
 use League\Fractal\Pagination\IlluminatePaginatorAdapter;
+use App\Services\InstanceService;
 
 class PublicApiController extends Controller
 {
@@ -661,6 +662,10 @@ class PublicApiController extends Controller
     public function account(Request $request, $id)
     {
         $res = AccountService::get($id);
+        if($res && isset($res['local'], $res['url']) && !$res['local']) {
+            $domain = parse_url($res['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
         return response()->json($res);
     }
 
@@ -680,6 +685,11 @@ class PublicApiController extends Controller
         $profile = AccountService::get($id);
         abort_if(!$profile, 404);
 
+        if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) {
+            $domain = parse_url($profile['url'], PHP_URL_HOST);
+            abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
+        }
+
         $limit = $request->limit ?? 9;
         $max_id = $request->max_id;
         $min_id = $request->min_id;

+ 67 - 53
app/Instance.php

@@ -6,63 +6,77 @@ use Illuminate\Database\Eloquent\Model;
 
 class Instance extends Model
 {
-	protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes'];
+    protected $casts = [
+        'last_crawled_at' => 'datetime',
+        'actors_last_synced_at' => 'datetime',
+        'notes' => 'array',
+        'nodeinfo_last_fetched' => 'datetime',
+        'delivery_next_after' => 'datetime',
+    ];
 
-	public function profiles()
-	{
-		return $this->hasMany(Profile::class, 'domain', 'domain');
-	}
+    protected $fillable = [
+        'domain',
+        'banned',
+        'auto_cw',
+        'unlisted',
+        'notes'
+    ];
 
-	public function statuses()
-	{
-		return $this->hasManyThrough(
-			Status::class,
-			Profile::class,
-			'domain',
-			'profile_id',
-			'domain',
-			'id'
-		);
-	}
+    public function profiles()
+    {
+        return $this->hasMany(Profile::class, 'domain', 'domain');
+    }
 
-	public function reported()
-	{
-		return $this->hasManyThrough(
-			Report::class,
-			Profile::class,
-			'domain',
-			'reported_profile_id',
-			'domain',
-			'id'
-		);
-	}
+    public function statuses()
+    {
+        return $this->hasManyThrough(
+            Status::class,
+            Profile::class,
+            'domain',
+            'profile_id',
+            'domain',
+            'id'
+        );
+    }
 
-	public function reports()
-	{
-		return $this->hasManyThrough(
-			Report::class,
-			Profile::class,
-			'domain',
-			'profile_id',
-			'domain',
-			'id'
-		);
-	}
+    public function reported()
+    {
+        return $this->hasManyThrough(
+            Report::class,
+            Profile::class,
+            'domain',
+            'reported_profile_id',
+            'domain',
+            'id'
+        );
+    }
 
-	public function media()
-	{
-		return $this->hasManyThrough(
-			Media::class,
-			Profile::class,
-			'domain',
-			'profile_id',
-			'domain',
-			'id'
-		);
-	}
+    public function reports()
+    {
+        return $this->hasManyThrough(
+            Report::class,
+            Profile::class,
+            'domain',
+            'profile_id',
+            'domain',
+            'id'
+        );
+    }
 
-	public function getUrl()
-	{
-		return url("/i/admin/instances/show/{$this->id}");
-	}
+    public function media()
+    {
+        return $this->hasManyThrough(
+            Media::class,
+            Profile::class,
+            'domain',
+            'profile_id',
+            'domain',
+            'id'
+        );
+    }
+
+    public function getUrl()
+    {
+        return url("/i/admin/instances/show/{$this->id}");
+    }
 }

+ 66 - 39
app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php

@@ -4,6 +4,7 @@ namespace App\Jobs\InstancePipeline;
 
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldBeUnique;
+use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
@@ -12,45 +13,71 @@ use Illuminate\Support\Facades\Http;
 use App\Instance;
 use App\Profile;
 use App\Services\NodeinfoService;
+use Illuminate\Contracts\Cache\Repository;
+use Illuminate\Support\Facades\Cache;
 
-class FetchNodeinfoPipeline implements ShouldQueue
+class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
 {
-	use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
-	protected $instance;
-
-	/**
-	 * Create a new job instance.
-	 *
-	 * @return void
-	 */
-	public function __construct(Instance $instance)
-	{
-		$this->instance = $instance;
-	}
-
-	/**
-	 * Execute the job.
-	 *
-	 * @return void
-	 */
-	public function handle()
-	{
-		$instance = $this->instance;
-
-		$ni = NodeinfoService::get($instance->domain);
-		if($ni) {
-			if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
-				$software = $ni['software']['name'];
-				$instance->software = strtolower(strip_tags($software));
-				$instance->last_crawled_at = now();
-				$instance->user_count = Profile::whereDomain($instance->domain)->count();
-				$instance->save();
-			}
-		} else {
-			$instance->user_count = Profile::whereDomain($instance->domain)->count();
-			$instance->last_crawled_at = now();
-			$instance->save();
-		}
-	}
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    protected $instance;
+
+    /**
+     * Create a new job instance.
+     *
+     * @return void
+     */
+    public function __construct(Instance $instance)
+    {
+        $this->instance = $instance;
+    }
+
+    /**
+     * The number of seconds after which the job's unique lock will be released.
+     *
+     * @var int
+     */
+    public $uniqueFor = 14400;
+
+    /**
+     * Get the unique ID for the job.
+     */
+    public function uniqueId(): string
+    {
+        return $this->instance->id;
+    }
+
+    /**
+     * Execute the job.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $instance = $this->instance;
+
+        if( $instance->nodeinfo_last_fetched &&
+            $instance->nodeinfo_last_fetched->gt(now()->subHours(12)) ||
+            $instance->delivery_timeout &&
+            $instance->delivery_next_after->gt(now())
+        ) {
+            return;
+        }
+
+        $ni = NodeinfoService::get($instance->domain);
+        $instance->last_crawled_at = now();
+        if($ni) {
+            if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
+                $software = $ni['software']['name'];
+                $instance->software = strtolower(strip_tags($software));
+                $instance->user_count = Profile::whereDomain($instance->domain)->count();
+                $instance->nodeinfo_last_fetched = now();
+                $instance->save();
+            }
+        } else {
+            $instance->delivery_timeout = 1;
+            $instance->delivery_next_after = now()->addHours(14);
+            $instance->save();
+        }
+    }
 }

+ 8 - 2
app/Services/NodeinfoService.php

@@ -22,7 +22,10 @@ class NodeinfoService
         $wk = $url . '/.well-known/nodeinfo';
 
         try {
-            $res = Http::withHeaders($headers)
+            $res = Http::withOptions([
+                'allow_redirects' => false,
+            ])
+            ->withHeaders($headers)
             ->timeout(5)
             ->get($wk);
         } catch (RequestException $e) {
@@ -61,7 +64,10 @@ class NodeinfoService
         }
 
         try {
-            $res = Http::withHeaders($headers)
+            $res = Http::withOptions([
+                'allow_redirects' => false,
+            ])
+            ->withHeaders($headers)
             ->timeout(5)
             ->get($href);
         } catch (RequestException $e) {

+ 198 - 197
config/horizon.php

@@ -2,201 +2,202 @@
 
 return [
 
-	/*
-	|--------------------------------------------------------------------------
-	| Horizon Domain
-	|--------------------------------------------------------------------------
-	|
-	| This is the subdomain where Horizon will be accessible from. If this
-	| setting is null, Horizon will reside under the same domain as the
-	| application. Otherwise, this value will serve as the subdomain.
-	|
-	*/
-
-	'domain' => null,
-
-	/*
-	|--------------------------------------------------------------------------
-	| Horizon Path
-	|--------------------------------------------------------------------------
-	|
-	| This is the URI path where Horizon will be accessible from. Feel free
-	| to change this path to anything you like. Note that the URI will not
-	| affect the paths of its internal API that aren't exposed to users.
-	|
-	*/
-
-	'path' => 'horizon',
-
-	/*
-	|--------------------------------------------------------------------------
-	| Horizon Redis Connection
-	|--------------------------------------------------------------------------
-	|
-	| This is the name of the Redis connection where Horizon will store the
-	| meta information required for it to function. It includes the list
-	| of supervisors, failed jobs, job metrics, and other information.
-	|
-	*/
-
-	'use' => 'default',
-
-	/*
-	|--------------------------------------------------------------------------
-	| Horizon Redis Prefix
-	|--------------------------------------------------------------------------
-	|
-	| This prefix will be used when storing all Horizon data in Redis. You
-	| may modify the prefix when you are running multiple installations
-	| of Horizon on the same server so that they don't have problems.
-	|
-	*/
-
-	'prefix' => env('HORIZON_PREFIX', 'horizon-'),
-
-	/*
-	|--------------------------------------------------------------------------
-	| Horizon Route Middleware
-	|--------------------------------------------------------------------------
-	|
-	| These middleware will get attached onto each Horizon route, giving you
-	| the chance to add your own middleware to this list or change any of
-	| the existing middleware. Or, you can simply stick with this list.
-	|
-	*/
-
-	'middleware' => ['web'],
-
-	/*
-	|--------------------------------------------------------------------------
-	| Queue Wait Time Thresholds
-	|--------------------------------------------------------------------------
-	|
-	| This option allows you to configure when the LongWaitDetected event
-	| will be fired. Every connection / queue combination may have its
-	| own, unique threshold (in seconds) before this event is fired.
-	|
-	*/
-
-	'waits' => [
-		'redis:feed' => 30,
-		'redis:follow' => 30,
-		'redis:shared' => 30,
-		'redis:default' => 30,
-		'redis:inbox' => 30,
-		'redis:low' => 30,
-		'redis:high' => 30,
-		'redis:delete' => 30,
-		'redis:story' => 30,
-		'redis:mmo' => 30,
-	],
-
-	/*
-	|--------------------------------------------------------------------------
-	| Job Trimming Times
-	|--------------------------------------------------------------------------
-	|
-	| Here you can configure for how long (in minutes) you desire Horizon to
-	| persist the recent and failed jobs. Typically, recent jobs are kept
-	| for one hour while all failed jobs are stored for an entire week.
-	|
-	*/
-
-	'trim' => [
-		'recent' => 60,
-		'pending' => 60,
-		'completed' => 60,
-		'recent_failed' => 10080,
-		'failed' => 10080,
-		'monitored' => 10080,
-	],
-
-	/*
-	|--------------------------------------------------------------------------
-	| Metrics
-	|--------------------------------------------------------------------------
-	|
-	| Here you can configure how many snapshots should be kept to display in
-	| the metrics graph. This will get used in combination with Horizon's
-	| `horizon:snapshot` schedule to define how long to retain metrics.
-	|
-	*/
-
-	'metrics' => [
-		'trim_snapshots' => [
-			'job' => 24,
-			'queue' => 24,
-		],
-	],
-
-	/*
-	|--------------------------------------------------------------------------
-	| Fast Termination
-	|--------------------------------------------------------------------------
-	|
-	| When this option is enabled, Horizon's "terminate" command will not
-	| wait on all of the workers to terminate unless the --wait option
-	| is provided. Fast termination can shorten deployment delay by
-	| allowing a new instance of Horizon to start while the last
-	| instance will continue to terminate each of its workers.
-	|
-	*/
-
-	'fast_termination' => false,
-
-	/*
-	|--------------------------------------------------------------------------
-	| Memory Limit (MB)
-	|--------------------------------------------------------------------------
-	|
-	| This value describes the maximum amount of memory the Horizon worker
-	| may consume before it is terminated and restarted. You should set
-	| this value according to the resources available to your server.
-	|
-	*/
-
-	'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64),
-
-	/*
-	|--------------------------------------------------------------------------
-	| Queue Worker Configuration
-	|--------------------------------------------------------------------------
-	|
-	| Here you may define the queue worker settings used by your application
-	| in all environments. These supervisors and settings handle all your
-	| queued jobs and will be provisioned by Horizon during deployment.
-	|
-	*/
-
-	'environments' => [
-		'production' => [
-			'supervisor-1' => [
-				'connection'    => 'redis',
-				'queue'         => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'],
-				'balance'       => env('HORIZON_BALANCE_STRATEGY', 'auto'),
-				'minProcesses'  => env('HORIZON_MIN_PROCESSES', 1),
-				'maxProcesses'  => env('HORIZON_MAX_PROCESSES', 20),
-				'memory'        => env('HORIZON_SUPERVISOR_MEMORY', 64),
-				'tries'         => env('HORIZON_SUPERVISOR_TRIES', 3),
-				'nice'          => env('HORIZON_SUPERVISOR_NICE', 0),
-				'timeout'		=> env('HORIZON_SUPERVISOR_TIMEOUT', 300),
-			],
-		],
-
-		'local' => [
-			'supervisor-1' => [
-				'connection'    => 'redis',
-				'queue'         => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'],
-				'balance'       => 'auto',
-				'minProcesses' => 1,
-				'maxProcesses'  => 20,
-				'memory'        => 128,
-				'tries'         => 3,
-				'nice'          => 0,
-				'timeout'       => 300
-			],
-		],
-	],
-
-	'darkmode' => env('HORIZON_DARKMODE', false),
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Domain
+    |--------------------------------------------------------------------------
+    |
+    | This is the subdomain where Horizon will be accessible from. If this
+    | setting is null, Horizon will reside under the same domain as the
+    | application. Otherwise, this value will serve as the subdomain.
+    |
+    */
+
+    'domain' => null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Path
+    |--------------------------------------------------------------------------
+    |
+    | This is the URI path where Horizon will be accessible from. Feel free
+    | to change this path to anything you like. Note that the URI will not
+    | affect the paths of its internal API that aren't exposed to users.
+    |
+    */
+
+    'path' => 'horizon',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Redis Connection
+    |--------------------------------------------------------------------------
+    |
+    | This is the name of the Redis connection where Horizon will store the
+    | meta information required for it to function. It includes the list
+    | of supervisors, failed jobs, job metrics, and other information.
+    |
+    */
+
+    'use' => 'default',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Redis Prefix
+    |--------------------------------------------------------------------------
+    |
+    | This prefix will be used when storing all Horizon data in Redis. You
+    | may modify the prefix when you are running multiple installations
+    | of Horizon on the same server so that they don't have problems.
+    |
+    */
+
+    'prefix' => env('HORIZON_PREFIX', 'horizon-'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Route Middleware
+    |--------------------------------------------------------------------------
+    |
+    | These middleware will get attached onto each Horizon route, giving you
+    | the chance to add your own middleware to this list or change any of
+    | the existing middleware. Or, you can simply stick with this list.
+    |
+    */
+
+    'middleware' => ['web'],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Queue Wait Time Thresholds
+    |--------------------------------------------------------------------------
+    |
+    | This option allows you to configure when the LongWaitDetected event
+    | will be fired. Every connection / queue combination may have its
+    | own, unique threshold (in seconds) before this event is fired.
+    |
+    */
+
+    'waits' => [
+        'redis:feed' => 30,
+        'redis:follow' => 30,
+        'redis:shared' => 30,
+        'redis:default' => 30,
+        'redis:inbox' => 30,
+        'redis:low' => 30,
+        'redis:high' => 30,
+        'redis:delete' => 30,
+        'redis:story' => 30,
+        'redis:mmo' => 30,
+        'redis:intbg' => 30,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Job Trimming Times
+    |--------------------------------------------------------------------------
+    |
+    | Here you can configure for how long (in minutes) you desire Horizon to
+    | persist the recent and failed jobs. Typically, recent jobs are kept
+    | for one hour while all failed jobs are stored for an entire week.
+    |
+    */
+
+    'trim' => [
+        'recent' => 60,
+        'pending' => 60,
+        'completed' => 60,
+        'recent_failed' => 10080,
+        'failed' => 10080,
+        'monitored' => 10080,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Metrics
+    |--------------------------------------------------------------------------
+    |
+    | Here you can configure how many snapshots should be kept to display in
+    | the metrics graph. This will get used in combination with Horizon's
+    | `horizon:snapshot` schedule to define how long to retain metrics.
+    |
+    */
+
+    'metrics' => [
+        'trim_snapshots' => [
+            'job' => 24,
+            'queue' => 24,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Fast Termination
+    |--------------------------------------------------------------------------
+    |
+    | When this option is enabled, Horizon's "terminate" command will not
+    | wait on all of the workers to terminate unless the --wait option
+    | is provided. Fast termination can shorten deployment delay by
+    | allowing a new instance of Horizon to start while the last
+    | instance will continue to terminate each of its workers.
+    |
+    */
+
+    'fast_termination' => false,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Memory Limit (MB)
+    |--------------------------------------------------------------------------
+    |
+    | This value describes the maximum amount of memory the Horizon worker
+    | may consume before it is terminated and restarted. You should set
+    | this value according to the resources available to your server.
+    |
+    */
+
+    'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Queue Worker Configuration
+    |--------------------------------------------------------------------------
+    |
+    | Here you may define the queue worker settings used by your application
+    | in all environments. These supervisors and settings handle all your
+    | queued jobs and will be provisioned by Horizon during deployment.
+    |
+    */
+
+    'environments' => [
+        'production' => [
+            'supervisor-1' => [
+                'connection'    => 'redis',
+                'queue'         => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'],
+                'balance'       => env('HORIZON_BALANCE_STRATEGY', 'auto'),
+                'minProcesses'  => env('HORIZON_MIN_PROCESSES', 1),
+                'maxProcesses'  => env('HORIZON_MAX_PROCESSES', 20),
+                'memory'        => env('HORIZON_SUPERVISOR_MEMORY', 64),
+                'tries'         => env('HORIZON_SUPERVISOR_TRIES', 3),
+                'nice'          => env('HORIZON_SUPERVISOR_NICE', 0),
+                'timeout'       => env('HORIZON_SUPERVISOR_TIMEOUT', 300),
+            ],
+        ],
+
+        'local' => [
+            'supervisor-1' => [
+                'connection'    => 'redis',
+                'queue'         => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'],
+                'balance'       => 'auto',
+                'minProcesses' => 1,
+                'maxProcesses'  => 20,
+                'memory'        => 128,
+                'tries'         => 3,
+                'nice'          => 0,
+                'timeout'       => 300
+            ],
+        ],
+    ],
+
+    'darkmode' => env('HORIZON_DARKMODE', false),
 ];