From 652654e24fe21a50ad01e044bc003a55442e34be Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Fri, 29 Mar 2024 22:48:22 +0100 Subject: [PATCH] WIP: Implement domain blocks --- app/Http/Controllers/Api/ApiController.php | 38 ++++++++++ .../Api/V1/Admin/DomainBlocksController.php | 76 +++++++++++++++++++ .../MastoApi/Admin/DomainBlockResource.php | 39 ++++++++++ app/Instance.php | 7 ++ routes/api.php | 6 ++ 5 files changed, 166 insertions(+) create mode 100644 app/Http/Controllers/Api/ApiController.php create mode 100644 app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php create mode 100644 app/Http/Resources/MastoApi/Admin/DomainBlockResource.php diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php new file mode 100644 index 000000000..76d9d7db2 --- /dev/null +++ b/app/Http/Controllers/Api/ApiController.php @@ -0,0 +1,38 @@ +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); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php b/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php new file mode 100644 index 000000000..f88da495a --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php @@ -0,0 +1,76 @@ +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) { + $res = Instance::moderated() + ->findOrFail($id); + + return $this->json(new DomainBlockResource($res)); + } + + 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'); + $private_comment = $request->input('private_comment'); + + abort_if(!strpos($domain, '.'), 400, 'Invalid domain'); + abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain'); + + $existing = Instance::moderated()->whereDomain($domain)->first(); + + if ($existing) { + return $this->json([ + 'error' => 'A domain block already exists for this domain', + 'existing_domain_block' => new DomainBlockResource($existing) + ], [], 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)); + } +} \ No newline at end of file diff --git a/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php b/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php new file mode 100644 index 000000000..a2056c94b --- /dev/null +++ b/app/Http/Resources/MastoApi/Admin/DomainBlockResource.php @@ -0,0 +1,39 @@ + + */ + 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, + '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 + ]; + } +} diff --git a/app/Instance.php b/app/Instance.php index 77752d498..a93d9e95e 100644 --- a/app/Instance.php +++ b/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'); diff --git a/routes/api.php b/routes/api.php index af40e27bc..95440d3c0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -101,6 +101,12 @@ 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); + })->middleware($middleware); }); Route::group(['prefix' => 'v2'], function() use($middleware) {