Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork

This commit is contained in:
Christian Winther 2024-02-29 10:46:45 +00:00
commit 5d56460082
14 changed files with 2747 additions and 1581 deletions

View file

@ -5,6 +5,8 @@
### Features
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
### Updates
@ -18,6 +20,10 @@
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)

View file

@ -2,38 +2,34 @@
namespace App\Http\Controllers\Admin;
use App\AccountInterstitial;
use App\Http\Resources\AdminReport;
use App\Http\Resources\AdminRemoteReport;
use App\Http\Resources\AdminSpamReport;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Notification;
use App\Profile;
use App\Report;
use App\Models\RemoteReport;
use App\Services\AccountService;
use App\Services\ModLogService;
use App\Services\NetworkTimelineService;
use App\Services\NotificationService;
use App\Services\PublicTimelineService;
use App\Services\StatusService;
use App\Status;
use App\Story;
use App\User;
use Cache;
use Storage;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use App\Services\AccountService;
use App\Services\StatusService;
use App\{
AccountInterstitial,
Contact,
Hashtag,
Newsroom,
Notification,
OauthClient,
Profile,
Report,
Status,
Story,
User
};
use Illuminate\Validation\Rule;
use App\Services\StoryService;
use App\Services\ModLogService;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Http\Resources\AdminReport;
use App\Http\Resources\AdminSpamReport;
use App\Services\NotificationService;
use App\Services\PublicTimelineService;
use App\Services\NetworkTimelineService;
trait AdminReportController
{
@ -53,7 +49,7 @@ trait AdminReportController
$mailVerifications = Redis::scard('email:manual');
if ($filter == 'open' && $page == 1) {
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) {
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) {
return Report::whereHas('status')
->whereHas('reportedUser')
->whereHas('reporter')
@ -87,6 +83,7 @@ trait AdminReportController
if ($request->has('ref') && $request->input('ref') == 'email') {
return redirect('/i/admin/reports?tab=report&id='.$report->id);
}
return view('admin.reports.show', compact('report'));
}
@ -96,6 +93,7 @@ trait AdminReportController
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.appeals', compact('appeals'));
}
@ -105,13 +103,14 @@ trait AdminReportController
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
}
public function spam(Request $request)
{
$this->validate($request, [
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions'
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions',
]);
$tab = $request->input('tab', 'home');
@ -143,6 +142,7 @@ trait AdminReportController
if (config('database.default') != 'mysql') {
return 0;
}
return AccountInterstitial::selectRaw('*, count(id) as counter')
->whereType('post.autospam')
->groupBy('user_id')
@ -152,11 +152,11 @@ trait AdminReportController
$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function () {
if (config('database.default') != 'mysql') {
return "0";
return '0';
}
$seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();
if (! $seconds) {
return "0";
return '0';
}
$mins = floor($seconds->avg('timediff') / 60);
@ -170,7 +170,7 @@ trait AdminReportController
return floor($mins / 60 / 24).' day(s)';
});
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0";
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0';
if (in_array($tab, ['home', 'spam', 'not-spam'])) {
$appeals = AccountInterstitial::whereType('post.autospam')
@ -194,18 +194,20 @@ trait AdminReportController
$appeals = $appeals->appends(['tab' => $tab]);
}
} else {
$appeals = new class {
public function count() {
$appeals = new class
{
public function count()
{
return 0;
}
public function render() {
return;
public function render()
{
}
};
}
return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
}
@ -217,6 +219,7 @@ trait AdminReportController
return redirect('/i/admin/reports?tab=autospam&id='.$appeal->id);
}
$meta = json_decode($appeal->meta);
return view('admin.reports.show_spam', compact('appeal', 'meta'));
}
@ -255,13 +258,14 @@ trait AdminReportController
});
Cache::forget('admin-dash:reports:spam-sync');
return redirect('/i/admin/reports/autospam');
}
public function updateSpam(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer'
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer',
]);
$action = $request->input('action');
@ -307,6 +311,7 @@ trait AdminReportController
Cache::forget('profiles:private');
DeleteAccountPipeline::dispatch($user);
return;
}
@ -318,6 +323,7 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -330,6 +336,7 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -355,6 +362,7 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -370,7 +378,7 @@ trait AdminReportController
$pro->update([
'unlisted' => true,
'cw' => true,
'no_autolink' => true
'no_autolink' => true,
]);
Status::whereProfileId($pro->id)
@ -386,6 +394,7 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -411,7 +420,7 @@ trait AdminReportController
public function updateAppeal(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve'
'action' => 'required|in:dismiss,approve',
]);
$action = $request->input('action');
@ -423,6 +432,7 @@ trait AdminReportController
$appeal->appeal_handled_at = now();
$appeal->save();
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals');
}
@ -441,7 +451,7 @@ trait AdminReportController
break;
default:
# code...
// code...
break;
}
@ -541,7 +551,7 @@ trait AdminReportController
'3' => 'unlist',
'4' => 'delete',
'5' => 'shadowban',
'6' => 'ban'
'6' => 'ban',
];
}
@ -549,7 +559,7 @@ trait AdminReportController
{
$this->validate($request, [
'action' => 'required|integer|min:1|max:10',
'ids' => 'required|array'
'ids' => 'required|array',
]);
$action = $this->actionMap()[$request->input('action')];
$ids = $request->input('ids');
@ -559,8 +569,9 @@ trait AdminReportController
}
$res = [
'message' => 'Success',
'code' => 200
'code' => 200,
];
return response()->json($res);
}
@ -584,6 +595,7 @@ trait AdminReportController
return [];
}
$account['email'] = $user->email;
return $account;
})
->filter(function ($res) {
@ -591,6 +603,7 @@ trait AdminReportController
})
->values();
}
return view('admin.reports.mail_verification', compact('reports', 'ignored'));
}
@ -598,6 +611,7 @@ trait AdminReportController
{
$id = $request->input('id');
Redis::sadd('email:manual-ignored', $id);
return redirect('/i/admin/reports');
}
@ -609,12 +623,14 @@ trait AdminReportController
Redis::srem('email:manual-ignored', $id);
$user->email_verified_at = now();
$user->save();
return redirect('/i/admin/reports');
}
public function reportMailVerifyClearIgnored(Request $request)
{
Redis::del('email:manual-ignored');
return [200];
}
@ -627,8 +643,10 @@ trait AdminReportController
'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
'email_verification_requests' => Redis::scard('email:manual')
'remote_open' => RemoteReport::whereNull('action_taken_at')->count(),
'email_verification_requests' => Redis::scard('email:manual'),
];
return $stats;
}
@ -651,9 +669,28 @@ trait AdminReportController
return $reports;
}
public function reportsApiRemote(Request $request)
{
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
$reports = AdminRemoteReport::collection(
RemoteReport::orderBy('id', 'desc')
->when($filter, function ($q, $filter) {
return $filter == 'open' ?
$q->whereNull('action_taken_at') :
$q->whereNotNull('action_taken_at');
})
->cursorPaginate(6)
->withQueryString()
);
return $reports;
}
public function reportsApiGet(Request $request, $id)
{
$report = Report::findOrFail($id);
return new AdminReport($report);
}
@ -663,8 +700,8 @@ trait AdminReportController
'object_id' => 'required',
'object_type' => 'required',
'id' => 'required',
'action' => 'required|in:ignore,nsfw,unlist,private,delete',
'action_type' => 'required|in:post,profile'
'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all',
'action_type' => 'required|in:post,profile,story',
]);
$report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
@ -673,11 +710,92 @@ trait AdminReportController
return $this->reportsHandleProfileAction($report, $request->input('action'));
} elseif ($request->input('action_type') === 'post') {
return $this->reportsHandleStatusAction($report, $request->input('action'));
} elseif ($request->input('action_type') === 'story') {
return $this->reportsHandleStoryAction($report, $request->input('action'));
}
return $report;
}
protected function reportsHandleStoryAction($report, $action)
{
switch ($action) {
case 'ignore':
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
]);
return [200];
break;
case 'delete':
$profile = Profile::find($report->reported_profile_id);
$story = Story::whereProfileId($profile->id)->find($report->object_id);
abort_if(! $story, 400, 'Invalid or missing story');
$story->active = false;
$story->save();
ModLogService::boot()
->objectUid($profile->id)
->objectId($report->object_id)
->objectType('App\Story::class')
->user(request()->user())
->action('admin.user.moderate')
->metadata([
'action' => 'delete',
'message' => 'Success!',
])
->accessLevel('admin')
->save();
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
]);
StoryDelete::dispatch($story)->onQueue('story');
return [200];
break;
case 'delete-all':
$profile = Profile::find($report->reported_profile_id);
$stories = Story::whereProfileId($profile->id)->whereActive(true)->get();
abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories');
ModLogService::boot()
->objectUid($profile->id)
->objectId($report->object_id)
->objectType('App\Story::class')
->user(request()->user())
->action('admin.user.moderate')
->metadata([
'action' => 'delete-all',
'message' => 'Success!',
])
->accessLevel('admin')
->save();
Report::where('reported_profile_id', $profile->id)
->whereObjectType('App\Story')
->whereNull('admin_seen')
->update([
'admin_seen' => now(),
]);
$stories->each(function ($story) {
StoryDelete::dispatch($story)->onQueue('story');
});
return [200];
break;
}
}
protected function reportsHandleProfileAction($report, $action)
{
switch ($action) {
@ -685,8 +803,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -725,7 +844,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -734,8 +853,9 @@ trait AdminReportController
->whereObjectType($report->object_type)
->update([
'nsfw' => true,
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -775,7 +895,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'unlisted',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -783,8 +903,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -824,7 +945,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'private',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -832,8 +953,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -884,7 +1006,7 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
if ($profile->user_id) {
@ -906,6 +1028,7 @@ trait AdminReportController
AccountService::del($profile->id);
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
}
return [200];
break;
}
@ -918,8 +1041,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -943,7 +1067,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -952,8 +1076,9 @@ trait AdminReportController
->whereObjectType($report->object_type)
->update([
'nsfw' => true,
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -980,7 +1105,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'private',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -988,8 +1113,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -1018,7 +1144,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'unlist',
'message' => 'Success!'
'message' => 'Success!',
])
->accessLevel('admin')
->save();
@ -1026,8 +1152,9 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
break;
@ -1055,7 +1182,7 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now()
'admin_seen' => now(),
]);
return [200];
@ -1103,6 +1230,7 @@ trait AdminReportController
Cache::forget('admin-dash:reports:spam-count');
Cache::forget('pf:bouncer_v0:exemption_by_pid:'.$report->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:'.$report->user->profile_id);
return [$action, $report];
}
@ -1150,7 +1278,7 @@ trait AdminReportController
->whereUserId($appeal->user_id)
->update([
'appeal_handled_at' => now(),
'is_spam' => true
'is_spam' => true,
]);
}
@ -1218,6 +1346,176 @@ trait AdminReportController
public function reportsApiSpamGet(Request $request, $id)
{
$report = AccountInterstitial::findOrFail($id);
return new AdminSpamReport($report);
}
public function reportsApiRemoteHandle(Request $request)
{
$this->validate($request, [
'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'
]);
$report = RemoteReport::findOrFail($request->input('id'));
$user = User::whereProfileId($report->account_id)->first();
$ogPublicStatuses = [];
$ogUnlistedStatuses = [];
$ogNonCwStatuses = [];
switch ($request->input('action')) {
case 'mark-read':
$report->action_taken_at = now();
$report->save();
break;
case 'mark-all-read-by-domain':
RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]);
break;
case 'cw-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if(!$status->is_nsfw) {
$ogNonCwStatuses[] = $status->id;
}
$status->is_nsfw = true;
$status->saveQuietly();
StatusService::del($status->id);
}
$report->action_taken_at = now();
$report->save();
break;
case 'cw-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if($status->is_nsfw || $status->reblog_of_id) {
continue;
}
if(!$status->is_nsfw) {
$ogNonCwStatuses[] = $status->id;
}
$status->is_nsfw = true;
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'unlist-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if($status->scope === 'public') {
$ogPublicStatuses[] = $status->id;
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->saveQuietly();
StatusService::del($status->id);
}
}
$report->action_taken_at = now();
$report->save();
break;
case 'unlist-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if($status->visibility !== 'public' || $status->reblog_of_id) {
continue;
}
$ogPublicStatuses[] = $status->id;
$status->visibility = 'unlisted';
$status->scope = 'unlisted';
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'private-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if(in_array($status->scope, ['public', 'unlisted', 'private'])) {
if($status->scope === 'public') {
$ogPublicStatuses[] = $status->id;
}
$status->scope = 'private';
$status->visibility = 'private';
$status->saveQuietly();
StatusService::del($status->id);
}
}
$report->action_taken_at = now();
$report->save();
break;
case 'private-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
continue;
}
if($status->visibility === 'public') {
$ogPublicStatuses[] = $status->id;
} else if($status->visibility === 'unlisted') {
$ogUnlistedStatuses[] = $status->id;
}
$status->visibility = 'private';
$status->scope = 'private';
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'delete-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
StatusDelete::dispatch($status);
}
$report->action_taken_at = now();
$report->save();
break;
case 'mark-all-read-by-username':
RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]);
break;
default:
abort(404);
break;
}
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));
}
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));
}
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));
}
ModLogService::boot()
->user(request()->user())
->objectUid($user ? $user->id : null)
->objectId($report->id)
->objectType('App\Report::class')
->action('admin.report.moderate')
->metadata([
'action' => $request->input('action'),
'duration_active' => now()->parse($report->created_at)->diffForHumans()
])
->accessLevel('admin')
->save();
if($report->status_ids) {
foreach($report->status_ids as $sid) {
RemoteReport::whereNull('action_taken_at')
->whereJsonContains('status_ids', [$sid])
->update(['action_taken_at' => now()]);
}
}
return [200];
}
}

View file

@ -104,6 +104,10 @@ class AdminCuratedRegisterController extends Controller
foreach ($activities as $activity) {
$idx++;
if ($activity->type === 'user_resend_email_confirmation') {
continue;
}
if ($activity->from_user) {
$userResponses->push($activity);

View file

@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Instance;
use App\Services\AccountService;
use App\Services\StatusService;
class AdminRemoteReport extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$instance = parse_url($this->uri, PHP_URL_HOST);
$statuses = [];
if($this->status_ids && count($this->status_ids)) {
foreach($this->status_ids as $sid) {
$s = StatusService::get($sid, false);
if($s && $s['in_reply_to_id'] != null) {
$parent = StatusService::get($s['in_reply_to_id'], false);
if($parent) {
$s['parent'] = $parent;
}
}
if($s) {
$statuses[] = $s;
}
}
}
$res = [
'id' => $this->id,
'instance' => $instance,
'reported' => AccountService::get($this->account_id, true),
'status_ids' => $this->status_ids,
'statuses' => $statuses,
'message' => $this->comment,
'report_meta' => $this->report_meta,
'created_at' => optional($this->created_at)->format('c'),
'action_taken_at' => optional($this->action_taken_at)->format('c'),
];
return $res;
}
}

View file

@ -1243,7 +1243,14 @@ class Inbox
return;
}
$content = isset($this->payload['content']) ? Purify::clean($this->payload['content']) : null;
$content = null;
if(isset($this->payload['content'])) {
if(strlen($this->payload['content']) > 5000) {
$content = Purify::clean(substr($this->payload['content'], 0, 5000) . ' ... (truncated message due to exceeding max length)');
} else {
$content = Purify::clean($this->payload['content']);
}
}
$object = $this->payload['object'];
if(empty($object) || (!is_array($object) && !is_string($object))) {
@ -1259,7 +1266,7 @@ class Inbox
foreach($object as $objectUrl) {
if(!Helpers::validateLocalUrl($objectUrl)) {
continue;
return;
}
if(str_contains($objectUrl, '/users/')) {
@ -1280,6 +1287,23 @@ class Inbox
return;
}
if($objects->count()) {
$obc = $objects->count();
if($obc > 25) {
if($obc > 30) {
return;
} else {
$objLimit = $objects->take(20);
$objects = collect($objLimit->all());
$obc = $objects->count();
}
}
$count = Status::whereProfileId($accountId)->find($objects)->count();
if($obc !== $count) {
return;
}
}
$instanceHost = parse_url($id, PHP_URL_HOST);
$instance = Instance::updateOrCreate([

BIN
public/js/admin.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -103,6 +103,21 @@
</span>
</a>
</li>
<li class="nav-item">
<a
:class="['nav-link d-flex align-items-center', { active: tabIndex == 3}]"
href="#"
@click.prevent="toggleTab(3)">
<span>Remote Reports</span>
<span
v-if="stats.remote_open"
class="badge badge-sm badge-floating badge-danger border-white ml-2"
style="background-color: red;color:white;font-size:11px;">
{{prettyCount(stats.remote_open)}}
</span>
</a>
</li>
<li class="d-none d-md-block nav-item">
<a
:class="['nav-link d-flex align-items-center', { active: tabIndex == 1}]"
@ -191,7 +206,11 @@
</a>
</td>
<td class="align-middle">
<a :href="`/i/web/profile/${report.reporter.id}`" target="_blank" class="text-white">
<a
v-if="report && report.reporter && report.reporter.id"
:href="`/i/web/profile/${report.reporter.id}`"
target="_blank"
class="text-white">
<div class="d-flex align-items-center" style="gap:0.61rem;">
<img
:src="report.reporter.avatar"
@ -320,6 +339,85 @@
Next
</button>
</div>
<div v-if="this.tabIndex === 3" class="table-responsive rounded">
<table v-if="reports && reports.length" class="table table-dark">
<thead class="thead-dark">
<tr>
<th scope="col">ID</th>
<th scope="col">Instance</th>
<th scope="col">Reported Account</th>
<th scope="col">Comment</th>
<th scope="col">Created</th>
<th scope="col">View Report</th>
</tr>
</thead>
<tbody>
<tr
v-for="(report, idx) in reports"
:key="`remote-reports-${report.id}-${idx}`">
<td class="font-weight-bold text-monospace text-muted align-middle">
<a href="#" @click.prevent="showRemoteReport(report)">
{{ report.id }}
</a>
</td>
<td class="align-middle">
<p class="font-weight-bold mb-0">{{ report.instance }}</p>
</td>
<td class="align-middle">
<a v-if="report.reported && report.reported.id" :href="`/i/web/profile/${report.reported.id}`" target="_blank" class="text-white">
<div class="d-flex align-items-center" style="gap:0.61rem;">
<img
:src="report.reported.avatar"
width="30"
height="30"
style="object-fit: cover;border-radius:30px;"
onerror="this.src='/storage/avatars/default.png';this.error=null;">
<div class="d-flex flex-column">
<p class="font-weight-bold mb-0" style="font-size: 14px;">@{{report.reported.username}}</p>
<div class="d-flex small text-muted mb-0" style="gap: 0.5rem;">
<span>{{report.reported.followers_count}} Followers</span>
<span>·</span>
<span>Joined {{ timeAgo(report.reported.created_at) }}</span>
</div>
</div>
</div>
</a>
</td>
<td class="align-middle">
<p class="small mb-0 text-wrap" style="max-width: 300px;word-break: break-all;">{{ report.message && report.message.length > 120 ? report.message.slice(0, 120) + '...' : report.message }}</p>
</td>
<td class="font-weight-bold align-middle">{{ timeAgo(report.created_at) }}</td>
<td class="align-middle"><a href="#" class="btn btn-primary btn-sm">View</a></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">
<p class="mt-3 mb-0"><i class="far fa-check-circle fa-5x text-success"></i></p>
<p class="lead">{{ tabIndex === 0 ? 'No Active Reports Found!' : 'No Closed Reports Found!' }}</p>
</div>
</div>
</div>
</div>
<div v-if="this.tabIndex === 3 && remoteReportsLoaded && reports && reports.length" class="d-flex align-items-center justify-content-center">
<button
class="btn btn-primary rounded-pill"
:disabled="!pagination.prev"
@click="remoteReportPaginate('prev')">
Prev
</button>
<button
class="btn btn-primary rounded-pill"
:disabled="!pagination.next"
@click="remoteReportPaginate('next')">
Next
</button>
</div>
</div>
</div>
@ -372,7 +470,7 @@
<div v-if="viewingReport && viewingReport.reporter" class="list-group-item d-flex align-items-center justify-content-between flex-column flex-grow-1" style="gap:0.4rem;">
<div class="text-muted small font-weight-bold mt-n1">Reporter Account</div>
<a v-if="viewingReport.reporter && viewingReport.reporter.id" :href="`/i/web/profile/${viewingReport.reporter.id}`" target="_blank" class="text-primary">
<a v-if="viewingReport.reporter && viewingReport.reporter?.id" :href="`/i/web/profile/${viewingReport.reporter?.id}`" target="_blank" class="text-primary">
<div class="d-flex align-items-center" style="gap:0.61rem;">
<img
:src="viewingReport.reporter.avatar"
@ -427,6 +525,31 @@
</div>
</div>
<div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story' && viewingReport.story" class="list-group mt-3">
<div v-if="viewingReport && viewingReport.story" class="list-group-item d-flex flex-column flex-grow-1" style="gap:0.4rem;">
<div class="d-flex justify-content-between mt-n1 text-muted small font-weight-bold">
<div>Reported Story</div>
<a class="font-weight-bold" :href="viewingReport.story.url" target="_blank">View</a>
</div>
<img
v-if="viewingReport.story.type === 'photo'"
:src="viewingReport.story.media_src"
height="140"
class="rounded"
style="object-fit: cover;"
onerror="this.src='/storage/no-preview.png';this.error=null;" />
<video
v-else-if="viewingReport.story.type === 'video'"
height="140"
controls
:src="viewingReport.story.media_src"
onerror="this.src='/storage/no-preview.png';this.onerror=null;"
></video>
</div>
</div>
<div v-if="viewingReport && viewingReport.admin_seen_at === null" class="mt-4">
<div v-if="viewingReport && viewingReport.object_type === 'App\\Profile'">
<button class="btn btn-dark btn-block rounded-pill" @click="handleAction('profile', 'ignore')">Ignore Report</button>
@ -454,7 +577,7 @@
</button>
</div>
<div v-if="viewingReport && viewingReport.object_type === 'App\\Status'">
<div v-else-if="viewingReport && viewingReport.object_type === 'App\\Status'">
<button class="btn btn-dark btn-block rounded-pill" @click="handleAction('post', 'ignore')">Ignore Report</button>
<hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
<div
@ -481,6 +604,24 @@
</div>
</div>
</div>
<div v-else-if="viewingReport && viewingReport.object_type === 'App\\Story'">
<button class="btn btn-dark btn-block rounded-pill" @click="handleAction('story', 'ignore')">Ignore Report</button>
<hr v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin" class="mt-3 mb-1">
<div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
<div class="d-flex flex-row mt-2" style="gap:0.3rem;">
<button class="btn btn-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete')">Delete Story</button>
<button class="btn btn-outline-danger btn-block rounded-pill mt-0" @click="handleAction('story', 'delete-all')">Delete All Stories</button>
</div>
</div>
<div v-if="viewingReport && viewingReport.reported && !viewingReport.reported.is_admin">
<hr class="my-2">
<div class="d-flex flex-row mt-2" style="gap:0.3rem;">
<button class="btn btn-outline-danger btn-sm btn-block rounded-pill mt-0" @click="handleAction('profile', 'delete')">Delete Account</button>
</div>
</div>
</div>
</div>
</template>
</b-modal>
@ -607,11 +748,25 @@
</div>
</template>
</b-modal>
<template v-if="showRemoteReportModal">
<admin-report-modal
:open="showRemoteReportModal"
:model="remoteReportModalModel"
v-on:close="handleCloseRemoteReportModal()"
v-on:refresh="refreshRemoteReports()" />
</template>
</div>
</template>
<script type="text/javascript">
import AdminRemoteReportModal from "./partial/AdminRemoteReportModal.vue";
export default {
components: {
"admin-report-modal": AdminRemoteReportModal
},
data() {
return {
loaded: false,
@ -621,6 +776,7 @@
closed: 0,
autospam: 0,
autospam_open: 0,
remote_open: 0,
},
tabIndex: 0,
reports: [],
@ -633,7 +789,10 @@
autospamLoaded: false,
showSpamReportModal: false,
viewingSpamReport: undefined,
viewingSpamReportLoading: false
viewingSpamReportLoading: false,
remoteReportsLoaded: false,
showRemoteReportModal: undefined,
remoteReportModalModel: {}
}
},
@ -669,6 +828,10 @@
case 2:
this.fetchStats(null, '/i/admin/api/reports/spam/all');
break;
case 3:
this.fetchRemoteReports();
break;
}
window.history.pushState(null, null, '/i/admin/reports');
this.tabIndex = idx;
@ -707,6 +870,9 @@
case 'App\\Status':
return `${report.type} Post`;
break;
case 'App\\Story':
return `${report.type} Story`;
break;
}
},
@ -739,6 +905,43 @@
});
},
fetchRemoteReports(url = '/i/admin/api/reports/remote') {
axios.get(url)
.then(res => {
this.reports = res.data.data;
this.pagination = {
next: res.data.links.next,
prev: res.data.links.prev
};
})
.finally(() => {
this.loaded = true;
this.remoteReportsLoaded = true;
});
},
remoteReportPaginate(dir) {
event.currentTarget.blur();
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
this.fetchRemoteReports(url);
},
handleCloseRemoteReportModal() {
this.showRemoteReportModal = false;
},
showRemoteReport(report) {
this.remoteReportModalModel = report;
this.showRemoteReportModal = true;
},
refreshRemoteReports() {
this.fetchStats('');
this.$nextTick(() => {
this.toggleTab(3);
})
},
paginate(dir) {
event.currentTarget.blur();
let url = dir == 'next' ? this.pagination.next : this.pagination.prev;
@ -766,6 +969,7 @@
}
this.loaded = false;
axios.post('/i/admin/api/reports/handle', {
id: this.viewingReport.id,
object_id: this.viewingReport.object_id,
@ -831,6 +1035,20 @@
return 'Are you sure you want to delete this post?';
break;
}
} else if(type === 'story') {
switch(action) {
case 'ignore':
return 'Are you sure you want to ignore this story report?';
break;
case 'delete':
return 'Are you sure you want to delete this story?';
break;
case 'delete-all':
return 'Are you sure you want to delete all stories by this account?';
break;
}
}
},

View file

@ -0,0 +1,139 @@
<template>
<div class="mb-3">
<div v-if="status.media_attachments && status.media_attachments.length" class="list-group-item" style="gap:1rem;overflow:hidden;">
<div class="text-center text-muted small font-weight-bold mb-3">Reported Post Media</div>
<div v-if="status.media_attachments && status.media_attachments.length" class="d-flex flex-grow-1" style="gap: 1rem;overflow-x:auto;">
<template
v-for="media in status.media_attachments">
<img
v-if="media.type === 'image'"
:src="media.url"
width="70"
height="70"
class="rounded"
style="object-fit: cover;"
@click="toggleLightbox"
onerror="this.src='/storage/no-preview.png';this.error=null;" />
<video
v-else-if="media.type === 'video'"
width="140"
height="90"
playsinline
@click.prevent="toggleVideoLightbox($event, media.url)"
class="rounded"
>
<source :src="media.url" :type="media.mime">
</video>
</template>
</div>
</div>
<div class="list-group-item d-flex flex-row flex-grow-1" style="gap:1rem;">
<div class="flex-grow-1">
<div v-if="status && status.in_reply_to_id && status.parent && status.parent.account" class="mb-3">
<template v-if="showInReplyTo">
<div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reply to</div>
<div class="media" style="gap: 1rem;">
<img
:src="status.parent.account.avatar"
width="40"
height="40"
class="rounded-lg"
onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
<div class="d-flex flex-column">
<p class="font-weight-bold mb-0" style="font-size: 11px;">
<a :href="`/i/web/profile/${status.parent.account.id}`" target="_blank">{{ status.parent.account.acct }}</a>
</p>
<admin-read-more :content="status.parent.content_text" />
<p class="mb-1">
<a :href="`/i/web/post/${status.parent.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
<i class="far fa-link mr-1"></i> {{ formatDate(status.parent.created_at)}}
</a>
</p>
</div>
</div>
<hr class="my-1">
</template>
<a v-else class="btn btn-dark font-weight-bold btn-block btn-sm" href="#" @click.prevent="showInReplyTo = true">Show parent post</a>
</div>
<div>
<div class="mt-n1 text-center text-muted small font-weight-bold mb-1">Reported Post</div>
<div class="media" style="gap: 1rem;">
<img
:src="status.account.avatar"
width="40"
height="40"
class="rounded-lg"
onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
<div class="d-flex flex-column">
<p class="font-weight-bold mb-0" style="font-size: 11px;">
<a :href="`/i/web/profile/${status.account.id}`" target="_blank">{{ status.account.acct }}</a>
</p>
<template v-if="status && status.content_text && status.content_text.length">
<admin-read-more :content="status.content_text" />
</template>
<template v-else>
<admin-read-more content="EMPTY CAPTION" class="font-weight-bold text-muted" />
</template>
<p class="mb-0">
<a :href="`/i/web/post/${status.id}`" target="_blank" class="text-muted" style="font-size: 11px;">
<i class="far fa-link mr-1"></i> {{ formatDate(status.created_at)}}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import BigPicture from 'bigpicture';
import AdminReadMore from './AdminReadMore.vue';
export default {
props: {
status: {
type: Object
}
},
data() {
return {
showInReplyTo: false,
}
},
components: {
"admin-read-more": AdminReadMore
},
methods: {
toggleLightbox(e) {
BigPicture({
el: e.target
})
},
toggleVideoLightbox($event, src) {
BigPicture({
el: event.target,
vidSrc: src
})
},
formatDate(str) {
let date = new Date(str);
return new Intl.DateTimeFormat('default', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
}).format(date);
},
}
}
</script>

View file

@ -0,0 +1,106 @@
<template>
<div>
<div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div>
<p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click="expand()">Read more</a></p>
</div>
</template>
<script>
export default {
props: {
content: {
type: String
},
maxLength: {
type: Number,
default: 140
},
fontSize: {
type: String,
default: "13"
},
step: {
type: Boolean,
default: false
},
stepLimit: {
type: Number,
default: 140
},
initialLimit: {
type: Number,
default: 10
}
},
computed: {
contentText: {
get() {
if(this.step) {
const len = this.content.length;
const steps = len / this.stepLimit;
if(this.stepIndex == 1 || steps < this.stepIndex) {
this.canStepExpand = true;
}
return this.steppedTruncate();
}
if(this.content && this.content.length > this.maxLength) {
this.canExpand = true;
}
return this.expanded ? this.content : this.truncate();
}
}
},
data() {
return {
expanded: false,
canExpand: false,
canStepExpand: false,
stepIndex: 1,
}
},
methods: {
expand() {
if(this.step) {
this.stepIndex++;
this.canStepExpand = true;
} else {
this.expanded = true;
}
},
truncate() {
if(!this.content || !this.content.length) {
return;
}
if(this.content && this.content.length < this.maxLength) {
return this.content;
}
return this.content.slice(0, this.maxLength) + '...';
},
steppedTruncate() {
if(!this.content || !this.content.length) {
return;
}
const len = this.content.length;
const steps = len / this.stepLimit;
const maxLen = this.stepLimit * this.stepIndex;
if(this.initialLimit != 10 && this.stepIndex === 1 && this.canStepExpand) {
this.canStepExpand = len > this.stepLimit;
return this.content.slice(0, this.initialLimit);
} else if(this.canStepExpand && this.stepIndex < steps) {
return this.content.slice(0, maxLen);
} else {
this.canStepExpand = false;
return this.content;
}
}
}
}
</script>

View file

@ -0,0 +1,304 @@
<template>
<b-modal
v-model="isOpen"
title="Remote Report"
:ok-only="true"
ok-title="Close"
:lazy="true"
:scrollable="true"
ok-variant="outline-primary"
v-on:hide="$emit('close')">
<div v-if="isLoading" class="d-flex align-items-center justify-content-center">
<b-spinner />
</div>
<template v-else>
<div class="list-group">
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="text-muted small font-weight-bold">Instance</div>
<div class="font-weight-bold">{{ model.instance }}</div>
</div>
<div v-if="model.message && model.message.length" class="list-group-item d-flex justify-content-between align-items-center flex-column gap-1">
<div class="text-muted small font-weight-bold mb-2">Message</div>
<div class="text-wrap w-100" style="word-break:break-all;font-size:12.5px;">
<admin-read-more
:content="model.message"
font-size="11"
:step="true"
:initial-limit="100"
:stepLimit="1000" />
</div>
</div>
</div>
<div class="list-group list-group-horizontal mt-3">
<div
v-if="model && model.reported"
class="list-group-item d-flex align-items-center justify-content-between flex-row flex-grow-1"
style="gap:0.4rem;">
<div class="text-muted small font-weight-bold">Reported Account</div>
<div class="d-flex justify-content-end flex-grow-1">
<a v-if="model.reported && model.reported.id" :href="`/i/web/profile/${model.reported.id}`" target="_blank" class="text-primary">
<div class="d-flex align-items-center" style="gap:0.61rem;">
<img
:src="model.reported.avatar"
width="30"
height="30"
style="object-fit: cover;border-radius:30px;"
onerror="this.src='/storage/avatars/default.png';this.error=null;">
<div class="d-flex flex-column">
<p class="font-weight-bold mb-0 text-break" style="font-size: 12px;max-width: 140px;line-height: 16px;" :class="[ model.reported.is_admin ? 'text-danger': '']">@{{model.reported.acct}}</p>
<div class="d-flex text-muted mb-0" style="font-size: 10px;gap: 0.5rem;">
<span>{{prettyCount(model.reported.followers_count)}} Followers</span>
<span>·</span>
<span>Joined {{ timeAgo(model.reported.created_at) }}</span>
</div>
</div>
</div>
</a>
</div>
</div>
<div
v-else
class="list-group-item d-flex align-items-center justify-content-center flex-column flex-grow-1">
<p class="font-weight-bold mb-0">Reported Account Unavailable</p>
<p class="small mb-0">The reported account may have been deleted, or is otherwise not currently active. You can safely <strong>Close Report</strong> to mark this report as read.</p>
</div>
</div>
<div v-if="model && model.statuses && model.statuses.length" class="list-group mt-3">
<admin-modal-post
v-for="(status, idx) in model.statuses"
:key="`admin-modal-post-remote-post:${status.id}:${idx}`"
:status="status"
/>
</div>
<div class="mt-4">
<div>
<button
type="button"
class="btn btn-dark btn-block rounded-pill"
@click="handleAction('mark-read')">
Close Report
</button>
<button
type="button"
class="btn btn-outline-dark btn-block text-center rounded-pill"
style="word-break: break-all;"
@click="handleAction('mark-all-read-by-domain')">
<span class="font-weight-light">Close all reports from</span> <strong>{{ model.instance}}</strong>
</button>
<button
v-if="model.reported"
type="button"
class="btn btn-outline-dark btn-block rounded-pill flex-grow-1"
@click="handleAction('mark-all-read-by-username')">
<span class="font-weight-light">Close all reports against</span> <strong>&commat;{{ model.reported.username }}</strong>
</button>
<template
v-if="model && model.statuses && model.statuses.length && model.reported">
<hr class="mt-3 mb-1">
<div
class="d-flex flex-row mt-2"
style="gap:0.3rem;">
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('cw-posts')">
Apply CW to Post(s)
</button>
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('unlist-posts')">
Unlist Post(s)
</button>
</div>
<div class="d-flex flex-row mt-2">
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('private-posts')">
Make Post(s) Private
</button>
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('delete-posts')">
Delete Post(s)
</button>
</div>
</template>
<template v-else-if="model && model.statuses && !model.statuses.length && model.reported">
<hr class="mt-3 mb-1">
<div
class="d-flex flex-row mt-2"
style="gap:0.3rem;">
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('cw-all-posts')">
Apply CW to all posts
</button>
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('unlist-all-posts')">
Unlist all account posts
</button>
</div>
<div
class="d-flex flex-row mt-2"
style="gap:0.3rem;">
<button
type="button"
class="btn btn-outline-danger btn-block btn-sm rounded-pill mt-0"
@click="handleAction('private-all-posts')">
Make all posts private
</button>
</div>
</template>
</div>
</div>
</template>
</b-modal>
</template>
<script>
import AdminModalPost from "./AdminModalPost.vue";
import AdminReadMore from "./AdminReadMore.vue";
export default {
props: {
open: {
type: Boolean,
default: false
},
model: {
type: Object
}
},
components: {
"admin-modal-post": AdminModalPost,
"admin-read-more": AdminReadMore
},
watch: {
open: {
handler() {
this.isOpen = this.open;
},
immediate: true,
deep: true,
}
},
data() {
return {
isLoading: true,
isOpen: false,
actions: [
'mark-read',
'cw-posts',
'unlist-posts',
'private-posts',
'delete-posts',
'mark-all-read-by-domain',
'mark-all-read-by-username',
'cw-all-posts',
'unlist-all-posts',
'private-all-posts',
],
actionMap: {
'cw-posts': 'apply content warnings to all post(s) in this report?',
'unlist-posts': 'unlist all post(s) in this report?',
'delete-posts': 'delete all post(s) in this report?',
'private-posts': 'make all post(s) in this report private/followers-only?',
'mark-all-read-by-domain': 'mark all reports by this instance as closed?',
'mark-all-read-by-username': 'mark all reports against this user as closed?',
'cw-all-posts': 'apply content warnings to all post(s) belonging to this account?',
'unlist-all-posts': 'make all post(s) belonging to this account as unlisted?',
'private-all-posts': 'make all post(s) belonging to this account as private?',
}
}
},
mounted() {
setTimeout(() => {
this.isLoading = false;
}, 300);
},
methods: {
prettyCount(str) {
if(str) {
return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
}
return str;
},
timeAgo(str) {
if(!str) {
return str;
}
return App.util.format.timeAgo(str);
},
formatDate(str) {
let date = new Date(str);
return new Intl.DateTimeFormat('default', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric'
}).format(date);
},
handleAction(action) {
if(action === 'mark-read') {
axios.post('/i/admin/api/reports/remote/handle', {
id: this.model.id,
action: action,
}).then(res => {
console.log(res.data)
})
.finally(() => {
this.$emit('refresh');
this.$emit('close');
})
return;
}
swal({
title: 'Confirm',
text: 'Are you sure you want to ' + this.actionMap[action],
icon: 'warning',
buttons: true,
dangerMode: true,
}).then(res => {
if(res === true) {
axios.post('/i/admin/api/reports/remote/handle', {
id: this.model.id,
action: action,
}).finally(() => {
this.$emit('refresh');
this.$emit('close');
})
}
});
}
}
}
</script>

View file

@ -0,0 +1,16 @@
@extends('site.help.partial.template', ['breadcrumb'=>'Email Confirmation Issues'])
@section('section')
<div class="title">
<h3 class="font-weight-bold">Email Confirmation Issues</h3>
</div>
<hr>
<p>If you have been redirected to this page, it may be due to one of the following reasons:</p>
<ul>
<li>The email confirmation link has already been used.</li>
<li>The email confirmation link may have expired, they are only valid for 24 hours.</li>
<li>You cannot confirm an email for another account while logged in to a different account. Try logging out, or use a different browser to open the email confirmation link.</li>
<li>The account the associated email belongs to may have been deleted, or the account may have changed the email address.</li>
</ul>
@endsection

View file

@ -146,6 +146,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::post('instances/import-data', 'AdminController@importBackup');
Route::get('reports/stats', 'AdminController@reportsStats');
Route::get('reports/all', 'AdminController@reportsApiAll');
Route::get('reports/remote', 'AdminController@reportsApiRemote');
Route::post('reports/remote/handle', 'AdminController@reportsApiRemoteHandle');
Route::get('reports/get/{id}', 'AdminController@reportsApiGet');
Route::post('reports/handle', 'AdminController@reportsApiHandle');
Route::get('reports/spam/all', 'AdminController@reportsApiSpamAll');

View file

@ -307,7 +307,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit');
Route::view('import', 'site.help.import')->name('help.import');
Route::view('parental-controls', 'site.help.parental-controls');
// Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues');
Route::view('email-confirmation-issues', 'site.help.email-confirmation-issues')->name('help.email-confirmation-issues');
Route::view('curated-onboarding', 'site.help.curated-onboarding')->name('help.curated-onboarding');
});
Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');