mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-29 01:33:16 +00:00
commit
ffd8815ee8
11 changed files with 1312 additions and 397 deletions
|
@ -3,18 +3,20 @@
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\AccountInterstitial;
|
use App\AccountInterstitial;
|
||||||
use App\Http\Resources\AdminReport;
|
use App\Http\Resources\Admin\AdminModeratedProfileResource;
|
||||||
use App\Http\Resources\AdminRemoteReport;
|
use App\Http\Resources\AdminRemoteReport;
|
||||||
|
use App\Http\Resources\AdminReport;
|
||||||
use App\Http\Resources\AdminSpamReport;
|
use App\Http\Resources\AdminSpamReport;
|
||||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||||
use App\Jobs\StatusPipeline\StatusDelete;
|
use App\Jobs\StatusPipeline\StatusDelete;
|
||||||
use App\Jobs\StoryPipeline\StoryDelete;
|
use App\Jobs\StoryPipeline\StoryDelete;
|
||||||
|
use App\Models\ModeratedProfile;
|
||||||
|
use App\Models\RemoteReport;
|
||||||
use App\Notification;
|
use App\Notification;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
use App\Report;
|
use App\Report;
|
||||||
use App\Models\RemoteReport;
|
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
use App\Services\ModLogService;
|
use App\Services\ModLogService;
|
||||||
use App\Services\NetworkTimelineService;
|
use App\Services\NetworkTimelineService;
|
||||||
|
@ -24,12 +26,13 @@ use App\Services\StatusService;
|
||||||
use App\Status;
|
use App\Status;
|
||||||
use App\Story;
|
use App\Story;
|
||||||
use App\User;
|
use App\User;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
use Cache;
|
use Cache;
|
||||||
use Storage;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Storage;
|
||||||
|
|
||||||
trait AdminReportController
|
trait AdminReportController
|
||||||
{
|
{
|
||||||
|
@ -201,10 +204,7 @@ trait AdminReportController
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render() {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -829,6 +829,16 @@ trait AdminReportController
|
||||||
$profile->cw = true;
|
$profile->cw = true;
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
|
||||||
|
if ($profile->remote_url) {
|
||||||
|
ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'is_nsfw' => true,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
|
foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
|
||||||
$status->is_nsfw = true;
|
$status->is_nsfw = true;
|
||||||
$status->save();
|
$status->save();
|
||||||
|
@ -879,6 +889,16 @@ trait AdminReportController
|
||||||
$profile->unlisted = true;
|
$profile->unlisted = true;
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
|
||||||
|
if ($profile->remote_url) {
|
||||||
|
ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'is_unlisted' => true,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
|
foreach (Status::whereProfileId($profile->id)->whereScope('public')->cursor() as $status) {
|
||||||
$status->scope = 'unlisted';
|
$status->scope = 'unlisted';
|
||||||
$status->visibility = 'unlisted';
|
$status->visibility = 'unlisted';
|
||||||
|
@ -929,6 +949,16 @@ trait AdminReportController
|
||||||
$profile->unlisted = true;
|
$profile->unlisted = true;
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
|
||||||
|
if ($profile->remote_url) {
|
||||||
|
ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'is_unlisted' => true,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
|
foreach (Status::whereProfileId($profile->id)->cursor() as $status) {
|
||||||
$status->scope = 'private';
|
$status->scope = 'private';
|
||||||
$status->visibility = 'private';
|
$status->visibility = 'private';
|
||||||
|
@ -982,6 +1012,16 @@ trait AdminReportController
|
||||||
|
|
||||||
$ts = now()->addMonth();
|
$ts = now()->addMonth();
|
||||||
|
|
||||||
|
if ($profile->remote_url) {
|
||||||
|
ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'is_banned' => true,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($profile->user_id) {
|
if ($profile->user_id) {
|
||||||
$user = $profile->user;
|
$user = $profile->user;
|
||||||
abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
|
abort_if($user->is_admin, 403, 'You cannot delete admin accounts.');
|
||||||
|
@ -1354,7 +1394,7 @@ trait AdminReportController
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'id' => 'required|exists:remote_reports,id',
|
'id' => 'required|exists:remote_reports,id',
|
||||||
'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts'
|
'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$report = RemoteReport::findOrFail($request->input('id'));
|
$report = RemoteReport::findOrFail($request->input('id'));
|
||||||
|
@ -1373,11 +1413,11 @@ trait AdminReportController
|
||||||
break;
|
break;
|
||||||
case 'cw-posts':
|
case 'cw-posts':
|
||||||
$statuses = Status::find($report->status_ids);
|
$statuses = Status::find($report->status_ids);
|
||||||
foreach($statuses as $status) {
|
foreach ($statuses as $status) {
|
||||||
if($report->account_id != $status->profile_id) {
|
if ($report->account_id != $status->profile_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(!$status->is_nsfw) {
|
if (! $status->is_nsfw) {
|
||||||
$ogNonCwStatuses[] = $status->id;
|
$ogNonCwStatuses[] = $status->id;
|
||||||
}
|
}
|
||||||
$status->is_nsfw = true;
|
$status->is_nsfw = true;
|
||||||
|
@ -1388,11 +1428,11 @@ trait AdminReportController
|
||||||
$report->save();
|
$report->save();
|
||||||
break;
|
break;
|
||||||
case 'cw-all-posts':
|
case 'cw-all-posts':
|
||||||
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
if($status->is_nsfw || $status->reblog_of_id) {
|
if ($status->is_nsfw || $status->reblog_of_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(!$status->is_nsfw) {
|
if (! $status->is_nsfw) {
|
||||||
$ogNonCwStatuses[] = $status->id;
|
$ogNonCwStatuses[] = $status->id;
|
||||||
}
|
}
|
||||||
$status->is_nsfw = true;
|
$status->is_nsfw = true;
|
||||||
|
@ -1402,11 +1442,11 @@ trait AdminReportController
|
||||||
break;
|
break;
|
||||||
case 'unlist-posts':
|
case 'unlist-posts':
|
||||||
$statuses = Status::find($report->status_ids);
|
$statuses = Status::find($report->status_ids);
|
||||||
foreach($statuses as $status) {
|
foreach ($statuses as $status) {
|
||||||
if($report->account_id != $status->profile_id) {
|
if ($report->account_id != $status->profile_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if($status->scope === 'public') {
|
if ($status->scope === 'public') {
|
||||||
$ogPublicStatuses[] = $status->id;
|
$ogPublicStatuses[] = $status->id;
|
||||||
$status->scope = 'unlisted';
|
$status->scope = 'unlisted';
|
||||||
$status->visibility = 'unlisted';
|
$status->visibility = 'unlisted';
|
||||||
|
@ -1418,8 +1458,8 @@ trait AdminReportController
|
||||||
$report->save();
|
$report->save();
|
||||||
break;
|
break;
|
||||||
case 'unlist-all-posts':
|
case 'unlist-all-posts':
|
||||||
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
if($status->visibility !== 'public' || $status->reblog_of_id) {
|
if ($status->visibility !== 'public' || $status->reblog_of_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$ogPublicStatuses[] = $status->id;
|
$ogPublicStatuses[] = $status->id;
|
||||||
|
@ -1431,12 +1471,12 @@ trait AdminReportController
|
||||||
break;
|
break;
|
||||||
case 'private-posts':
|
case 'private-posts':
|
||||||
$statuses = Status::find($report->status_ids);
|
$statuses = Status::find($report->status_ids);
|
||||||
foreach($statuses as $status) {
|
foreach ($statuses as $status) {
|
||||||
if($report->account_id != $status->profile_id) {
|
if ($report->account_id != $status->profile_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
if (in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||||
if($status->scope === 'public') {
|
if ($status->scope === 'public') {
|
||||||
$ogPublicStatuses[] = $status->id;
|
$ogPublicStatuses[] = $status->id;
|
||||||
}
|
}
|
||||||
$status->scope = 'private';
|
$status->scope = 'private';
|
||||||
|
@ -1449,13 +1489,13 @@ trait AdminReportController
|
||||||
$report->save();
|
$report->save();
|
||||||
break;
|
break;
|
||||||
case 'private-all-posts':
|
case 'private-all-posts':
|
||||||
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
foreach (Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
|
||||||
if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
|
if (! in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if($status->visibility === 'public') {
|
if ($status->visibility === 'public') {
|
||||||
$ogPublicStatuses[] = $status->id;
|
$ogPublicStatuses[] = $status->id;
|
||||||
} else if($status->visibility === 'unlisted') {
|
} elseif ($status->visibility === 'unlisted') {
|
||||||
$ogUnlistedStatuses[] = $status->id;
|
$ogUnlistedStatuses[] = $status->id;
|
||||||
}
|
}
|
||||||
$status->visibility = 'private';
|
$status->visibility = 'private';
|
||||||
|
@ -1466,8 +1506,8 @@ trait AdminReportController
|
||||||
break;
|
break;
|
||||||
case 'delete-posts':
|
case 'delete-posts':
|
||||||
$statuses = Status::find($report->status_ids);
|
$statuses = Status::find($report->status_ids);
|
||||||
foreach($statuses as $status) {
|
foreach ($statuses as $status) {
|
||||||
if($report->account_id != $status->profile_id) {
|
if ($report->account_id != $status->profile_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
StatusDelete::dispatch($status);
|
StatusDelete::dispatch($status);
|
||||||
|
@ -1484,16 +1524,16 @@ trait AdminReportController
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($ogPublicStatuses && count($ogPublicStatuses)) {
|
if ($ogPublicStatuses && count($ogPublicStatuses)) {
|
||||||
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||||
}
|
}
|
||||||
|
|
||||||
if($ogNonCwStatuses && count($ogNonCwStatuses)) {
|
if ($ogNonCwStatuses && count($ogNonCwStatuses)) {
|
||||||
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||||
}
|
}
|
||||||
|
|
||||||
if($ogUnlistedStatuses && count($ogUnlistedStatuses)) {
|
if ($ogUnlistedStatuses && count($ogUnlistedStatuses)) {
|
||||||
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
Storage::disk('local')->put('mod-log-cache/'.$report->account_id.'/'.now()->format('Y-m-d').'-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||||
}
|
}
|
||||||
|
|
||||||
ModLogService::boot()
|
ModLogService::boot()
|
||||||
|
@ -1504,18 +1544,210 @@ trait AdminReportController
|
||||||
->action('admin.report.moderate')
|
->action('admin.report.moderate')
|
||||||
->metadata([
|
->metadata([
|
||||||
'action' => $request->input('action'),
|
'action' => $request->input('action'),
|
||||||
'duration_active' => now()->parse($report->created_at)->diffForHumans()
|
'duration_active' => now()->parse($report->created_at)->diffForHumans(),
|
||||||
])
|
])
|
||||||
->accessLevel('admin')
|
->accessLevel('admin')
|
||||||
->save();
|
->save();
|
||||||
|
|
||||||
if($report->status_ids) {
|
if ($report->status_ids) {
|
||||||
foreach($report->status_ids as $sid) {
|
foreach ($report->status_ids as $sid) {
|
||||||
RemoteReport::whereNull('action_taken_at')
|
RemoteReport::whereNull('action_taken_at')
|
||||||
->whereJsonContains('status_ids', [$sid])
|
->whereJsonContains('status_ids', [$sid])
|
||||||
->update(['action_taken_at' => now()]);
|
->update(['action_taken_at' => now()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [200];
|
return [200];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getModeratedProfiles(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'search' => 'sometimes|string|min:3|max:120',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$query = '%'.$request->input('search').'%';
|
||||||
|
$profiles = DB::table('moderated_profiles')
|
||||||
|
->join('profiles', 'moderated_profiles.profile_id', '=', 'profiles.id')
|
||||||
|
->where('profiles.username', 'LIKE', $query)
|
||||||
|
->select('moderated_profiles.*', 'profiles.username')
|
||||||
|
->orderByDesc('moderated_profiles.id')
|
||||||
|
->cursorPaginate(10);
|
||||||
|
|
||||||
|
return AdminModeratedProfileResource::collection($profiles);
|
||||||
|
}
|
||||||
|
$profiles = ModeratedProfile::orderByDesc('id')->cursorPaginate(10);
|
||||||
|
|
||||||
|
return AdminModeratedProfileResource::collection($profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModeratedProfile(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile = ModeratedProfile::findOrFail($request->input('id'));
|
||||||
|
|
||||||
|
return new AdminModeratedProfileResource($profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportModeratedProfiles(Request $request)
|
||||||
|
{
|
||||||
|
return response()->streamDownload(function () {
|
||||||
|
$profiles = ModeratedProfile::get();
|
||||||
|
$res = AdminModeratedProfileResource::collection($profiles);
|
||||||
|
echo json_encode([
|
||||||
|
'_pixelfed_export' => true,
|
||||||
|
'meta' => [
|
||||||
|
'ns' => 'https://pixelfed.org',
|
||||||
|
'origin' => config('pixelfed.domain.app'),
|
||||||
|
'date' => now()->format('c'),
|
||||||
|
'type' => 'moderated-profiles',
|
||||||
|
'version' => "1.0"
|
||||||
|
],
|
||||||
|
'data' => $res
|
||||||
|
], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||||
|
}, 'data-export.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteModeratedProfile(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile = ModeratedProfile::findOrFail($request->input('id'));
|
||||||
|
|
||||||
|
ModLogService::boot()
|
||||||
|
->objectUid($profile->profile_id)
|
||||||
|
->objectId($profile->id)
|
||||||
|
->objectType('App\Models\ModeratedProfile::class')
|
||||||
|
->user(request()->user())
|
||||||
|
->action('admin.moderated-profiles.delete')
|
||||||
|
->metadata([
|
||||||
|
'profile_url' => $profile->profile_url,
|
||||||
|
'profile_id' => $profile->profile_id,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
'note' => $profile->note,
|
||||||
|
'is_banned' => $profile->is_banned,
|
||||||
|
'is_nsfw' => $profile->is_nsfw,
|
||||||
|
'is_unlisted' => $profile->is_unlisted,
|
||||||
|
'is_noautolink' => $profile->is_noautolink,
|
||||||
|
'is_nodms' => $profile->is_nodms,
|
||||||
|
'is_notrending' => $profile->is_notrending,
|
||||||
|
])
|
||||||
|
->accessLevel('admin')
|
||||||
|
->save();
|
||||||
|
|
||||||
|
$profile->delete();
|
||||||
|
|
||||||
|
return ['status' => 200, 'message' => 'Successfully deleted moderated profile!'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateModeratedProfile(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required|exists:moderated_profiles',
|
||||||
|
'note' => 'sometimes|nullable|string|max:500',
|
||||||
|
'is_banned' => 'required|boolean',
|
||||||
|
'is_noautolink' => 'required|boolean',
|
||||||
|
'is_nodms' => 'required|boolean',
|
||||||
|
'is_notrending' => 'required|boolean',
|
||||||
|
'is_nsfw' => 'required|boolean',
|
||||||
|
'is_unlisted' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'note',
|
||||||
|
'is_banned',
|
||||||
|
'is_noautolink',
|
||||||
|
'is_nodms',
|
||||||
|
'is_notrending',
|
||||||
|
'is_nsfw',
|
||||||
|
'is_unlisted',
|
||||||
|
];
|
||||||
|
|
||||||
|
$profile = ModeratedProfile::findOrFail($request->input('id'));
|
||||||
|
$profile->update($request->only($fields));
|
||||||
|
|
||||||
|
ModLogService::boot()
|
||||||
|
->objectUid($profile->profile_id)
|
||||||
|
->objectId($profile->id)
|
||||||
|
->objectType('App\Models\ModeratedProfile::class')
|
||||||
|
->user(request()->user())
|
||||||
|
->action('admin.moderated-profiles.update')
|
||||||
|
->metadata($request->only($fields))
|
||||||
|
->accessLevel('admin')
|
||||||
|
->save();
|
||||||
|
|
||||||
|
return [200];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createModeratedProfile(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'url' => 'required|url|starts_with:https://',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = $request->input('url');
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
|
||||||
|
abort_if($host === config('pixelfed.domain.app'), 400, 'You cannot add local users!');
|
||||||
|
|
||||||
|
$exists = ModeratedProfile::whereProfileUrl($url)->exists();
|
||||||
|
abort_if($exists, 400, 'Moderated profile already exists!');
|
||||||
|
|
||||||
|
$profile = Profile::whereRemoteUrl($url)->first();
|
||||||
|
|
||||||
|
if ($profile) {
|
||||||
|
$rec = ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ModLogService::boot()
|
||||||
|
->objectUid($rec->profile_id)
|
||||||
|
->objectId($rec->id)
|
||||||
|
->objectType('App\Models\ModeratedProfile::class')
|
||||||
|
->user(request()->user())
|
||||||
|
->action('admin.moderated-profiles.create')
|
||||||
|
->metadata([
|
||||||
|
'profile_existed' => true,
|
||||||
|
])
|
||||||
|
->accessLevel('admin')
|
||||||
|
->save();
|
||||||
|
|
||||||
|
return $rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteSearch = Helpers::profileFetch($url);
|
||||||
|
|
||||||
|
if ($remoteSearch) {
|
||||||
|
$rec = ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_id' => $remoteSearch->id,
|
||||||
|
], [
|
||||||
|
'profile_url' => $remoteSearch->remote_url,
|
||||||
|
'domain' => $remoteSearch->domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ModLogService::boot()
|
||||||
|
->objectUid($rec->profile_id)
|
||||||
|
->objectId($rec->id)
|
||||||
|
->objectType('App\Models\ModeratedProfile::class')
|
||||||
|
->user(request()->user())
|
||||||
|
->action('admin.moderated-profiles.create')
|
||||||
|
->metadata([
|
||||||
|
'profile_existed' => false,
|
||||||
|
])
|
||||||
|
->accessLevel('admin')
|
||||||
|
->save();
|
||||||
|
|
||||||
|
return $rec;
|
||||||
|
}
|
||||||
|
abort(400, 'Invalid account');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
45
app/Http/Resources/Admin/AdminModeratedProfileResource.php
Normal file
45
app/Http/Resources/Admin/AdminModeratedProfileResource.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Admin;
|
||||||
|
|
||||||
|
use App\Profile;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class AdminModeratedProfileResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$profileObj = [];
|
||||||
|
$profile = Profile::withTrashed()->find($this->profile_id);
|
||||||
|
if ($profile) {
|
||||||
|
$profileObj = [
|
||||||
|
'name' => $profile->name,
|
||||||
|
'username' => $profile->username,
|
||||||
|
'username_str' => explode('@', $profile->username)[1],
|
||||||
|
'remote_url' => $profile->remote_url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'domain' => $this->domain,
|
||||||
|
'profile' => $profileObj,
|
||||||
|
'profile_id' => $this->profile_id,
|
||||||
|
'profile_url' => $this->profile_url,
|
||||||
|
'note' => $this->note,
|
||||||
|
'is_banned' => (bool) $this->is_banned,
|
||||||
|
'is_nsfw' => (bool) $this->is_nsfw,
|
||||||
|
'is_unlisted' => (bool) $this->is_unlisted,
|
||||||
|
'is_noautolink' => (bool) $this->is_noautolink,
|
||||||
|
'is_nodms' => (bool) $this->is_nodms,
|
||||||
|
'is_notrending' => (bool) $this->is_notrending,
|
||||||
|
'created_at' => now()->parse($this->created_at)->format('c'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
use App\Services\StatusService;
|
use App\Services\StatusService;
|
||||||
|
use App\Story;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
class AdminReport extends JsonResource
|
class AdminReport extends JsonResource
|
||||||
{
|
{
|
||||||
|
@ -16,22 +17,29 @@ class AdminReport extends JsonResource
|
||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
$res = [
|
$res = [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'reporter' => AccountService::get($this->profile_id, true),
|
'reporter' => AccountService::get($this->profile_id, true),
|
||||||
'type' => $this->type,
|
'type' => $this->type,
|
||||||
'object_id' => (string) $this->object_id,
|
'object_id' => (string) $this->object_id,
|
||||||
'object_type' => $this->object_type,
|
'object_type' => $this->object_type,
|
||||||
'reported' => AccountService::get($this->reported_profile_id, true),
|
'reported' => AccountService::get($this->reported_profile_id, true),
|
||||||
'status' => null,
|
'status' => null,
|
||||||
'reporter_message' => $this->message,
|
'reporter_message' => $this->message,
|
||||||
'admin_seen_at' => $this->admin_seen,
|
'admin_seen_at' => $this->admin_seen,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
];
|
];
|
||||||
|
|
||||||
if($this->object_id && $this->object_type === 'App\Status') {
|
if ($this->object_id && $this->object_type === 'App\Status') {
|
||||||
$res['status'] = StatusService::get($this->object_id, false);
|
$res['status'] = StatusService::get($this->object_id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->object_id && $this->object_type === 'App\Story') {
|
||||||
|
$story = Story::find($this->object_id);
|
||||||
|
if ($story) {
|
||||||
|
$res['story'] = $story->toAdminEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
13
app/Models/ModeratedProfile.php
Normal file
13
app/Models/ModeratedProfile.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ModeratedProfile extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $guarded = [];
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ use App\Jobs\MediaPipeline\MediaStoragePipeline;
|
||||||
use App\Jobs\StatusPipeline\StatusReplyPipeline;
|
use App\Jobs\StatusPipeline\StatusReplyPipeline;
|
||||||
use App\Jobs\StatusPipeline\StatusTagsPipeline;
|
use App\Jobs\StatusPipeline\StatusTagsPipeline;
|
||||||
use App\Media;
|
use App\Media;
|
||||||
|
use App\Models\ModeratedProfile;
|
||||||
use App\Models\Poll;
|
use App\Models\Poll;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
use App\Services\Account\AccountStatService;
|
use App\Services\Account\AccountStatService;
|
||||||
|
@ -814,6 +815,11 @@ class Helpers
|
||||||
if (! self::validateUrl($res['id'])) {
|
if (! self::validateUrl($res['id'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ModeratedProfile::whereProfileUrl($res['id'])->whereIsBanned(true)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$urlDomain = parse_url($url, PHP_URL_HOST);
|
$urlDomain = parse_url($url, PHP_URL_HOST);
|
||||||
$domain = parse_url($res['id'], PHP_URL_HOST);
|
$domain = parse_url($res['id'], PHP_URL_HOST);
|
||||||
if (strtolower($urlDomain) !== strtolower($domain)) {
|
if (strtolower($urlDomain) !== strtolower($domain)) {
|
||||||
|
|
678
composer.lock
generated
678
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use App\Profile;
|
||||||
|
use App\ModLog;
|
||||||
|
use App\Models\ModeratedProfile;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('moderated_profiles', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('profile_url')->unique()->nullable()->index();
|
||||||
|
$table->unsignedBigInteger('profile_id')->unique()->nullable();
|
||||||
|
$table->string('domain')->nullable();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->boolean('is_banned')->default(false);
|
||||||
|
$table->boolean('is_nsfw')->default(false);
|
||||||
|
$table->boolean('is_unlisted')->default(false);
|
||||||
|
$table->boolean('is_noautolink')->default(false);
|
||||||
|
$table->boolean('is_nodms')->default(false);
|
||||||
|
$table->boolean('is_notrending')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
$logs = ModLog::whereObjectType('App\Profile::class')->whereAction('admin.user.delete')->get();
|
||||||
|
|
||||||
|
foreach($logs as $log) {
|
||||||
|
$profile = Profile::withTrashed()->find($log->object_id);
|
||||||
|
if(!$profile || $profile->private_key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ModeratedProfile::updateOrCreate([
|
||||||
|
'profile_url' => $profile->remote_url,
|
||||||
|
'profile_id' => $profile->id,
|
||||||
|
], [
|
||||||
|
'is_banned' => true,
|
||||||
|
'domain' => $profile->domain,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('moderated_profiles');
|
||||||
|
}
|
||||||
|
};
|
BIN
public/js/admin.js
vendored
BIN
public/js/admin.js
vendored
Binary file not shown.
Binary file not shown.
|
@ -147,15 +147,10 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="d-none d-md-block nav-item">
|
<li class="d-none d-md-block nav-item">
|
||||||
<a
|
<a
|
||||||
href="/i/admin/reports/appeals"
|
:class="['nav-link d-flex align-items-center', { active: tabIndex == 4}]"
|
||||||
class="nav-link d-flex align-items-center">
|
href="#"
|
||||||
<span>Appeal Requests</span>
|
@click.prevent="toggleTab(4)">
|
||||||
<span
|
<span>Moderated Profiles</span>
|
||||||
v-if="stats.appeals"
|
|
||||||
class="badge badge-sm badge-floating badge-secondary border-white ml-2"
|
|
||||||
style="font-size:11px;">
|
|
||||||
{{ prettyCount(stats.appeals) }}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -418,6 +413,114 @@
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="this.tabIndex === 4" class="table-responsive rounded">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<form class="navbar-search navbar-search-dark form-inline mr-sm-3" @submit.prevent="handleModeratedProfileSearch">
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<div class="input-group input-group-alternative input-group-merge">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
placeholder="Search by username"
|
||||||
|
class="form-control"
|
||||||
|
v-model="moderatedProfilesSearchInput">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<button type="button" class="btn btn-outline-primary fw-bold" @click="exportModeratedProfiles()">Export</button>
|
||||||
|
<button type="button" class="btn btn-primary fw-bold" @click.prevent="addModeratedProfile()">Add Moderated Profile</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-if="moderatedProfiles && moderatedProfiles.length" class="table table-dark">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Username</th>
|
||||||
|
<th scope="col">Moderation</th>
|
||||||
|
<th scope="col">Comment</th>
|
||||||
|
<th scope="col">Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(report, idx) in moderatedProfiles"
|
||||||
|
:key="`remote-reports-${report.id}-${idx}`">
|
||||||
|
<td class="font-weight-bold text-monospace text-muted align-middle">
|
||||||
|
<button class="btn btn-primary btn-sm" @click.prevent="openModeratedProfileModal(report)">
|
||||||
|
{{ report.id }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<p v-if="report.profile.name" class="small mb-0 text-muted">
|
||||||
|
{{ truncateText(report.profile.name, 40) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="font-weight-bold mb-0"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="bottom"
|
||||||
|
:title="report.profile.username">
|
||||||
|
{{ truncateText(report.profile.username, 40) }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<p class="mb-0" v-html="getModerationLabels(report)"></p>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<p class="small mb-0 text-wrap" style="max-width: 200px;word-break: break-word;">{{ truncateText(report.note, 140) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="font-weight-bold align-middle">
|
||||||
|
<span
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="bottom"
|
||||||
|
:title="report.created_at">
|
||||||
|
{{ timeAgo(report.created_at) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="card card-body p-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-column">
|
||||||
|
|
||||||
|
<template v-if="moderatedProfilesSearchInput">
|
||||||
|
<p class="mt-3 mb-0"><i class="far fa-times fa-5x text-danger"></i></p>
|
||||||
|
<p class="lead">No results found!</p>
|
||||||
|
<button class="btn btn-primary" @click.prevent="clearModeratedProfileSearch()">Go back</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
|
||||||
|
<p class="lead">No active moderation accounts found!</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="moderatedProfiles && moderatedProfiles.length && (moderatedProfilesPagination.prev || moderatedProfilesPagination.next)" class="mt-3 d-flex align-items-center justify-content-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!moderatedProfilesPagination.prev"
|
||||||
|
@click="paginateModeratedAccounts('prev')">
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-pill"
|
||||||
|
:disabled="!moderatedProfilesPagination.next"
|
||||||
|
@click="paginateModeratedAccounts('next')">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -756,6 +859,165 @@
|
||||||
v-on:close="handleCloseRemoteReportModal()"
|
v-on:close="handleCloseRemoteReportModal()"
|
||||||
v-on:refresh="refreshRemoteReports()" />
|
v-on:refresh="refreshRemoteReports()" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="moderatedProfileView"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="moderatedProfileViewLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-backdrop="static"
|
||||||
|
ref="moderatedProfileModal">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div v-if="modModalData" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="w-100 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<i class="far fa-shield-alt"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-0 lead mt-0 font-weight-bold">Moderated Profile</h5>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="closeModeratedProfileModal()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="card mb-0">
|
||||||
|
<div class="card-body bg-lighter text-dark p-3 font-weight-bold d-flex align-items-center justify-content-center flex-column">
|
||||||
|
<p v-if="modModalData?.profile?.name" class="mb-0 small text-muted">{{ modModalData?.profile?.name }}</p>
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
{{ modModalData?.profile?.username }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="modModalData?.profile?.remote_url" class="small text-muted text-right mb-1">
|
||||||
|
<a :href="modModalData?.profile?.remote_url" rel="noreferrer" target="_blank">
|
||||||
|
View remote profile
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="list-group mpl-form">
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
Banned
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Ban any activities from this account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_banned" v-model="modModalModel.is_banned">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_banned"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item d-none justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
No Autolink
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Disable hashtag, mention and url autolinking from this account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_noautolink" v-model="modModalModel.is_noautolink">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_noautolink"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item d-none justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
No DMs
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Ignore DMs from this account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_nodms" v-model="modModalModel.is_nodms">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_nodms"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item d-none justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
No Trending
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Prevent posts from this account from appearing in trending lists or feeds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_notrending" v-model="modModalModel.is_notrending">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_notrending"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item d-none justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
Mark NSFW
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Mark all posts as sensitive, and apply CWs to future posts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_nsfw" v-model="modModalModel.is_nsfw">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_nsfw"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item d-none justify-content-between align-items-center">
|
||||||
|
<div class="mp-form-label">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<p class="mb-0 font-weight-bold">
|
||||||
|
Mark Unlisted
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 small text-muted">
|
||||||
|
Mark all future posts as unlisted, hidden from global/tag feeds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="mp-form-is_unlisted" v-model="modModalModel.is_unlisted">
|
||||||
|
<label class="custom-control-label" for="mp-form-is_unlisted"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-3">
|
||||||
|
<label class="small text-muted">Account Notes (only visible to admins)</label>
|
||||||
|
<textarea class="form-control" v-model="modModalData.note" placeholder="Add an optional note" maxlength="500"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer d-flex justify-content-between align-items-center">
|
||||||
|
<button type="button" class="btn btn-link text-dark" data-dismiss="modal" @click="closeModeratedProfileModal()">Close</button>
|
||||||
|
<div class="d-flex flex-grow-1 align-items-center gap-1">
|
||||||
|
<button type="button" class="btn btn-danger" @click.prevent="handleModProfileModalDelete()">Delete</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-block" @click.prevent="handleModProfileModalUpdate()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -792,13 +1054,34 @@
|
||||||
viewingSpamReportLoading: false,
|
viewingSpamReportLoading: false,
|
||||||
remoteReportsLoaded: false,
|
remoteReportsLoaded: false,
|
||||||
showRemoteReportModal: undefined,
|
showRemoteReportModal: undefined,
|
||||||
remoteReportModalModel: {}
|
remoteReportModalModel: {},
|
||||||
|
moderatedProfiles: [],
|
||||||
|
moderatedProfilesPagination: {},
|
||||||
|
moderatedProfilesSearchInput: undefined,
|
||||||
|
modModalData: undefined,
|
||||||
|
modModalModel: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
let u = new URLSearchParams(window.location.search);
|
let u = new URLSearchParams(window.location.search);
|
||||||
if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
|
if(u.has('tab') && u.get('tab') === 'moderated-profiles' && u.has('action') && u.has('id') && u.get('action') === 'view') {
|
||||||
|
this.tabIndex = 4;
|
||||||
|
this.fetchModeratedAccounts();
|
||||||
|
this.fetchModeratedProfile(u.get('id'));
|
||||||
|
} else if(u.has('tab') && u.get('tab') === 'autospam' && !u.has('id')) {
|
||||||
|
this.tabIndex = 2;
|
||||||
|
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
||||||
|
} else if(u.has('tab') && u.get('tab') === 'closed') {
|
||||||
|
this.tabIndex = 1;
|
||||||
|
this.fetchStats('/i/admin/api/reports/all?filter=closed')
|
||||||
|
} else if(u.has('tab') && u.get('tab') === 'closed') {
|
||||||
|
this.tabIndex = 3;
|
||||||
|
this.fetchStats('/i/admin/api/reports/all?filter=remote')
|
||||||
|
} else if(u.has('tab') && u.get('tab') === 'moderated-profiles') {
|
||||||
|
this.tabIndex = 4;
|
||||||
|
this.fetchModeratedAccounts();
|
||||||
|
} else if(u.has('tab') && u.has('id') && u.get('tab') === 'autospam') {
|
||||||
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
||||||
this.fetchSpamReport(u.get('id'));
|
this.fetchSpamReport(u.get('id'));
|
||||||
} else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
|
} else if(u.has('tab') && u.has('id') && u.get('tab') === 'report') {
|
||||||
|
@ -819,21 +1102,29 @@
|
||||||
switch(idx) {
|
switch(idx) {
|
||||||
case 0:
|
case 0:
|
||||||
this.fetchStats('/i/admin/api/reports/all');
|
this.fetchStats('/i/admin/api/reports/all');
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
this.fetchStats('/i/admin/api/reports/all?filter=closed')
|
this.fetchStats('/i/admin/api/reports/all?filter=closed')
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=closed');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
this.fetchStats(null, '/i/admin/api/reports/spam/all');
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=autospam');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
this.fetchRemoteReports();
|
this.fetchRemoteReports();
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=remote');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
this.fetchModeratedAccounts();
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
window.history.pushState(null, null, '/i/admin/reports');
|
|
||||||
this.tabIndex = idx;
|
this.tabIndex = idx;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -891,6 +1182,27 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchModeratedAccounts(apiUrl = '/i/admin/api/reports/moderated-profiles') {
|
||||||
|
axios.get(apiUrl)
|
||||||
|
.then(res => {
|
||||||
|
this.moderatedProfiles = res.data.data;
|
||||||
|
this.moderatedProfilesPagination = {
|
||||||
|
prev: res.data.links.prev,
|
||||||
|
next: res.data.links.next
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
paginateModeratedAccounts(dir) {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
let url = dir == 'next' ? this.moderatedProfilesPagination.next : this.moderatedProfilesPagination.prev;
|
||||||
|
this.fetchModeratedAccounts(url);
|
||||||
|
},
|
||||||
|
|
||||||
fetchReports(url = '/i/admin/api/reports/all') {
|
fetchReports(url = '/i/admin/api/reports/all') {
|
||||||
axios.get(url)
|
axios.get(url)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -1149,7 +1461,226 @@
|
||||||
this.fetchStats();
|
this.fetchStats();
|
||||||
window.history.pushState(null, null, '/i/admin/reports');
|
window.history.pushState(null, null, '/i/admin/reports');
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
truncateText(text, maxLength, appendEllipsis = true) {
|
||||||
|
if(!text || !text.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncated = text.slice(0, maxLength).trim();
|
||||||
|
return appendEllipsis ? truncated + '...' : truncated;
|
||||||
|
},
|
||||||
|
|
||||||
|
getModerationLabels(acct) {
|
||||||
|
if(acct.is_banned) {
|
||||||
|
return `<span class="badge badge-danger">Banned</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
let labels = [];
|
||||||
|
|
||||||
|
if(acct.is_banned) labels.push('Banned')
|
||||||
|
if(acct.is_noautolink) labels.push('No Autolink')
|
||||||
|
if(acct.is_nodms) labels.push('No DMS')
|
||||||
|
if(acct.is_notrending) labels.push('No Trending')
|
||||||
|
if(acct.is_nsfw) labels.push('NSFW')
|
||||||
|
if(acct.is_unlisted) labels.push('Unlisted')
|
||||||
|
|
||||||
|
return labels.map((item, index) => {
|
||||||
|
const colorClass = item === 'Banned' ? 'danger' : 'primary';
|
||||||
|
return `<span class="badge badge-${colorClass}">${item}</span>`;
|
||||||
|
}).join(' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleModeratedProfileSearch(event) {
|
||||||
|
event.currentTarget.blur()
|
||||||
|
let url = `/i/admin/api/reports/moderated-profiles?search=${this.moderatedProfilesSearchInput}`
|
||||||
|
this.fetchModeratedAccounts(url)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearModeratedProfileSearch() {
|
||||||
|
this.moderatedProfilesSearchInput = undefined;
|
||||||
|
this.fetchModeratedAccounts();
|
||||||
|
},
|
||||||
|
|
||||||
|
openModeratedProfileModal(report) {
|
||||||
|
this.modModalData = report;
|
||||||
|
this.modModalModel = {
|
||||||
|
is_banned: report.is_banned,
|
||||||
|
is_noautolink: report.is_noautolink,
|
||||||
|
is_nodms: report.is_nodms,
|
||||||
|
is_notrending: report.is_notrending,
|
||||||
|
is_nsfw: report.is_nsfw,
|
||||||
|
is_unlisted: report.is_unlisted,
|
||||||
|
}
|
||||||
|
$(this.$refs.moderatedProfileModal).modal('show');
|
||||||
|
window.history.pushState(null, null, `/i/admin/reports?tab=moderated-profiles&action=view&id=${report.id}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleModProfileModalUpdate() {
|
||||||
|
axios.post(
|
||||||
|
'/i/admin/api/reports/moderated-profiles/update',
|
||||||
|
{...this.modModalData, ...this.modModalModel}
|
||||||
|
).then(res => {
|
||||||
|
window.history.pushState(null, null, `/i/admin/reports?tab=moderated-profiles`)
|
||||||
|
window.location.reload();
|
||||||
|
}).catch(error => {
|
||||||
|
let errorMessage = 'An error occurred';
|
||||||
|
if (error.response) {
|
||||||
|
errorMessage = `Error ${error.response.status}: ${error.response.data.error || error.response.data.message || error.response.statusText}`;
|
||||||
|
} else if (error.request) {
|
||||||
|
errorMessage = 'No response received from server';
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
swal('Error', errorMessage, 'error')
|
||||||
|
}).finally(() => {
|
||||||
|
$(this.$refs.moderatedProfileModal).modal('hide');
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleModProfileModalDelete() {
|
||||||
|
swal({
|
||||||
|
title: 'Confirm Delete',
|
||||||
|
text: 'Are you sure you want to delete this moderated profile ruleset?',
|
||||||
|
buttons: {
|
||||||
|
cancel: "Cancel",
|
||||||
|
danger: {
|
||||||
|
text: "Delete",
|
||||||
|
value: 'delete',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((val) => {
|
||||||
|
if(val === 'delete') {
|
||||||
|
axios.post('/i/admin/api/reports/moderated-profiles/delete', { id: this.modModalData.id})
|
||||||
|
.then(res => {
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$(this.$refs.moderatedProfileModal).modal('hide');
|
||||||
|
swal.close()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchModeratedProfile(id) {
|
||||||
|
axios.get(`/i/admin/api/reports/moderated-profiles/show?id=${id}`)
|
||||||
|
.then(res => {
|
||||||
|
this.modModalData = res.data.data;
|
||||||
|
let report = res.data.data;
|
||||||
|
|
||||||
|
this.modModalModel = {
|
||||||
|
is_banned: report.is_banned,
|
||||||
|
is_noautolink: report.is_noautolink,
|
||||||
|
is_nodms: report.is_nodms,
|
||||||
|
is_notrending: report.is_notrending,
|
||||||
|
is_nsfw: report.is_nsfw,
|
||||||
|
is_unlisted: report.is_unlisted,
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this.$refs.moderatedProfileModal).modal('show');
|
||||||
|
}).catch(err => {
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
|
||||||
|
swal('Error', 'Invalid moderated profile id!', 'error');
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
addModeratedProfile() {
|
||||||
|
swal({
|
||||||
|
text: 'Enter profile URL (ie: https://mastodon.social/@Mastodon)',
|
||||||
|
content: "input",
|
||||||
|
button: {
|
||||||
|
text: "Add",
|
||||||
|
closeModal: false,
|
||||||
|
},
|
||||||
|
}).then(val => {
|
||||||
|
if (!val) throw null;
|
||||||
|
|
||||||
|
if(val.startsWith('@')) {
|
||||||
|
swal('Error', 'Invalid URL, webfinger is not supported yet.', 'error');
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!val.startsWith('http')) {
|
||||||
|
swal('Error', 'Invalid URL', 'error');
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(val.indexOf('.') === -1) {
|
||||||
|
swal('Error', 'Invalid URL', 'error');
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
url: val
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post('/i/admin/api/reports/moderated-profiles/create', params);
|
||||||
|
}).then(json => {
|
||||||
|
if(json && json.data && json.data?.id) {
|
||||||
|
window.location.href = `/i/admin/reports?tab=moderated-profiles&action=view&id=${json.data?.id}`
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
swal.stopLoading();
|
||||||
|
swal.close();
|
||||||
|
}).catch(err => {
|
||||||
|
if (err) {
|
||||||
|
if(err?.response?.data?.error) {
|
||||||
|
swal("Error", err?.response?.data?.error, "error");
|
||||||
|
} else {
|
||||||
|
swal("Error", "Something went wrong!", "error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
swal.stopLoading();
|
||||||
|
swal.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModeratedProfileModal() {
|
||||||
|
window.history.pushState(null, null, '/i/admin/reports?tab=moderated-profiles');
|
||||||
|
},
|
||||||
|
|
||||||
|
exportModeratedProfiles() {
|
||||||
|
axios.get('/i/admin/api/reports/moderated-profiles/export', {
|
||||||
|
responseType: "blob"
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let host = new URL(window.location.href)
|
||||||
|
let date = new Date();
|
||||||
|
let dateStamp = `${date.getMonth()}-${date.getDate()}-${date.getFullYear()}-${Date.now()}`;
|
||||||
|
let filename = host.host + '-moderated-profiles-' + dateStamp + '.json';
|
||||||
|
let el = document.createElement('a');
|
||||||
|
el.setAttribute('download', filename)
|
||||||
|
const href = URL.createObjectURL(res.data);
|
||||||
|
el.href = href;
|
||||||
|
el.setAttribute('target', '_blank');
|
||||||
|
el.click();
|
||||||
|
|
||||||
|
swal(
|
||||||
|
'Success!',
|
||||||
|
'You have successfully exported the moderated profile backup.',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mpl-form {
|
||||||
|
p {
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -148,6 +148,12 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
|
||||||
Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi');
|
Route::post('instances/refresh-stats', 'AdminController@postInstanceRefreshStatsApi');
|
||||||
Route::get('instances/download-backup', 'AdminController@downloadBackup');
|
Route::get('instances/download-backup', 'AdminController@downloadBackup');
|
||||||
Route::post('instances/import-data', 'AdminController@importBackup');
|
Route::post('instances/import-data', 'AdminController@importBackup');
|
||||||
|
Route::get('reports/moderated-profiles', 'AdminController@getModeratedProfiles');
|
||||||
|
Route::post('reports/moderated-profiles/update', 'AdminController@updateModeratedProfile');
|
||||||
|
Route::post('reports/moderated-profiles/create', 'AdminController@createModeratedProfile');
|
||||||
|
Route::get('reports/moderated-profiles/show', 'AdminController@getModeratedProfile');
|
||||||
|
Route::post('reports/moderated-profiles/delete', 'AdminController@deleteModeratedProfile');
|
||||||
|
Route::get('reports/moderated-profiles/export', 'AdminController@exportModeratedProfiles');
|
||||||
Route::get('reports/stats', 'AdminController@reportsStats');
|
Route::get('reports/stats', 'AdminController@reportsStats');
|
||||||
Route::get('reports/all', 'AdminController@reportsApiAll');
|
Route::get('reports/all', 'AdminController@reportsApiAll');
|
||||||
Route::get('reports/remote', 'AdminController@reportsApiRemote');
|
Route::get('reports/remote', 'AdminController@reportsApiRemote');
|
||||||
|
|
Loading…
Reference in a new issue