Jelajahi Sumber

Add Remote Reports to Admin Dashboard Reports page

Daniel Supernault 1 tahun lalu
induk
melakukan
ef0ff78e4a

+ 191 - 0
app/Http/Controllers/Admin/AdminReportController.php

@@ -4,6 +4,7 @@ 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;
@@ -13,6 +14,7 @@ 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;
@@ -23,6 +25,7 @@ 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;
@@ -640,6 +643,7 @@ 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(),
+            'remote_open' => RemoteReport::whereNull('action_taken_at')->count(),
             'email_verification_requests' => Redis::scard('email:manual'),
         ];
 
@@ -665,6 +669,24 @@ 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);
@@ -1327,4 +1349,173 @@ trait AdminReportController
 
         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];
+    }
 }

+ 49 - 0
app/Http/Resources/AdminRemoteReport.php

@@ -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;
+    }
+}

+ 26 - 2
app/Util/ActivityPub/Inbox.php

@@ -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([

+ 160 - 3
resources/assets/components/admin/AdminReports.vue

@@ -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"
@@ -650,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,
@@ -664,6 +776,7 @@
                     closed: 0,
                     autospam: 0,
                     autospam_open: 0,
+                    remote_open: 0,
                 },
                 tabIndex: 0,
                 reports: [],
@@ -676,7 +789,10 @@
                 autospamLoaded: false,
                 showSpamReportModal: false,
                 viewingSpamReport: undefined,
-                viewingSpamReportLoading: false
+                viewingSpamReportLoading: false,
+                remoteReportsLoaded: false,
+                showRemoteReportModal: undefined,
+                remoteReportModalModel: {}
             }
         },
 
@@ -712,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;
@@ -785,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;

+ 139 - 0
resources/assets/components/admin/partial/AdminModalPost.vue

@@ -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>

+ 2 - 0
routes/web-admin.php

@@ -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');