Browse Source

Merge pull request #5248 from pixelfed/staging

Staging
daniel 11 months ago
parent
commit
c33e5b0777

+ 5 - 0
CHANGELOG.md

@@ -2,12 +2,17 @@
 
 ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
 
+### Added
+- Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
+
 ### Updates
 - Update ApiV1Controller, add support for notification filter types ([f61159a1](https://github.com/pixelfed/pixelfed/commit/f61159a1))
 - Update ApiV1Dot1Controller, fix mutual api ([a8bb97b2](https://github.com/pixelfed/pixelfed/commit/a8bb97b2))
 - Update ApiV1Controller, fix /api/v1/favourits pagination ([72f68160](https://github.com/pixelfed/pixelfed/commit/72f68160))
 - Update RegisterController, update username constraints, require atleast one alpha char ([dd6e3cc2](https://github.com/pixelfed/pixelfed/commit/dd6e3cc2))
 - Update AdminUser, fix entity casting ([cb5620d4](https://github.com/pixelfed/pixelfed/commit/cb5620d4))
+- Update instance config, update network cache feed max_hours_old falloff to 90 days instead of 6 hours to allow for less active instances to have more results ([c042d135](https://github.com/pixelfed/pixelfed/commit/c042d135))
+- Update ApiV1Dot1Controller, add new single media status create endpoint ([b03f5cec](https://github.com/pixelfed/pixelfed/commit/b03f5cec))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 -  ([](https://github.com/pixelfed/pixelfed/commit/))
 

+ 38 - 0
app/Http/Controllers/Api/ApiController.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+
+class ApiController extends Controller {
+  public function json($res, $headers = [], $code = 200) {
+    return response()->json($res, $code, $this->filterHeaders($headers), JSON_UNESCAPED_SLASHES);
+  }
+
+  public function linksForCollection($paginator) {
+    $link = null;
+
+    if ($paginator->onFirstPage()) {
+      if ($paginator->hasMorePages()) {
+          $link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
+      }
+    } else {
+      if ($paginator->previousPageUrl()) {
+          $link = '<'.$paginator->previousPageUrl().'>; rel="next"';
+      }
+
+      if ($paginator->hasMorePages()) {
+          $link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
+      }
+    }
+
+    return $link;
+  }
+
+  private function filterHeaders($headers) {
+    return array_filter($headers, function($v, $k) {
+      return $v != null;
+    }, ARRAY_FILTER_USE_BOTH);
+  }
+}

+ 147 - 0
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace App\Http\Controllers\Api\V1\Admin;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use App\Http\Controllers\Api\ApiController;
+use App\Instance;
+use App\Services\InstanceService;
+use App\Http\Resources\MastoApi\Admin\DomainBlockResource;
+
+class DomainBlocksController extends ApiController {
+
+  public function __construct() {
+    $this->middleware(['auth:api', 'api.admin', 'scope:admin:read,admin:read:domain_blocks'])->only(['index', 'show']);
+    $this->middleware(['auth:api', 'api.admin', 'scope:admin:write,admin:write:domain_blocks'])->only(['create', 'update', 'delete']);
+  }
+
+  public function index(Request $request) {
+    $this->validate($request, [
+      'limit' => 'sometimes|integer|max:100|min:1',
+    ]);
+
+    $limit = $request->input('limit', 100);
+
+    $res = Instance::moderated()
+      ->orderBy('id')
+      ->cursorPaginate($limit)
+      ->withQueryString();
+
+    return $this->json(DomainBlockResource::collection($res), [
+      'Link' => $this->linksForCollection($res)
+    ]);
+  }
+
+  public function show(Request $request, $id) {
+    $domain_block = Instance::moderated()->find($id);
+
+    if (!$domain_block) {
+      return $this->json([ 'error' => 'Record not found'], [], 404);
+    }
+
+    return $this->json(new DomainBlockResource($domain_block));
+  }
+
+  public function create(Request $request) {
+    $this->validate($request, [
+      'domain' => 'required|string|min:1|max:120',
+      'severity' => [
+        'sometimes',
+        Rule::in(['noop', 'silence', 'suspend'])
+      ],
+      'reject_media' => 'sometimes|required|boolean',
+      'reject_reports' => 'sometimes|required|boolean',
+      'private_comment' => 'sometimes|string|min:1|max:1000',
+      'public_comment' => 'sometimes|string|min:1|max:1000',
+      'obfuscate' => 'sometimes|required|boolean'
+    ]);
+
+    $domain = $request->input('domain');
+    $severity = $request->input('severity', 'silence');
+    $private_comment = $request->input('private_comment');
+
+    abort_if(!strpos($domain, '.'), 400, 'Invalid domain');
+    abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain');
+
+    // This is because Pixelfed can't currently support wildcard domain blocks
+    // We have to find something that could plausibly be an instance
+    $parts = explode('.', $domain);
+    if ($parts[0] == '*') {
+      // If we only have two parts, e.g., "*", "example", then we want to fail:
+      abort_if(count($parts) <= 2, 400, 'Invalid domain: This API does not support wildcard domain blocks yet');
+
+      // Otherwise we convert the *.foo.example to foo.example
+      $domain = implode('.', array_slice($parts, 1));
+    }
+
+    // Double check we definitely haven't let anything through:
+    abort_if(str_contains($domain, '*'), 400, 'Invalid domain');
+
+    $existing_domain_block = Instance::moderated()->whereDomain($domain)->first();
+
+    if ($existing_domain_block) {
+      return $this->json([
+        'error' => 'A domain block already exists for this domain',
+        'existing_domain_block' => new DomainBlockResource($existing_domain_block)
+      ], [], 422);
+    }
+
+    $domain_block = Instance::updateOrCreate(
+      [ 'domain' => $domain ],
+      [ 'banned' => $severity === 'suspend', 'unlisted' => $severity === 'silence', 'notes' => [$private_comment]]
+    );
+
+    InstanceService::refresh();
+
+    return $this->json(new DomainBlockResource($domain_block));
+  }
+
+  public function update(Request $request, $id) {
+    $this->validate($request, [
+      'severity' => [
+        'sometimes',
+        Rule::in(['noop', 'silence', 'suspend'])
+      ],
+      'reject_media' => 'sometimes|required|boolean',
+      'reject_reports' => 'sometimes|required|boolean',
+      'private_comment' => 'sometimes|string|min:1|max:1000',
+      'public_comment' => 'sometimes|string|min:1|max:1000',
+      'obfuscate' => 'sometimes|required|boolean'
+    ]);
+
+    $severity = $request->input('severity', 'silence');
+    $private_comment = $request->input('private_comment');
+
+    $domain_block = Instance::moderated()->find($id);
+
+    if (!$domain_block) {
+      return $this->json([ 'error' => 'Record not found'], [], 404);
+    }
+
+    $domain_block->banned = $severity === 'suspend';
+    $domain_block->unlisted = $severity === 'silence';
+    $domain_block->notes = [$private_comment];
+    $domain_block->save();
+
+    InstanceService::refresh();
+
+    return $this->json(new DomainBlockResource($domain_block));
+  }
+
+  public function delete(Request $request, $id) {
+    $domain_block = Instance::moderated()->find($id);
+
+    if (!$domain_block) {
+      return $this->json([ 'error' => 'Record not found'], [], 404);
+    }
+
+    $domain_block->banned = false;
+    $domain_block->unlisted = false;
+    $domain_block->save();
+
+    InstanceService::refresh();
+
+    return $this->json(null, [], 200);
+  }
+}

+ 3 - 0
app/Http/Kernel.php

@@ -54,6 +54,7 @@ class Kernel extends HttpKernel
      * @var array
      */
     protected $routeMiddleware = [
+        'api.admin'     => \App\Http\Middleware\Api\Admin::class,
         'admin'         => \App\Http\Middleware\Admin::class,
         'auth'          => \Illuminate\Auth\Middleware\Authenticate::class,
         'auth.basic'    => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
@@ -68,6 +69,8 @@ class Kernel extends HttpKernel
         'twofactor'     => \App\Http\Middleware\TwoFactorAuth::class,
         'validemail'    => \App\Http\Middleware\EmailVerificationCheck::class,
         'interstitial'  => \App\Http\Middleware\AccountInterstitial::class,
+        'scopes'        => \Laravel\Passport\Http\Middleware\CheckScopes::class,
+        'scope'         => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
         // 'restricted'    => \App\Http\Middleware\RestrictedAccess::class,
     ];
 }

+ 26 - 0
app/Http/Middleware/Api/Admin.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Http\Middleware\Api;
+
+use Auth;
+use Closure;
+
+class Admin
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param \Closure                 $next
+     *
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        if (Auth::check() == false || Auth::user()->is_admin == false) {
+          return abort(403, "You must be an administrator to do that");
+        }
+
+        return $next($request);
+    }
+}

+ 42 - 0
app/Http/Resources/MastoApi/Admin/DomainBlockResource.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace App\Http\Resources\MastoApi\Admin;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class DomainBlockResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        $severity = 'noop';
+        if ($this->banned) {
+            $severity = 'suspend';
+        } else if ($this->unlisted) {
+            $severity = 'silence';
+        }
+
+        return [
+            'id' => $this->id,
+            'domain' => $this->domain,
+            // This property is coming in Mastodon 4.3, although it'll only be
+            // useful if Pixelfed supports obfuscating domains:
+            'digest' => hash('sha256', $this->domain),
+            'severity' => $severity,
+            // Using the updated_at value as this is going to be the closest to
+            // when the domain was banned
+            'created_at' => $this->updated_at,
+            // We don't have data for these fields
+            'reject_media' => false,
+            'reject_reports' => false,
+            'private_comment' => $this->notes ? join('; ', $this->notes) : null,
+            'public_comment' => $this->limit_reason,
+            'obfuscate' => false
+        ];
+    }
+}

+ 7 - 0
app/Instance.php

@@ -22,6 +22,13 @@ class Instance extends Model
         'notes'
     ];
 
+    // To get all moderated instances, we need to search where (banned OR unlisted)
+    public function scopeModerated($query): void {
+        $query->where(function ($query) {
+            $query->where('banned', true)->orWhere('unlisted', true);
+        });
+    }
+
     public function profiles()
     {
         return $this->hasMany(Profile::class, 'domain', 'domain');

+ 6 - 0
app/Providers/AppServiceProvider.php

@@ -5,6 +5,7 @@ namespace App\Providers;
 use App\Observers\{
 	AvatarObserver,
 	FollowerObserver,
+	HashtagFollowObserver,
 	LikeObserver,
 	NotificationObserver,
 	ModLogObserver,
@@ -17,6 +18,7 @@ use App\Observers\{
 use App\{
 	Avatar,
 	Follower,
+	HashtagFollow,
 	Like,
 	Notification,
 	ModLog,
@@ -32,6 +34,7 @@ use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Facades\Validator;
+use Illuminate\Database\Eloquent\Model;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -50,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
 		Paginator::useBootstrap();
 		Avatar::observe(AvatarObserver::class);
 		Follower::observe(FollowerObserver::class);
+		HashtagFollow::observe(HashtagFollowObserver::class);
 		Like::observe(LikeObserver::class);
 		Notification::observe(NotificationObserver::class);
 		ModLog::observe(ModLogObserver::class);
@@ -62,6 +66,8 @@ class AppServiceProvider extends ServiceProvider
 			return Auth::check() && $request->user()->is_admin;
 		});
 		Validator::includeUnvalidatedArrayKeys();
+
+		// Model::preventLazyLoading(true);
 	}
 
 	/**

+ 4 - 2
app/Providers/AuthServiceProvider.php

@@ -24,7 +24,7 @@ class AuthServiceProvider extends ServiceProvider
      */
     public function boot()
     {
-        if (config('app.env') === 'production' && (bool) config_cache('pixelfed.oauth_enabled') == true) {
+        if(config('pixelfed.oauth_enabled') == true) {
             Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
             Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
             Passport::enableImplicitGrant();
@@ -37,8 +37,10 @@ class AuthServiceProvider extends ServiceProvider
                 'write' => 'Full write access to your account',
                 'follow' => 'Ability to follow other profiles',
                 'admin:read' => 'Read all data on the server',
+                'admin:read:domain_blocks' => 'Read sensitive information of all domain blocks',
                 'admin:write' => 'Modify all data on the server',
-                'push' => 'Receive your push notifications',
+                'admin:write:domain_blocks' => 'Perform moderation actions on domain blocks',
+                'push'  => 'Receive your push notifications'
             ]);
 
             Passport::setDefaultScope([

+ 8 - 0
routes/api.php

@@ -169,6 +169,14 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 
         Route::get('statuses/{id}/history', 'StatusEditController@history')->middleware($middleware);
         Route::put('statuses/{id}', 'StatusEditController@store')->middleware($middleware);
+
+        Route::group(['prefix' => 'admin'], function() use($middleware) {
+            Route::get('domain_blocks', 'Api\V1\Admin\DomainBlocksController@index')->middleware($middleware);
+            Route::post('domain_blocks', 'Api\V1\Admin\DomainBlocksController@create')->middleware($middleware);
+            Route::get('domain_blocks/{id}', 'Api\V1\Admin\DomainBlocksController@show')->middleware($middleware);
+            Route::put('domain_blocks/{id}', 'Api\V1\Admin\DomainBlocksController@update')->middleware($middleware);
+            Route::delete('domain_blocks/{id}', 'Api\V1\Admin\DomainBlocksController@delete')->middleware($middleware);
+        })->middleware($middleware);
     });
 
     Route::group(['prefix' => 'v2'], function() use($middleware) {