Просмотр исходного кода

Update v1.1 api, add post moderation endpoint

Daniel Supernault 2 лет назад
Родитель
Сommit
9bbd6dcd23

+ 223 - 174
app/Http/Controllers/AdminInviteController.php

@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
 use App\Models\AdminInvite;
 use App\Profile;
 use App\User;
+use Purify;
 use App\Util\Lexer\RestrictedNames;
 use Illuminate\Foundation\Auth\RegistersUsers;
 use Illuminate\Support\Facades\Auth;
@@ -17,178 +18,226 @@ use App\Http\Controllers\Auth\RegisterController;
 
 class AdminInviteController extends Controller
 {
-    public function __construct()
-    {
-        abort_if(!config('instance.admin_invites.enabled'), 404);
-    }
-
-    public function index(Request $request, $code)
-    {
-        if($request->user()) {
-            return redirect('/');
-        }
-        return view('invite.admin_invite', compact('code'));
-    }
-
-    public function apiVerifyCheck(Request $request)
-    {
-        $this->validate($request, [
-            'token' => 'required',
-        ]);
-
-        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
-        abort_if(!$invite, 404);
-        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
-        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
-        $res = [
-            'message' => $invite->message,
-            'max_uses' => $invite->max_uses,
-            'sev' => $invite->skip_email_verification
-        ];
-        return response()->json($res);
-    }
-
-    public function apiUsernameCheck(Request $request)
-    {
-        $this->validate($request, [
-            'token' => 'required',
-            'username' => 'required'
-        ]);
-
-        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
-        abort_if(!$invite, 404);
-        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
-        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
-
-        $usernameRules = [
-            'required',
-            'min:2',
-            'max:15',
-            'unique:users',
-            function ($attribute, $value, $fail) {
-                $dash = substr_count($value, '-');
-                $underscore = substr_count($value, '_');
-                $period = substr_count($value, '.');
-
-                if(ends_with($value, ['.php', '.js', '.css'])) {
-                    return $fail('Username is invalid.');
-                }
-
-                if(($dash + $underscore + $period) > 1) {
-                    return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
-                }
-
-                if (!ctype_alnum($value[0])) {
-                    return $fail('Username is invalid. Must start with a letter or number.');
-                }
-
-                if (!ctype_alnum($value[strlen($value) - 1])) {
-                    return $fail('Username is invalid. Must end with a letter or number.');
-                }
-
-                $val = str_replace(['_', '.', '-'], '', $value);
-                if(!ctype_alnum($val)) {
-                    return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
-                }
-
-                $restricted = RestrictedNames::get();
-                if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
-                    return $fail('Username cannot be used.');
-                }
-            },
-        ];
-
-        $rules = ['username' => $usernameRules];
-        $validator = Validator::make($request->all(), $rules);
-
-        if($validator->fails()) {
-            return response()->json($validator->errors(), 400);
-        }
-
-        return response()->json([]);
-    }
-
-    public function apiEmailCheck(Request $request)
-    {
-        $this->validate($request, [
-            'token' => 'required',
-            'email' => 'required'
-        ]);
-
-        $invite = AdminInvite::whereInviteCode($request->input('token'))->first();
-        abort_if(!$invite, 404);
-        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
-        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
-
-        $emailRules = [
-            'required',
-            'string',
-            'email',
-            'max:255',
-            'unique:users',
-            function ($attribute, $value, $fail) {
-                $banned = EmailService::isBanned($value);
-                if($banned) {
-                    return $fail('Email is invalid.');
-                }
-            },
-        ];
-
-        $rules = ['email' => $emailRules];
-        $validator = Validator::make($request->all(), $rules);
-
-        if($validator->fails()) {
-            return response()->json($validator->errors(), 400);
-        }
-
-        return response()->json([]);
-    }
-
-    public function apiRegister(Request $request)
-    {
-        $this->validate($request, [
-            'token' => 'required',
-            'username' => 'required',
-            'name' => 'nullable',
-            'email' => 'required|email',
-            'password' => 'required',
-            'password_confirm' => 'required'
-        ]);
-
-        $invite = AdminInvite::whereInviteCode($request->input('token'))->firstOrFail();
-        abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite expired');
-        abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
-
-        $invite->uses = $invite->uses + 1;
-
-        event(new Registered($user = User::create([
-            'name'     => $request->input('name') ?? $request->input('username'),
-            'username' => $request->input('username'),
-            'email'    => $request->input('email'),
-            'password' => Hash::make($request->input('password')),
-        ])));
-
-        sleep(5);
-
-        $invite->used_by = array_merge($invite->used_by ?? [], [[
-            'user_id' => $user->id,
-            'username' => $user->username
-        ]]);
-        $invite->save();
-
-        if($invite->skip_email_verification) {
-            $user->email_verified_at = now();
-            $user->save();
-        }
-
-        if(Auth::attempt([
-            'email' => $request->input('email'),
-            'password' => $request->input('password')
-        ])) {
-            $request->session()->regenerate();
-            return redirect()->intended('/');
-        } else {
-            return response()->json([], 400);
-        }
-    }
+	public function __construct()
+	{
+		abort_if(!config('instance.admin_invites.enabled'), 404);
+	}
+
+	public function index(Request $request, $code)
+	{
+		if($request->user()) {
+			return redirect('/');
+		}
+		return view('invite.admin_invite', compact('code'));
+	}
+
+	public function apiVerifyCheck(Request $request)
+	{
+		$this->validate($request, [
+			'token' => 'required',
+		]);
+
+		$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+		abort_if(!$invite, 404);
+		abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+		abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+		$res = [
+			'message' => $invite->message,
+			'max_uses' => $invite->max_uses,
+			'sev' => $invite->skip_email_verification
+		];
+		return response()->json($res);
+	}
+
+	public function apiUsernameCheck(Request $request)
+	{
+		$this->validate($request, [
+			'token' => 'required',
+			'username' => 'required'
+		]);
+
+		$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+		abort_if(!$invite, 404);
+		abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+		abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+		$usernameRules = [
+			'required',
+			'min:2',
+			'max:15',
+			'unique:users',
+			function ($attribute, $value, $fail) {
+				$dash = substr_count($value, '-');
+				$underscore = substr_count($value, '_');
+				$period = substr_count($value, '.');
+
+				if(ends_with($value, ['.php', '.js', '.css'])) {
+					return $fail('Username is invalid.');
+				}
+
+				if(($dash + $underscore + $period) > 1) {
+					return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+				}
+
+				if (!ctype_alnum($value[0])) {
+					return $fail('Username is invalid. Must start with a letter or number.');
+				}
+
+				if (!ctype_alnum($value[strlen($value) - 1])) {
+					return $fail('Username is invalid. Must end with a letter or number.');
+				}
+
+				$val = str_replace(['_', '.', '-'], '', $value);
+				if(!ctype_alnum($val)) {
+					return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+				}
+
+				$restricted = RestrictedNames::get();
+				if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+					return $fail('Username cannot be used.');
+				}
+			},
+		];
+
+		$rules = ['username' => $usernameRules];
+		$validator = Validator::make($request->all(), $rules);
+
+		if($validator->fails()) {
+			return response()->json($validator->errors(), 400);
+		}
+
+		return response()->json([]);
+	}
+
+	public function apiEmailCheck(Request $request)
+	{
+		$this->validate($request, [
+			'token' => 'required',
+			'email' => 'required'
+		]);
+
+		$invite = AdminInvite::whereInviteCode($request->input('token'))->first();
+		abort_if(!$invite, 404);
+		abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite has expired.');
+		abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+		$emailRules = [
+			'required',
+			'string',
+			'email',
+			'max:255',
+			'unique:users',
+			function ($attribute, $value, $fail) {
+				$banned = EmailService::isBanned($value);
+				if($banned) {
+					return $fail('Email is invalid.');
+				}
+			},
+		];
+
+		$rules = ['email' => $emailRules];
+		$validator = Validator::make($request->all(), $rules);
+
+		if($validator->fails()) {
+			return response()->json($validator->errors(), 400);
+		}
+
+		return response()->json([]);
+	}
+
+	public function apiRegister(Request $request)
+	{
+		$this->validate($request, [
+			'token' => 'required',
+			'username' => [
+				'required',
+				'min:2',
+				'max:15',
+				'unique:users',
+				function ($attribute, $value, $fail) {
+					$dash = substr_count($value, '-');
+					$underscore = substr_count($value, '_');
+					$period = substr_count($value, '.');
+
+					if(ends_with($value, ['.php', '.js', '.css'])) {
+						return $fail('Username is invalid.');
+					}
+
+					if(($dash + $underscore + $period) > 1) {
+						return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+					}
+
+					if (!ctype_alnum($value[0])) {
+						return $fail('Username is invalid. Must start with a letter or number.');
+					}
+
+					if (!ctype_alnum($value[strlen($value) - 1])) {
+						return $fail('Username is invalid. Must end with a letter or number.');
+					}
+
+					$val = str_replace(['_', '.', '-'], '', $value);
+					if(!ctype_alnum($val)) {
+						return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+					}
+
+					$restricted = RestrictedNames::get();
+					if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+						return $fail('Username cannot be used.');
+					}
+				},
+			],
+			'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
+			'email' => [
+				'required',
+				'string',
+				'email',
+				'max:255',
+				'unique:users',
+				function ($attribute, $value, $fail) {
+					$banned = EmailService::isBanned($value);
+					if($banned) {
+						return $fail('Email is invalid.');
+					}
+				},
+			],
+			'password' => 'required',
+			'password_confirm' => 'required'
+		]);
+
+		$invite = AdminInvite::whereInviteCode($request->input('token'))->firstOrFail();
+		abort_if($invite->expires_at && $invite->expires_at->lt(now()), 400, 'Invite expired');
+		abort_if($invite->max_uses && $invite->uses >= $invite->max_uses, 400, 'Maximum invites reached.');
+
+		$invite->uses = $invite->uses + 1;
+
+		event(new Registered($user = User::create([
+			'name'     => Purify::clean($request->input('name')) ?? $request->input('username'),
+			'username' => $request->input('username'),
+			'email'    => $request->input('email'),
+			'password' => Hash::make($request->input('password')),
+		])));
+
+		sleep(5);
+
+		$invite->used_by = array_merge($invite->used_by ?? [], [[
+			'user_id' => $user->id,
+			'username' => $user->username
+		]]);
+		$invite->save();
+
+		if($invite->skip_email_verification) {
+			$user->email_verified_at = now();
+			$user->save();
+		}
+
+		if(Auth::attempt([
+			'email' => $request->input('email'),
+			'password' => $request->input('password')
+		])) {
+			$request->session()->regenerate();
+			return redirect()->intended('/');
+		} else {
+			return response()->json([], 400);
+		}
+	}
 }

+ 736 - 633
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -20,6 +20,8 @@ use App\User;
 use App\Services\AccountService;
 use App\Services\StatusService;
 use App\Services\ProfileStatusService;
+use App\Services\PublicTimelineService;
+use App\Services\NetworkTimelineService;
 use App\Util\Lexer\RestrictedNames;
 use App\Services\EmailService;
 use Illuminate\Support\Str;
@@ -29,640 +31,741 @@ use Mail;
 use App\Mail\PasswordChange;
 use App\Mail\ConfirmAppEmail;
 use App\Http\Resources\StatusStateless;
+use App\Jobs\StatusPipeline\StatusDelete;
 
 class ApiV1Dot1Controller extends Controller
 {
-    protected $fractal;
-
-    public function __construct()
-    {
-        $this->fractal = new Fractal\Manager();
-        $this->fractal->setSerializer(new ArraySerializer());
-    }
-
-    public function json($res, $code = 200, $headers = [])
-    {
-        return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
-    }
-
-    public function error($msg, $code = 400, $extra = [], $headers = [])
-    {
-        $res = [
-            "msg" => $msg,
-            "code" => $code
-        ];
-        return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
-    }
-
-    public function report(Request $request)
-    {
-        $user = $request->user();
-
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $report_type = $request->input('report_type');
-        $object_id = $request->input('object_id');
-        $object_type = $request->input('object_type');
-
-        $types = [
-            'spam',
-            'sensitive',
-            'abusive',
-            'underage',
-            'violence',
-            'copyright',
-            'impersonation',
-            'scam',
-            'terrorism'
-        ];
-
-        if (!$report_type || !$object_id || !$object_type) {
-            return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]);
-        }
-
-        if (!in_array($report_type, $types)) {
-            return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]);
-        }
-
-        if ($object_type === "user" && $object_id == $user->profile_id) {
-            return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
-        }
-
-        $rpid = null;
-
-        switch ($object_type) {
-            case 'post':
-                $object = Status::find($object_id);
-                if (!$object) {
-                    return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
-                }
-                $object_type = 'App\Status';
-                $exists = Report::whereUserId($user->id)
-                    ->whereObjectId($object->id)
-                    ->whereObjectType('App\Status')
-                    ->count();
-
-                $rpid = $object->profile_id;
-            break;
-
-            case 'user':
-                $object = Profile::find($object_id);
-                if (!$object) {
-                    return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
-                }
-                $object_type = 'App\Profile';
-                $exists = Report::whereUserId($user->id)
-                    ->whereObjectId($object->id)
-                    ->whereObjectType('App\Profile')
-                    ->count();
-                $rpid = $object->id;
-            break;
-
-            default:
-                return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]);
-            break;
-      }
-
-        if ($exists !== 0) {
-            return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]);
-        }
-
-        if ($object->profile_id == $user->profile_id) {
-            return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
-        }
-
-        $report = new Report;
-        $report->profile_id = $user->profile_id;
-        $report->user_id = $user->id;
-        $report->object_id = $object->id;
-        $report->object_type = $object_type;
-        $report->reported_profile_id = $rpid;
-        $report->type = $report_type;
-        $report->save();
-
-        $res = [
-            "msg" => "Successfully sent report",
-            "code" => 200
-        ];
-        return $this->json($res);
-    }
-
-    /**
-     * DELETE /api/v1.1/accounts/avatar
-     *
-     * @return \App\Transformer\Api\AccountTransformer
-     */
-    public function deleteAvatar(Request $request)
-    {
-        $user = $request->user();
-
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $avatar = $user->profile->avatar;
-
-        if( $avatar->media_path == 'public/avatars/default.png' ||
-            $avatar->media_path == 'public/avatars/default.jpg'
-        ) {
-            return AccountService::get($user->profile_id);
-        }
-
-        if(is_file(storage_path('app/' . $avatar->media_path))) {
-            @unlink(storage_path('app/' . $avatar->media_path));
-        }
-
-        $avatar->media_path = 'public/avatars/default.jpg';
-        $avatar->change_count = $avatar->change_count + 1;
-        $avatar->save();
-
-        Cache::forget('avatar:' . $user->profile_id);
-        Cache::forget("avatar:{$user->profile_id}");
-        Cache::forget('user:account:id:'.$user->id);
-        AccountService::del($user->profile_id);
-
-        return AccountService::get($user->profile_id);
-    }
-
-    /**
-     * GET /api/v1.1/accounts/{id}/posts
-     *
-     * @return \App\Transformer\Api\StatusTransformer
-     */
-    public function accountPosts(Request $request, $id)
-    {
-        $user = $request->user();
-
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $account = AccountService::get($id);
-
-        if(!$account || $account['username'] !== $request->input('username')) {
-            return $this->json([]);
-        }
-
-        $posts = ProfileStatusService::get($id);
-
-        if(!$posts) {
-            return $this->json([]);
-        }
-
-        $res = collect($posts)
-            ->map(function($id) {
-                return StatusService::get($id);
-            })
-            ->filter(function($post) {
-                return $post && isset($post['account']);
-            })
-            ->toArray();
-
-        return $this->json($res);
-    }
-
-    /**
-     * POST /api/v1.1/accounts/change-password
-     *
-     * @return \App\Transformer\Api\AccountTransformer
-     */
-    public function accountChangePassword(Request $request)
-    {
-        $user = $request->user();
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $this->validate($request, [
-            'current_password' => 'bail|required|current_password',
-            'new_password' => 'required|min:' . config('pixelfed.min_password_length', 8),
-            'confirm_password' => 'required|same:new_password'
-        ],[
-            'current_password' => 'The password you entered is incorrect'
-        ]);
-
-        $user->password = bcrypt($request->input('new_password'));
-        $user->save();
-
-        $log = new AccountLog;
-        $log->user_id = $user->id;
-        $log->item_id = $user->id;
-        $log->item_type = 'App\User';
-        $log->action = 'account.edit.password';
-        $log->message = 'Password changed';
-        $log->link = null;
-        $log->ip_address = $request->ip();
-        $log->user_agent = $request->userAgent();
-        $log->save();
-
-        Mail::to($request->user())->send(new PasswordChange($user));
-
-        return $this->json(AccountService::get($user->profile_id));
-    }
-
-    /**
-     * GET /api/v1.1/accounts/login-activity
-     *
-     * @return array
-     */
-    public function accountLoginActivity(Request $request)
-    {
-        $user = $request->user();
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-        $agent = new Agent();
-        $currentIp = $request->ip();
-
-        $activity = AccountLog::whereUserId($user->id)
-            ->whereAction('auth.login')
-            ->orderBy('created_at', 'desc')
-            ->groupBy('ip_address')
-            ->limit(10)
-            ->get()
-            ->map(function($item) use($agent, $currentIp) {
-                $agent->setUserAgent($item->user_agent);
-                return [
-                    'id' => $item->id,
-                    'action' => $item->action,
-                    'ip' => $item->ip_address,
-                    'ip_current' => $item->ip_address === $currentIp,
-                    'is_mobile' => $agent->isMobile(),
-                    'device' => $agent->device(),
-                    'browser' => $agent->browser(),
-                    'platform' => $agent->platform(),
-                    'created_at' => $item->created_at->format('c')
-                ];
-            });
-
-        return $this->json($activity);
-    }
-
-    /**
-     * GET /api/v1.1/accounts/two-factor
-     *
-     * @return array
-     */
-    public function accountTwoFactor(Request $request)
-    {
-        $user = $request->user();
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $res = [
-            'active' => (bool) $user->{'2fa_enabled'},
-            'setup_at' => $user->{'2fa_setup_at'}
-        ];
-        return $this->json($res);
-    }
-
-    /**
-     * GET /api/v1.1/accounts/emails-from-pixelfed
-     *
-     * @return array
-     */
-    public function accountEmailsFromPixelfed(Request $request)
-    {
-        $user = $request->user();
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-        $from = config('mail.from.address');
-
-        $emailVerifications = EmailVerification::whereUserId($user->id)
-            ->orderByDesc('id')
-            ->where('created_at', '>', now()->subDays(14))
-            ->limit(10)
-            ->get()
-            ->map(function($mail) use($user, $from) {
-                return [
-                    'type' => 'Email Verification',
-                    'subject' => 'Confirm Email',
-                    'to_address' => $user->email,
-                    'from_address' => $from,
-                    'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A'))
-                ];
-            })
-            ->toArray();
-
-        $passwordResets = DB::table('password_resets')
-            ->whereEmail($user->email)
-            ->where('created_at', '>', now()->subDays(14))
-            ->orderByDesc('created_at')
-            ->limit(10)
-            ->get()
-            ->map(function($mail) use($user, $from) {
-                return [
-                    'type' => 'Password Reset',
-                    'subject' => 'Reset Password Notification',
-                    'to_address' => $user->email,
-                    'from_address' => $from,
-                    'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
-                ];
-            })
-            ->toArray();
-
-        $passwordChanges = AccountLog::whereUserId($user->id)
-            ->whereAction('account.edit.password')
-            ->where('created_at', '>', now()->subDays(14))
-            ->orderByDesc('created_at')
-            ->limit(10)
-            ->get()
-            ->map(function($mail) use($user, $from) {
-                return [
-                    'type' => 'Password Change',
-                    'subject' => 'Password Change',
-                    'to_address' => $user->email,
-                    'from_address' => $from,
-                    'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
-                ];
-            })
-            ->toArray();
-
-        $res = collect([])
-            ->merge($emailVerifications)
-            ->merge($passwordResets)
-            ->merge($passwordChanges)
-            ->sortByDesc('created_at')
-            ->values();
-
-        return $this->json($res);
-    }
-
-
-    /**
-     * GET /api/v1.1/accounts/apps-and-applications
-     *
-     * @return array
-     */
-    public function accountApps(Request $request)
-    {
-        $user = $request->user();
-        abort_if(!$user, 403);
-        abort_if($user->status != null, 403);
-
-        $res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) {
-            return [
-                'id' => $key + 1,
-                'did' => encrypt($token->id),
-                'name' => $token->client->name,
-                'scopes' => $token->scopes,
-                'revoked' => $token->revoked,
-                'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
-                'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A'))
-            ];
-        });
-
-        return $this->json($res);
-    }
-
-    public function inAppRegistrationPreFlightCheck(Request $request)
-    {
-        return [
-            'open' => config('pixelfed.open_registration'),
-            'iara' => config('pixelfed.allow_app_registration')
-        ];
-    }
-
-    public function inAppRegistration(Request $request)
-    {
-        abort_if($request->user(), 404);
-        abort_unless(config('pixelfed.open_registration'), 404);
-        abort_unless(config('pixelfed.allow_app_registration'), 404);
-        abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
-        $this->validate($request, [
-            'email' => [
-                'required',
-                'string',
-                'email',
-                'max:255',
-                'unique:users',
-                function ($attribute, $value, $fail) {
-                    $banned = EmailService::isBanned($value);
-                    if($banned) {
-                        return $fail('Email is invalid.');
-                    }
-                },
-            ],
-            'username' => [
-                'required',
-                'min:2',
-                'max:15',
-                'unique:users',
-                function ($attribute, $value, $fail) {
-                    $dash = substr_count($value, '-');
-                    $underscore = substr_count($value, '_');
-                    $period = substr_count($value, '.');
-
-                    if(ends_with($value, ['.php', '.js', '.css'])) {
-                        return $fail('Username is invalid.');
-                    }
-
-                    if(($dash + $underscore + $period) > 1) {
-                        return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
-                    }
-
-                    if (!ctype_alnum($value[0])) {
-                        return $fail('Username is invalid. Must start with a letter or number.');
-                    }
-
-                    if (!ctype_alnum($value[strlen($value) - 1])) {
-                        return $fail('Username is invalid. Must end with a letter or number.');
-                    }
-
-                    $val = str_replace(['_', '.', '-'], '', $value);
-                    if(!ctype_alnum($val)) {
-                        return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
-                    }
-
-                    $restricted = RestrictedNames::get();
-                    if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
-                        return $fail('Username cannot be used.');
-                    }
-                },
-            ],
-            'password' => 'required|string|min:8',
-        ]);
-
-        $email = $request->input('email');
-        $username = $request->input('username');
-        $password = $request->input('password');
-
-        if(config('database.default') == 'pgsql') {
-            $username = strtolower($username);
-            $email = strtolower($email);
-        }
-
-        $user = new User;
-        $user->name = $username;
-        $user->username = $username;
-        $user->email = $email;
-        $user->password = Hash::make($password);
-        $user->register_source = 'app';
-        $user->app_register_ip = $request->ip();
-        $user->app_register_token = Str::random(32);
-        $user->save();
-
-        $rtoken = Str::random(mt_rand(64, 70));
-
-        $verify = new EmailVerification();
-        $verify->user_id = $user->id;
-        $verify->email = $user->email;
-        $verify->user_token = $user->app_register_token;
-        $verify->random_token = $rtoken;
-        $verify->save();
-
-        $appUrl = url('/api/v1.1/auth/iarer?ut=' . $user->app_register_token . '&rt=' . $rtoken);
-
-        Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
-
-        return response()->json([
-            'success' => true,
-        ]);
-    }
-
-    public function inAppRegistrationEmailRedirect(Request $request)
-    {
-        $this->validate($request, [
-            'ut' => 'required',
-            'rt' => 'required'
-        ]);
-        $ut = $request->input('ut');
-        $rt = $request->input('rt');
-        $url = 'pixelfed://confirm-account/'. $ut . '?rt=' . $rt;
-        return redirect()->away($url);
-    }
-
-    public function inAppRegistrationConfirm(Request $request)
-    {
-        abort_if($request->user(), 404);
-        abort_unless(config('pixelfed.open_registration'), 404);
-        abort_unless(config('pixelfed.allow_app_registration'), 404);
-        abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
-        $this->validate($request, [
-            'user_token' => 'required',
-            'random_token' => 'required',
-            'email' => 'required'
-        ]);
-
-        $verify = EmailVerification::whereEmail($request->input('email'))
-            ->whereUserToken($request->input('user_token'))
-            ->whereRandomToken($request->input('random_token'))
-            ->first();
-
-        if(!$verify) {
-            return response()->json(['error' => 'Invalid tokens'], 403);
-        }
-
-        if($verify->created_at->lt(now()->subHours(24))) {
-            $verify->delete();
-            return response()->json(['error' => 'Invalid tokens'], 403);
-        }
-
-        $user = User::findOrFail($verify->user_id);
-        $user->email_verified_at = now();
-        $user->last_active_at = now();
-        $user->save();
-
-        $token = $user->createToken('Pixelfed');
-
-        return response()->json([
-            'access_token' => $token->accessToken
-        ]);
-    }
-
-    public function archive(Request $request, $id)
-    {
-        abort_if(!$request->user(), 403);
-
-        $status = Status::whereNull('in_reply_to_id')
-            ->whereNull('reblog_of_id')
-            ->whereProfileId($request->user()->profile_id)
-            ->findOrFail($id);
-
-        if($status->scope === 'archived') {
-            return [200];
-        }
-
-        $archive = new StatusArchived;
-        $archive->status_id = $status->id;
-        $archive->profile_id = $status->profile_id;
-        $archive->original_scope = $status->scope;
-        $archive->save();
-
-        $status->scope = 'archived';
-        $status->visibility = 'draft';
-        $status->save();
-        StatusService::del($status->id, true);
-        AccountService::syncPostCount($status->profile_id);
-
-        return [200];
-    }
-
-
-    public function unarchive(Request $request, $id)
-    {
-        abort_if(!$request->user(), 403);
-
-        $status = Status::whereNull('in_reply_to_id')
-            ->whereNull('reblog_of_id')
-            ->whereProfileId($request->user()->profile_id)
-            ->findOrFail($id);
-
-        if($status->scope !== 'archived') {
-            return [200];
-        }
-
-        $archive = StatusArchived::whereStatusId($status->id)
-            ->whereProfileId($status->profile_id)
-            ->firstOrFail();
-
-        $status->scope = $archive->original_scope;
-        $status->visibility = $archive->original_scope;
-        $status->save();
-        $archive->delete();
-        StatusService::del($status->id, true);
-        AccountService::syncPostCount($status->profile_id);
-
-        return [200];
-    }
-
-    public function archivedPosts(Request $request)
-    {
-        abort_if(!$request->user(), 403);
-
-        $statuses = Status::whereProfileId($request->user()->profile_id)
-            ->whereScope('archived')
-            ->orderByDesc('id')
-            ->cursorPaginate(10);
-
-        return StatusStateless::collection($statuses);
-    }
-
-    public function placesById(Request $request, $id, $slug)
-    {
-        abort_if(!$request->user(), 403);
-
-        $place = Place::whereSlug($slug)->findOrFail($id);
-
-        $posts = Cache::remember('pf-api:v1.1:places-by-id:' . $place->id, 3600, function() use($place) {
-            return Status::wherePlaceId($place->id)
-                ->whereNull('uri')
-                ->whereScope('public')
-                ->orderByDesc('created_at')
-                ->limit(60)
-                ->pluck('id');
-        });
-
-        $posts = $posts->map(function($id) {
-            return StatusService::get($id);
-        })
-        ->filter()
-        ->values();
-
-        return ['place' => [
-            'id' => $place->id,
-            'name' => $place->name,
-            'slug' => $place->slug,
-            'country' => $place->country,
-            'lat' => $place->lat,
-            'long' => $place->long
-        ], 'posts' => $posts];
-    }
+	protected $fractal;
+
+	public function __construct()
+	{
+		$this->fractal = new Fractal\Manager();
+		$this->fractal->setSerializer(new ArraySerializer());
+	}
+
+	public function json($res, $code = 200, $headers = [])
+	{
+		return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
+	}
+
+	public function error($msg, $code = 400, $extra = [], $headers = [])
+	{
+		$res = [
+			"msg" => $msg,
+			"code" => $code
+		];
+		return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
+	}
+
+	public function report(Request $request)
+	{
+		$user = $request->user();
+
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$report_type = $request->input('report_type');
+		$object_id = $request->input('object_id');
+		$object_type = $request->input('object_type');
+
+		$types = [
+			'spam',
+			'sensitive',
+			'abusive',
+			'underage',
+			'violence',
+			'copyright',
+			'impersonation',
+			'scam',
+			'terrorism'
+		];
+
+		if (!$report_type || !$object_id || !$object_type) {
+			return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]);
+		}
+
+		if (!in_array($report_type, $types)) {
+			return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]);
+		}
+
+		if ($object_type === "user" && $object_id == $user->profile_id) {
+			return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
+		}
+
+		$rpid = null;
+
+		switch ($object_type) {
+			case 'post':
+				$object = Status::find($object_id);
+				if (!$object) {
+					return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
+				}
+				$object_type = 'App\Status';
+				$exists = Report::whereUserId($user->id)
+					->whereObjectId($object->id)
+					->whereObjectType('App\Status')
+					->count();
+
+				$rpid = $object->profile_id;
+			break;
+
+			case 'user':
+				$object = Profile::find($object_id);
+				if (!$object) {
+					return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
+				}
+				$object_type = 'App\Profile';
+				$exists = Report::whereUserId($user->id)
+					->whereObjectId($object->id)
+					->whereObjectType('App\Profile')
+					->count();
+				$rpid = $object->id;
+			break;
+
+			default:
+				return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]);
+			break;
+	  }
+
+		if ($exists !== 0) {
+			return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]);
+		}
+
+		if ($object->profile_id == $user->profile_id) {
+			return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
+		}
+
+		$report = new Report;
+		$report->profile_id = $user->profile_id;
+		$report->user_id = $user->id;
+		$report->object_id = $object->id;
+		$report->object_type = $object_type;
+		$report->reported_profile_id = $rpid;
+		$report->type = $report_type;
+		$report->save();
+
+		$res = [
+			"msg" => "Successfully sent report",
+			"code" => 200
+		];
+		return $this->json($res);
+	}
+
+	/**
+	 * DELETE /api/v1.1/accounts/avatar
+	 *
+	 * @return \App\Transformer\Api\AccountTransformer
+	 */
+	public function deleteAvatar(Request $request)
+	{
+		$user = $request->user();
+
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$avatar = $user->profile->avatar;
+
+		if( $avatar->media_path == 'public/avatars/default.png' ||
+			$avatar->media_path == 'public/avatars/default.jpg'
+		) {
+			return AccountService::get($user->profile_id);
+		}
+
+		if(is_file(storage_path('app/' . $avatar->media_path))) {
+			@unlink(storage_path('app/' . $avatar->media_path));
+		}
+
+		$avatar->media_path = 'public/avatars/default.jpg';
+		$avatar->change_count = $avatar->change_count + 1;
+		$avatar->save();
+
+		Cache::forget('avatar:' . $user->profile_id);
+		Cache::forget("avatar:{$user->profile_id}");
+		Cache::forget('user:account:id:'.$user->id);
+		AccountService::del($user->profile_id);
+
+		return AccountService::get($user->profile_id);
+	}
+
+	/**
+	 * GET /api/v1.1/accounts/{id}/posts
+	 *
+	 * @return \App\Transformer\Api\StatusTransformer
+	 */
+	public function accountPosts(Request $request, $id)
+	{
+		$user = $request->user();
+
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$account = AccountService::get($id);
+
+		if(!$account || $account['username'] !== $request->input('username')) {
+			return $this->json([]);
+		}
+
+		$posts = ProfileStatusService::get($id);
+
+		if(!$posts) {
+			return $this->json([]);
+		}
+
+		$res = collect($posts)
+			->map(function($id) {
+				return StatusService::get($id);
+			})
+			->filter(function($post) {
+				return $post && isset($post['account']);
+			})
+			->toArray();
+
+		return $this->json($res);
+	}
+
+	/**
+	 * POST /api/v1.1/accounts/change-password
+	 *
+	 * @return \App\Transformer\Api\AccountTransformer
+	 */
+	public function accountChangePassword(Request $request)
+	{
+		$user = $request->user();
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$this->validate($request, [
+			'current_password' => 'bail|required|current_password',
+			'new_password' => 'required|min:' . config('pixelfed.min_password_length', 8),
+			'confirm_password' => 'required|same:new_password'
+		],[
+			'current_password' => 'The password you entered is incorrect'
+		]);
+
+		$user->password = bcrypt($request->input('new_password'));
+		$user->save();
+
+		$log = new AccountLog;
+		$log->user_id = $user->id;
+		$log->item_id = $user->id;
+		$log->item_type = 'App\User';
+		$log->action = 'account.edit.password';
+		$log->message = 'Password changed';
+		$log->link = null;
+		$log->ip_address = $request->ip();
+		$log->user_agent = $request->userAgent();
+		$log->save();
+
+		Mail::to($request->user())->send(new PasswordChange($user));
+
+		return $this->json(AccountService::get($user->profile_id));
+	}
+
+	/**
+	 * GET /api/v1.1/accounts/login-activity
+	 *
+	 * @return array
+	 */
+	public function accountLoginActivity(Request $request)
+	{
+		$user = $request->user();
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+		$agent = new Agent();
+		$currentIp = $request->ip();
+
+		$activity = AccountLog::whereUserId($user->id)
+			->whereAction('auth.login')
+			->orderBy('created_at', 'desc')
+			->groupBy('ip_address')
+			->limit(10)
+			->get()
+			->map(function($item) use($agent, $currentIp) {
+				$agent->setUserAgent($item->user_agent);
+				return [
+					'id' => $item->id,
+					'action' => $item->action,
+					'ip' => $item->ip_address,
+					'ip_current' => $item->ip_address === $currentIp,
+					'is_mobile' => $agent->isMobile(),
+					'device' => $agent->device(),
+					'browser' => $agent->browser(),
+					'platform' => $agent->platform(),
+					'created_at' => $item->created_at->format('c')
+				];
+			});
+
+		return $this->json($activity);
+	}
+
+	/**
+	 * GET /api/v1.1/accounts/two-factor
+	 *
+	 * @return array
+	 */
+	public function accountTwoFactor(Request $request)
+	{
+		$user = $request->user();
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$res = [
+			'active' => (bool) $user->{'2fa_enabled'},
+			'setup_at' => $user->{'2fa_setup_at'}
+		];
+		return $this->json($res);
+	}
+
+	/**
+	 * GET /api/v1.1/accounts/emails-from-pixelfed
+	 *
+	 * @return array
+	 */
+	public function accountEmailsFromPixelfed(Request $request)
+	{
+		$user = $request->user();
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+		$from = config('mail.from.address');
+
+		$emailVerifications = EmailVerification::whereUserId($user->id)
+			->orderByDesc('id')
+			->where('created_at', '>', now()->subDays(14))
+			->limit(10)
+			->get()
+			->map(function($mail) use($user, $from) {
+				return [
+					'type' => 'Email Verification',
+					'subject' => 'Confirm Email',
+					'to_address' => $user->email,
+					'from_address' => $from,
+					'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A'))
+				];
+			})
+			->toArray();
+
+		$passwordResets = DB::table('password_resets')
+			->whereEmail($user->email)
+			->where('created_at', '>', now()->subDays(14))
+			->orderByDesc('created_at')
+			->limit(10)
+			->get()
+			->map(function($mail) use($user, $from) {
+				return [
+					'type' => 'Password Reset',
+					'subject' => 'Reset Password Notification',
+					'to_address' => $user->email,
+					'from_address' => $from,
+					'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
+				];
+			})
+			->toArray();
+
+		$passwordChanges = AccountLog::whereUserId($user->id)
+			->whereAction('account.edit.password')
+			->where('created_at', '>', now()->subDays(14))
+			->orderByDesc('created_at')
+			->limit(10)
+			->get()
+			->map(function($mail) use($user, $from) {
+				return [
+					'type' => 'Password Change',
+					'subject' => 'Password Change',
+					'to_address' => $user->email,
+					'from_address' => $from,
+					'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
+				];
+			})
+			->toArray();
+
+		$res = collect([])
+			->merge($emailVerifications)
+			->merge($passwordResets)
+			->merge($passwordChanges)
+			->sortByDesc('created_at')
+			->values();
+
+		return $this->json($res);
+	}
+
+	/**
+	 * GET /api/v1.1/accounts/apps-and-applications
+	 *
+	 * @return array
+	 */
+	public function accountApps(Request $request)
+	{
+		$user = $request->user();
+		abort_if(!$user, 403);
+		abort_if($user->status != null, 403);
+
+		$res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) {
+			return [
+				'id' => $key + 1,
+				'did' => encrypt($token->id),
+				'name' => $token->client->name,
+				'scopes' => $token->scopes,
+				'revoked' => $token->revoked,
+				'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
+				'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A'))
+			];
+		});
+
+		return $this->json($res);
+	}
+
+	public function inAppRegistrationPreFlightCheck(Request $request)
+	{
+		return [
+			'open' => config('pixelfed.open_registration'),
+			'iara' => config('pixelfed.allow_app_registration')
+		];
+	}
+
+	public function inAppRegistration(Request $request)
+	{
+		abort_if($request->user(), 404);
+		abort_unless(config('pixelfed.open_registration'), 404);
+		abort_unless(config('pixelfed.allow_app_registration'), 404);
+		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
+		$this->validate($request, [
+			'email' => [
+				'required',
+				'string',
+				'email',
+				'max:255',
+				'unique:users',
+				function ($attribute, $value, $fail) {
+					$banned = EmailService::isBanned($value);
+					if($banned) {
+						return $fail('Email is invalid.');
+					}
+				},
+			],
+			'username' => [
+				'required',
+				'min:2',
+				'max:15',
+				'unique:users',
+				function ($attribute, $value, $fail) {
+					$dash = substr_count($value, '-');
+					$underscore = substr_count($value, '_');
+					$period = substr_count($value, '.');
+
+					if(ends_with($value, ['.php', '.js', '.css'])) {
+						return $fail('Username is invalid.');
+					}
+
+					if(($dash + $underscore + $period) > 1) {
+						return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+					}
+
+					if (!ctype_alnum($value[0])) {
+						return $fail('Username is invalid. Must start with a letter or number.');
+					}
+
+					if (!ctype_alnum($value[strlen($value) - 1])) {
+						return $fail('Username is invalid. Must end with a letter or number.');
+					}
+
+					$val = str_replace(['_', '.', '-'], '', $value);
+					if(!ctype_alnum($val)) {
+						return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+					}
+
+					$restricted = RestrictedNames::get();
+					if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+						return $fail('Username cannot be used.');
+					}
+				},
+			],
+			'password' => 'required|string|min:8',
+		]);
+
+		$email = $request->input('email');
+		$username = $request->input('username');
+		$password = $request->input('password');
+
+		if(config('database.default') == 'pgsql') {
+			$username = strtolower($username);
+			$email = strtolower($email);
+		}
+
+		$user = new User;
+		$user->name = $username;
+		$user->username = $username;
+		$user->email = $email;
+		$user->password = Hash::make($password);
+		$user->register_source = 'app';
+		$user->app_register_ip = $request->ip();
+		$user->app_register_token = Str::random(32);
+		$user->save();
+
+		$rtoken = Str::random(mt_rand(64, 70));
+
+		$verify = new EmailVerification();
+		$verify->user_id = $user->id;
+		$verify->email = $user->email;
+		$verify->user_token = $user->app_register_token;
+		$verify->random_token = $rtoken;
+		$verify->save();
+
+		$appUrl = url('/api/v1.1/auth/iarer?ut=' . $user->app_register_token . '&rt=' . $rtoken);
+
+		Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
+
+		return response()->json([
+			'success' => true,
+		]);
+	}
+
+	public function inAppRegistrationEmailRedirect(Request $request)
+	{
+		$this->validate($request, [
+			'ut' => 'required',
+			'rt' => 'required'
+		]);
+		$ut = $request->input('ut');
+		$rt = $request->input('rt');
+		$url = 'pixelfed://confirm-account/'. $ut . '?rt=' . $rt;
+		return redirect()->away($url);
+	}
+
+	public function inAppRegistrationConfirm(Request $request)
+	{
+		abort_if($request->user(), 404);
+		abort_unless(config('pixelfed.open_registration'), 404);
+		abort_unless(config('pixelfed.allow_app_registration'), 404);
+		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
+		$this->validate($request, [
+			'user_token' => 'required',
+			'random_token' => 'required',
+			'email' => 'required'
+		]);
+
+		$verify = EmailVerification::whereEmail($request->input('email'))
+			->whereUserToken($request->input('user_token'))
+			->whereRandomToken($request->input('random_token'))
+			->first();
+
+		if(!$verify) {
+			return response()->json(['error' => 'Invalid tokens'], 403);
+		}
+
+		if($verify->created_at->lt(now()->subHours(24))) {
+			$verify->delete();
+			return response()->json(['error' => 'Invalid tokens'], 403);
+		}
+
+		$user = User::findOrFail($verify->user_id);
+		$user->email_verified_at = now();
+		$user->last_active_at = now();
+		$user->save();
+
+		$token = $user->createToken('Pixelfed');
+
+		return response()->json([
+			'access_token' => $token->accessToken
+		]);
+	}
+
+	public function archive(Request $request, $id)
+	{
+		abort_if(!$request->user(), 403);
+
+		$status = Status::whereNull('in_reply_to_id')
+			->whereNull('reblog_of_id')
+			->whereProfileId($request->user()->profile_id)
+			->findOrFail($id);
+
+		if($status->scope === 'archived') {
+			return [200];
+		}
+
+		$archive = new StatusArchived;
+		$archive->status_id = $status->id;
+		$archive->profile_id = $status->profile_id;
+		$archive->original_scope = $status->scope;
+		$archive->save();
+
+		$status->scope = 'archived';
+		$status->visibility = 'draft';
+		$status->save();
+		StatusService::del($status->id, true);
+		AccountService::syncPostCount($status->profile_id);
+
+		return [200];
+	}
+
+	public function unarchive(Request $request, $id)
+	{
+		abort_if(!$request->user(), 403);
+
+		$status = Status::whereNull('in_reply_to_id')
+			->whereNull('reblog_of_id')
+			->whereProfileId($request->user()->profile_id)
+			->findOrFail($id);
+
+		if($status->scope !== 'archived') {
+			return [200];
+		}
+
+		$archive = StatusArchived::whereStatusId($status->id)
+			->whereProfileId($status->profile_id)
+			->firstOrFail();
+
+		$status->scope = $archive->original_scope;
+		$status->visibility = $archive->original_scope;
+		$status->save();
+		$archive->delete();
+		StatusService::del($status->id, true);
+		AccountService::syncPostCount($status->profile_id);
+
+		return [200];
+	}
+
+	public function archivedPosts(Request $request)
+	{
+		abort_if(!$request->user(), 403);
+
+		$statuses = Status::whereProfileId($request->user()->profile_id)
+			->whereScope('archived')
+			->orderByDesc('id')
+			->cursorPaginate(10);
+
+		return StatusStateless::collection($statuses);
+	}
+
+	public function placesById(Request $request, $id, $slug)
+	{
+		abort_if(!$request->user(), 403);
+
+		$place = Place::whereSlug($slug)->findOrFail($id);
+
+		$posts = Cache::remember('pf-api:v1.1:places-by-id:' . $place->id, 3600, function() use($place) {
+			return Status::wherePlaceId($place->id)
+				->whereNull('uri')
+				->whereScope('public')
+				->orderByDesc('created_at')
+				->limit(60)
+				->pluck('id');
+		});
+
+		$posts = $posts->map(function($id) {
+			return StatusService::get($id);
+		})
+		->filter()
+		->values();
+
+		return [
+			'place' =>
+			[
+				'id' => $place->id,
+				'name' => $place->name,
+				'slug' => $place->slug,
+				'country' => $place->country,
+				'lat' => $place->lat,
+				'long' => $place->long
+			],
+		'posts' => $posts];
+	}
+
+	public function moderatePost(Request $request, $id)
+	{
+		abort_if(!$request->user(), 403);
+		abort_if($request->user()->is_admin != true, 403);
+
+		$this->validate($request, [
+			'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete'
+		]);
+
+		$action = $request->input('action');
+		$status = Status::find($id);
+
+		if(!$status) {
+			return response()->json(['error' => 'Cannot find status'], 400);
+		}
+
+		if($status->uri == null) {
+			if($status->profile->user && $status->profile->user->is_admin) {
+				return response()->json(['error' => 'Cannot moderate admin accounts'], 400);
+			}
+		}
+
+		if($action == 'mark-spammer') {
+			$status->profile->update([
+				'unlisted' => true,
+				'cw' => true,
+				'no_autolink' => true
+			]);
+
+			Status::whereProfileId($status->profile_id)
+				->get()
+				->each(function($s) {
+					if(in_array($s->scope, ['public', 'unlisted'])) {
+						$s->scope = 'private';
+						$s->visibility = 'private';
+					}
+					$s->is_nsfw = true;
+					$s->save();
+					StatusService::del($s->id, true);
+				});
+
+			Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $status->profile_id);
+			Cache::forget('pf:bouncer_v0:recent_by_pid:' . $status->profile_id);
+			Cache::forget('admin-dash:reports:spam-count');
+		} else if ($action == 'cw') {
+			$state = $status->is_nsfw;
+			$status->is_nsfw = !$state;
+			$status->save();
+			StatusService::del($status->id);
+		} else if ($action == 'mark-public') {
+			$state = $status->scope;
+			$status->scope = 'public';
+			$status->visibility = 'public';
+			$status->save();
+			StatusService::del($status->id, true);
+			if($state !== 'public') {
+				if($status->uri) {
+					NetworkTimelineService::add($status->id);
+				} else {
+					PublicTimelineService::add($status->id);
+				}
+			}
+		} else if ($action == 'mark-unlisted') {
+			$state = $status->scope;
+			$status->scope = 'unlisted';
+			$status->visibility = 'unlisted';
+			$status->save();
+			StatusService::del($status->id);
+			if($state == 'public') {
+				PublicTimelineService::del($status->id);
+				NetworkTimelineService::del($status->id);
+			}
+		} else if ($action == 'mark-private') {
+			$state = $status->scope;
+			$status->scope = 'private';
+			$status->visibility = 'private';
+			$status->save();
+			StatusService::del($status->id);
+			if($state == 'public') {
+				PublicTimelineService::del($status->id);
+				NetworkTimelineService::del($status->id);
+			}
+		} else if ($action == 'delete') {
+			PublicTimelineService::del($status->id);
+			NetworkTimelineService::del($status->id);
+			Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
+			Cache::forget('profile:status_count:' . $status->profile_id);
+			Cache::forget('profile:embed:' . $status->profile_id);
+			StatusService::del($status->id, true);
+			Cache::forget('profile:status_count:'.$status->profile_id);
+			StatusDelete::dispatch($status);
+			return [];
+		}
+
+		Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
+
+		return StatusService::get($status->id, false);
+	}
 }

+ 2 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
 
 use App\Http\Controllers\Controller;
 use App\User;
+use Purify;
 use App\Util\Lexer\RestrictedNames;
 use Illuminate\Foundation\Auth\RegistersUsers;
 use Illuminate\Support\Facades\Hash;
@@ -157,7 +158,7 @@ class RegisterController extends Controller
 		}
 
 		return User::create([
-			'name'     => $data['name'],
+			'name'     => Purify::clean($data['name']),
 			'username' => $data['username'],
 			'email'    => $data['email'],
 			'password' => Hash::make($data['password']),

+ 1 - 0
routes/api.php

@@ -191,6 +191,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
 	});
 
 	Route::group(['prefix' => 'admin'], function() use($middleware) {
+		Route::post('moderate/post/{id}', 'Api\ApiV1Dot1Controller@moderatePost')->middleware($middleware);
 		Route::get('supported', 'Api\AdminApiController@supported')->middleware($middleware);
 		Route::get('stats', 'Api\AdminApiController@getStats')->middleware($middleware);