mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-17 20:11:27 +00:00
commit
c33e5b0777
10 changed files with 286 additions and 2 deletions
|
@ -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
app/Http/Controllers/Api/ApiController.php
Normal file
38
app/Http/Controllers/Api/ApiController.php
Normal file
|
@ -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
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
147
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
app/Http/Middleware/Api/Admin.php
Normal file
26
app/Http/Middleware/Api/Admin.php
Normal file
|
@ -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
app/Http/Resources/MastoApi/Admin/DomainBlockResource.php
Normal file
42
app/Http/Resources/MastoApi/Admin/DomainBlockResource.php
Normal file
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue